packages feed

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