{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE CPP #-}
module Text.ICalendar.Parser.Common where
import Control.Applicative
import Control.Arrow (second)
import Control.Monad.Error hiding (mapM)
import Control.Monad.RWS (MonadState (get, put),
MonadWriter (tell), RWS, asks,
modify)
import qualified Data.ByteString.Lazy.Builder as Bu
import Data.ByteString.Lazy.Char8 (ByteString)
import qualified Data.ByteString.Lazy.Char8 as B
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Data.Char
import Data.Default
import Data.List (partition)
import Data.Maybe
import Data.Monoid
import Data.Set (Set)
import qualified Data.Set as S
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.Encoding as TE
import Data.Time (Day, LocalTime (LocalTime),
TimeOfDay (), UTCTime (UTCTime))
import qualified Data.Time as Time
import Data.Traversable (mapM)
import qualified Network.URI as URI
import Prelude hiding (mapM)
#if MIN_VERSION_time(1,5,0)
import Data.Time (defaultTimeLocale)
#else
import System.Locale (defaultTimeLocale)
#endif
import qualified Text.Parsec as P
import Text.Parsec.Combinator hiding (optional)
import Text.Parsec.Prim hiding ((<|>))
import Text.ICalendar.Types
data Content = ContentLine P.SourcePos (CI Text) [(CI Text, [Text])] ByteString
| Component P.SourcePos (CI Text) [Content]
deriving (Show, Eq, Ord)
type TextParser = P.Parsec ByteString DecodingFunctions
type ContentParser = ErrorT String
(RWS DecodingFunctions
[String]
(P.SourcePos, [Content]))
data DecodingFunctions = DecodingFunctions
{ dfBS2Text :: ByteString -> Text
, dfBS2IText :: ByteString -> CI Text
}
instance Default DecodingFunctions where
def = DecodingFunctions TE.decodeUtf8 (CI.mk . TE.decodeUtf8)
parseText' :: ByteString -> ContentParser ([Text], ByteString)
parseText' bs = do c <- asks dfBS2Text
case runParser ((,) <$> texts <*> getInput) () "text" bs of
Left e -> throwError $ "parseText': " ++ show e
Right (x, r) -> return ( map (c . Bu.toLazyByteString) x
, r)
where texts = sepBy1 text (P.char ',') <|> return [mempty]
text = do x <- P.satisfy isTSafe'
case x of
'\\' -> do y <- P.anyChar
case y of
'\\' -> nxt '\\'
';' -> nxt ';'
',' -> nxt ','
z | z `elem` ['n','N'] -> nxt '\n'
_ -> fail $ "unexpected " ++ show x
y -> nxt y
isTSafe' c = let n = ord c
in n == 9 || (n >= 0x20 && n <= 0x2B)
|| (n >= 0x2D && n <= 0x3A)
|| (n >= 0x3C && n /= 0x7F)
nxt c = (Bu.char8 c <>) <$> (text <|> return mempty)
noRestText :: ([Text], ByteString) -> ContentParser [Text]
noRestText (x, "") = return x
noRestText (_, x) = throwError $ "noRestText: remainding text: " ++ show x
parseText :: ByteString -> ContentParser [Text]
parseText = noRestText <=< parseText'
parseDateTime :: Maybe Text
-> ByteString -> ContentParser DateTime
parseDateTime mTZ bs = do
str <- asks $ T.unpack . ($ bs) . dfBS2Text
let dayRes = parseDateStr str
Just (day, rest') = dayRes
t = take 1 rest'
timeRes = parseTimeStr $ drop 1 rest'
Just (time, isUTC) = timeRes
when (isNothing (dayRes >> timeRes) || t /= "T") .
throwError $ "parseDateTime: " ++ str
when (isUTC && isJust mTZ) $
tell ["parseDateTime: TZID on UTC timezone: " ++ str]
return $ case (mTZ, isUTC) of
(Nothing, False) -> FloatingDateTime (LocalTime day time)
(Just tz, False) -> ZonedDateTime (LocalTime day time) tz
(_, True) -> UTCDateTime (UTCTime day
$ Time.timeOfDayToTime time)
parseDateStr :: String -> Maybe (Day, String)
parseDateStr = lastToMaybe . Time.readSTime True defaultTimeLocale "%Y%m%d"
parseTimeStr :: String -> Maybe (TimeOfDay, Bool)
parseTimeStr s = do
(t, r) <- lastToMaybe (Time.readSTime True defaultTimeLocale "%H%M%S" s)
case r of
"Z" -> return (t, True)
"" -> return (t, False)
_ -> fail ""
parseDate :: ByteString -> ContentParser Date
parseDate bs = do
str <- asks $ T.unpack . ($ bs) . dfBS2Text
let dayRes = parseDateStr str
Just (day, rest) = dayRes
when (isNothing dayRes) .
throwError $ "parseDate: " ++ str
unless (null rest) $
tell ["parseDate: extra content: " ++ rest]
return $ Date day
parseURI :: String -> ContentParser URI.URI
parseURI s = case URI.parseURI s of
Just x -> return x
Nothing -> throwError $ "Invalid URI: " ++ show s
mustBeUTC :: DateTime -> ContentParser UTCTime
mustBeUTC (UTCDateTime x) = return x
mustBeUTC _ = throwError "DateTime-value must be UTC"
parseSimple :: (Text -> OtherParams -> b) -> Content -> ContentParser b
parseSimple k (ContentLine _ _ o bs) = do c <- valueOnlyOne =<< parseText bs
return $ k c (toO o)
parseSimple _ x = throwError $ "parseSimple: " ++ show x
parseSimpleI :: (CI Text -> OtherParams -> b) -> Content -> ContentParser b
parseSimpleI k (ContentLine _ _ o bs) = do c <- asks dfBS2IText
return $ k (c bs) (toO o)
parseSimpleI _ x = throwError $ "parseSimpleI: " ++ show x
parseSimpleRead :: forall a b. Read a
=> (a -> OtherParams -> b) -> Content -> ContentParser b
parseSimpleRead k (ContentLine _ _ o bs) = do
let r = maybeRead $ B.unpack bs :: Maybe a
when (isNothing r) . throwError $ "parseSimpleRead: " ++ show bs
return $ k (fromJust r) (toO o)
parseSimpleRead _ x = throwError $ "parseSimpleRead: " ++ show x
parseAltRepLang' :: ([Text] -> ContentParser b)
-> (b -> Maybe URI.URI -> Maybe Language -> OtherParams -> a)
-> Content -> ContentParser a
parseAltRepLang' m f (ContentLine _ _ o bs) = do
t <- m =<< parseText bs
uri <- mapM (parseURI <=< paramOnlyOne) $ T.unpack .: lookup "ALTREP" o
lang <- mapM paramOnlyOne $ Language . CI.mk .: lookup "LANGUAGE" o
let o' = filter (\(x, _) -> x `notElem` ["ALTREP", "LANGUAGE"]) o
return $ f t uri lang (toO o')
parseAltRepLang' _ _ x = throwError $ "parseAltRepLang': " ++ show x
parseAltRepLang :: (Text -> Maybe URI.URI -> Maybe Language -> OtherParams -> a)
-> Content -> ContentParser a
parseAltRepLang = parseAltRepLang' lenientTextOnlyOne
where lenientTextOnlyOne :: [Text] -> ContentParser Text
lenientTextOnlyOne [x] = return x
lenientTextOnlyOne [] = throwError "Must have one value, not zero."
lenientTextOnlyOne xs = do
tell ["Illegal comma in value that only allows one TEXT, assuming literal comma was intended."]
return $ T.intercalate "," xs
parseAltRepLangN :: (Set Text -> Maybe URI.URI -> Maybe Language
-> OtherParams -> a)
-> Content -> ContentParser a
parseAltRepLangN = parseAltRepLang' (return . S.fromList)
parseSimpleURI :: (URI.URI -> OtherParams -> a) -> Content -> ContentParser a
parseSimpleURI f (ContentLine _ _ o bs) = do
uri <- parseURI =<< asks (T.unpack . ($ bs) . dfBS2Text)
return . f uri $ toO o
parseSimpleURI _ x = throwError $ "parseSimpleURI: " ++ show x
parseSimpleDateOrDateTime :: (DateTime -> OtherParams -> a)
-> (Date -> OtherParams -> a)
-> Content
-> ContentParser a
parseSimpleDateOrDateTime dt d (ContentLine _ _ o bs) = do
(typ, tzid, o') <- typTzIdO o
case typ of
"DATE-TIME" -> do x <- parseDateTime tzid bs
return . dt x $ toO o'
"DATE" -> do x <- parseDate bs
return . d x $ toO o'
_ -> throwError $ "Invalid type: " ++ show typ
parseSimpleDateOrDateTime _ _ x =
throwError $ "parseSimpleDateOrDateTime: " ++ show x
parseSimpleDatesOrDateTimes :: (Set DateTime -> OtherParams -> a)
-> (Set Date -> OtherParams -> a)
-> Content
-> ContentParser a
parseSimpleDatesOrDateTimes dt d (ContentLine _ _ o bs) = do
(typ, tzid, o') <- typTzIdO o
case typ of
"DATE-TIME" -> do x <- S.fromList .: mapM (parseDateTime tzid) $
B.split ',' bs
return . dt x $ toO o'
"DATE" -> do x <- S.fromList .: mapM parseDate $ B.split ',' bs
return . d x $ toO o'
_ -> throwError $ "Invalid type: " ++ show typ
parseSimpleDatesOrDateTimes _ _ x =
throwError $ "parseSimpleDatesOrDateTimes: " ++ show x
typTzIdO :: [(CI Text, [Text])]
-> ContentParser (Text, Maybe Text, [(CI Text, [Text])])
typTzIdO o = do
typ <- paramOnlyOne . fromMaybe ["DATE-TIME"] $ lookup "VALUE" o
tzid <- mapM paramOnlyOne $ if typ == "DATE-TIME" then lookup "TZID" o
else Nothing
let f x = x /= "VALUE" && (typ /= "DATE-TIME" || x /= "TZID")
o' = filter (f . fst) o
return (typ, tzid, o')
parseSimpleDateTime :: (DateTime -> OtherParams -> a)
-> Content
-> ContentParser a
parseSimpleDateTime dt (ContentLine _ _ o bs) = do
tzid <- mapM paramOnlyOne $ lookup "TZID" o
let o' = filter ((/="TZID") . fst) o
flip dt (toO o') <$> parseDateTime tzid bs
parseSimpleDateTime _ x = throwError $ "parseSimpleDateTime: " ++ show x
parseSimpleUTC :: (UTCTime -> OtherParams -> a)
-> Content
-> ContentParser a
parseSimpleUTC dt (ContentLine _ _ o bs) =
flip dt (toO o) <$> (mustBeUTC =<< parseDateTime Nothing bs)
parseSimpleUTC _ x = throwError $ "parseSimpleUTC: " ++ show x
toO :: [(CI Text, [Text])] -> OtherParams
toO = OtherParams . S.fromList . map (uncurry OtherParam)
otherProperties :: ContentParser (Set OtherProperty)
otherProperties = do opts <- snd <$> get
modify (second $ const [])
S.fromList <$> mapM lineToOtherProp opts
where lineToOtherProp (ContentLine _ n opts bs) =
return (OtherProperty n bs $ toO opts)
lineToOtherProp c@Component {} =
down c . throwError $ "Unconsumed component: " ++ show c
neg :: TextParser (Int -> Int)
neg = maybe id (\x -> if x == '-' then negate else id)
<$> optional (P.oneOf "+-")
digits :: TextParser Int
digits = foldl1 ((+).(*10)) . map digitToInt <$> many1 P.digit
digitsN :: TextParser [Int]
digitsN = sepBy1 digits (P.char ',')
down :: Content -> ContentParser a -> ContentParser a
down (Component p _ x) = down' (p, x)
down x@(ContentLine p _ _ _) = down' (p, [x])
down' :: (P.SourcePos, [Content]) -> ContentParser a -> ContentParser a
down' x m = get >>= \old -> put x >> m <* put old
optCompN :: Ord a
=> CI Text -> (Content -> ContentParser a) -> ContentParser (Set a)
optCompN s f = optN f . partition (`isComponentNamed` s) =<< snd <$> get
reqLine1 :: CI Text -> (Content -> ContentParser a) -> ContentParser a
reqLine1 s f = req1 s f . partition (`isLineNamed` s) =<< snd <$> get
optLine1 :: Default b
=> CI Text -> (Content -> ContentParser b) -> ContentParser b
optLine1 s f = opt1 f . partition (`isLineNamed` s) =<< snd <$> get
optLineN :: Ord b
=> CI Text -> (Content -> ContentParser b) -> ContentParser (Set b)
optLineN s f = optN f . partition (`isLineNamed` s) =<< snd <$> get
reqLineN :: Ord b
=> CI Text -> (Content -> ContentParser b) -> ContentParser (Set b)
reqLineN s f = reqN s f . partition (`isLineNamed` s) =<< snd <$> get
req1 :: CI Text -> (Content -> ContentParser b) -> ([Content], [Content])
-> ContentParser b
req1 _ f ([x], xs) = modify (second $ const xs) >> down x (f x)
req1 s _ ([], _) = throwError $ "Missing content: " ++ show s
req1 _ f (x:xs, xs') = do modify (second $ const xs')
tell (map (("Extra content: " ++) . show) xs)
down x $ f x
opt1 :: Default b
=> (Content -> ContentParser b) -> ([Content], [Content])
-> ContentParser b
opt1 f ([x], xs) = modify (second $ const xs) >> down x (f x)
opt1 _ ([], _) = return def
opt1 f (x:xs, xs') = do modify (second $ const xs')
tell (map (("Extra content: " ++) . show) xs)
down x $ f x
optN :: Ord b
=> (Content -> ContentParser b) -> ([Content], [Content])
-> ContentParser (Set b)
optN f (xs, xs') = do modify (second $ const xs')
S.fromList <$> mapM (\x -> down x (f x)) xs
reqN :: Ord b
=> CI Text
-> (Content -> ContentParser b) -> ([Content], [Content])
-> ContentParser (Set b)
reqN w f (xs, xs') = do modify (second $ const xs')
o <- S.fromList <$> mapM (\x -> down x (f x)) xs
when (S.size o < 1) .
throwError $ "At least one required: " ++ show w
return o
paramOnlyOne :: [a] -> ContentParser a
paramOnlyOne [x] = return x
paramOnlyOne _ = throwError "Only one parameter value allowed."
valueOnlyOne :: [a] -> ContentParser a
valueOnlyOne [x] = return x
valueOnlyOne [] = throwError "Must have one value, not zero."
valueOnlyOne _ = throwError "Only one value allowed."
isLineNamed :: Content -> CI Text -> Bool
isLineNamed (ContentLine _ n _ _) n' | n == n' = True
isLineNamed _ _ = False
isComponentNamed :: Content -> CI Text -> Bool
isComponentNamed (Component _ n _) n' | n == n' = True
isComponentNamed _ _ = False
isComponent :: Content -> Bool
isComponent Component {} = True
isComponent _ = False
maybeRead :: Read a => String -> Maybe a
maybeRead = fst .: lastToMaybe . reads
lastToMaybe :: [a] -> Maybe a
lastToMaybe x = if null x then Nothing else Just $ last x
(.:) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b)
(.:) = fmap fmap fmap
infixl 4 .: