orgstat-0.1.8: src/OrgStat/Parser.hs
-- | 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