{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE CPP #-} module Text.ICalendar.Parser.Common where import Control.Applicative import Control.Arrow (second) import Control.Monad (when, unless, (<=<)) import Control.Monad.Except hiding (mapM) import Control.Monad.RWS (MonadState (get, put), MonadWriter (tell), RWS, asks, modify) import qualified Data.ByteString.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 -- | Content lines, separated into components. 3.1. 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 = ExceptT String -- Fatal errors. (RWS DecodingFunctions [String] -- Warnings. (P.SourcePos, [Content])) -- | Functions for decoding 'ByteString's into 'Text'. data DecodingFunctions = DecodingFunctions { dfBS2Text :: ByteString -> Text , dfBS2IText :: ByteString -> CI Text } -- | UTF8. instance Default DecodingFunctions where def = DecodingFunctions TE.decodeUtf8 (CI.mk . TE.decodeUtf8) -- | Parse text. 3.3.11 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 + 0x22, 0x3A, and 0x5C is pattern matched against. 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) -- | Chech that there's no remainding text after the parser is done. noRestText :: ([Text], ByteString) -> ContentParser [Text] noRestText (x, "") = return x noRestText (_, x) = throwError $ "noRestText: remainding text: " ++ show x -- | Parse text, not allowing any remainding text. parseText :: ByteString -> ContentParser [Text] parseText = noRestText <=< parseText' -- | Parse a DateTime value. 3.3.5 parseDateTime :: Maybe Text -- ^ Time Zone ID -> 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) -- | Parse a string to a Day. 3.3.4 parseDateStr :: String -> Maybe (Day, String) parseDateStr = lastToMaybe . Time.readSTime True defaultTimeLocale "%Y%m%d" -- | Parse a string to a TimeOfDay, and a bool if it's in UTC. 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 "" -- | Parse a Date value. 3.3.4 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 -- {{{ Misc parsers parseURI :: String -> ContentParser URI.URI parseURI s = case URI.parseURI s of Just x -> return x Nothing -> throwError $ "Invalid URI: " ++ show s -- | Convert a 'DateTime' to 'UTCTime', giving an appropriate error. mustBeUTC :: DateTime -> ContentParser UTCTime mustBeUTC (UTCDateTime x) = return x mustBeUTC _ = throwError "DateTime-value must be UTC" -- | Parse something simple with only a Text-field for the content, and -- 'OtherParams'. 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 -- | Parse something simple with only a CI Text-field for the content, and -- 'OtherParams'. 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 -- | Parse something simple with only a Int-field for the content, and -- 'OtherParams'. 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 -- | Parse something b with alternative representations, language -- specification, and 'OtherParams'. 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 -- | Parse something 'Text' with alternative representations, language -- specification, and 'OtherParams'. 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 -- | Parse something '[Text]' with alternative representations, language -- specification, and 'OtherParams'. parseAltRepLangN :: (Set Text -> Maybe URI.URI -> Maybe Language -> OtherParams -> a) -> Content -> ContentParser a parseAltRepLangN = parseAltRepLang' (return . S.fromList) -- | Parse something simple with only a URI-field for the content, and -- 'OtherParams'. 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 -- | Parse something which has either a 'Date' or a 'DateTime' value, and -- 'OtherParams'. Uses DateTime if there is no value parameter. 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 -- | Parse something which has a set of either a 'Date' or a 'DateTime' value, -- and 'OtherParams'. Uses DateTime if there is no value parameter. 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') -- | Parse something which has only a DateTime value, and 'OtherParams'. 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 -- | Convert a property dictionary to 'OtherParams'. toO :: [(CI Text, [Text])] -> OtherParams toO = OtherParams . S.fromList . map (uncurry OtherParam) -- | Get the remaining properties. 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 ',') -- }}} -- | Set the parser context. down :: Content -> ContentParser a -> ContentParser a down (Component p _ x) = down' (p, x) down x@(ContentLine p _ _ _) = down' (p, [x]) -- | Set the parser context. down' :: (P.SourcePos, [Content]) -> ContentParser a -> ContentParser a down' x m = get >>= \old -> put x >> m <* put old -- | Many optional components named ... optCompN :: Ord a => CI Text -> (Content -> ContentParser a) -> ContentParser (Set a) optCompN s f = optN f . partition (`isComponentNamed` s) =<< snd <$> get -- | One required line named ... reqLine1 :: CI Text -> (Content -> ContentParser a) -> ContentParser a reqLine1 s f = req1 s f . partition (`isLineNamed` s) =<< snd <$> get -- | One optional line named ... optLine1 :: Default b => CI Text -> (Content -> ContentParser b) -> ContentParser b optLine1 s f = opt1 f . partition (`isLineNamed` s) =<< snd <$> get -- | Many optional lines named ... optLineN :: Ord b => CI Text -> (Content -> ContentParser b) -> ContentParser (Set b) optLineN s f = optN f . partition (`isLineNamed` s) =<< snd <$> get -- | Many lines named ..., at least one required. reqLineN :: Ord b => CI Text -> (Content -> ContentParser b) -> ContentParser (Set b) reqLineN s f = reqN s f . partition (`isLineNamed` s) =<< snd <$> get -- | One required ... 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 -- | One optional ... 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 -- | Many optional ... 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 -- | Many ..., at least one required. reqN :: Ord b => CI Text -- ^ What, needed for the error. -> (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 -- | Only allow one parameter value. 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." -- | Line predicate. isLineNamed :: Content -> CI Text -> Bool isLineNamed (ContentLine _ n _ _) n' | n == n' = True isLineNamed _ _ = False -- | Component name predicate. isComponentNamed :: Content -> CI Text -> Bool isComponentNamed (Component _ n _) n' | n == n' = True isComponentNamed _ _ = False isComponent :: Content -> Bool isComponent Component {} = True isComponent _ = False -- Util 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 .: