{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Text.Pandoc.Readers.Org.DocumentTree
( documentTree
, unprunedHeadlineToBlocks
) where
import Control.Arrow ((***), first)
import Control.Monad (guard)
import Data.List (intersperse)
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import Text.Pandoc.Builder (Blocks, Inlines)
import Text.Pandoc.Class.PandocMonad (PandocMonad)
import Text.Pandoc.Definition
import Text.Pandoc.Readers.Org.BlockStarts
import Text.Pandoc.Readers.Org.ParserState
import Text.Pandoc.Readers.Org.Parsing
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Text.Pandoc.Builder as B
documentTree :: PandocMonad m
=> OrgParser m (F Blocks)
-> OrgParser m (F Inlines)
-> OrgParser m (F Headline)
documentTree blocks inline = do
initialBlocks <- blocks
headlines <- sequence <$> manyTill (headline blocks inline 1) eof
title <- fmap docTitle . orgStateMeta <$> getState
return $ do
headlines' <- headlines
initialBlocks' <- initialBlocks
title' <- title
return Headline
{ headlineLevel = 0
, headlineTodoMarker = Nothing
, headlineText = B.fromList title'
, headlineTags = mempty
, headlinePlanning = emptyPlanning
, headlineProperties = mempty
, headlineContents = initialBlocks'
, headlineChildren = headlines'
}
toTag :: Text -> Tag
toTag = Tag
newtype PropertyKey = PropertyKey { fromKey :: Text }
deriving (Show, Eq, Ord)
toPropertyKey :: Text -> PropertyKey
toPropertyKey = PropertyKey . T.toLower
newtype PropertyValue = PropertyValue { fromValue :: Text }
toPropertyValue :: Text -> PropertyValue
toPropertyValue = PropertyValue
isNonNil :: PropertyValue -> Bool
isNonNil p = T.toLower (fromValue p) `notElem` ["()", "{}", "nil"]
type Properties = [(PropertyKey, PropertyValue)]
data Headline = Headline
{ headlineLevel :: Int
, headlineTodoMarker :: Maybe TodoMarker
, headlineText :: Inlines
, headlineTags :: [Tag]
, headlinePlanning :: PlanningInfo
, headlineProperties :: Properties
, headlineContents :: Blocks
, headlineChildren :: [Headline]
}
headline :: PandocMonad m
=> OrgParser m (F Blocks)
-> OrgParser m (F Inlines)
-> Int
-> OrgParser m (F Headline)
headline blocks inline lvl = try $ do
level <- headerStart
guard (lvl <= level)
todoKw <- optionMaybe todoKeyword
(title, tags) <- manyThen inline endOfTitle
planning <- option emptyPlanning planningInfo
properties <- option mempty propertiesDrawer
contents <- blocks
children <- many (headline blocks inline (level + 1))
return $ do
title' <- trimInlinesF (mconcat title)
contents' <- contents
children' <- sequence children
return Headline
{ headlineLevel = level
, headlineTodoMarker = todoKw
, headlineText = title'
, headlineTags = tags
, headlinePlanning = planning
, headlineProperties = properties
, headlineContents = contents'
, headlineChildren = children'
}
where
endOfTitle :: Monad m => OrgParser m [Tag]
endOfTitle = try $ do
skipSpaces
tags <- option [] (headerTags <* skipSpaces)
newline
return tags
headerTags :: Monad m => OrgParser m [Tag]
headerTags = try $ do
char ':'
endBy1 (toTag <$> orgTagWord) (char ':')
manyThen :: Monad m
=> OrgParser m a
-> OrgParser m b
-> OrgParser m ([a], b)
manyThen p end = (([],) <$> try end) <|> do
x <- p
first (x:) <$> manyThen p end
unprunedHeadlineToBlocks :: Monad m => Headline -> OrgParserState -> OrgParser m [Block]
unprunedHeadlineToBlocks hdln st =
let usingSelectedTags = docContainsSelectTags hdln st
rootNode = if not usingSelectedTags
then hdln
else includeRootAndSelected hdln st
rootNode' = removeExplicitlyExcludedNodes rootNode st
in if not usingSelectedTags ||
any (`Set.member` orgStateSelectTags st) (headlineTags rootNode')
then do headlineBlocks <- headlineToBlocks rootNode'
return . drop 1 . B.toList $ headlineBlocks
else do headlineBlocks <- mconcat <$> mapM headlineToBlocks
(headlineChildren rootNode')
return . B.toList $ headlineBlocks
headlineToBlocks :: Monad m => Headline -> OrgParser m Blocks
headlineToBlocks hdln = do
maxLevel <- getExportSetting exportHeadlineLevels
let tags = headlineTags hdln
let text = headlineText hdln
let level = headlineLevel hdln
case () of
_ | any isArchiveTag tags -> archivedHeadlineToBlocks hdln
_ | isCommentTitle text -> return mempty
_ | maxLevel <= level -> headlineToHeaderWithList hdln
_ | otherwise -> headlineToHeaderWithContents hdln
removeExplicitlyExcludedNodes :: Headline -> OrgParserState -> Headline
removeExplicitlyExcludedNodes hdln st =
hdln { headlineChildren =
[removeExplicitlyExcludedNodes childHdln st |
childHdln <- headlineChildren hdln,
not $ headlineContainsExcludeTags childHdln st] }
includeRootAndSelected :: Headline -> OrgParserState -> Headline
includeRootAndSelected hdln st =
hdln { headlineChildren = mapMaybe (`includeAncestorsAndSelected` st)
(headlineChildren hdln)}
docContainsSelectTags :: Headline -> OrgParserState -> Bool
docContainsSelectTags hdln st =
headlineContainsSelectTags hdln st ||
any (`docContainsSelectTags` st) (headlineChildren hdln)
includeAncestorsAndSelected :: Headline -> OrgParserState -> Maybe Headline
includeAncestorsAndSelected hdln st =
if headlineContainsSelectTags hdln st
then Just hdln
else let children = mapMaybe (`includeAncestorsAndSelected` st)
(headlineChildren hdln)
in case children of
[] -> Nothing
_ -> Just $ hdln { headlineChildren = children }
headlineContainsSelectTags :: Headline -> OrgParserState -> Bool
headlineContainsSelectTags hdln st =
any (`Set.member` orgStateSelectTags st) (headlineTags hdln)
headlineContainsExcludeTags :: Headline -> OrgParserState -> Bool
headlineContainsExcludeTags hdln st =
any (`Set.member` orgStateExcludeTags st) (headlineTags hdln)
isArchiveTag :: Tag -> Bool
isArchiveTag = (== toTag "ARCHIVE")
isCommentTitle :: Inlines -> Bool
isCommentTitle inlns = case B.toList inlns of
(Str "COMMENT":_) -> True
_ -> False
archivedHeadlineToBlocks :: Monad m => Headline -> OrgParser m Blocks
archivedHeadlineToBlocks hdln = do
archivedTreesOption <- getExportSetting exportArchivedTrees
case archivedTreesOption of
ArchivedTreesNoExport -> return mempty
ArchivedTreesExport -> headlineToHeaderWithContents hdln
ArchivedTreesHeadlineOnly -> headlineToHeader hdln
headlineToHeaderWithList :: Monad m => Headline -> OrgParser m Blocks
headlineToHeaderWithList hdln = do
maxHeadlineLevels <- getExportSetting exportHeadlineLevels
header <- headlineToHeader hdln
listElements <- mapM headlineToBlocks (headlineChildren hdln)
planningBlock <- planningToBlock (headlinePlanning hdln)
let listBlock = if null listElements
then mempty
else B.orderedList listElements
let headerText = if maxHeadlineLevels == headlineLevel hdln
then header
else flattenHeader header
return . mconcat $
[ headerText
, planningBlock
, headlineContents hdln
, listBlock
]
where
flattenHeader :: Blocks -> Blocks
flattenHeader blks =
case B.toList blks of
(Header _ _ inlns:_) -> B.para (B.fromList inlns)
_ -> mempty
headlineToHeaderWithContents :: Monad m => Headline -> OrgParser m Blocks
headlineToHeaderWithContents hdln = do
header <- headlineToHeader hdln
planningBlock <- planningToBlock (headlinePlanning hdln)
childrenBlocks <- mconcat <$> mapM headlineToBlocks (headlineChildren hdln)
return $ header <> planningBlock <> headlineContents hdln <> childrenBlocks
headlineToHeader :: Monad m => Headline -> OrgParser m Blocks
headlineToHeader hdln = do
exportTodoKeyword <- getExportSetting exportWithTodoKeywords
exportTags <- getExportSetting exportWithTags
let todoText = if exportTodoKeyword
then case headlineTodoMarker hdln of
Just kw -> todoKeywordToInlines kw <> B.space
Nothing -> mempty
else mempty
let text = todoText <> headlineText hdln <>
if exportTags
then tagsToInlines (headlineTags hdln)
else mempty
let propAttr = propertiesToAttr (headlineProperties hdln)
attr <- registerHeader propAttr (headlineText hdln)
return $ B.headerWith attr (headlineLevel hdln) text
todoKeyword :: Monad m => OrgParser m TodoMarker
todoKeyword = try $ do
taskStates <- activeTodoMarkers <$> getState
let kwParser tdm = try (tdm <$ textStr (todoMarkerName tdm)
<* spaceChar
<* updateLastPreCharPos)
choice (map kwParser taskStates)
todoKeywordToInlines :: TodoMarker -> Inlines
todoKeywordToInlines tdm =
let todoText = todoMarkerName tdm
todoState = T.toLower . T.pack . show $ todoMarkerState tdm
classes = [todoState, todoText]
in B.spanWith (mempty, classes, mempty) (B.str todoText)
propertiesToAttr :: Properties -> Attr
propertiesToAttr properties =
let
toTextPair = fromKey *** fromValue
customIdKey = toPropertyKey "custom_id"
classKey = toPropertyKey "class"
unnumberedKey = toPropertyKey "unnumbered"
specialProperties = [customIdKey, classKey, unnumberedKey]
id' = maybe mempty fromValue . lookup customIdKey $ properties
cls = maybe mempty fromValue . lookup classKey $ properties
kvs' = map toTextPair . filter ((`notElem` specialProperties) . fst)
$ properties
isUnnumbered =
maybe False isNonNil . lookup unnumberedKey $ properties
in
(id', T.words cls ++ ["unnumbered" | isUnnumbered], kvs')
tagsToInlines :: [Tag] -> Inlines
tagsToInlines [] = mempty
tagsToInlines tags =
(B.space <>) . mconcat . intersperse (B.str "\160") . map tagToInline $ tags
where
tagToInline :: Tag -> Inlines
tagToInline t = tagSpan t . B.smallcaps . B.str $ fromTag t
tagSpan :: Tag -> Inlines -> Inlines
tagSpan t = B.spanWith ("", ["tag"], [("tag-name", fromTag t)])
planningToBlock :: Monad m => PlanningInfo -> OrgParser m Blocks
planningToBlock planning = do
includePlanning <- getExportSetting exportWithPlanning
return $
if includePlanning
then B.plain . mconcat . intersperse B.space . filter (/= mempty) $
[ datumInlines planningClosed "CLOSED"
, datumInlines planningDeadline "DEADLINE"
, datumInlines planningScheduled "SCHEDULED"
]
else mempty
where
datumInlines field name =
case field planning of
Nothing -> mempty
Just time -> B.strong (B.str name <> B.str ":")
<> B.space
<> B.emph (B.str time)
type Timestamp = Text
timestamp :: Monad m => OrgParser m Timestamp
timestamp = try $ do
openChar <- oneOf "<["
let isActive = openChar == '<'
let closeChar = if isActive then '>' else ']'
content <- many1TillChar anyChar (char closeChar)
return $ T.cons openChar $ content `T.snoc` closeChar
data PlanningInfo = PlanningInfo
{ planningClosed :: Maybe Timestamp
, planningDeadline :: Maybe Timestamp
, planningScheduled :: Maybe Timestamp
}
emptyPlanning :: PlanningInfo
emptyPlanning = PlanningInfo Nothing Nothing Nothing
planningInfo :: Monad m => OrgParser m PlanningInfo
planningInfo = try $ do
updaters <- many1 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 *> timestamp)
propertiesDrawer :: Monad m => OrgParser m Properties
propertiesDrawer = try $ do
drawerType <- drawerStart
guard $ T.toUpper drawerType == "PROPERTIES"
manyTill property (try endOfDrawer)
where
property :: Monad m => OrgParser m (PropertyKey, PropertyValue)
property = try $ (,) <$> key <*> value
key :: Monad m => OrgParser m PropertyKey
key = fmap toPropertyKey . try $
skipSpaces *> char ':' *> many1TillChar nonspaceChar (char ':')
value :: Monad m => OrgParser m PropertyValue
value = fmap toPropertyValue . try $
skipSpaces *> manyTillChar anyChar (try $ skipSpaces *> newline)
endOfDrawer :: Monad m => OrgParser m Text
endOfDrawer = try $
skipSpaces *> stringAnyCase ":END:" <* skipSpaces <* newline