-- | Org-mode format parsing. module OrgStat.Parser ( ParsingException (..) , parseOrg , runParser ) where import Universum import qualified Data.Attoparsec.Text as A import Data.Char (isSpace) import qualified Data.OrgMode.Parse as O import qualified Data.OrgMode.Types as O import qualified Data.Text as T import Data.Time (LocalTime(..), TimeOfDay(..), fromGregorian, getZonedTime, zonedTimeToLocalTime) import Data.Time.Calendar () import OrgStat.Ast (Clock(..), Org(..), orgTags, traverseTree) ---------------------------------------------------------------------------- -- Exceptions ---------------------------------------------------------------------------- data ParsingException = ParsingException Text deriving (Show, Typeable) instance Exception ParsingException ---------------------------------------------------------------------------- -- Parsing ---------------------------------------------------------------------------- parseOrg :: LocalTime -> [Text] -> A.Parser Org parseOrg curTime todoKeywords = convertDocument <$> O.parseDocument todoKeywords where convertDocument :: O.Document -> Org convertDocument (O.Document textBefore headings) = let fileLvlTags = extractFileTags textBefore addTags t = ordNub $ fileLvlTags <> t o = Org { _orgTitle = "" , _orgTags = [] , _orgClocks = [] , _orgSubtrees = map convertHeading headings } in o & traverseTree . orgTags %~ addTags convertHeading :: O.Headline -> Org convertHeading headline = Org { _orgTitle = O.title headline , _orgTags = O.tags headline , _orgClocks = getClocks $ O.section headline , _orgSubtrees = map convertHeading $ O.subHeadlines headline } mapEither :: (a -> Either e b) -> ([a] -> [b]) mapEither f xs = rights $ map f xs getClocks :: O.Section -> [Clock] getClocks section = mapMaybe convertClock $ concat [ O.sectionClocks section , O.unLogbook (O.sectionLogbook section) , mapEither (A.parseOnly O.parseClock) $ concat [ concatMap lines $ map O.contents $ O.sectionDrawers section , lines $ O.sectionParagraph section ] ] -- convert clocks from orgmode-parse format, returns Nothing for clocks -- without end time or time-of-day convertClock :: O.Clock -> Maybe Clock convertClock (O.Clock (Just (O.Timestamp start _active (Just end)), _duration)) = Clock <$> convertDateTime start <*> convertDateTime end convertClock (O.Clock (Just (O.Timestamp start _active Nothing), _duration)) = Clock <$> convertDateTime start <*> pure curTime convertClock _ = Nothing -- Nothing for DateTime without time-of-day convertDateTime :: O.DateTime -> Maybe LocalTime convertDateTime O.DateTime { yearMonthDay = O.YearMonthDay year month day , hourMinute = Just (hour, minute) } = Just $ LocalTime (fromGregorian (toInteger year) month day) (TimeOfDay hour minute 0) convertDateTime _ = Nothing extractFileTags (T.lines -> inputLines) = let prfx = "#+FILETAGS: " matching = map (T.drop (length prfx)) $ (filter (prfx `T.isPrefixOf`) inputLines) toTags (T.strip -> line) = let parts = filter (not . T.null) $ T.splitOn ":" line -- Correct tag shouldn't contain spaces inside correctPart p = not $ T.any isSpace p in if all correctPart parts then parts else [] in ordNub $ concatMap toTags matching -- Throw parsing exception if it can't be parsed (use Control.Monad.Catch#throwM) runParser :: (MonadIO m, MonadThrow m) => [Text] -> Text -> m Org runParser todoKeywords t = do localTime <- liftIO $ zonedTimeToLocalTime <$> getZonedTime case A.parseOnly (parseOrg localTime todoKeywords) t of Left err -> throwM $ ParsingException $ T.pack err Right res -> pure res