org-parser-0.1.0.0: src/Org/Parser/Document.hs
-- | Parsers for Org documents.
module Org.Parser.Document
( -- Document and sections
orgDocument
, section
-- * Components
, propertyDrawer
) where
import Data.Text qualified as T
import Org.Parser.Common
import Org.Parser.Definitions
import Org.Parser.Elements
import Org.Parser.MarkupContexts
import Org.Parser.Objects
import Prelude hiding (many, some)
-- | Parse an Org document.
orgDocument :: OrgParser OrgDocument
orgDocument = do
skipMany commentLine
properties <- option mempty propertyDrawer
topLevel <- elements
sections <- many (section 1)
eof
return $
OrgDocument
{ documentProperties = properties
, documentChildren = toList topLevel
, documentSections = sections
}
{- | Parse an Org section and its contents. @lvl@ gives the minimum acceptable
level of the heading.
-}
section :: Int -> OrgParser OrgSection
section lvl = try $ do
level <- headingStart
guard (lvl <= level)
todoKw <- optional todoKeyword
isComment <- option False $ try $ string "COMMENT" *> hspace1 $> True
priority <- optional priorityCookie
(title, tags, titleTxt) <- titleObjects
planning <- option emptyPlanning planningInfo
properties <- option mempty propertyDrawer
contents <- elements
children <- many (section (level + 1))
return
OrgSection
{ sectionLevel = level
, sectionProperties = properties
, sectionTodo = todoKw
, sectionIsComment = isComment
, sectionPriority = priority
, sectionTitle = toList title
, sectionRawTitle = titleTxt
, sectionAnchor = "" -- Dealt with later
, sectionTags = tags
, sectionPlanning = planning
, sectionChildren = toList contents
, sectionSubsections = children
}
where
titleObjects :: OrgParser (OrgObjects, [Tag], Text)
titleObjects =
option mempty $
withContext__
(anySingle *> takeWhileP Nothing (\c -> not (isSpace c || c == ':')))
endOfTitle
(plainMarkupContext standardSet)
endOfTitle :: OrgParser [Tag]
endOfTitle = try $ do
hspace
tags <- option [] (headerTags <* hspace)
newline'
return tags
headerTags :: OrgParser [Tag]
headerTags = try $ do
_ <- char ':'
endBy1 orgTagWord (char ':')
-- | Parse a to-do keyword that is registered in the state.
todoKeyword :: OrgParser TodoKeyword
todoKeyword = try $ do
taskStates <- getsO orgTodoKeywords
choice (map kwParser taskStates)
where
kwParser :: TodoKeyword -> OrgParser TodoKeyword
kwParser tdm =
-- NOTE to self: space placement - "TO" is subset of "TODOKEY"
try (string (todoName tdm) *> hspace1 $> tdm)
-- | Parse a priority cookie like @[#A]@.
priorityCookie :: OrgParser Priority
priorityCookie =
try $
string "[#"
*> priorityFromChar
<* char ']'
where
priorityFromChar :: OrgParser Priority
priorityFromChar =
NumericPriority <$> digitIntChar
<|> LetterPriority <$> upperAscii
orgTagWord :: OrgParser Text
orgTagWord =
takeWhile1P
(Just "tag characters (alphanumeric, @, %, # or _)")
(\c -> isAlphaNum c || c `elem` ['@', '%', '#', '_'])
-- | TODO READ ABOUT PLANNING
emptyPlanning :: PlanningInfo
emptyPlanning = PlanningInfo Nothing Nothing Nothing
-- | Parse a single planning-related and timestamped line.
planningInfo :: OrgParser PlanningInfo
planningInfo = try $ do
updaters <- some planningDatum <* skipSpaces <* newline
return $ foldr ($) emptyPlanning updaters
where
planningDatum =
skipSpaces
*> choice
[ updateWith (\s p -> p {planningScheduled = Just s}) "SCHEDULED"
, updateWith (\d p -> p {planningDeadline = Just d}) "DEADLINE"
, updateWith (\c p -> p {planningClosed = Just c}) "CLOSED"
]
updateWith fn cs = fn <$> (string cs *> char ':' *> skipSpaces *> parseTimestamp)
-- | Parse a :PROPERTIES: drawer and return the key/value pairs contained within.
propertyDrawer :: OrgParser Properties
propertyDrawer = try $ do
_ <- skipSpaces
_ <- string' ":properties:"
_ <- skipSpaces
_ <- newline
fromList <$> manyTill nodeProperty (try endOfDrawer)
where
endOfDrawer :: OrgParser Text
endOfDrawer =
try $
hspace *> string' ":end:" <* blankline'
nodeProperty :: OrgParser (Text, Text)
nodeProperty = try $ liftA2 (,) name value
name :: OrgParser Text
name =
skipSpaces
*> char ':'
*> takeWhile1P (Just "node property name") (not . isSpace)
<&> T.stripSuffix ":"
>>= guardMaybe "expecting ':' at end of node property name"
<&> T.toLower
value :: OrgParser Text
value =
skipSpaces
*> ( takeWhileP (Just "node property value") (/= '\n')
<&> T.stripEnd
)
<* newline