ical-0.0.0: src/ICal/Tokenizer.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE LambdaCase #-}
-- | The basic tokenizer.
module ICal.Tokenizer
(-- * Top-level functions
tokenizeObjectFromFile
,tokenizeObjectFromText
,tokenizeAesonFromText
-- * Raw tokenizers
,objectTokenizer
,linesTokenizer
,lineTokenizer)
where
import Control.Monad.Fix
import Data.Aeson (FromJSON(..),toJSON,fromJSON,Result(..))
import Data.Attoparsec.Text (Parser)
import qualified Data.Attoparsec.Text as P
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import ICal.Types
-- | Tokenizer a complete document from a .ics file.
tokenizeObjectFromFile :: FilePath -> IO Object
tokenizeObjectFromFile fp =
fmap tokenizeObjectFromText (T.readFile fp) >>=
\case
Left err -> error err
Right ok -> return ok
-- | Tokenizer a complete document.
tokenizeObjectFromText :: Text -> Either String Object
tokenizeObjectFromText t =
do ls <- P.parseOnly linesTokenizer t
(object,remainder) <- objectTokenizer ls
if null remainder
then return object
else Left ("Unexpected extraneous input: " ++ show remainder)
-- | Tokenize an Aeson instance from the document.
tokenizeAesonFromText :: FromJSON a => Text -> Either String a
tokenizeAesonFromText t =
do doc <- tokenizeObjectFromText t
case fromJSON (toJSON doc) of
Error e -> Left e
Success a -> Right a
-- | Tokenize a list of lines into an object.
objectTokenizer :: [Line] -> Either String (Object,[Line])
objectTokenizer [] = Left "Unexpected end of input."
objectTokenizer (Begin name:linesInAndAfterObject) =
do (values,linesAfterObject) <-
fix (\loop ->
\case
[] -> return ([],[])
nextLineSet@(next:linesAfterObject) ->
case next of
End{} -> return ([],linesAfterObject)
_ ->
do (x,linesAfterChildObject) <- objectTokenizer nextLineSet
(xs,linesAfterRestOfChildren) <-
loop linesAfterChildObject
return (x : xs,linesAfterRestOfChildren))
linesInAndAfterObject
return (Object name values,linesAfterObject)
objectTokenizer (Pair key value:linesAfterPair) =
return (Property key value,linesAfterPair)
objectTokenizer (End name:_) =
Left ("Unexpected end of object: " ++ show name)
-- | Tokenize lines of iCalendar format.
linesTokenizer :: Parser [Line]
linesTokenizer = P.many1 lineTokenizer
-- | Tokenize a single line.
lineTokenizer :: Parser Line
lineTokenizer =
do (key,value) <- propertyTokenizer
case key of
"BEGIN" -> return (Begin value)
"END" -> return (End value)
_ -> return (Pair key value)
-- | Tokenize a (possibly-mult-line) property.
propertyTokenizer :: Parser (Text,Text)
propertyTokenizer =
do key <- P.takeWhile1 (not . propertySeparator)
_ <- P.satisfy propertySeparator
fmap ((key,) . T.concat)
(fix (\loop ->
do value <- P.takeTill newline
_ <- P.takeWhile1 newline
mnext <- P.peekChar
case mnext of
Just ' ' ->
do _ <- P.anyChar
rest <- loop
return (value : rest)
_ -> return [value]))
where propertySeparator c = c == ':' || c == ';'
newline c = c == '\r' || c == '\n'