sgf-0.1.3.3: Data/SGF/Parse/Util.hs
-- boilerplate {{{
module Data.SGF.Parse.Util where
import Control.Arrow (Arrow (first, second, (&&&)))
import Control.Monad (liftM, liftM2, when, (>=>))
import Control.Monad.State (MonadState (get), MonadTrans (lift), StateT (StateT), gets, modify)
import qualified Control.Monad.Trans.Except as Either
import Control.Monad.Writer (MonadTrans (lift), MonadWriter (tell), WriterT)
import Data.Char (isDigit, isSpace, toLower)
import Data.Encoding (DynEncoding)
import Data.Function (on)
import Data.Ix (Ix (range))
import Data.List (groupBy, isPrefixOf, nub, partition, sortBy)
import Data.Map (Map (..), fromList, keys)
import Data.Maybe (fromJust, listToMaybe)
import Data.Ord (comparing)
import Data.SGF.Parse.Encodings (decodeWordStringExplicit)
import Data.SGF.Parse.Raw (Property (..), enum)
import Data.SGF.Types (Color (..), Emphasis (..), Judgment, Mark, PartialDate, Point)
import Data.Set (Set)
import Data.Tree (Tree (rootLabel))
import Data.Word (Word8)
import Text.Parsec (SourcePos)
-- }}}
-- new types {{{
-- Header {{{
data Header = Header
{ format :: Integer,
encoding :: DynEncoding
}
-- }}}
-- Error {{{
data ErrorType
= UnknownEncoding
| AmbiguousEncoding
| FormatUnsupported
| GameUnsupported
| OutOfBounds
| BadlyFormattedValue
| BadlyEncodedValue
| ConcurrentMoveAndSetup
| ConcurrentBlackAndWhiteMove
| ConcurrentAnnotations
| ExtraMoveAnnotations
deriving (Eq, Ord, Show, Read, Bounded, Enum)
-- Errors signify unrecoverable errors.
data Error
= KnownError {errorType :: ErrorType, errorPosition :: SourcePos}
| UnknownError {errorDescription :: Maybe String}
deriving (Eq, Ord, Show)
die :: Error -> Translator a
dieWithPos :: ErrorType -> SourcePos -> Translator a
dieWith :: ErrorType -> Property -> Translator a
dieWithJust :: ErrorType -> Maybe Property -> Translator a
die = lift . StateT . const . Left
dieWithPos e = die . KnownError e
dieWith e = dieWithPos e . position
dieWithJust e = dieWith e . fromJust
-- }}}
-- Warning {{{
-- by convention, a warning that does not end in a verb just "did the right thing" to correct the problem
-- |
-- Warnings signify recoverable errors.
data Warning
= DuplicatePropertyOmitted Property
| SquareSizeSpecifiedAsRectangle SourcePos
| DanglingEscapeCharacterOmitted SourcePos
| PropValueForNonePropertyOmitted Property
| UnknownPropertyPreserved String
| PointSpecifiedAsPointRange Property
| DuplicatePointsOmitted Property [Point]
| InvalidDatesClipped (Set PartialDate)
| AnnotationWithNoMoveOmitted Property
| ExtraGameInfoOmitted Property
| NestedRootPropertyOmitted Property
| MovelessAnnotationOmitted Property
| DuplicateSetupOperationsOmitted [Point]
| ExtraPositionalJudgmentOmitted (Judgment, Emphasis)
| DuplicateMarkupOmitted (Mark, Point)
| ExtraPropertyValuesOmitted Property
| DuplicateLabelOmitted (Point, String)
| UnknownNumberingIgnored Integer
deriving (Eq, Ord, Show)
-- }}}
-- State, Translator a, PTranslator a {{{
type State = Tree [Property]
type Translator a = WriterT [Warning] (StateT State (Either Error)) a
type PTranslator a = Property -> Translator a
transMap, transMapMulti :: PTranslator a -> String -> Translator (Maybe a)
transMap f = consumeSingle >=> transMap' f
transMapMulti f = consume >=> transMap' f
transMap' :: (a -> Translator b) -> (Maybe a -> Translator (Maybe b))
transMap' f = maybe (return Nothing) (liftM Just . f)
transMapList :: PTranslator [a] -> String -> Translator [a]
transMapList f = consume >=> maybe (return []) f
-- }}}
-- }}}
-- handy Translators {{{
-- helper functions {{{
duplicatesOn :: (Ord b) => (a -> b) -> [a] -> [a]
duplicatesOn f =
map fst
. concatMap (drop 1)
. groupBy ((==) `on` snd)
. sortBy (comparing snd)
. map (id &&& f)
duplicateProperties :: State -> [Warning]
duplicateProperties = map DuplicatePropertyOmitted . duplicatesOn name . rootLabel
duplicates :: Translator ()
duplicates = get >>= tell . duplicateProperties
readNumber :: String -> SourcePos -> Translator Integer
readNumber "" _ = return 0
readNumber s pos
| all isDigit s = return (read s)
| otherwise = dieWithPos BadlyFormattedValue pos
newline :: a -> (String -> a) -> (Char -> String -> a) -> String -> a
newline empty with without xs = case xs of
'\r' : '\n' : xs -> with xs
'\n' : '\r' : xs -> with xs
'\r' : xs -> with xs
'\n' : xs -> with xs
x : xs -> without x xs
[] -> empty
trim :: Char -> Char
trim x = if isSpace x then ' ' else x
descape :: Char -> SourcePos -> String -> Translator String
descape hard pos s = case s of
('\\' : xs) -> newline' (tell [DanglingEscapeCharacterOmitted pos]) id xs
xs -> newline' (return ()) (hard :) xs
where
newline' warn prefix = newline (warn >> return "") (fmap prefix . descape hard pos) (\c -> fmap (trim c :) . descape hard pos)
decodeAndDescape :: Char -> Header -> PTranslator String
decodeAndDescape hard (Header {encoding = e}) (Property {values = v : _, position = pos}) =
case decodeWordStringExplicit e v of
Left exception -> dieWithPos BadlyEncodedValue pos
Right decoded -> descape hard pos decoded
splitColon :: [Word8] -> Maybe ([Word8], [Word8])
splitColons :: [[Word8]] -> Maybe ([[Word8]], [[Word8]])
splitColons = fmap unzip . mapM splitColon
splitColon xs
| null xs = Nothing
| [enum ':'] `isPrefixOf` xs = Just ([], drop 1 xs)
| [enum '\\'] `isPrefixOf` xs = continue 2
| otherwise = continue 1
where
continue n = fmap (first (take n xs ++)) (splitColon (drop n xs))
warnAboutDuplicatePoints :: Property -> [Point] -> Translator [Point]
warnAboutDuplicatePoints p ps =
let ds = duplicatesOn id ps
in do
when (not $ null ds) (tell [DuplicatePointsOmitted p ds])
return (nub ps)
checkPointList :: (PTranslator [Point] -> PTranslator [[Point]]) -> (PTranslator Point -> PTranslator [Point])
checkPointList listType a p = listType (mayBeCompoundPoint a) p >>= warnAboutDuplicatePoints p . concat
-- }}}
-- low-level {{{
has :: String -> Translator Bool
has s = gets (any ((s ==) . name) . rootLabel)
hasAny :: [String] -> Translator Bool
hasAny = fmap or . mapM has
consume :: String -> Translator (Maybe Property)
consume s = do
(v, rest) <- gets (partition ((== s) . name) . rootLabel)
modify (\s -> s {rootLabel = rest})
return (listToMaybe v)
consumeSingle :: String -> Translator (Maybe Property)
consumeSingle s = do
maybeProperty <- consume s
case maybeProperty of
Just p@(Property {values = (v : _ : _)}) -> do
tell [ExtraPropertyValuesOmitted p]
return (Just p {values = [v]})
_ -> return maybeProperty
unknownProperties :: Translator (Map String [[Word8]])
unknownProperties = do
m <- gets (fromList . map (name &&& values) . rootLabel)
tell [UnknownPropertyPreserved name | name <- keys m]
return m
-- }}}
-- PTranslators and combinators {{{
number :: PTranslator Integer
number p@(Property {values = v : _})
| enum '.' `elem` v = dieWith BadlyFormattedValue p
| otherwise = fmap floor (real p)
real :: PTranslator Rational
real (Property {values = v : _, position = pos})
| [enum '+'] `isPrefixOf` v = result 1
| [enum '-'] `isPrefixOf` v = fmap negate (result 1)
| otherwise = result 0
where
split i = second (drop 1) . break (== '.') . map enum . drop i $ v
result i =
let (n, d) = split i
in do
whole <- readNumber n pos
fract <- readNumber d pos
return (fromInteger whole + fromInteger fract / 10 ^ length d)
simple :: Header -> PTranslator String
text :: Header -> PTranslator String
simple = decodeAndDescape ' '
text = decodeAndDescape '\n'
none :: PTranslator ()
none (Property {values = [[]]}) = return ()
none p = tell [PropValueForNonePropertyOmitted p]
choice :: [([Word8], a)] -> PTranslator a
choice vs p@(Property {values = []}) = dieWith BadlyFormattedValue p -- can't happen
choice vs p@(Property {values = v : _}) = maybe (dieWith BadlyFormattedValue p) return (lookup v vs)
choice' :: [(String, a)] -> PTranslator a
choice' vs = choice [(map enum k, v) | (k', v) <- vs, k <- [k', map toLower k']]
double :: PTranslator Emphasis
color :: PTranslator Color
double = choice' [("1", Normal), ("2", Strong)]
color = choice' [("B", Black), ("W", White)]
compose :: PTranslator a -> PTranslator b -> PTranslator (a, b)
compose a b p@(Property {values = vs}) = case splitColons vs of
Nothing -> dieWith BadlyFormattedValue p
Just (as, bs) -> liftM2 (,) (a p {values = as}) (b p {values = bs})
listOf :: PTranslator a -> PTranslator [a]
listOf a p@(Property {values = vs}) = mapM a [p {values = [v]} | v <- vs]
elistOf :: PTranslator a -> PTranslator [a]
elistOf _ (Property {values = [[]]}) = return []
elistOf a p = listOf a p
mayBeCompoundPoint, listOfPoint, elistOfPoint :: PTranslator Point -> PTranslator [Point]
mayBeCompoundPoint a p@(Property {values = v : _}) = case splitColon v of
Nothing -> return <$> a p
Just {} -> do
pointRange <- compose a a p {values = [v]}
when (uncurry (==) pointRange) (tell [PointSpecifiedAsPointRange p])
return (range pointRange)
listOfPoint = checkPointList listOf
elistOfPoint = checkPointList elistOf
-- }}}
-- }}}