-- | Org-mode format parsing. module OrgStat.Parser ( ParsingException (..) , parseOrg , runParser ) where import Control.Exception (Exception) import qualified Data.Attoparsec.Text as A import qualified Data.OrgMode.Parse as OP import qualified Data.Text as T import Data.Time (LocalTime (..), TimeOfDay (..), fromGregorian) import Data.Time.Calendar () import Universum import OrgStat.Ast (Clock (..), Org (..)) ---------------------------------------------------------------------------- -- Exceptions ---------------------------------------------------------------------------- data ParsingException = ParsingException Text deriving (Show, Typeable) instance Exception ParsingException ---------------------------------------------------------------------------- -- Parsing ---------------------------------------------------------------------------- parseOrg :: [Text] -> A.Parser Org parseOrg todoKeywords = convertDocument <$> OP.parseDocument todoKeywords where convertDocument :: OP.Document -> Org convertDocument (OP.Document _ headings) = Org { _orgTitle = "" , _orgTags = [] , _orgClocks = [] , _orgSubtrees = map convertHeading headings } convertHeading :: OP.Heading -> Org convertHeading heading = Org { _orgTitle = OP.title heading , _orgTags = OP.tags heading , _orgClocks = getClocks $ OP.section heading , _orgSubtrees = map convertHeading $ OP.subHeadings heading } mapEither :: (a -> Either e b) -> ([a] -> [b]) mapEither f xs = rights $ map f xs getClocks :: OP.Section -> [Clock] getClocks section = mapMaybe convertClock $ concat [ OP.sectionClocks section , mapEither (A.parseOnly OP.parseClock) $ lines $ OP.sectionParagraph section ] -- convert clocks from orgmode-parse format, returns Nothing for clocks -- without end time or time-of-day convertClock :: (Maybe OP.Timestamp, Maybe OP.Duration) -> Maybe Clock convertClock (Just (OP.Timestamp start _active (Just end)), _duration) = Clock <$> convertDateTime start <*> convertDateTime end convertClock _ = Nothing -- Nothing for DateTime without time-of-day convertDateTime :: OP.DateTime -> Maybe LocalTime convertDateTime OP.DateTime { yearMonthDay = OP.YMD' (OP.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 :: (MonadThrow m) => [Text] -> Text -> m Org runParser todoKeywords t = case A.parseOnly (parseOrg todoKeywords) t of Left err -> throwM $ ParsingException $ T.pack err Right res -> pure res