git.iko.soy/typeable/pandoc
on master help sign in
Org reader: put tree parsing code into dedicated module
commita27e2e8a4e6b4f8a28fe540511f48afccc503ef6 authorAlbert Krewinkel <albert@zeitkraut.de> dateTue 16 May 2017, 22:42 (9 years ago) parentaf4bf91
3 files changed+263−210
pandoc.cabal+1 src/Text/Pandoc/Readers/Org/Blocks.hs+2−210 src/Text/Pandoc/Readers/Org/DocumentTree.hs+260
pandoc.cabal+1
@@ -445,6 +445,7 @@ Library
445445 Text.Pandoc.Readers.Odt.Arrows.Utils,
446446 Text.Pandoc.Readers.Org.BlockStarts,
447447 Text.Pandoc.Readers.Org.Blocks,
448+ Text.Pandoc.Readers.Org.DocumentTree,
448449 Text.Pandoc.Readers.Org.ExportSettings,
449450 Text.Pandoc.Readers.Org.Inlines,
450451 Text.Pandoc.Readers.Org.Meta,
src/Text/Pandoc/Readers/Org/Blocks.hs+2−210
@@ -33,6 +33,7 @@ module Text.Pandoc.Readers.Org.Blocks
3333 ) where
3434
3535import Text.Pandoc.Readers.Org.BlockStarts
36+import Text.Pandoc.Readers.Org.DocumentTree (headline, headlineToBlocks)
3637import Text.Pandoc.Readers.Org.Inlines
3738import Text.Pandoc.Readers.Org.Meta (metaExport, metaKey, metaLine)
3839import Text.Pandoc.Readers.Org.ParserState
@@ -54,196 +55,6 @@ import Data.List (foldl', isPrefixOf)
5455import Data.Maybe (fromMaybe, isNothing)
5556import Data.Monoid ((<>))
5657
57−--
58−-- Org headers
59−--
60−newtype Tag = Tag { fromTag :: String }
61− deriving (Show, Eq)
62−
63−-- | Create a tag containing the given string.
64−toTag :: String -> Tag
65−toTag = Tag
66−
67−-- | The key (also called name or type) of a property.
68−newtype PropertyKey = PropertyKey { fromKey :: String }
69− deriving (Show, Eq, Ord)
70−
71−-- | Create a property key containing the given string. Org mode keys are
72−-- case insensitive and are hence converted to lower case.
73−toPropertyKey :: String -> PropertyKey
74−toPropertyKey = PropertyKey . map toLower
75−
76−-- | The value assigned to a property.
77−newtype PropertyValue = PropertyValue { fromValue :: String }
78−
79−-- | Create a property value containing the given string.
80−toPropertyValue :: String -> PropertyValue
81−toPropertyValue = PropertyValue
82−
83−-- | Check whether the property value is non-nil (i.e. truish).
84−isNonNil :: PropertyValue -> Bool
85−isNonNil p = map toLower (fromValue p) `notElem` ["()", "{}", "nil"]
86−
87−-- | Key/value pairs from a PROPERTIES drawer
88−type Properties = [(PropertyKey, PropertyValue)]
89−
90−-- | Org mode headline (i.e. a document subtree).
91−data Headline = Headline
92− { headlineLevel :: Int
93− , headlineTodoMarker :: Maybe TodoMarker
94− , headlineText :: Inlines
95− , headlineTags :: [Tag]
96− , headlineProperties :: Properties
97− , headlineContents :: Blocks
98− , headlineChildren :: [Headline]
99− }
100−
101−--
102−-- Parsing headlines and subtrees
103−--
104−
105−-- | Read an Org mode headline and its contents (i.e. a document subtree).
106−-- @lvl@ gives the minimum acceptable level of the tree.
107−headline :: PandocMonad m => Int -> OrgParser m (F Headline)
108−headline lvl = try $ do
109− level <- headerStart
110− guard (lvl <= level)
111− todoKw <- optionMaybe todoKeyword
112− title <- trimInlinesF . mconcat <$> manyTill inline endOfTitle
113− tags <- option [] headerTags
114− newline
115− properties <- option mempty propertiesDrawer
116− contents <- blocks
117− children <- many (headline (level + 1))
118− return $ do
119− title' <- title
120− contents' <- contents
121− children' <- sequence children
122− return $ Headline
123− { headlineLevel = level
124− , headlineTodoMarker = todoKw
125− , headlineText = title'
126− , headlineTags = tags
127− , headlineProperties = properties
128− , headlineContents = contents'
129− , headlineChildren = children'
130− }
131− where
132− endOfTitle :: Monad m => OrgParser m ()
133− endOfTitle = void . lookAhead $ optional headerTags *> newline
134−
135− headerTags :: Monad m => OrgParser m [Tag]
136− headerTags = try $
137− let tag = many1 (alphaNum <|> oneOf "@%#_") <* char ':'
138− in map toTag <$> (skipSpaces *> char ':' *> many1 tag <* skipSpaces)
139−
140−-- | Convert an Org mode headline (i.e. a document tree) into pandoc's Blocks
141−headlineToBlocks :: Monad m => Headline -> OrgParser m Blocks
142−headlineToBlocks hdln@(Headline {..}) = do
143− maxHeadlineLevels <- getExportSetting exportHeadlineLevels
144− case () of
145− _ | any isNoExportTag headlineTags -> return mempty
146− _ | any isArchiveTag headlineTags -> archivedHeadlineToBlocks hdln
147− _ | isCommentTitle headlineText -> return mempty
148− _ | headlineLevel >= maxHeadlineLevels -> headlineToHeaderWithList hdln
149− _ | otherwise -> headlineToHeaderWithContents hdln
150−
151−isNoExportTag :: Tag -> Bool
152−isNoExportTag = (== toTag "noexport")
153−
154−isArchiveTag :: Tag -> Bool
155−isArchiveTag = (== toTag "ARCHIVE")
156−
157−-- | Check if the title starts with COMMENT.
158−-- FIXME: This accesses builder internals not intended for use in situations
159−-- like these. Replace once keyword parsing is supported.
160−isCommentTitle :: Inlines -> Bool
161−isCommentTitle (B.toList -> (Str "COMMENT":_)) = True
162−isCommentTitle _ = False
163−
164−archivedHeadlineToBlocks :: Monad m => Headline -> OrgParser m Blocks
165−archivedHeadlineToBlocks hdln = do
166− archivedTreesOption <- getExportSetting exportArchivedTrees
167− case archivedTreesOption of
168− ArchivedTreesNoExport -> return mempty
169− ArchivedTreesExport -> headlineToHeaderWithContents hdln
170− ArchivedTreesHeadlineOnly -> headlineToHeader hdln
171−
172−headlineToHeaderWithList :: Monad m => Headline -> OrgParser m Blocks
173−headlineToHeaderWithList hdln@(Headline {..}) = do
174− maxHeadlineLevels <- getExportSetting exportHeadlineLevels
175− header <- headlineToHeader hdln
176− listElements <- sequence (map headlineToBlocks headlineChildren)
177− let listBlock = if null listElements
178− then mempty
179− else B.orderedList listElements
180− let headerText = if maxHeadlineLevels == headlineLevel
181− then header
182− else flattenHeader header
183− return $ headerText <> headlineContents <> listBlock
184− where
185− flattenHeader :: Blocks -> Blocks
186− flattenHeader blks =
187− case B.toList blks of
188− (Header _ _ inlns:_) -> B.para (B.fromList inlns)
189− _ -> mempty
190−
191−headlineToHeaderWithContents :: Monad m => Headline -> OrgParser m Blocks
192−headlineToHeaderWithContents hdln@(Headline {..}) = do
193− header <- headlineToHeader hdln
194− childrenBlocks <- mconcat <$> sequence (map headlineToBlocks headlineChildren)
195− return $ header <> headlineContents <> childrenBlocks
196−
197−headlineToHeader :: Monad m => Headline -> OrgParser m Blocks
198−headlineToHeader (Headline {..}) = do
199− exportTodoKeyword <- getExportSetting exportWithTodoKeywords
200− let todoText = if exportTodoKeyword
201− then case headlineTodoMarker of
202− Just kw -> todoKeywordToInlines kw <> B.space
203− Nothing -> mempty
204− else mempty
205− let text = tagTitle (todoText <> headlineText) headlineTags
206− let propAttr = propertiesToAttr headlineProperties
207− attr <- registerHeader propAttr headlineText
208− return $ B.headerWith attr headlineLevel text
209−
210−todoKeyword :: Monad m => OrgParser m TodoMarker
211−todoKeyword = try $ do
212− taskStates <- activeTodoMarkers <$> getState
213− let kwParser tdm = try $ (tdm <$ string (todoMarkerName tdm) <* spaceChar)
214− choice (map kwParser taskStates)
215−
216−todoKeywordToInlines :: TodoMarker -> Inlines
217−todoKeywordToInlines tdm =
218− let todoText = todoMarkerName tdm
219− todoState = map toLower . show $ todoMarkerState tdm
220− classes = [todoState, todoText]
221− in B.spanWith (mempty, classes, mempty) (B.str todoText)
222−
223−propertiesToAttr :: Properties -> Attr
224−propertiesToAttr properties =
225− let
226− toStringPair prop = (fromKey (fst prop), fromValue (snd prop))
227− customIdKey = toPropertyKey "custom_id"
228− classKey = toPropertyKey "class"
229− unnumberedKey = toPropertyKey "unnumbered"
230− specialProperties = [customIdKey, classKey, unnumberedKey]
231− id' = fromMaybe mempty . fmap fromValue . lookup customIdKey $ properties
232− cls = fromMaybe mempty . fmap fromValue . lookup classKey $ properties
233− kvs' = map toStringPair . filter ((`notElem` specialProperties) . fst)
234− $ properties
235− isUnnumbered =
236− fromMaybe False . fmap isNonNil . lookup unnumberedKey $ properties
237− in
238− (id', words cls ++ (if isUnnumbered then ["unnumbered"] else []), kvs')
239−
240−tagTitle :: Inlines -> [Tag] -> Inlines
241−tagTitle title tags = title <> (mconcat $ map tagToInline tags)
242−
243−tagToInline :: Tag -> Inlines
244−tagToInline t = B.spanWith ("", ["tag"], [("data-tag-name", fromTag t)]) mempty
245−
246−
24758--
24859-- parsing blocks
24960--
@@ -252,7 +63,7 @@ tagToInline t = B.spanWith ("", ["tag"], [("data-tag-name", fromTag t)]) mempty
25263blockList :: PandocMonad m => OrgParser m [Block]
25364blockList = do
25465 initialBlocks <- blocks
255− headlines <- sequence <$> manyTill (headline 1) eof
66+ headlines <- sequence <$> manyTill (headline blocks inline 1) eof
25667 st <- getState
25768 headlineBlocks <- fmap mconcat . sequence . map headlineToBlocks $ runF headlines st
25869 return . B.toList $ (runF initialBlocks st) <> headlineBlocks
@@ -631,25 +442,6 @@ drawerEnd :: Monad m => OrgParser m String
631442drawerEnd = try $
632443 skipSpaces *> stringAnyCase ":END:" <* skipSpaces <* newline
633444
634−-- | Read a :PROPERTIES: drawer and return the key/value pairs contained
635−-- within.
636−propertiesDrawer :: Monad m => OrgParser m Properties
637−propertiesDrawer = try $ do
638− drawerType <- drawerStart
639− guard $ map toUpper drawerType == "PROPERTIES"
640− manyTill property (try drawerEnd)
641− where
642− property :: Monad m => OrgParser m (PropertyKey, PropertyValue)
643− property = try $ (,) <$> key <*> value
644−
645− key :: Monad m => OrgParser m PropertyKey
646− key = fmap toPropertyKey . try $
647− skipSpaces *> char ':' *> many1Till nonspaceChar (char ':')
648−
649− value :: Monad m => OrgParser m PropertyValue
650− value = fmap toPropertyValue . try $
651− skipSpaces *> manyTill anyChar (try $ skipSpaces *> newline)
652−
653445
654446--
655447-- Figures
src/Text/Pandoc/Readers/Org/DocumentTree.hsnew file+260
@@ -0,0 +1,260 @@
1+{-
2+Copyright (C) 2014-2017 Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
3+
4+This program is free software; you can redistribute it and/or modify
5+it under the terms of the GNU General Public License as published by
6+the Free Software Foundation; either version 2 of the License, or
7+(at your option) any later version.
8+
9+This program is distributed in the hope that it will be useful,
10+but WITHOUT ANY WARRANTY; without even the implied warranty of
11+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+GNU General Public License for more details.
13+
14+You should have received a copy of the GNU General Public License
15+along with this program; if not, write to the Free Software
16+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17+-}
18+{-# LANGUAGE FlexibleContexts #-}
19+{-# LANGUAGE RecordWildCards #-}
20+{-# LANGUAGE ViewPatterns #-}
21+{- |
22+ Module : Text.Pandoc.Readers.Org.DocumentTree
23+ Copyright : Copyright (C) 2014-2017 Albert Krewinkel
24+ License : GNU GPL, version 2 or above
25+
26+ Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
27+
28+Parsers for org-mode headlines and document subtrees
29+-}
30+module Text.Pandoc.Readers.Org.DocumentTree
31+ ( headline
32+ , headlineToBlocks
33+ ) where
34+
35+import Control.Monad (guard, void)
36+import Data.Char (toLower, toUpper)
37+import Data.Maybe (fromMaybe)
38+import Data.Monoid ((<>))
39+import Text.Pandoc.Builder (Blocks, Inlines)
40+import Text.Pandoc.Class (PandocMonad)
41+import Text.Pandoc.Definition
42+import Text.Pandoc.Readers.Org.BlockStarts
43+import Text.Pandoc.Readers.Org.Parsing
44+import Text.Pandoc.Readers.Org.ParserState
45+
46+import qualified Text.Pandoc.Builder as B
47+
48+--
49+-- Org headers
50+--
51+newtype Tag = Tag { fromTag :: String }
52+ deriving (Show, Eq)
53+
54+-- | Create a tag containing the given string.
55+toTag :: String -> Tag
56+toTag = Tag
57+
58+-- | The key (also called name or type) of a property.
59+newtype PropertyKey = PropertyKey { fromKey :: String }
60+ deriving (Show, Eq, Ord)
61+
62+-- | Create a property key containing the given string. Org mode keys are
63+-- case insensitive and are hence converted to lower case.
64+toPropertyKey :: String -> PropertyKey
65+toPropertyKey = PropertyKey . map toLower
66+
67+-- | The value assigned to a property.
68+newtype PropertyValue = PropertyValue { fromValue :: String }
69+
70+-- | Create a property value containing the given string.
71+toPropertyValue :: String -> PropertyValue
72+toPropertyValue = PropertyValue
73+
74+-- | Check whether the property value is non-nil (i.e. truish).
75+isNonNil :: PropertyValue -> Bool
76+isNonNil p = map toLower (fromValue p) `notElem` ["()", "{}", "nil"]
77+
78+-- | Key/value pairs from a PROPERTIES drawer
79+type Properties = [(PropertyKey, PropertyValue)]
80+
81+-- | Org mode headline (i.e. a document subtree).
82+data Headline = Headline
83+ { headlineLevel :: Int
84+ , headlineTodoMarker :: Maybe TodoMarker
85+ , headlineText :: Inlines
86+ , headlineTags :: [Tag]
87+ , headlineProperties :: Properties
88+ , headlineContents :: Blocks
89+ , headlineChildren :: [Headline]
90+ }
91+
92+-- | Read an Org mode headline and its contents (i.e. a document subtree).
93+-- @lvl@ gives the minimum acceptable level of the tree.
94+headline :: PandocMonad m
95+ => OrgParser m (F Blocks)
96+ -> OrgParser m (F Inlines)
97+ -> Int
98+ -> OrgParser m (F Headline)
99+headline blocks inline lvl = try $ do
100+ level <- headerStart
101+ guard (lvl <= level)
102+ todoKw <- optionMaybe todoKeyword
103+ title <- trimInlinesF . mconcat <$> manyTill inline endOfTitle
104+ tags <- option [] headerTags
105+ newline
106+ properties <- option mempty propertiesDrawer
107+ contents <- blocks
108+ children <- many (headline blocks inline (level + 1))
109+ return $ do
110+ title' <- title
111+ contents' <- contents
112+ children' <- sequence children
113+ return $ Headline
114+ { headlineLevel = level
115+ , headlineTodoMarker = todoKw
116+ , headlineText = title'
117+ , headlineTags = tags
118+ , headlineProperties = properties
119+ , headlineContents = contents'
120+ , headlineChildren = children'
121+ }
122+ where
123+ endOfTitle :: Monad m => OrgParser m ()
124+ endOfTitle = void . lookAhead $ optional headerTags *> newline
125+
126+ headerTags :: Monad m => OrgParser m [Tag]
127+ headerTags = try $
128+ let tag = many1 (alphaNum <|> oneOf "@%#_") <* char ':'
129+ in map toTag <$> (skipSpaces *> char ':' *> many1 tag <* skipSpaces)
130+
131+-- | Convert an Org mode headline (i.e. a document tree) into pandoc's Blocks
132+headlineToBlocks :: Monad m => Headline -> OrgParser m Blocks
133+headlineToBlocks hdln@(Headline {..}) = do
134+ maxHeadlineLevels <- getExportSetting exportHeadlineLevels
135+ case () of
136+ _ | any isNoExportTag headlineTags -> return mempty
137+ _ | any isArchiveTag headlineTags -> archivedHeadlineToBlocks hdln
138+ _ | isCommentTitle headlineText -> return mempty
139+ _ | headlineLevel >= maxHeadlineLevels -> headlineToHeaderWithList hdln
140+ _ | otherwise -> headlineToHeaderWithContents hdln
141+
142+isNoExportTag :: Tag -> Bool
143+isNoExportTag = (== toTag "noexport")
144+
145+isArchiveTag :: Tag -> Bool
146+isArchiveTag = (== toTag "ARCHIVE")
147+
148+-- | Check if the title starts with COMMENT.
149+-- FIXME: This accesses builder internals not intended for use in situations
150+-- like these. Replace once keyword parsing is supported.
151+isCommentTitle :: Inlines -> Bool
152+isCommentTitle (B.toList -> (Str "COMMENT":_)) = True
153+isCommentTitle _ = False
154+
155+archivedHeadlineToBlocks :: Monad m => Headline -> OrgParser m Blocks
156+archivedHeadlineToBlocks hdln = do
157+ archivedTreesOption <- getExportSetting exportArchivedTrees
158+ case archivedTreesOption of
159+ ArchivedTreesNoExport -> return mempty
160+ ArchivedTreesExport -> headlineToHeaderWithContents hdln
161+ ArchivedTreesHeadlineOnly -> headlineToHeader hdln
162+
163+headlineToHeaderWithList :: Monad m => Headline -> OrgParser m Blocks
164+headlineToHeaderWithList hdln@(Headline {..}) = do
165+ maxHeadlineLevels <- getExportSetting exportHeadlineLevels
166+ header <- headlineToHeader hdln
167+ listElements <- sequence (map headlineToBlocks headlineChildren)
168+ let listBlock = if null listElements
169+ then mempty
170+ else B.orderedList listElements
171+ let headerText = if maxHeadlineLevels == headlineLevel
172+ then header
173+ else flattenHeader header
174+ return $ headerText <> headlineContents <> listBlock
175+ where
176+ flattenHeader :: Blocks -> Blocks
177+ flattenHeader blks =
178+ case B.toList blks of
179+ (Header _ _ inlns:_) -> B.para (B.fromList inlns)
180+ _ -> mempty
181+
182+headlineToHeaderWithContents :: Monad m => Headline -> OrgParser m Blocks
183+headlineToHeaderWithContents hdln@(Headline {..}) = do
184+ header <- headlineToHeader hdln
185+ childrenBlocks <- mconcat <$> sequence (map headlineToBlocks headlineChildren)
186+ return $ header <> headlineContents <> childrenBlocks
187+
188+headlineToHeader :: Monad m => Headline -> OrgParser m Blocks
189+headlineToHeader (Headline {..}) = do
190+ exportTodoKeyword <- getExportSetting exportWithTodoKeywords
191+ let todoText = if exportTodoKeyword
192+ then case headlineTodoMarker of
193+ Just kw -> todoKeywordToInlines kw <> B.space
194+ Nothing -> mempty
195+ else mempty
196+ let text = tagTitle (todoText <> headlineText) headlineTags
197+ let propAttr = propertiesToAttr headlineProperties
198+ attr <- registerHeader propAttr headlineText
199+ return $ B.headerWith attr headlineLevel text
200+
201+todoKeyword :: Monad m => OrgParser m TodoMarker
202+todoKeyword = try $ do
203+ taskStates <- activeTodoMarkers <$> getState
204+ let kwParser tdm = try $ (tdm <$ string (todoMarkerName tdm) <* spaceChar)
205+ choice (map kwParser taskStates)
206+
207+todoKeywordToInlines :: TodoMarker -> Inlines
208+todoKeywordToInlines tdm =
209+ let todoText = todoMarkerName tdm
210+ todoState = map toLower . show $ todoMarkerState tdm
211+ classes = [todoState, todoText]
212+ in B.spanWith (mempty, classes, mempty) (B.str todoText)
213+
214+propertiesToAttr :: Properties -> Attr
215+propertiesToAttr properties =
216+ let
217+ toStringPair prop = (fromKey (fst prop), fromValue (snd prop))
218+ customIdKey = toPropertyKey "custom_id"
219+ classKey = toPropertyKey "class"
220+ unnumberedKey = toPropertyKey "unnumbered"
221+ specialProperties = [customIdKey, classKey, unnumberedKey]
222+ id' = fromMaybe mempty . fmap fromValue . lookup customIdKey $ properties
223+ cls = fromMaybe mempty . fmap fromValue . lookup classKey $ properties
224+ kvs' = map toStringPair . filter ((`notElem` specialProperties) . fst)
225+ $ properties
226+ isUnnumbered =
227+ fromMaybe False . fmap isNonNil . lookup unnumberedKey $ properties
228+ in
229+ (id', words cls ++ (if isUnnumbered then ["unnumbered"] else []), kvs')
230+
231+tagTitle :: Inlines -> [Tag] -> Inlines
232+tagTitle title tags = title <> (mconcat $ map tagToInline tags)
233+
234+-- | Convert
235+tagToInline :: Tag -> Inlines
236+tagToInline t = B.spanWith ("", ["tag"], [("data-tag-name", fromTag t)]) mempty
237+
238+-- | Read a :PROPERTIES: drawer and return the key/value pairs contained
239+-- within.
240+propertiesDrawer :: Monad m => OrgParser m Properties
241+propertiesDrawer = try $ do
242+ drawerType <- drawerStart
243+ guard $ map toUpper drawerType == "PROPERTIES"
244+ manyTill property (try endOfDrawer)
245+ where
246+ property :: Monad m => OrgParser m (PropertyKey, PropertyValue)
247+ property = try $ (,) <$> key <*> value
248+
249+ key :: Monad m => OrgParser m PropertyKey
250+ key = fmap toPropertyKey . try $
251+ skipSpaces *> char ':' *> many1Till nonspaceChar (char ':')
252+
253+ value :: Monad m => OrgParser m PropertyValue
254+ value = fmap toPropertyValue . try $
255+ skipSpaces *> manyTill anyChar (try $ skipSpaces *> newline)
256+
257+ endOfDrawer :: Monad m => OrgParser m String
258+ endOfDrawer = try $
259+ skipSpaces *> stringAnyCase ":END:" <* skipSpaces <* newline
260+