@@ -1,4 +1,6 @@
1{-# LANGUAGE FlexibleContexts #-}
1{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE RecordWildCards #-}
3+{-# OPTIONS_GHC -fno-warn-overlapping-patterns #-}
3Copyright (C) 2014-2016 Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
5Copyright (C) 2014-2016 Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
@@ -32,6 +34,7 @@ module Text.Pandoc.Readers.Org.Blocks
34import Text.Pandoc.Readers.Org.BlockStarts
36import Text.Pandoc.Readers.Org.BlockStarts
37+import Text.Pandoc.Readers.Org.ExportSettings ( exportSettings )
35import Text.Pandoc.Readers.Org.Inlines
38import Text.Pandoc.Readers.Org.Inlines
36import Text.Pandoc.Readers.Org.ParserState
39import Text.Pandoc.Readers.Org.ParserState
37import Text.Pandoc.Readers.Org.Parsing
40import Text.Pandoc.Readers.Org.Parsing
@@ -46,13 +49,173 @@ import Text.Pandoc.Compat.Monoid ((<>))
46import Text.Pandoc.Options
49import Text.Pandoc.Options
47import Text.Pandoc.Shared ( compactify', compactify'DL )
50import Text.Pandoc.Shared ( compactify', compactify'DL )
49−import Control.Monad ( foldM, guard, mzero )
52+import Control.Monad ( foldM, guard, mzero, void )
50import Data.Char ( isSpace, toLower, toUpper)
53import Data.Char ( isSpace, toLower, toUpper)
51import Data.List ( foldl', intersperse, isPrefixOf )
54import Data.List ( foldl', intersperse, isPrefixOf )
52import qualified Data.Map as M
55import qualified Data.Map as M
53import Data.Maybe ( fromMaybe, isNothing )
56import Data.Maybe ( fromMaybe, isNothing )
57+import qualified Data.Sequence as S
54import Network.HTTP ( urlEncode )
58import Network.HTTP ( urlEncode )
63+newtype Tag = Tag { fromTag :: String }
66+-- | Create a tag containing the given string.
67+toTag :: String -> Tag
70+-- | The key (also called name or type) of a property.
71+newtype PropertyKey = PropertyKey { fromKey :: String }
72+ deriving (Show, Eq, Ord)
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
79+-- | The value assigned to a property.
80+newtype PropertyValue = PropertyValue { fromValue :: String }
82+-- | Create a property value containing the given string.
83+toPropertyValue :: String -> PropertyValue
84+toPropertyValue = PropertyValue
86+-- | Key/value pairs from a PROPERTIES drawer
87+type Properties = [(PropertyKey, PropertyValue)]
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]
100+-- Parsing headlines and subtrees
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
112+ properties <- option mempty propertiesDrawer
114+ children <- many (headline (level + 1))
117+ contents' <- contents
118+ children' <- sequence children
120+ { headlineLevel = level
121+ , headlineText = title'
122+ , headlineTags = tags
123+ , headlineProperties = properties
124+ , headlineContents = contents'
125+ , headlineChildren = children'
128+ endOfTitle :: OrgParser ()
129+ endOfTitle = void . lookAhead $ optional headerTags *> newline
131+ headerTags :: OrgParser [Tag]
133+ let tag = many1 (alphaNum <|> oneOf "@%#_") <* char ':'
134+ in map toTag <$> (skipSpaces *> char ':' *> many1 tag <* skipSpaces)
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
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
147+isNoExportTag :: Tag -> Bool
148+isNoExportTag = (== toTag "noexport")
150+isArchiveTag :: Tag -> Bool
151+isArchiveTag = (== toTag "ARCHIVE")
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
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
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
175+ else B.orderedList listElements
176+ let headerText = if maxHeadlineLevels == headlineLevel
178+ else flattenHeader header
179+ return $ headerText <> headlineContents <> listBlock
181+ flattenHeader :: Blocks -> Blocks
182+ flattenHeader blks =
183+ case B.toList blks of
184+ (Header _ _ inlns:_) -> B.para (B.fromList inlns)
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
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
200+propertiesToAttr :: Properties -> Attr
201+propertiesToAttr properties =
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)
211+ (id', words cls, kvs')
213+tagTitle :: Inlines -> [Tag] -> Inlines
214+tagTitle title tags = title <> (mconcat $ map tagToInline tags)
216+tagToInline :: Tag -> Inlines
217+tagToInline t = B.spanWith ("", ["tag"], [("data-tag-name", fromTag t)]) mempty
58-- parsing blocks
221-- parsing blocks
@@ -61,9 +224,11 @@ import Network.HTTP ( urlEncode )
61-- | Get a list of blocks.
224-- | Get a list of blocks.
62blockList :: OrgParser [Block]
225blockList :: OrgParser [Block]
63blockList = do
226blockList = do
64− blocks' <- blocks
227+ initialBlocks <- blocks
228+ headlines <- sequence <$> manyTill (headline 1) eof
65− st <- getState
229+ st <- getState
230+ headlineBlocks <- fmap mconcat . sequence . map headlineToBlocks $ runF headlines st
66− return . B.toList $ runF blocks' st
231+ return . B.toList $ (runF initialBlocks st) <> headlineBlocks
68-- | Get the meta information safed in the state.
233-- | Get the meta information safed in the state.
69meta :: OrgParser Meta
234meta :: OrgParser Meta
@@ -72,7 +237,7 @@ meta = do
72 return $ runF (orgStateMeta st) st
237 return $ runF (orgStateMeta st) st
74blocks :: OrgParser (F Blocks)
239blocks :: OrgParser (F Blocks)
75−blocks = mconcat <$> manyTill block eof
240+blocks = mconcat <$> manyTill block (void (lookAhead headerStart) <|> eof)
77block :: OrgParser (F Blocks)
242block :: OrgParser (F Blocks)
78block = choice [ mempty <$ blanklines
243block = choice [ mempty <$ blanklines
@@ -82,7 +247,6 @@ block = choice [ mempty <$ blanklines
82 , example
247 , example
83 , genericDrawer
248 , genericDrawer
84 , specialLine
249 , specialLine
86 , horizontalRule
250 , horizontalRule
88 , latexFragment
252 , latexFragment
@@ -381,30 +545,22 @@ drawerEnd = try $
382-- | Read a :PROPERTIES: drawer and return the key/value pairs contained
546-- | Read a :PROPERTIES: drawer and return the key/value pairs contained
383-- within.
547-- within.
384−propertiesDrawer :: OrgParser [(String, String)]
548+propertiesDrawer :: OrgParser Properties
385propertiesDrawer = try $ do
549propertiesDrawer = try $ do
386 drawerType <- drawerStart
550 drawerType <- drawerStart
387 guard $ map toUpper drawerType == "PROPERTIES"
551 guard $ map toUpper drawerType == "PROPERTIES"
388 manyTill property (try drawerEnd)
552 manyTill property (try drawerEnd)
390− property :: OrgParser (String, String)
554+ property :: OrgParser (PropertyKey, PropertyValue)
391 property = try $ (,) <$> key <*> value
555 property = try $ (,) <$> key <*> value
393− key :: OrgParser String
557+ key :: OrgParser PropertyKey
558+ key = fmap toPropertyKey . try $
394− key = try $ skipSpaces *> char ':' *> many1Till nonspaceChar (char ':')
559+ skipSpaces *> char ':' *> many1Till nonspaceChar (char ':')
396− value :: OrgParser String
561+ value :: OrgParser PropertyValue
562+ value = fmap toPropertyValue . try $
397− value = try $ skipSpaces *> manyTill anyChar (try $ skipSpaces *> newline)
563+ skipSpaces *> manyTill anyChar (try $ skipSpaces *> newline)
399−keyValuesToAttr :: [(String, String)] -> Attr
400−keyValuesToAttr kvs =
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
407− (id', words cls, kvs')
@@ -486,7 +642,7 @@ optionLine = try $ do
486 key <- metaKey
642 key <- metaKey
487 case key of
643 case key of
488 "link" -> parseLinkFormat >>= uncurry addLinkFormat
644 "link" -> parseLinkFormat >>= uncurry addLinkFormat
489− "options" -> () <$ sepBy spaces exportSetting
645+ "options" -> exportSettings
490 _ -> mzero
646 _ -> mzero
492addLinkFormat :: String
648addLinkFormat :: String
@@ -496,100 +652,6 @@ addLinkFormat key formatter = updateState $ \s ->
496 let fs = orgStateLinkFormatters s
652 let fs = orgStateLinkFormatters s
497 in s{ orgStateLinkFormatters = M.insert key formatter fs }
653 in s{ orgStateLinkFormatters = M.insert key formatter fs }
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"
540−booleanSetting :: String -> ExportSettingSetter Bool -> OrgParser ()
541−booleanSetting settingIdentifier setter = try $ do
542− string settingIdentifier
544− value <- elispBoolean
545− updateState $ modifyExportSettings setter value
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
558−-- | A list or a complement list (i.e. a list starting with `not`).
559−complementableListSetting :: String
560− -> ExportSettingSetter (Either [String] [String])
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
568− updateState $ modifyExportSettings setter value
570− -- Read a plain list of strings.
571− stringList :: OrgParser [String]
574− *> sepBy elispString spaces
577− -- Read an emacs lisp list specifying a complement set.
578− complementStringList :: OrgParser [String]
579− complementStringList = try $
581− *> sepBy elispString spaces
584− elispString :: OrgParser String
587− *> manyTill alphaNum (char '"')
589−ignoredSetting :: String -> OrgParser ()
590−ignoredSetting s = try (() <$ string s <* char ':' <* many1 nonspaceChar)
593parseLinkFormat :: OrgParser ((String, String -> String))
655parseLinkFormat :: OrgParser ((String, String -> String))
594parseLinkFormat = try $ do
656parseLinkFormat = try $ do
595 linkType <- (:) <$> letter <*> many (alphaNum <|> oneOf "-_") <* skipSpaces
657 linkType <- (:) <$> letter <*> many (alphaNum <|> oneOf "-_") <* skipSpaces
@@ -612,36 +674,6 @@ parseFormat = try $ do
612 rest = manyTill anyChar (eof <|> () <$ oneOf "\n\r")
674 rest = manyTill anyChar (eof <|> () <$ oneOf "\n\r")
613 tillSpecifier c = manyTill (noneOf "\n\r") (try $ string ('%':c:""))
675 tillSpecifier c = manyTill (noneOf "\n\r") (try $ string ('%':c:""))
620−header :: OrgParser (F Blocks)
622− level <- headerStart
623− title <- manyTill inline (lookAhead $ optional headerTags <* newline)
624− tags <- option [] headerTags
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)
631− tagTitle :: [F Inlines] -> [String] -> F Inlines
632− tagTitle title tags = trimInlinesF . mconcat $ title <> map tagToInlineF tags
634− tagToInlineF :: String -> F Inlines
635− tagToInlineF t = return $ B.spanWith ("", ["tag"], [("data-tag-name", t)]) mempty
637− headerTags :: OrgParser [String]
639− let tag = many1 (alphaNum <|> oneOf "@%#_") <* char ':'
@@ -806,6 +838,8 @@ noteBlock = try $ do
806-- Paragraphs or Plain text
838-- Paragraphs or Plain text
807paraOrPlain :: OrgParser (F Blocks)
839paraOrPlain :: OrgParser (F Blocks)
808paraOrPlain = try $ do
840paraOrPlain = try $ do
841+ -- Make sure we are not looking at a headline
842+ notFollowedBy' (char '*' *> (oneOf " *"))
809 ils <- inlines
843 ils <- inlines
810 nl <- option False (newline *> return True)
844 nl <- option False (newline *> return True)
811 -- Read block as paragraph, except if we are in a list context and the block
845 -- Read block as paragraph, except if we are in a list context and the block