-- | Org-mode format parsing. module OrgStat.Parser ( ParsingException (..) , parseOrg , runParser ) where import Universum import Control.Exception (Exception) import qualified Data.Attoparsec.Text as A 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 (..)) ---------------------------------------------------------------------------- -- 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 _ headings) = Org { _orgTitle = "" , _orgTags = [] , _orgClocks = [] , _orgSubtrees = map convertHeading headings } 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 -- 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