@@ -1,4 +1,6 @@
11{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE RecordWildCards #-}
3+{-# OPTIONS_GHC -fno-warn-overlapping-patterns #-}
24{-
35Copyright (C) 2014-2016 Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
46
@@ -32,6 +34,7 @@ module Text.Pandoc.Readers.Org.Blocks
3234 ) where
3335
3436import Text.Pandoc.Readers.Org.BlockStarts
37+import Text.Pandoc.Readers.Org.ExportSettings ( exportSettings )
3538import Text.Pandoc.Readers.Org.Inlines
3639import Text.Pandoc.Readers.Org.ParserState
3740import Text.Pandoc.Readers.Org.Parsing
@@ -46,13 +49,173 @@ import Text.Pandoc.Compat.Monoid ((<>))
4649import Text.Pandoc.Options
4750import Text.Pandoc.Shared ( compactify', compactify'DL )
4851
49−import Control.Monad ( foldM, guard, mzero )
52+import Control.Monad ( foldM, guard, mzero, void )
5053import Data.Char ( isSpace, toLower, toUpper)
5154import Data.List ( foldl', intersperse, isPrefixOf )
5255import qualified Data.Map as M
5356import Data.Maybe ( fromMaybe, isNothing )
57+import qualified Data.Sequence as S
5458import Network.HTTP ( urlEncode )
5559
60+--
61+-- Org headers
62+--
63+newtype Tag = Tag { fromTag :: String }
64+ deriving (Show, Eq)
65+
66+-- | Create a tag containing the given string.
67+toTag :: String -> Tag
68+toTag = Tag
69+
70+-- | The key (also called name or type) of a property.
71+newtype PropertyKey = PropertyKey { fromKey :: String }
72+ deriving (Show, Eq, Ord)
73+
74+-- | Create a property key containing the given string. Org mode keys are
75+-- case insensitive and are hence converted to lower case.
76+toPropertyKey :: String -> PropertyKey
77+toPropertyKey = PropertyKey . map toLower
78+
79+-- | The value assigned to a property.
80+newtype PropertyValue = PropertyValue { fromValue :: String }
81+
82+-- | Create a property value containing the given string.
83+toPropertyValue :: String -> PropertyValue
84+toPropertyValue = PropertyValue
85+
86+-- | Key/value pairs from a PROPERTIES drawer
87+type Properties = [(PropertyKey, PropertyValue)]
88+
89+-- | Org mode headline (i.e. a document subtree).
90+data Headline = Headline
91+ { headlineLevel :: Int
92+ , headlineText :: Inlines
93+ , headlineTags :: [Tag]
94+ , headlineProperties :: Properties
95+ , headlineContents :: Blocks
96+ , headlineChildren :: [Headline]
97+ }
98+
99+--
100+-- Parsing headlines and subtrees
101+--
102+
103+-- | Read an Org mode headline and its contents (i.e. a document subtree).
104+-- @lvl@ gives the minimum acceptable level of the tree.
105+headline :: Int -> OrgParser (F Headline)
106+headline lvl = try $ do
107+ level <- headerStart
108+ guard (lvl <= level)
109+ title <- trimInlinesF . mconcat <$> manyTill inline endOfTitle
110+ tags <- option [] headerTags
111+ newline
112+ properties <- option mempty propertiesDrawer
113+ contents <- blocks
114+ children <- many (headline (level + 1))
115+ return $ do
116+ title' <- title
117+ contents' <- contents
118+ children' <- sequence children
119+ return $ Headline
120+ { headlineLevel = level
121+ , headlineText = title'
122+ , headlineTags = tags
123+ , headlineProperties = properties
124+ , headlineContents = contents'
125+ , headlineChildren = children'
126+ }
127+ where
128+ endOfTitle :: OrgParser ()
129+ endOfTitle = void . lookAhead $ optional headerTags *> newline
130+
131+ headerTags :: OrgParser [Tag]
132+ headerTags = try $
133+ let tag = many1 (alphaNum <|> oneOf "@%#_") <* char ':'
134+ in map toTag <$> (skipSpaces *> char ':' *> many1 tag <* skipSpaces)
135+
136+-- | Convert an Org mode headline (i.e. a document tree) into pandoc's Blocks
137+headlineToBlocks :: Headline -> OrgParser Blocks
138+headlineToBlocks hdln@(Headline {..}) = do
139+ maxHeadlineLevels <- getExportSetting exportHeadlineLevels
140+ case () of
141+ _ | any isNoExportTag headlineTags -> return mempty
142+ _ | any isArchiveTag headlineTags -> archivedHeadlineToBlocks hdln
143+ _ | isCommentTitle headlineText -> return mempty
144+ _ | headlineLevel >= maxHeadlineLevels -> headlineToHeaderWithList hdln
145+ _ -> headlineToHeaderWithContents hdln
146+
147+isNoExportTag :: Tag -> Bool
148+isNoExportTag = (== toTag "noexport")
149+
150+isArchiveTag :: Tag -> Bool
151+isArchiveTag = (== toTag "ARCHIVE")
152+
153+-- | Check if the title starts with COMMENT.
154+-- FIXME: This accesses builder internals not intended for use in situations
155+-- like these. Replace once keyword parsing is supported.
156+isCommentTitle :: Inlines -> Bool
157+isCommentTitle xs = (B.Many . S.take 1 . B.unMany) xs == B.str "COMMENT"
158+isCommentTitle _ = False
159+
160+archivedHeadlineToBlocks :: Headline -> OrgParser Blocks
161+archivedHeadlineToBlocks hdln = do
162+ archivedTreesOption <- getExportSetting exportArchivedTrees
163+ case archivedTreesOption of
164+ ArchivedTreesNoExport -> return mempty
165+ ArchivedTreesExport -> headlineToHeaderWithContents hdln
166+ ArchivedTreesHeadlineOnly -> headlineToHeader hdln
167+
168+headlineToHeaderWithList :: Headline -> OrgParser Blocks
169+headlineToHeaderWithList hdln@(Headline {..}) = do
170+ maxHeadlineLevels <- getExportSetting exportHeadlineLevels
171+ header <- headlineToHeader hdln
172+ listElements <- sequence (map headlineToBlocks headlineChildren)
173+ let listBlock = if null listElements
174+ then mempty
175+ else B.orderedList listElements
176+ let headerText = if maxHeadlineLevels == headlineLevel
177+ then header
178+ else flattenHeader header
179+ return $ headerText <> headlineContents <> listBlock
180+ where
181+ flattenHeader :: Blocks -> Blocks
182+ flattenHeader blks =
183+ case B.toList blks of
184+ (Header _ _ inlns:_) -> B.para (B.fromList inlns)
185+ _ -> mempty
186+
187+headlineToHeaderWithContents :: Headline -> OrgParser Blocks
188+headlineToHeaderWithContents hdln@(Headline {..}) = do
189+ header <- headlineToHeader hdln
190+ childrenBlocks <- mconcat <$> sequence (map headlineToBlocks headlineChildren)
191+ return $ header <> headlineContents <> childrenBlocks
192+
193+headlineToHeader :: Headline -> OrgParser Blocks
194+headlineToHeader (Headline {..}) = do
195+ let text = tagTitle headlineText headlineTags
196+ let propAttr = propertiesToAttr headlineProperties
197+ attr <- registerHeader propAttr headlineText
198+ return $ B.headerWith attr headlineLevel text
199+
200+propertiesToAttr :: Properties -> Attr
201+propertiesToAttr properties =
202+ let
203+ toStringPair prop = (fromKey (fst prop), fromValue (snd prop))
204+ customIdKey = toPropertyKey "custom_id"
205+ classKey = toPropertyKey "class"
206+ id' = fromMaybe mempty . fmap fromValue . lookup customIdKey $ properties
207+ cls = fromMaybe mempty . fmap fromValue . lookup classKey $ properties
208+ kvs' = map toStringPair . filter ((`notElem` [customIdKey, classKey]) . fst)
209+ $ properties
210+ in
211+ (id', words cls, kvs')
212+
213+tagTitle :: Inlines -> [Tag] -> Inlines
214+tagTitle title tags = title <> (mconcat $ map tagToInline tags)
215+
216+tagToInline :: Tag -> Inlines
217+tagToInline t = B.spanWith ("", ["tag"], [("data-tag-name", fromTag t)]) mempty
218+
56219
57220--
58221-- parsing blocks
@@ -61,9 +224,11 @@ import Network.HTTP ( urlEncode )
61224-- | Get a list of blocks.
62225blockList :: OrgParser [Block]
63226blockList = do
64− blocks' <- blocks
65− st <- getState
66− return . B.toList $ runF blocks' st
227+ initialBlocks <- blocks
228+ headlines <- sequence <$> manyTill (headline 1) eof
229+ st <- getState
230+ headlineBlocks <- fmap mconcat . sequence . map headlineToBlocks $ runF headlines st
231+ return . B.toList $ (runF initialBlocks st) <> headlineBlocks
67232
68233-- | Get the meta information safed in the state.
69234meta :: OrgParser Meta
@@ -72,7 +237,7 @@ meta = do
72237 return $ runF (orgStateMeta st) st
73238
74239blocks :: OrgParser (F Blocks)
75−blocks = mconcat <$> manyTill block eof
240+blocks = mconcat <$> manyTill block (void (lookAhead headerStart) <|> eof)
76241
77242block :: OrgParser (F Blocks)
78243block = choice [ mempty <$ blanklines
@@ -82,7 +247,6 @@ block = choice [ mempty <$ blanklines
82247 , example
83248 , genericDrawer
84249 , specialLine
85− , header
86250 , horizontalRule
87251 , list
88252 , latexFragment
@@ -381,30 +545,22 @@ drawerEnd = try $
381545
382546-- | Read a :PROPERTIES: drawer and return the key/value pairs contained
383547-- within.
384−propertiesDrawer :: OrgParser [(String, String)]
548+propertiesDrawer :: OrgParser Properties
385549propertiesDrawer = try $ do
386550 drawerType <- drawerStart
387551 guard $ map toUpper drawerType == "PROPERTIES"
388552 manyTill property (try drawerEnd)
389553 where
390− property :: OrgParser (String, String)
554+ property :: OrgParser (PropertyKey, PropertyValue)
391555 property = try $ (,) <$> key <*> value
392556
393− key :: OrgParser String
394− key = try $ skipSpaces *> char ':' *> many1Till nonspaceChar (char ':')
557+ key :: OrgParser PropertyKey
558+ key = fmap toPropertyKey . try $
559+ skipSpaces *> char ':' *> many1Till nonspaceChar (char ':')
395560
396− value :: OrgParser String
397− value = try $ skipSpaces *> manyTill anyChar (try $ skipSpaces *> newline)
398−
399−keyValuesToAttr :: [(String, String)] -> Attr
400−keyValuesToAttr kvs =
401− let
402− lowerKvs = map (\(k, v) -> (map toLower k, v)) kvs
403− id' = fromMaybe mempty . lookup "custom_id" $ lowerKvs
404− cls = fromMaybe mempty . lookup "class" $ lowerKvs
405− kvs' = filter (flip notElem ["custom_id", "class"] . fst) lowerKvs
406− in
407− (id', words cls, kvs')
561+ value :: OrgParser PropertyValue
562+ value = fmap toPropertyValue . try $
563+ skipSpaces *> manyTill anyChar (try $ skipSpaces *> newline)
408564
409565
410566--
@@ -486,7 +642,7 @@ optionLine = try $ do
486642 key <- metaKey
487643 case key of
488644 "link" -> parseLinkFormat >>= uncurry addLinkFormat
489− "options" -> () <$ sepBy spaces exportSetting
645+ "options" -> exportSettings
490646 _ -> mzero
491647
492648addLinkFormat :: String
@@ -496,100 +652,6 @@ addLinkFormat key formatter = updateState $ \s ->
496652 let fs = orgStateLinkFormatters s
497653 in s{ orgStateLinkFormatters = M.insert key formatter fs }
498654
499−
500−--
501−-- Export Settings
502−--
503−
504−-- | Read and process org-mode specific export options.
505−exportSetting :: OrgParser ()
506−exportSetting = choice
507− [ booleanSetting "^" setExportSubSuperscripts
508− , booleanSetting "'" setExportSmartQuotes
509− , booleanSetting "*" setExportEmphasizedText
510− , booleanSetting "-" setExportSpecialStrings
511− , ignoredSetting ":"
512− , ignoredSetting "<"
513− , ignoredSetting "\\n"
514− , ignoredSetting "arch"
515− , ignoredSetting "author"
516− , ignoredSetting "c"
517− , ignoredSetting "creator"
518− , complementableListSetting "d" setExportDrawers
519− , ignoredSetting "date"
520− , ignoredSetting "e"
521− , ignoredSetting "email"
522− , ignoredSetting "f"
523− , ignoredSetting "H"
524− , ignoredSetting "inline"
525− , ignoredSetting "num"
526− , ignoredSetting "p"
527− , ignoredSetting "pri"
528− , ignoredSetting "prop"
529− , ignoredSetting "stat"
530− , ignoredSetting "tags"
531− , ignoredSetting "tasks"
532− , ignoredSetting "tex"
533− , ignoredSetting "timestamp"
534− , ignoredSetting "title"
535− , ignoredSetting "toc"
536− , ignoredSetting "todo"
537− , ignoredSetting "|"
538− ] <?> "export setting"
539−
540−booleanSetting :: String -> ExportSettingSetter Bool -> OrgParser ()
541−booleanSetting settingIdentifier setter = try $ do
542− string settingIdentifier
543− char ':'
544− value <- elispBoolean
545− updateState $ modifyExportSettings setter value
546−
547−-- | Read an elisp boolean. Only NIL is treated as false, non-NIL values are
548−-- interpreted as true.
549−elispBoolean :: OrgParser Bool
550−elispBoolean = try $ do
551− value <- many1 nonspaceChar
552− return $ case map toLower value of
553− "nil" -> False
554− "{}" -> False
555− "()" -> False
556− _ -> True
557−
558−-- | A list or a complement list (i.e. a list starting with `not`).
559−complementableListSetting :: String
560− -> ExportSettingSetter (Either [String] [String])
561− -> OrgParser ()
562−complementableListSetting settingIdentifier setter = try $ do
563− _ <- string settingIdentifier <* char ':'
564− value <- choice [ Left <$> complementStringList
565− , Right <$> stringList
566− , (\b -> if b then Left [] else Right []) <$> elispBoolean
567− ]
568− updateState $ modifyExportSettings setter value
569− where
570− -- Read a plain list of strings.
571− stringList :: OrgParser [String]
572− stringList = try $
573− char '('
574− *> sepBy elispString spaces
575− <* char ')'
576−
577− -- Read an emacs lisp list specifying a complement set.
578− complementStringList :: OrgParser [String]
579− complementStringList = try $
580− string "(not "
581− *> sepBy elispString spaces
582− <* char ')'
583−
584− elispString :: OrgParser String
585− elispString = try $
586− char '"'
587− *> manyTill alphaNum (char '"')
588−
589−ignoredSetting :: String -> OrgParser ()
590−ignoredSetting s = try (() <$ string s <* char ':' <* many1 nonspaceChar)
591−
592−
593655parseLinkFormat :: OrgParser ((String, String -> String))
594656parseLinkFormat = try $ do
595657 linkType <- (:) <$> letter <*> many (alphaNum <|> oneOf "-_") <* skipSpaces
@@ -612,36 +674,6 @@ parseFormat = try $ do
612674 rest = manyTill anyChar (eof <|> () <$ oneOf "\n\r")
613675 tillSpecifier c = manyTill (noneOf "\n\r") (try $ string ('%':c:""))
614676
615−--
616−-- Headers
617−--
618−
619−-- | Headers
620−header :: OrgParser (F Blocks)
621−header = try $ do
622− level <- headerStart
623− title <- manyTill inline (lookAhead $ optional headerTags <* newline)
624− tags <- option [] headerTags
625− newline
626− let text = tagTitle title tags
627− propAttr <- option nullAttr (keyValuesToAttr <$> propertiesDrawer)
628− attr <- registerHeader propAttr (runF text def)
629− return (B.headerWith attr level <$> text)
630− where
631− tagTitle :: [F Inlines] -> [String] -> F Inlines
632− tagTitle title tags = trimInlinesF . mconcat $ title <> map tagToInlineF tags
633−
634− tagToInlineF :: String -> F Inlines
635− tagToInlineF t = return $ B.spanWith ("", ["tag"], [("data-tag-name", t)]) mempty
636−
637− headerTags :: OrgParser [String]
638− headerTags = try $
639− let tag = many1 (alphaNum <|> oneOf "@%#_") <* char ':'
640− in skipSpaces
641− *> char ':'
642− *> many1 tag
643− <* skipSpaces
644−
645677
646678--
647679-- Tables
@@ -806,6 +838,8 @@ noteBlock = try $ do
806838-- Paragraphs or Plain text
807839paraOrPlain :: OrgParser (F Blocks)
808840paraOrPlain = try $ do
841+ -- Make sure we are not looking at a headline
842+ notFollowedBy' (char '*' *> (oneOf " *"))
809843 ils <- inlines
810844 nl <- option False (newline *> return True)
811845 -- Read block as paragraph, except if we are in a list context and the block