git.iko.soy/typeable/pandoc
on master help sign in
Merge pull request #2946 from tarleb/org-modularization

Org-mode reader modularization

commitf54873d5ea757787ec816336c61c24a813799554 authorJohn MacFarlane <jgm@berkeley.edu> dateWed 25 May 2016, 22:37 (10 years ago) parentcc937eeeea6d65
7 files changed+1939−1629
pandoc.cabal+4 src/Text/Pandoc/Readers/Org.hs+11−1627 src/Text/Pandoc/Readers/Org/BlockStarts.hs+112 src/Text/Pandoc/Readers/Org/Blocks.hs+891 src/Text/Pandoc/Readers/Org/Inlines.hs+715 src/Text/Pandoc/Readers/Org/ParserState.hs+5−2 src/Text/Pandoc/Readers/Org/Parsing.hs+201
pandoc.cabal+4
@@ -393,7 +393,11 @@ Library
393393 Text.Pandoc.Readers.Odt.Generic.XMLConverter,
394394 Text.Pandoc.Readers.Odt.Arrows.State,
395395 Text.Pandoc.Readers.Odt.Arrows.Utils,
396+ Text.Pandoc.Readers.Org.BlockStarts,
397+ Text.Pandoc.Readers.Org.Blocks,
398+ Text.Pandoc.Readers.Org.Inlines,
396399 Text.Pandoc.Readers.Org.ParserState,
400+ Text.Pandoc.Readers.Org.Parsing,
397401 Text.Pandoc.Writers.Shared,
398402 Text.Pandoc.Asciify,
399403 Text.Pandoc.MIME,
src/Text/Pandoc/Readers/Org.hs+11−1627
@@ -1,5 +1,3 @@
1−{-# LANGUAGE FlexibleContexts #-}
2−{-# LANGUAGE OverloadedStrings #-}
31{-
42Copyright (C) 2014-2016 Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
53
@@ -29,32 +27,15 @@ Conversion of org-mode formatted plain text to 'Pandoc' document.
2927-}
3028module Text.Pandoc.Readers.Org ( readOrg ) where
3129
32−import qualified Text.Pandoc.Builder as B
33−import Text.Pandoc.Builder ( Inlines, Blocks )
30+import Text.Pandoc.Readers.Org.Blocks ( blockList, meta )
31+import Text.Pandoc.Readers.Org.Parsing ( OrgParser, readWithM )
32+import Text.Pandoc.Readers.Org.ParserState ( OrgParserState (..) )
33+
3434import Text.Pandoc.Definition
35−import Text.Pandoc.Compat.Monoid ((<>))
3635import Text.Pandoc.Error
3736import Text.Pandoc.Options
38−import qualified Text.Pandoc.Parsing as P
39−import Text.Pandoc.Parsing hiding ( F, unF, askF, asksF, runF
40− , anyLine, blanklines, newline
41− , orderedListMarker
42− , parseFromString
43− )
44−import Text.Pandoc.Readers.LaTeX (inlineCommand, rawLaTeXInline)
45−import Text.Pandoc.Readers.Org.ParserState
46−import Text.Pandoc.Shared (compactify', compactify'DL)
47−import Text.TeXMath (readTeX, writePandoc, DisplayType(..))
48−import qualified Text.TeXMath.Readers.MathML.EntityMap as MathMLEntityMap
4937
50−import Control.Arrow (first)
51−import Control.Monad (foldM, guard, mplus, mzero, when)
52−import Control.Monad.Reader ( Reader, runReader )
53−import Data.Char (isAlphaNum, isSpace, toLower, toUpper)
54−import Data.List ( foldl', intersperse, isPrefixOf, isSuffixOf )
55−import qualified Data.Map as M
56−import Data.Maybe ( fromMaybe, isNothing )
57−import Network.HTTP (urlEncode)
38+import Control.Monad.Reader ( runReader )
5839
5940
6041-- | Parse org-mode string and return a Pandoc document.
@@ -63,165 +44,17 @@ readOrg :: ReaderOptions -- ^ Reader options
6344 -> Either PandocError Pandoc
6445readOrg opts s = flip runReader def $ readWithM parseOrg def{ orgStateOptions = opts } (s ++ "\n\n")
6546
66−-- | The parser used to read org files.
67−type OrgParser = ParserT [Char] OrgParserState (Reader OrgParserLocal)
68−
69−--
70−-- Functions acting on the parser state
71−--
72−recordAnchorId :: String -> OrgParser ()
73−recordAnchorId i = updateState $ \s ->
74− s{ orgStateAnchorIds = i : (orgStateAnchorIds s) }
75−
76−updateLastForbiddenCharPos :: OrgParser ()
77−updateLastForbiddenCharPos = getPosition >>= \p ->
78− updateState $ \s -> s{ orgStateLastForbiddenCharPos = Just p}
79−
80−updateLastPreCharPos :: OrgParser ()
81−updateLastPreCharPos = getPosition >>= \p ->
82− updateState $ \s -> s{ orgStateLastPreCharPos = Just p}
83−
84−pushToInlineCharStack :: Char -> OrgParser ()
85−pushToInlineCharStack c = updateState $ \s ->
86− s{ orgStateEmphasisCharStack = c:orgStateEmphasisCharStack s }
87−
88−popInlineCharStack :: OrgParser ()
89−popInlineCharStack = updateState $ \s ->
90− s{ orgStateEmphasisCharStack = drop 1 . orgStateEmphasisCharStack $ s }
91−
92−surroundingEmphasisChar :: OrgParser [Char]
93−surroundingEmphasisChar =
94− take 1 . drop 1 . orgStateEmphasisCharStack <$> getState
95−
96−startEmphasisNewlinesCounting :: Int -> OrgParser ()
97−startEmphasisNewlinesCounting maxNewlines = updateState $ \s ->
98− s{ orgStateEmphasisNewlines = Just maxNewlines }
99−
100−decEmphasisNewlinesCount :: OrgParser ()
101−decEmphasisNewlinesCount = updateState $ \s ->
102− s{ orgStateEmphasisNewlines = (\n -> n - 1) <$> orgStateEmphasisNewlines s }
103−
104−newlinesCountWithinLimits :: OrgParser Bool
105−newlinesCountWithinLimits = do
106− st <- getState
107− return $ ((< 0) <$> orgStateEmphasisNewlines st) /= Just True
108−
109−resetEmphasisNewlines :: OrgParser ()
110−resetEmphasisNewlines = updateState $ \s ->
111− s{ orgStateEmphasisNewlines = Nothing }
112−
113−addLinkFormat :: String
114− -> (String -> String)
115− -> OrgParser ()
116−addLinkFormat key formatter = updateState $ \s ->
117− let fs = orgStateLinkFormatters s
118− in s{ orgStateLinkFormatters = M.insert key formatter fs }
119−
120−addToNotesTable :: OrgNoteRecord -> OrgParser ()
121−addToNotesTable note = do
122− oldnotes <- orgStateNotes' <$> getState
123− updateState $ \s -> s{ orgStateNotes' = note:oldnotes }
124−
125−--
126−-- Export Settings
127−--
128−exportSetting :: OrgParser ()
129−exportSetting = choice
130− [ booleanSetting "^" setExportSubSuperscripts
131− , ignoredSetting "'"
132− , ignoredSetting "*"
133− , ignoredSetting "-"
134− , ignoredSetting ":"
135− , ignoredSetting "<"
136− , ignoredSetting "\\n"
137− , ignoredSetting "arch"
138− , ignoredSetting "author"
139− , ignoredSetting "c"
140− , ignoredSetting "creator"
141− , complementableListSetting "d" setExportDrawers
142− , ignoredSetting "date"
143− , ignoredSetting "e"
144− , ignoredSetting "email"
145− , ignoredSetting "f"
146− , ignoredSetting "H"
147− , ignoredSetting "inline"
148− , ignoredSetting "num"
149− , ignoredSetting "p"
150− , ignoredSetting "pri"
151− , ignoredSetting "prop"
152− , ignoredSetting "stat"
153− , ignoredSetting "tags"
154− , ignoredSetting "tasks"
155− , ignoredSetting "tex"
156− , ignoredSetting "timestamp"
157− , ignoredSetting "title"
158− , ignoredSetting "toc"
159− , ignoredSetting "todo"
160− , ignoredSetting "|"
161− ] <?> "export setting"
162−
163−booleanSetting :: String -> ExportSettingSetter Bool -> OrgParser ()
164−booleanSetting settingIdentifier setter = try $ do
165− string settingIdentifier
166− char ':'
167− value <- elispBoolean
168− updateState $ modifyExportSettings setter value
169−
170−-- | Read an elisp boolean. Only NIL is treated as false, non-NIL values are
171−-- interpreted as true.
172−elispBoolean :: OrgParser Bool
173−elispBoolean = try $ do
174− value <- many1 nonspaceChar
175− return $ case map toLower value of
176− "nil" -> False
177− "{}" -> False
178− "()" -> False
179− _ -> True
180−
181−-- | A list or a complement list (i.e. a list starting with `not`).
182−complementableListSetting :: String
183− -> ExportSettingSetter (Either [String] [String])
184− -> OrgParser ()
185−complementableListSetting settingIdentifier setter = try $ do
186− _ <- string settingIdentifier <* char ':'
187− value <- choice [ Left <$> complementStringList
188− , Right <$> stringList
189− , (\b -> if b then Left [] else Right []) <$> elispBoolean
190− ]
191− updateState $ modifyExportSettings setter value
192− where
193− -- Read a plain list of strings.
194− stringList :: OrgParser [String]
195− stringList = try $
196− char '('
197− *> sepBy elispString spaces
198− <* char ')'
199−
200− -- Read an emacs lisp list specifying a complement set.
201− complementStringList :: OrgParser [String]
202− complementStringList = try $
203− string "(not "
204− *> sepBy elispString spaces
205− <* char ')'
206−
207− elispString :: OrgParser String
208− elispString = try $
209− char '"'
210− *> manyTill alphaNum (char '"')
211−
212−ignoredSetting :: String -> OrgParser ()
213−ignoredSetting s = try (() <$ string s <* char ':' <* many1 nonspaceChar)
214−
21547--
21648-- Parser
21749--
21850parseOrg :: OrgParser Pandoc
21951parseOrg = do
220− blocks' <- parseBlocks
221− st <- getState
222− let meta = runF (orgStateMeta' st) st
223− let removeUnwantedBlocks = dropCommentTrees . filter (/= Null)
224− return $ Pandoc meta $ removeUnwantedBlocks (B.toList $ runF blocks' st)
52+ blocks' <- blockList
53+ meta' <- meta
54+ return . Pandoc meta' $ removeUnwantedBlocks blocks'
55+ where
56+ removeUnwantedBlocks :: [Block] -> [Block]
57+ removeUnwantedBlocks = dropCommentTrees . filter (/= Null)
22558
22659-- | Drop COMMENT headers and the document tree below those headers.
22760dropCommentTrees :: [Block] -> [Block]
@@ -256,1452 +89,3 @@ isHeaderLevelLowerEq n blk =
25689 case blk of
25790 (Header level _ _) -> n >= level
25891 _ -> False
259−
260−
261−--
262−-- Adaptions and specializations of parsing utilities
263−--
264−
265−-- The version Text.Pandoc.Parsing cannot be used, as we need additional parts
266−-- of the state saved and restored.
267−parseFromString :: OrgParser a -> String -> OrgParser a
268−parseFromString parser str' = do
269− oldLastPreCharPos <- orgStateLastPreCharPos <$> getState
270− updateState $ \s -> s{ orgStateLastPreCharPos = Nothing }
271− result <- P.parseFromString parser str'
272− updateState $ \s -> s{ orgStateLastPreCharPos = oldLastPreCharPos }
273− return result
274−
275−-- | Like @Text.Parsec.Char.newline@, but causes additional state changes.
276−newline :: OrgParser Char
277−newline =
278− P.newline
279− <* updateLastPreCharPos
280− <* updateLastForbiddenCharPos
281−
282−-- | Like @Text.Parsec.Char.blanklines@, but causes additional state changes.
283−blanklines :: OrgParser [Char]
284−blanklines =
285− P.blanklines
286− <* updateLastPreCharPos
287− <* updateLastForbiddenCharPos
288−
289−anyLine :: OrgParser String
290−anyLine =
291− P.anyLine
292− <* updateLastPreCharPos
293− <* updateLastForbiddenCharPos
294−
295−-- | Succeeds when we're in list context.
296−inList :: OrgParser ()
297−inList = do
298− ctx <- orgStateParserContext <$> getState
299− guard (ctx == ListItemState)
300−
301−-- | Parse in different context
302−withContext :: ParserContext -- ^ New parser context
303− -> OrgParser a -- ^ Parser to run in that context
304− -> OrgParser a
305−withContext context parser = do
306− oldContext <- orgStateParserContext <$> getState
307− updateState $ \s -> s{ orgStateParserContext = context }
308− result <- parser
309− updateState $ \s -> s{ orgStateParserContext = oldContext }
310− return result
311−
312−--
313−-- parsing blocks
314−--
315−
316−parseBlocks :: OrgParser (F Blocks)
317−parseBlocks = mconcat <$> manyTill block eof
318−
319−block :: OrgParser (F Blocks)
320−block = choice [ mempty <$ blanklines
321− , table
322− , orgBlock
323− , figure
324− , example
325− , genericDrawer
326− , specialLine
327− , header
328− , return <$> hline
329− , list
330− , latexFragment
331− , noteBlock
332− , paraOrPlain
333− ] <?> "block"
334−
335−
336−--
337−-- Block Attributes
338−--
339−
340−-- | Attributes that may be added to figures (like a name or caption).
341−data BlockAttributes = BlockAttributes
342− { blockAttrName :: Maybe String
343− , blockAttrCaption :: Maybe (F Inlines)
344− , blockAttrKeyValues :: [(String, String)]
345− }
346−
347−stringyMetaAttribute :: (String -> Bool) -> OrgParser (String, String)
348−stringyMetaAttribute attrCheck = try $ do
349− metaLineStart
350− attrName <- map toUpper <$> many1Till nonspaceChar (char ':')
351− guard $ attrCheck attrName
352− skipSpaces
353− attrValue <- anyLine
354− return (attrName, attrValue)
355−
356−blockAttributes :: OrgParser BlockAttributes
357−blockAttributes = try $ do
358− kv <- many (stringyMetaAttribute attrCheck)
359− let caption = foldl' (appendValues "CAPTION") Nothing kv
360− let kvAttrs = foldl' (appendValues "ATTR_HTML") Nothing kv
361− let name = lookup "NAME" kv
362− caption' <- maybe (return Nothing)
363− (fmap Just . parseFromString parseInlines)
364− caption
365− kvAttrs' <- parseFromString keyValues . (++ "\n") $ fromMaybe mempty kvAttrs
366− return $ BlockAttributes
367− { blockAttrName = name
368− , blockAttrCaption = caption'
369− , blockAttrKeyValues = kvAttrs'
370− }
371− where
372− attrCheck :: String -> Bool
373− attrCheck attr =
374− case attr of
375− "NAME" -> True
376− "CAPTION" -> True
377− "ATTR_HTML" -> True
378− _ -> False
379−
380− appendValues :: String -> Maybe String -> (String, String) -> Maybe String
381− appendValues attrName accValue (key, value) =
382− if key /= attrName
383− then accValue
384− else case accValue of
385− Just acc -> Just $ acc ++ ' ':value
386− Nothing -> Just value
387−
388−keyValues :: OrgParser [(String, String)]
389−keyValues = try $
390− manyTill ((,) <$> key <*> value) newline
391− where
392− key :: OrgParser String
393− key = try $ skipSpaces *> char ':' *> many1 nonspaceChar
394−
395− value :: OrgParser String
396− value = skipSpaces *> manyTill anyChar endOfValue
397−
398− endOfValue :: OrgParser ()
399− endOfValue =
400− lookAhead $ (() <$ try (many1 spaceChar <* key))
401− <|> () <$ P.newline
402−
403−
404−--
405−-- Org Blocks (#+BEGIN_... / #+END_...)
406−--
407−
408−type BlockProperties = (Int, String) -- (Indentation, Block-Type)
409−
410−updateIndent :: BlockProperties -> Int -> BlockProperties
411−updateIndent (_, blkType) indent = (indent, blkType)
412−
413−orgBlock :: OrgParser (F Blocks)
414−orgBlock = try $ do
415− blockAttrs <- blockAttributes
416− blockProp@(_, blkType) <- blockHeaderStart
417− ($ blockProp) $
418− case blkType of
419− "comment" -> withRaw' (const mempty)
420− "html" -> withRaw' (return . (B.rawBlock blkType))
421− "latex" -> withRaw' (return . (B.rawBlock blkType))
422− "ascii" -> withRaw' (return . (B.rawBlock blkType))
423− "example" -> withRaw' (return . exampleCode)
424− "quote" -> withParsed (fmap B.blockQuote)
425− "verse" -> verseBlock
426− "src" -> codeBlock blockAttrs
427− _ -> withParsed (fmap $ divWithClass blkType)
428−
429−blockHeaderStart :: OrgParser (Int, String)
430−blockHeaderStart = try $ (,) <$> indentation <*> blockType
431− where
432− blockType = map toLower <$> (stringAnyCase "#+begin_" *> orgArgWord)
433−
434−indentation :: OrgParser Int
435−indentation = try $ do
436− tabStop <- getOption readerTabStop
437− s <- many spaceChar
438− return $ spaceLength tabStop s
439−
440−spaceLength :: Int -> String -> Int
441−spaceLength tabStop s = (sum . map charLen) s
442− where
443− charLen ' ' = 1
444− charLen '\t' = tabStop
445− charLen _ = 0
446−
447−withRaw' :: (String -> F Blocks) -> BlockProperties -> OrgParser (F Blocks)
448−withRaw' f blockProp = (ignHeaders *> (f <$> rawBlockContent blockProp))
449−
450−withParsed :: (F Blocks -> F Blocks) -> BlockProperties -> OrgParser (F Blocks)
451−withParsed f blockProp = (ignHeaders *> (f <$> parsedBlockContent blockProp))
452−
453−ignHeaders :: OrgParser ()
454−ignHeaders = (() <$ newline) <|> (() <$ anyLine)
455−
456−divWithClass :: String -> Blocks -> Blocks
457−divWithClass cls = B.divWith ("", [cls], [])
458−
459−verseBlock :: BlockProperties -> OrgParser (F Blocks)
460−verseBlock blkProp = try $ do
461− ignHeaders
462− content <- rawBlockContent blkProp
463− fmap B.para . mconcat . intersperse (pure B.linebreak)
464− <$> mapM (parseFromString parseInlines) (map (++ "\n") . lines $ content)
465−
466−exportsCode :: [(String, String)] -> Bool
467−exportsCode attrs = not (("rundoc-exports", "none") `elem` attrs
468− || ("rundoc-exports", "results") `elem` attrs)
469−
470−exportsResults :: [(String, String)] -> Bool
471−exportsResults attrs = ("rundoc-exports", "results") `elem` attrs
472− || ("rundoc-exports", "both") `elem` attrs
473−
474−followingResultsBlock :: OrgParser (Maybe (F Blocks))
475−followingResultsBlock =
476− optionMaybe (try $ blanklines *> stringAnyCase "#+RESULTS:"
477− *> blankline
478− *> block)
479−
480−codeBlock :: BlockAttributes -> BlockProperties -> OrgParser (F Blocks)
481−codeBlock blockAttrs blkProp = do
482− skipSpaces
483− (classes, kv) <- codeHeaderArgs <|> (mempty <$ ignHeaders)
484− leadingIndent <- lookAhead indentation
485− content <- rawBlockContent (updateIndent blkProp leadingIndent)
486− resultsContent <- followingResultsBlock
487− let id' = fromMaybe mempty $ blockAttrName blockAttrs
488− let includeCode = exportsCode kv
489− let includeResults = exportsResults kv
490− let codeBlck = B.codeBlockWith ( id', classes, kv ) content
491− let labelledBlck = maybe (pure codeBlck)
492− (labelDiv codeBlck)
493− (blockAttrCaption blockAttrs)
494− let resultBlck = fromMaybe mempty resultsContent
495− return $ (if includeCode then labelledBlck else mempty)
496− <> (if includeResults then resultBlck else mempty)
497− where
498− labelDiv blk value =
499− B.divWith nullAttr <$> (mappend <$> labelledBlock value
500− <*> pure blk)
501− labelledBlock = fmap (B.plain . B.spanWith ("", ["label"], []))
502−
503−rawBlockContent :: BlockProperties -> OrgParser String
504−rawBlockContent (indent, blockType) = try $
505− unlines . map commaEscaped <$> manyTill indentedLine blockEnder
506− where
507− indentedLine = try $ ("" <$ blankline) <|> (indentWith indent *> anyLine)
508− blockEnder = try $ skipSpaces *> stringAnyCase ("#+end_" <> blockType)
509−
510−parsedBlockContent :: BlockProperties -> OrgParser (F Blocks)
511−parsedBlockContent blkProps = try $ do
512− raw <- rawBlockContent blkProps
513− parseFromString parseBlocks (raw ++ "\n")
514−
515−-- indent by specified number of spaces (or equiv. tabs)
516−indentWith :: Int -> OrgParser String
517−indentWith num = do
518− tabStop <- getOption readerTabStop
519− if num < tabStop
520− then count num (char ' ')
521− else choice [ try (count num (char ' '))
522− , try (char '\t' >> count (num - tabStop) (char ' ')) ]
523−
524−type SwitchOption = (Char, Maybe String)
525−
526−orgArgWord :: OrgParser String
527−orgArgWord = many1 orgArgWordChar
528−
529−-- | Parse code block arguments
530−-- TODO: We currently don't handle switches.
531−codeHeaderArgs :: OrgParser ([String], [(String, String)])
532−codeHeaderArgs = try $ do
533− language <- skipSpaces *> orgArgWord
534− _ <- skipSpaces *> (try $ switch `sepBy` (many1 spaceChar))
535− parameters <- manyTill blockOption newline
536− let pandocLang = translateLang language
537− return $
538− if hasRundocParameters parameters
539− then ( [ pandocLang, rundocBlockClass ]
540− , map toRundocAttrib (("language", language) : parameters)
541− )
542− else ([ pandocLang ], parameters)
543− where hasRundocParameters = not . null
544−
545−switch :: OrgParser SwitchOption
546−switch = try $ simpleSwitch <|> lineNumbersSwitch
547− where
548− simpleSwitch = (\c -> (c, Nothing)) <$> (oneOf "-+" *> letter)
549− lineNumbersSwitch = (\ls -> ('l', Just ls)) <$>
550− (string "-l \"" *> many1Till nonspaceChar (char '"'))
551−
552−translateLang :: String -> String
553−translateLang "C" = "c"
554−translateLang "C++" = "cpp"
555−translateLang "emacs-lisp" = "commonlisp" -- emacs lisp is not supported
556−translateLang "js" = "javascript"
557−translateLang "lisp" = "commonlisp"
558−translateLang "R" = "r"
559−translateLang "sh" = "bash"
560−translateLang "sqlite" = "sql"
561−translateLang cs = cs
562−
563−-- | Prefix used for Rundoc classes and arguments.
564−rundocPrefix :: String
565−rundocPrefix = "rundoc-"
566−
567−-- | The class-name used to mark rundoc blocks.
568−rundocBlockClass :: String
569−rundocBlockClass = rundocPrefix ++ "block"
570−
571−blockOption :: OrgParser (String, String)
572−blockOption = try $ do
573− argKey <- orgArgKey
574− paramValue <- option "yes" orgParamValue
575− return (argKey, paramValue)
576−
577−inlineBlockOption :: OrgParser (String, String)
578−inlineBlockOption = try $ do
579− argKey <- orgArgKey
580− paramValue <- option "yes" orgInlineParamValue
581− return (argKey, paramValue)
582−
583−orgArgKey :: OrgParser String
584−orgArgKey = try $
585− skipSpaces *> char ':'
586− *> many1 orgArgWordChar
587−
588−orgParamValue :: OrgParser String
589−orgParamValue = try $
590− skipSpaces
591− *> notFollowedBy (char ':' )
592− *> many1 (noneOf "\t\n\r ")
593− <* skipSpaces
594−
595−orgInlineParamValue :: OrgParser String
596−orgInlineParamValue = try $
597− skipSpaces
598− *> notFollowedBy (char ':')
599− *> many1 (noneOf "\t\n\r ]")
600− <* skipSpaces
601−
602−orgArgWordChar :: OrgParser Char
603−orgArgWordChar = alphaNum <|> oneOf "-_"
604−
605−toRundocAttrib :: (String, String) -> (String, String)
606−toRundocAttrib = first ("rundoc-" ++)
607−
608−commaEscaped :: String -> String
609−commaEscaped (',':cs@('*':_)) = cs
610−commaEscaped (',':cs@('#':'+':_)) = cs
611−commaEscaped cs = cs
612−
613−example :: OrgParser (F Blocks)
614−example = try $ do
615− return . return . exampleCode =<< unlines <$> many1 exampleLine
616−
617−exampleCode :: String -> Blocks
618−exampleCode = B.codeBlockWith ("", ["example"], [])
619−
620−exampleLine :: OrgParser String
621−exampleLine = try $ skipSpaces *> string ": " *> anyLine
622−
623−
624−--
625−-- Drawers
626−--
627−
628−-- | A generic drawer which has no special meaning for org-mode.
629−-- Whether or not this drawer is included in the output depends on the drawers
630−-- export setting.
631−genericDrawer :: OrgParser (F Blocks)
632−genericDrawer = try $ do
633− name <- map toUpper <$> drawerStart
634− content <- manyTill drawerLine (try drawerEnd)
635− state <- getState
636− -- Include drawer if it is explicitly included in or not explicitly excluded
637− -- from the list of drawers that should be exported. PROPERTIES drawers are
638− -- never exported.
639− case (exportDrawers . orgStateExportSettings $ state) of
640− _ | name == "PROPERTIES" -> return mempty
641− Left names | name `elem` names -> return mempty
642− Right names | name `notElem` names -> return mempty
643− _ -> drawerDiv name <$> parseLines content
644− where
645− parseLines :: [String] -> OrgParser (F Blocks)
646− parseLines = parseFromString parseBlocks . (++ "\n") . unlines
647−
648− drawerDiv :: String -> F Blocks -> F Blocks
649− drawerDiv drawerName = fmap $ B.divWith (mempty, [drawerName, "drawer"], mempty)
650−
651−drawerStart :: OrgParser String
652−drawerStart = try $
653− skipSpaces *> drawerName <* skipSpaces <* newline
654− where drawerName = char ':' *> manyTill nonspaceChar (char ':')
655−
656−drawerLine :: OrgParser String
657−drawerLine = anyLine
658−
659−drawerEnd :: OrgParser String
660−drawerEnd = try $
661− skipSpaces *> stringAnyCase ":END:" <* skipSpaces <* newline
662−
663−-- | Read a :PROPERTIES: drawer and return the key/value pairs contained
664−-- within.
665−propertiesDrawer :: OrgParser [(String, String)]
666−propertiesDrawer = try $ do
667− drawerType <- drawerStart
668− guard $ map toUpper drawerType == "PROPERTIES"
669− manyTill property (try drawerEnd)
670− where
671− property :: OrgParser (String, String)
672− property = try $ (,) <$> key <*> value
673−
674− key :: OrgParser String
675− key = try $ skipSpaces *> char ':' *> many1Till nonspaceChar (char ':')
676−
677− value :: OrgParser String
678− value = try $ skipSpaces *> manyTill anyChar (try $ skipSpaces *> P.newline)
679−
680−keyValuesToAttr :: [(String, String)] -> Attr
681−keyValuesToAttr kvs =
682− let
683− lowerKvs = map (\(k, v) -> (map toLower k, v)) kvs
684− id' = fromMaybe mempty . lookup "custom_id" $ lowerKvs
685− cls = fromMaybe mempty . lookup "class" $ lowerKvs
686− kvs' = filter (flip notElem ["custom_id", "class"] . fst) lowerKvs
687− in
688− (id', words cls, kvs')
689−
690−
691−--
692−-- Figures
693−--
694−
695−-- | Figures (Image on a line by itself, preceded by name and/or caption)
696−figure :: OrgParser (F Blocks)
697−figure = try $ do
698− figAttrs <- blockAttributes
699− src <- skipSpaces *> selfTarget <* skipSpaces <* P.newline
700− guard . not . isNothing . blockAttrCaption $ figAttrs
701− guard (isImageFilename src)
702− let figName = fromMaybe mempty $ blockAttrName figAttrs
703− let figCaption = fromMaybe mempty $ blockAttrCaption figAttrs
704− let figKeyVals = blockAttrKeyValues figAttrs
705− let attr = (mempty, mempty, figKeyVals)
706− return $ (B.para . B.imageWith attr src (withFigPrefix figName) <$> figCaption)
707− where
708− withFigPrefix cs =
709− if "fig:" `isPrefixOf` cs
710− then cs
711− else "fig:" ++ cs
712−
713−--
714−-- Comments, Options and Metadata
715−--
716−specialLine :: OrgParser (F Blocks)
717−specialLine = fmap return . try $ metaLine <|> commentLine
718−
719−metaLine :: OrgParser Blocks
720−metaLine = mempty <$ metaLineStart <* (optionLine <|> declarationLine)
721−
722−-- The order, in which blocks are tried, makes sure that we're not looking at
723−-- the beginning of a block, so we don't need to check for it
724−metaLineStart :: OrgParser ()
725−metaLineStart = try $ skipSpaces <* string "#+"
726−
727−commentLine :: OrgParser Blocks
728−commentLine = commentLineStart *> anyLine *> pure mempty
729−
730−commentLineStart :: OrgParser ()
731−commentLineStart = try $ skipSpaces <* string "# "
732−
733−declarationLine :: OrgParser ()
734−declarationLine = try $ do
735− key <- metaKey
736− inlinesF <- metaInlines
737− updateState $ \st ->
738− let meta' = B.setMeta <$> pure key <*> inlinesF <*> pure nullMeta
739− in st { orgStateMeta' = orgStateMeta' st <> meta' }
740− return ()
741−
742−metaInlines :: OrgParser (F MetaValue)
743−metaInlines = fmap (MetaInlines . B.toList) <$> inlinesTillNewline
744−
745−metaKey :: OrgParser String
746−metaKey = map toLower <$> many1 (noneOf ": \n\r")
747− <* char ':'
748− <* skipSpaces
749−
750−optionLine :: OrgParser ()
751−optionLine = try $ do
752− key <- metaKey
753− case key of
754− "link" -> parseLinkFormat >>= uncurry addLinkFormat
755− "options" -> () <$ sepBy spaces exportSetting
756− _ -> mzero
757−
758−parseLinkFormat :: OrgParser ((String, String -> String))
759−parseLinkFormat = try $ do
760− linkType <- (:) <$> letter <*> many (alphaNum <|> oneOf "-_") <* skipSpaces
761− linkSubst <- parseFormat
762− return (linkType, linkSubst)
763−
764−-- | An ad-hoc, single-argument-only implementation of a printf-style format
765−-- parser.
766−parseFormat :: OrgParser (String -> String)
767−parseFormat = try $ do
768− replacePlain <|> replaceUrl <|> justAppend
769− where
770− -- inefficient, but who cares
771− replacePlain = try $ (\x -> concat . flip intersperse x)
772− <$> sequence [tillSpecifier 's', rest]
773− replaceUrl = try $ (\x -> concat . flip intersperse x . urlEncode)
774− <$> sequence [tillSpecifier 'h', rest]
775− justAppend = try $ (++) <$> rest
776−
777− rest = manyTill anyChar (eof <|> () <$ oneOf "\n\r")
778− tillSpecifier c = manyTill (noneOf "\n\r") (try $ string ('%':c:""))
779−
780−--
781−-- Headers
782−--
783−
784−-- | Headers
785−header :: OrgParser (F Blocks)
786−header = try $ do
787− level <- headerStart
788− title <- manyTill inline (lookAhead $ optional headerTags <* P.newline)
789− tags <- option [] headerTags
790− newline
791− propAttr <- option nullAttr (keyValuesToAttr <$> propertiesDrawer)
792− inlines <- runF (tagTitle title tags) <$> getState
793− attr <- registerHeader propAttr inlines
794− return $ pure (B.headerWith attr level inlines)
795− where
796− tagTitle :: [F Inlines] -> [String] -> F Inlines
797− tagTitle title tags = trimInlinesF . mconcat $ title <> map tagToInlineF tags
798−
799− tagToInlineF :: String -> F Inlines
800− tagToInlineF t = return $ B.spanWith ("", ["tag"], [("data-tag-name", t)]) mempty
801−
802− headerTags :: OrgParser [String]
803− headerTags = try $
804− let tag = many1 (alphaNum <|> oneOf "@%#_") <* char ':'
805− in skipSpaces
806− *> char ':'
807− *> many1 tag
808− <* skipSpaces
809−
810−headerStart :: OrgParser Int
811−headerStart = try $
812− (length <$> many1 (char '*')) <* many1 (char ' ') <* updateLastPreCharPos
813−
814−
815−-- Don't use (or need) the reader wrapper here, we want hline to be
816−-- @show@able. Otherwise we can't use it with @notFollowedBy'@.
817−
818−-- | Horizontal Line (five -- dashes or more)
819−hline :: OrgParser Blocks
820−hline = try $ do
821− skipSpaces
822− string "-----"
823− many (char '-')
824− skipSpaces
825− newline
826− return B.horizontalRule
827−
828−--
829−-- Tables
830−--
831−
832−data OrgTableRow = OrgContentRow (F [Blocks])
833− | OrgAlignRow [Alignment]
834− | OrgHlineRow
835−
836−-- OrgTable is strongly related to the pandoc table ADT. Using the same
837−-- (i.e. pandoc-global) ADT would mean that the reader would break if the
838−-- global structure was to be changed, which would be bad. The final table
839−-- should be generated using a builder function. Column widths aren't
840−-- implemented yet, so they are not tracked here.
841−data OrgTable = OrgTable
842− { orgTableAlignments :: [Alignment]
843− , orgTableHeader :: [Blocks]
844− , orgTableRows :: [[Blocks]]
845− }
846−
847−table :: OrgParser (F Blocks)
848−table = try $ do
849− blockAttrs <- blockAttributes
850− lookAhead tableStart
851− do
852− rows <- tableRows
853− let caption = fromMaybe (return mempty) $ blockAttrCaption blockAttrs
854− return $ (<$> caption) . orgToPandocTable . normalizeTable =<< rowsToTable rows
855−
856−orgToPandocTable :: OrgTable
857− -> Inlines
858− -> Blocks
859−orgToPandocTable (OrgTable aligns heads lns) caption =
860− B.table caption (zip aligns $ repeat 0) heads lns
861−
862−tableStart :: OrgParser Char
863−tableStart = try $ skipSpaces *> char '|'
864−
865−tableRows :: OrgParser [OrgTableRow]
866−tableRows = try $ many (tableAlignRow <|> tableHline <|> tableContentRow)
867−
868−tableContentRow :: OrgParser OrgTableRow
869−tableContentRow = try $
870− OrgContentRow . sequence <$> (tableStart *> many1Till tableContentCell newline)
871−
872−tableContentCell :: OrgParser (F Blocks)
873−tableContentCell = try $
874− fmap B.plain . trimInlinesF . mconcat <$> manyTill inline endOfCell
875−
876−tableAlignRow :: OrgParser OrgTableRow
877−tableAlignRow = try $ do
878− tableStart
879− cells <- many1Till tableAlignCell newline
880− -- Empty rows are regular (i.e. content) rows, not alignment rows.
881− guard $ any (/= AlignDefault) cells
882− return $ OrgAlignRow cells
883−
884−tableAlignCell :: OrgParser Alignment
885−tableAlignCell =
886− choice [ try $ emptyCell *> return AlignDefault
887− , try $ skipSpaces
888− *> char '<'
889− *> tableAlignFromChar
890− <* many digit
891− <* char '>'
892− <* emptyCell
893− ] <?> "alignment info"
894− where emptyCell = try $ skipSpaces *> endOfCell
895−
896−tableAlignFromChar :: OrgParser Alignment
897−tableAlignFromChar = try $
898− choice [ char 'l' *> return AlignLeft
899− , char 'c' *> return AlignCenter
900− , char 'r' *> return AlignRight
901− ]
902−
903−tableHline :: OrgParser OrgTableRow
904−tableHline = try $
905− OrgHlineRow <$ (tableStart *> char '-' *> anyLine)
906−
907−endOfCell :: OrgParser Char
908−endOfCell = try $ char '|' <|> lookAhead newline
909−
910−rowsToTable :: [OrgTableRow]
911− -> F OrgTable
912−rowsToTable = foldM rowToContent emptyTable
913− where emptyTable = OrgTable mempty mempty mempty
914−
915−normalizeTable :: OrgTable -> OrgTable
916−normalizeTable (OrgTable aligns heads rows) = OrgTable aligns' heads rows
917− where
918− refRow = if heads /= mempty
919− then heads
920− else if rows == mempty then mempty else head rows
921− cols = length refRow
922− fillColumns base padding = take cols $ base ++ repeat padding
923− aligns' = fillColumns aligns AlignDefault
924−
925−-- One or more horizontal rules after the first content line mark the previous
926−-- line as a header. All other horizontal lines are discarded.
927−rowToContent :: OrgTable
928− -> OrgTableRow
929− -> F OrgTable
930−rowToContent orgTable row =
931− case row of
932− OrgHlineRow -> return singleRowPromotedToHeader
933− OrgAlignRow as -> return . setAligns $ as
934− OrgContentRow cs -> appendToBody cs
935− where
936− singleRowPromotedToHeader :: OrgTable
937− singleRowPromotedToHeader = case orgTable of
938− OrgTable{ orgTableHeader = [], orgTableRows = b:[] } ->
939− orgTable{ orgTableHeader = b , orgTableRows = [] }
940− _ -> orgTable
941−
942− setAligns :: [Alignment] -> OrgTable
943− setAligns aligns = orgTable{ orgTableAlignments = aligns }
944−
945− appendToBody :: F [Blocks] -> F OrgTable
946− appendToBody frow = do
947− newRow <- frow
948− let oldRows = orgTableRows orgTable
949− -- NOTE: This is an inefficient O(n) operation. This should be changed
950− -- if performance ever becomes a problem.
951− return orgTable{ orgTableRows = oldRows ++ [newRow] }
952−
953−
954−--
955−-- LaTeX fragments
956−--
957−latexFragment :: OrgParser (F Blocks)
958−latexFragment = try $ do
959− envName <- latexEnvStart
960− content <- mconcat <$> manyTill anyLineNewline (latexEnd envName)
961− return . return $ B.rawBlock "latex" (content `inLatexEnv` envName)
962− where
963− c `inLatexEnv` e = mconcat [ "\\begin{", e, "}\n"
964− , c
965− , "\\end{", e, "}\n"
966− ]
967−
968−latexEnvStart :: OrgParser String
969−latexEnvStart = try $ do
970− skipSpaces *> string "\\begin{"
971− *> latexEnvName
972− <* string "}"
973− <* blankline
974−
975−latexEnd :: String -> OrgParser ()
976−latexEnd envName = try $
977− () <$ skipSpaces
978− <* string ("\\end{" ++ envName ++ "}")
979− <* blankline
980−
981−-- | Parses a LaTeX environment name.
982−latexEnvName :: OrgParser String
983−latexEnvName = try $ do
984− mappend <$> many1 alphaNum
985− <*> option "" (string "*")
986−
987−
988−--
989−-- Footnote defintions
990−--
991−noteBlock :: OrgParser (F Blocks)
992−noteBlock = try $ do
993− ref <- noteMarker <* skipSpaces
994− content <- mconcat <$> blocksTillHeaderOrNote
995− addToNotesTable (ref, content)
996− return mempty
997− where
998− blocksTillHeaderOrNote =
999− many1Till block (eof <|> () <$ lookAhead noteMarker
1000− <|> () <$ lookAhead headerStart)
1001−
1002−-- Paragraphs or Plain text
1003−paraOrPlain :: OrgParser (F Blocks)
1004−paraOrPlain = try $ do
1005− ils <- parseInlines
1006− nl <- option False (newline *> return True)
1007− -- Read block as paragraph, except if we are in a list context and the block
1008− -- is directly followed by a list item, in which case the block is read as
1009− -- plain text.
1010− try (guard nl
1011− *> notFollowedBy (inList *> (orderedListStart <|> bulletListStart))
1012− *> return (B.para <$> ils))
1013− <|> (return (B.plain <$> ils))
1014−
1015−inlinesTillNewline :: OrgParser (F Inlines)
1016−inlinesTillNewline = trimInlinesF . mconcat <$> manyTill inline newline
1017−
1018−
1019−--
1020−-- list blocks
1021−--
1022−
1023−list :: OrgParser (F Blocks)
1024−list = choice [ definitionList, bulletList, orderedList ] <?> "list"
1025−
1026−definitionList :: OrgParser (F Blocks)
1027−definitionList = try $ do n <- lookAhead (bulletListStart' Nothing)
1028− fmap B.definitionList . fmap compactify'DL . sequence
1029− <$> many1 (definitionListItem $ bulletListStart' (Just n))
1030−
1031−bulletList :: OrgParser (F Blocks)
1032−bulletList = try $ do n <- lookAhead (bulletListStart' Nothing)
1033− fmap B.bulletList . fmap compactify' . sequence
1034− <$> many1 (listItem (bulletListStart' $ Just n))
1035−
1036−orderedList :: OrgParser (F Blocks)
1037−orderedList = fmap B.orderedList . fmap compactify' . sequence
1038− <$> many1 (listItem orderedListStart)
1039−
1040−genericListStart :: OrgParser String
1041− -> OrgParser Int
1042−genericListStart listMarker = try $
1043− (+) <$> (length <$> many spaceChar)
1044− <*> (length <$> listMarker <* many1 spaceChar)
1045−
1046−-- parses bullet list marker. maybe we know the indent level
1047−bulletListStart :: OrgParser Int
1048−bulletListStart = bulletListStart' Nothing
1049−
1050−bulletListStart' :: Maybe Int -> OrgParser Int
1051−-- returns length of bulletList prefix, inclusive of marker
1052−bulletListStart' Nothing = do ind <- length <$> many spaceChar
1053− when (ind == 0) $ notFollowedBy (char '*')
1054− oneOf bullets
1055− many1 spaceChar
1056− return (ind + 1)
1057− -- Unindented lists are legal, but they can't use '*' bullets
1058− -- We return n to maintain compatibility with the generic listItem
1059−bulletListStart' (Just n) = do count (n-1) spaceChar
1060− when (n == 1) $ notFollowedBy (char '*')
1061− oneOf bullets
1062− many1 spaceChar
1063− return n
1064−
1065−bullets :: String
1066−bullets = "*+-"
1067−
1068−orderedListStart :: OrgParser Int
1069−orderedListStart = genericListStart orderedListMarker
1070− -- Ordered list markers allowed in org-mode
1071− where orderedListMarker = mappend <$> many1 digit <*> (pure <$> oneOf ".)")
1072−
1073−definitionListItem :: OrgParser Int
1074− -> OrgParser (F (Inlines, [Blocks]))
1075−definitionListItem parseMarkerGetLength = try $ do
1076− markerLength <- parseMarkerGetLength
1077− term <- manyTill (noneOf "\n\r") (try definitionMarker)
1078− line1 <- anyLineNewline
1079− blank <- option "" ("\n" <$ blankline)
1080− cont <- concat <$> many (listContinuation markerLength)
1081− term' <- parseFromString parseInlines term
1082− contents' <- parseFromString parseBlocks $ line1 ++ blank ++ cont
1083− return $ (,) <$> term' <*> fmap (:[]) contents'
1084− where
1085− definitionMarker =
1086− spaceChar *> string "::" <* (spaceChar <|> lookAhead P.newline)
1087−
1088−
1089−-- parse raw text for one list item, excluding start marker and continuations
1090−listItem :: OrgParser Int
1091− -> OrgParser (F Blocks)
1092−listItem start = try . withContext ListItemState $ do
1093− markerLength <- try start
1094− firstLine <- anyLineNewline
1095− blank <- option "" ("\n" <$ blankline)
1096− rest <- concat <$> many (listContinuation markerLength)
1097− parseFromString parseBlocks $ firstLine ++ blank ++ rest
1098−
1099−-- continuation of a list item - indented and separated by blankline or endline.
1100−-- Note: nested lists are parsed as continuations.
1101−listContinuation :: Int
1102− -> OrgParser String
1103−listContinuation markerLength = try $
1104− notFollowedBy' blankline
1105− *> (mappend <$> (concat <$> many1 listLine)
1106− <*> many blankline)
1107− where listLine = try $ indentWith markerLength *> anyLineNewline
1108−
1109−anyLineNewline :: OrgParser String
1110−anyLineNewline = (++ "\n") <$> anyLine
1111−
1112−
1113−--
1114−-- inline
1115−--
1116−
1117−inline :: OrgParser (F Inlines)
1118−inline =
1119− choice [ whitespace
1120− , linebreak
1121− , cite
1122− , footnote
1123− , linkOrImage
1124− , anchor
1125− , inlineCodeBlock
1126− , str
1127− , endline
1128− , emph
1129− , strong
1130− , strikeout
1131− , underline
1132− , code
1133− , math
1134− , displayMath
1135− , verbatim
1136− , subscript
1137− , superscript
1138− , inlineLaTeX
1139− , smart
1140− , symbol
1141− ] <* (guard =<< newlinesCountWithinLimits)
1142− <?> "inline"
1143−
1144−parseInlines :: OrgParser (F Inlines)
1145−parseInlines = trimInlinesF . mconcat <$> many1 inline
1146−
1147−-- treat these as potentially non-text when parsing inline:
1148−specialChars :: [Char]
1149−specialChars = "\"$'()*+-,./:<=>[\\]^_{|}~"
1150−
1151−
1152−whitespace :: OrgParser (F Inlines)
1153−whitespace = pure B.space <$ skipMany1 spaceChar
1154− <* updateLastPreCharPos
1155− <* updateLastForbiddenCharPos
1156− <?> "whitespace"
1157−
1158−linebreak :: OrgParser (F Inlines)
1159−linebreak = try $ pure B.linebreak <$ string "\\\\" <* skipSpaces <* newline
1160−
1161−str :: OrgParser (F Inlines)
1162−str = return . B.str <$> many1 (noneOf $ specialChars ++ "\n\r ")
1163− <* updateLastStrPos
1164−
1165−-- | An endline character that can be treated as a space, not a structural
1166−-- break. This should reflect the values of the Emacs variable
1167−-- @org-element-pagaraph-separate@.
1168−endline :: OrgParser (F Inlines)
1169−endline = try $ do
1170− newline
1171− notFollowedBy blankline
1172− notFollowedBy' exampleLine
1173− notFollowedBy' hline
1174− notFollowedBy' noteMarker
1175− notFollowedBy' tableStart
1176− notFollowedBy' drawerStart
1177− notFollowedBy' headerStart
1178− notFollowedBy' metaLineStart
1179− notFollowedBy' latexEnvStart
1180− notFollowedBy' commentLineStart
1181− notFollowedBy' bulletListStart
1182− notFollowedBy' orderedListStart
1183− decEmphasisNewlinesCount
1184− guard =<< newlinesCountWithinLimits
1185− updateLastPreCharPos
1186− return . return $ B.softbreak
1187−
1188−cite :: OrgParser (F Inlines)
1189−cite = try $ do
1190− guardEnabled Ext_citations
1191− (cs, raw) <- withRaw normalCite
1192− return $ (flip B.cite (B.text raw)) <$> cs
1193−
1194−normalCite :: OrgParser (F [Citation])
1195−normalCite = try $ char '['
1196− *> skipSpaces
1197− *> citeList
1198− <* skipSpaces
1199− <* char ']'
1200−
1201−citeList :: OrgParser (F [Citation])
1202−citeList = sequence <$> sepBy1 citation (try $ char ';' *> skipSpaces)
1203−
1204−citation :: OrgParser (F Citation)
1205−citation = try $ do
1206− pref <- prefix
1207− (suppress_author, key) <- citeKey
1208− suff <- suffix
1209− return $ do
1210− x <- pref
1211− y <- suff
1212− return $ Citation{ citationId = key
1213− , citationPrefix = B.toList x
1214− , citationSuffix = B.toList y
1215− , citationMode = if suppress_author
1216− then SuppressAuthor
1217− else NormalCitation
1218− , citationNoteNum = 0
1219− , citationHash = 0
1220− }
1221− where
1222− prefix = trimInlinesF . mconcat <$>
1223− manyTill inline (char ']' <|> (']' <$ lookAhead citeKey))
1224− suffix = try $ do
1225− hasSpace <- option False (notFollowedBy nonspaceChar >> return True)
1226− skipSpaces
1227− rest <- trimInlinesF . mconcat <$>
1228− many (notFollowedBy (oneOf ";]") *> inline)
1229− return $ if hasSpace
1230− then (B.space <>) <$> rest
1231− else rest
1232−
1233−footnote :: OrgParser (F Inlines)
1234−footnote = try $ inlineNote <|> referencedNote
1235−
1236−inlineNote :: OrgParser (F Inlines)
1237−inlineNote = try $ do
1238− string "[fn:"
1239− ref <- many alphaNum
1240− char ':'
1241− note <- fmap B.para . trimInlinesF . mconcat <$> many1Till inline (char ']')
1242− when (not $ null ref) $
1243− addToNotesTable ("fn:" ++ ref, note)
1244− return $ B.note <$> note
1245−
1246−referencedNote :: OrgParser (F Inlines)
1247−referencedNote = try $ do
1248− ref <- noteMarker
1249− return $ do
1250− notes <- asksF orgStateNotes'
1251− case lookup ref notes of
1252− Nothing -> return $ B.str $ "[" ++ ref ++ "]"
1253− Just contents -> do
1254− st <- askF
1255− let contents' = runF contents st{ orgStateNotes' = [] }
1256− return $ B.note contents'
1257−
1258−noteMarker :: OrgParser String
1259−noteMarker = try $ do
1260− char '['
1261− choice [ many1Till digit (char ']')
1262− , (++) <$> string "fn:"
1263− <*> many1Till (noneOf "\n\r\t ") (char ']')
1264− ]
1265−
1266−linkOrImage :: OrgParser (F Inlines)
1267−linkOrImage = explicitOrImageLink
1268− <|> selflinkOrImage
1269− <|> angleLink
1270− <|> plainLink
1271− <?> "link or image"
1272−
1273−explicitOrImageLink :: OrgParser (F Inlines)
1274−explicitOrImageLink = try $ do
1275− char '['
1276− srcF <- applyCustomLinkFormat =<< possiblyEmptyLinkTarget
1277− title <- enclosedRaw (char '[') (char ']')
1278− title' <- parseFromString (mconcat <$> many inline) title
1279− char ']'
1280− return $ do
1281− src <- srcF
1282− if isImageFilename title
1283− then pure $ B.link src "" $ B.image title mempty mempty
1284− else linkToInlinesF src =<< title'
1285−
1286−selflinkOrImage :: OrgParser (F Inlines)
1287−selflinkOrImage = try $ do
1288− src <- char '[' *> linkTarget <* char ']'
1289− return $ linkToInlinesF src (B.str src)
1290−
1291−plainLink :: OrgParser (F Inlines)
1292−plainLink = try $ do
1293− (orig, src) <- uri
1294− returnF $ B.link src "" (B.str orig)
1295−
1296−angleLink :: OrgParser (F Inlines)
1297−angleLink = try $ do
1298− char '<'
1299− link <- plainLink
1300− char '>'
1301− return link
1302−
1303−selfTarget :: OrgParser String
1304−selfTarget = try $ char '[' *> linkTarget <* char ']'
1305−
1306−linkTarget :: OrgParser String
1307−linkTarget = enclosedByPair '[' ']' (noneOf "\n\r[]")
1308−
1309−possiblyEmptyLinkTarget :: OrgParser String
1310−possiblyEmptyLinkTarget = try linkTarget <|> ("" <$ string "[]")
1311−
1312−applyCustomLinkFormat :: String -> OrgParser (F String)
1313−applyCustomLinkFormat link = do
1314− let (linkType, rest) = break (== ':') link
1315− return $ do
1316− formatter <- M.lookup linkType <$> asksF orgStateLinkFormatters
1317− return $ maybe link ($ drop 1 rest) formatter
1318−
1319−-- | Take a link and return a function which produces new inlines when given
1320−-- description inlines.
1321−linkToInlinesF :: String -> Inlines -> F Inlines
1322−linkToInlinesF linkStr =
1323− case linkStr of
1324− "" -> pure . B.link mempty "" -- wiki link (empty by convention)
1325− ('#':_) -> pure . B.link linkStr "" -- document-local fraction
1326− _ -> case cleanLinkString linkStr of
1327− (Just cleanedLink) -> if isImageFilename cleanedLink
1328− then const . pure $ B.image cleanedLink "" ""
1329− else pure . B.link cleanedLink ""
1330− Nothing -> internalLink linkStr -- other internal link
1331−
1332−-- | Cleanup and canonicalize a string describing a link. Return @Nothing@ if
1333−-- the string does not appear to be a link.
1334−cleanLinkString :: String -> Maybe String
1335−cleanLinkString s =
1336− case s of
1337− '/':_ -> Just $ "file://" ++ s -- absolute path
1338− '.':'/':_ -> Just s -- relative path
1339− '.':'.':'/':_ -> Just s -- relative path
1340− -- Relative path or URL (file schema)
1341− 'f':'i':'l':'e':':':s' -> Just $ if ("//" `isPrefixOf` s') then s else s'
1342− _ | isUrl s -> Just s -- URL
1343− _ -> Nothing
1344− where
1345− isUrl :: String -> Bool
1346− isUrl cs =
1347− let (scheme, path) = break (== ':') cs
1348− in all (\c -> isAlphaNum c || c `elem` (".-"::String)) scheme
1349− && not (null path)
1350−
1351−isImageFilename :: String -> Bool
1352−isImageFilename filename =
1353− any (\x -> ('.':x) `isSuffixOf` filename) imageExtensions &&
1354− (any (\x -> (x++":") `isPrefixOf` filename) protocols ||
1355− ':' `notElem` filename)
1356− where
1357− imageExtensions = [ "jpeg" , "jpg" , "png" , "gif" , "svg" ]
1358− protocols = [ "file", "http", "https" ]
1359−
1360−internalLink :: String -> Inlines -> F Inlines
1361−internalLink link title = do
1362− anchorB <- (link `elem`) <$> asksF orgStateAnchorIds
1363− if anchorB
1364− then return $ B.link ('#':link) "" title
1365− else return $ B.emph title
1366−
1367−-- | Parse an anchor like @<<anchor-id>>@ and return an empty span with
1368−-- @anchor-id@ set as id. Legal anchors in org-mode are defined through
1369−-- @org-target-regexp@, which is fairly liberal. Since no link is created if
1370−-- @anchor-id@ contains spaces, we are more restrictive in what is accepted as
1371−-- an anchor.
1372−
1373−anchor :: OrgParser (F Inlines)
1374−anchor = try $ do
1375− anchorId <- parseAnchor
1376− recordAnchorId anchorId
1377− returnF $ B.spanWith (solidify anchorId, [], []) mempty
1378− where
1379− parseAnchor = string "<<"
1380− *> many1 (noneOf "\t\n\r<>\"' ")
1381− <* string ">>"
1382− <* skipSpaces
1383−
1384−-- | Replace every char but [a-zA-Z0-9_.-:] with a hypen '-'. This mirrors
1385−-- the org function @org-export-solidify-link-text@.
1386−
1387−solidify :: String -> String
1388−solidify = map replaceSpecialChar
1389− where replaceSpecialChar c
1390− | isAlphaNum c = c
1391− | c `elem` ("_.-:" :: String) = c
1392− | otherwise = '-'
1393−
1394−-- | Parses an inline code block and marks it as an babel block.
1395−inlineCodeBlock :: OrgParser (F Inlines)
1396−inlineCodeBlock = try $ do
1397− string "src_"
1398− lang <- many1 orgArgWordChar
1399− opts <- option [] $ enclosedByPair '[' ']' inlineBlockOption
1400− inlineCode <- enclosedByPair '{' '}' (noneOf "\n\r")
1401− let attrClasses = [translateLang lang, rundocBlockClass]
1402− let attrKeyVal = map toRundocAttrib (("language", lang) : opts)
1403− returnF $ B.codeWith ("", attrClasses, attrKeyVal) inlineCode
1404−
1405−enclosedByPair :: Char -- ^ opening char
1406− -> Char -- ^ closing char
1407− -> OrgParser a -- ^ parser
1408− -> OrgParser [a]
1409−enclosedByPair s e p = char s *> many1Till p (char e)
1410−
1411−emph :: OrgParser (F Inlines)
1412−emph = fmap B.emph <$> emphasisBetween '/'
1413−
1414−strong :: OrgParser (F Inlines)
1415−strong = fmap B.strong <$> emphasisBetween '*'
1416−
1417−strikeout :: OrgParser (F Inlines)
1418−strikeout = fmap B.strikeout <$> emphasisBetween '+'
1419−
1420−-- There is no underline, so we use strong instead.
1421−underline :: OrgParser (F Inlines)
1422−underline = fmap B.strong <$> emphasisBetween '_'
1423−
1424−verbatim :: OrgParser (F Inlines)
1425−verbatim = return . B.code <$> verbatimBetween '='
1426−
1427−code :: OrgParser (F Inlines)
1428−code = return . B.code <$> verbatimBetween '~'
1429−
1430−subscript :: OrgParser (F Inlines)
1431−subscript = fmap B.subscript <$> try (char '_' *> subOrSuperExpr)
1432−
1433−superscript :: OrgParser (F Inlines)
1434−superscript = fmap B.superscript <$> try (char '^' *> subOrSuperExpr)
1435−
1436−math :: OrgParser (F Inlines)
1437−math = return . B.math <$> choice [ math1CharBetween '$'
1438− , mathStringBetween '$'
1439− , rawMathBetween "\\(" "\\)"
1440− ]
1441−
1442−displayMath :: OrgParser (F Inlines)
1443−displayMath = return . B.displayMath <$> choice [ rawMathBetween "\\[" "\\]"
1444− , rawMathBetween "$$" "$$"
1445− ]
1446−
1447−updatePositions :: Char
1448− -> OrgParser (Char)
1449−updatePositions c = do
1450− when (c `elem` emphasisPreChars) updateLastPreCharPos
1451− when (c `elem` emphasisForbiddenBorderChars) updateLastForbiddenCharPos
1452− return c
1453−
1454−symbol :: OrgParser (F Inlines)
1455−symbol = return . B.str . (: "") <$> (oneOf specialChars >>= updatePositions)
1456−
1457−emphasisBetween :: Char
1458− -> OrgParser (F Inlines)
1459−emphasisBetween c = try $ do
1460− startEmphasisNewlinesCounting emphasisAllowedNewlines
1461− res <- enclosedInlines (emphasisStart c) (emphasisEnd c)
1462− isTopLevelEmphasis <- null . orgStateEmphasisCharStack <$> getState
1463− when isTopLevelEmphasis
1464− resetEmphasisNewlines
1465− return res
1466−
1467−verbatimBetween :: Char
1468− -> OrgParser String
1469−verbatimBetween c = try $
1470− emphasisStart c *>
1471− many1TillNOrLessNewlines 1 (noneOf "\n\r") (emphasisEnd c)
1472−
1473−-- | Parses a raw string delimited by @c@ using Org's math rules
1474−mathStringBetween :: Char
1475− -> OrgParser String
1476−mathStringBetween c = try $ do
1477− mathStart c
1478− body <- many1TillNOrLessNewlines mathAllowedNewlines
1479− (noneOf (c:"\n\r"))
1480− (lookAhead $ mathEnd c)
1481− final <- mathEnd c
1482− return $ body ++ [final]
1483−
1484−-- | Parse a single character between @c@ using math rules
1485−math1CharBetween :: Char
1486− -> OrgParser String
1487−math1CharBetween c = try $ do
1488− char c
1489− res <- noneOf $ c:mathForbiddenBorderChars
1490− char c
1491− eof <|> () <$ lookAhead (oneOf mathPostChars)
1492− return [res]
1493−
1494−rawMathBetween :: String
1495− -> String
1496− -> OrgParser String
1497−rawMathBetween s e = try $ string s *> manyTill anyChar (try $ string e)
1498−
1499−-- | Parses the start (opening character) of emphasis
1500−emphasisStart :: Char -> OrgParser Char
1501−emphasisStart c = try $ do
1502− guard =<< afterEmphasisPreChar
1503− guard =<< notAfterString
1504− char c
1505− lookAhead (noneOf emphasisForbiddenBorderChars)
1506− pushToInlineCharStack c
1507− return c
1508−
1509−-- | Parses the closing character of emphasis
1510−emphasisEnd :: Char -> OrgParser Char
1511−emphasisEnd c = try $ do
1512− guard =<< notAfterForbiddenBorderChar
1513− char c
1514− eof <|> () <$ lookAhead acceptablePostChars
1515− updateLastStrPos
1516− popInlineCharStack
1517− return c
1518− where acceptablePostChars =
1519− surroundingEmphasisChar >>= \x -> oneOf (x ++ emphasisPostChars)
1520−
1521−mathStart :: Char -> OrgParser Char
1522−mathStart c = try $
1523− char c <* notFollowedBy' (oneOf (c:mathForbiddenBorderChars))
1524−
1525−mathEnd :: Char -> OrgParser Char
1526−mathEnd c = try $ do
1527− res <- noneOf (c:mathForbiddenBorderChars)
1528− char c
1529− eof <|> () <$ lookAhead (oneOf mathPostChars)
1530− return res
1531−
1532−
1533−enclosedInlines :: OrgParser a
1534− -> OrgParser b
1535− -> OrgParser (F Inlines)
1536−enclosedInlines start end = try $
1537− trimInlinesF . mconcat <$> enclosed start end inline
1538−
1539−enclosedRaw :: OrgParser a
1540− -> OrgParser b
1541− -> OrgParser String
1542−enclosedRaw start end = try $
1543− start *> (onSingleLine <|> spanningTwoLines)
1544− where onSingleLine = try $ many1Till (noneOf "\n\r") end
1545− spanningTwoLines = try $
1546− anyLine >>= \f -> mappend (f <> " ") <$> onSingleLine
1547−
1548−-- | Like many1Till, but parses at most @n+1@ lines. @p@ must not consume
1549−-- newlines.
1550−many1TillNOrLessNewlines :: Int
1551− -> OrgParser Char
1552− -> OrgParser a
1553− -> OrgParser String
1554−many1TillNOrLessNewlines n p end = try $
1555− nMoreLines (Just n) mempty >>= oneOrMore
1556− where
1557− nMoreLines Nothing cs = return cs
1558− nMoreLines (Just 0) cs = try $ (cs ++) <$> finalLine
1559− nMoreLines k cs = try $ (final k cs <|> rest k cs)
1560− >>= uncurry nMoreLines
1561− final _ cs = (\x -> (Nothing, cs ++ x)) <$> try finalLine
1562− rest m cs = (\x -> (minus1 <$> m, cs ++ x ++ "\n")) <$> try (manyTill p P.newline)
1563− finalLine = try $ manyTill p end
1564− minus1 k = k - 1
1565− oneOrMore cs = guard (not $ null cs) *> return cs
1566−
1567−-- Org allows customization of the way it reads emphasis. We use the defaults
1568−-- here (see, e.g., the Emacs Lisp variable `org-emphasis-regexp-components`
1569−-- for details).
1570−
1571−-- | Chars allowed to occur before emphasis (spaces and newlines are ok, too)
1572−emphasisPreChars :: [Char]
1573−emphasisPreChars = "\t \"'({"
1574−
1575−-- | Chars allowed at after emphasis
1576−emphasisPostChars :: [Char]
1577−emphasisPostChars = "\t\n !\"'),-.:;?\\}"
1578−
1579−-- | Chars not allowed at the (inner) border of emphasis
1580−emphasisForbiddenBorderChars :: [Char]
1581−emphasisForbiddenBorderChars = "\t\n\r \"',"
1582−
1583−-- | The maximum number of newlines within
1584−emphasisAllowedNewlines :: Int
1585−emphasisAllowedNewlines = 1
1586−
1587−-- LaTeX-style math: see `org-latex-regexps` for details
1588−
1589−-- | Chars allowed after an inline ($...$) math statement
1590−mathPostChars :: [Char]
1591−mathPostChars = "\t\n \"'),-.:;?"
1592−
1593−-- | Chars not allowed at the (inner) border of math
1594−mathForbiddenBorderChars :: [Char]
1595−mathForbiddenBorderChars = "\t\n\r ,;.$"
1596−
1597−-- | Maximum number of newlines in an inline math statement
1598−mathAllowedNewlines :: Int
1599−mathAllowedNewlines = 2
1600−
1601−-- | Whether we are right behind a char allowed before emphasis
1602−afterEmphasisPreChar :: OrgParser Bool
1603−afterEmphasisPreChar = do
1604− pos <- getPosition
1605− lastPrePos <- orgStateLastPreCharPos <$> getState
1606− return . fromMaybe True $ (== pos) <$> lastPrePos
1607−
1608−-- | Whether the parser is right after a forbidden border char
1609−notAfterForbiddenBorderChar :: OrgParser Bool
1610−notAfterForbiddenBorderChar = do
1611− pos <- getPosition
1612− lastFBCPos <- orgStateLastForbiddenCharPos <$> getState
1613− return $ lastFBCPos /= Just pos
1614−
1615−-- | Read a sub- or superscript expression
1616−subOrSuperExpr :: OrgParser (F Inlines)
1617−subOrSuperExpr = try $
1618− choice [ id <$> charsInBalanced '{' '}' (noneOf "\n\r")
1619− , enclosing ('(', ')') <$> charsInBalanced '(' ')' (noneOf "\n\r")
1620− , simpleSubOrSuperString
1621− ] >>= parseFromString (mconcat <$> many inline)
1622− where enclosing (left, right) s = left : s ++ [right]
1623−
1624−simpleSubOrSuperString :: OrgParser String
1625−simpleSubOrSuperString = try $ do
1626− state <- getState
1627− guard . exportSubSuperscripts . orgStateExportSettings $ state
1628− choice [ string "*"
1629− , mappend <$> option [] ((:[]) <$> oneOf "+-")
1630− <*> many1 alphaNum
1631− ]
1632−
1633−inlineLaTeX :: OrgParser (F Inlines)
1634−inlineLaTeX = try $ do
1635− cmd <- inlineLaTeXCommand
1636− maybe mzero returnF $
1637− parseAsMath cmd `mplus` parseAsMathMLSym cmd `mplus` parseAsInlineLaTeX cmd
1638− where
1639− parseAsMath :: String -> Maybe Inlines
1640− parseAsMath cs = B.fromList <$> texMathToPandoc cs
1641−
1642− parseAsInlineLaTeX :: String -> Maybe Inlines
1643− parseAsInlineLaTeX cs = maybeRight $ runParser inlineCommand state "" cs
1644−
1645− parseAsMathMLSym :: String -> Maybe Inlines
1646− parseAsMathMLSym cs = B.str <$> MathMLEntityMap.getUnicode (clean cs)
1647− -- drop initial backslash and any trailing "{}"
1648− where clean = dropWhileEnd (`elem` ("{}" :: String)) . drop 1
1649−
1650− state :: ParserState
1651− state = def{ stateOptions = def{ readerParseRaw = True }}
1652−
1653− texMathToPandoc :: String -> Maybe [Inline]
1654− texMathToPandoc cs = (maybeRight $ readTeX cs) >>= writePandoc DisplayInline
1655−
1656−maybeRight :: Either a b -> Maybe b
1657−maybeRight = either (const Nothing) Just
1658−
1659−inlineLaTeXCommand :: OrgParser String
1660−inlineLaTeXCommand = try $ do
1661− rest <- getInput
1662− case runParser rawLaTeXInline def "source" rest of
1663− Right (RawInline _ cs) -> do
1664− -- drop any trailing whitespace, those are not be part of the command as
1665− -- far as org mode is concerned.
1666− let cmdNoSpc = dropWhileEnd isSpace cs
1667− let len = length cmdNoSpc
1668− count len anyChar
1669− return cmdNoSpc
1670− _ -> mzero
1671−
1672−-- Taken from Data.OldList.
1673−dropWhileEnd :: (a -> Bool) -> [a] -> [a]
1674−dropWhileEnd p = foldr (\x xs -> if p x && null xs then [] else x : xs) []
1675−
1676−smart :: OrgParser (F Inlines)
1677−smart = do
1678− getOption readerSmart >>= guard
1679− doubleQuoted <|> singleQuoted <|>
1680− choice (map (return <$>) [orgApostrophe, orgDash, orgEllipses])
1681− where
1682− orgDash = dash <* updatePositions '-'
1683− orgEllipses = ellipses <* updatePositions '.'
1684− orgApostrophe =
1685− (char '\'' <|> char '\8217') <* updateLastPreCharPos
1686− <* updateLastForbiddenCharPos
1687− *> return (B.str "\x2019")
1688−
1689−singleQuoted :: OrgParser (F Inlines)
1690−singleQuoted = try $ do
1691− singleQuoteStart
1692− updatePositions '\''
1693− withQuoteContext InSingleQuote $
1694− fmap B.singleQuoted . trimInlinesF . mconcat <$>
1695− many1Till inline (singleQuoteEnd <* updatePositions '\'')
1696−
1697−-- doubleQuoted will handle regular double-quoted sections, as well
1698−-- as dialogues with an open double-quote without a close double-quote
1699−-- in the same paragraph.
1700−doubleQuoted :: OrgParser (F Inlines)
1701−doubleQuoted = try $ do
1702− doubleQuoteStart
1703− updatePositions '"'
1704− contents <- mconcat <$> many (try $ notFollowedBy doubleQuoteEnd >> inline)
1705− (withQuoteContext InDoubleQuote $ (doubleQuoteEnd <* updateLastForbiddenCharPos) >> return
1706− (fmap B.doubleQuoted . trimInlinesF $ contents))
1707− <|> (return $ return (B.str "\8220") <> contents)
src/Text/Pandoc/Readers/Org/BlockStarts.hsnew file+112
@@ -0,0 +1,112 @@
1+{-
2+Copyright (C) 2014-2016 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+
19+{- |
20+ Module : Text.Pandoc.Readers.Org.Options
21+ Copyright : Copyright (C) 2014-2016 Albert Krewinkel
22+ License : GNU GPL, version 2 or above
23+
24+ Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
25+
26+Parsers for Org-mode inline elements.
27+-}
28+module Text.Pandoc.Readers.Org.BlockStarts
29+ ( exampleLineStart
30+ , hline
31+ , noteMarker
32+ , tableStart
33+ , drawerStart
34+ , headerStart
35+ , metaLineStart
36+ , latexEnvStart
37+ , commentLineStart
38+ , bulletListStart
39+ , orderedListStart
40+ ) where
41+
42+import Text.Pandoc.Readers.Org.Parsing
43+
44+-- | Horizontal Line (five -- dashes or more)
45+hline :: OrgParser ()
46+hline = try $ do
47+ skipSpaces
48+ string "-----"
49+ many (char '-')
50+ skipSpaces
51+ newline
52+ return ()
53+
54+-- | Read the start of a header line, return the header level
55+headerStart :: OrgParser Int
56+headerStart = try $
57+ (length <$> many1 (char '*')) <* many1 (char ' ') <* updateLastPreCharPos
58+
59+tableStart :: OrgParser Char
60+tableStart = try $ skipSpaces *> char '|'
61+
62+latexEnvStart :: OrgParser String
63+latexEnvStart = try $ do
64+ skipSpaces *> string "\\begin{"
65+ *> latexEnvName
66+ <* string "}"
67+ <* blankline
68+ where
69+ latexEnvName :: OrgParser String
70+ latexEnvName = try $ mappend <$> many1 alphaNum <*> option "" (string "*")
71+
72+
73+-- | Parses bullet list marker.
74+bulletListStart :: OrgParser ()
75+bulletListStart = try $
76+ choice
77+ [ () <$ skipSpaces <* oneOf "+-" <* skipSpaces1
78+ , () <$ skipSpaces1 <* char '*' <* skipSpaces1
79+ ]
80+
81+genericListStart :: OrgParser String
82+ -> OrgParser Int
83+genericListStart listMarker = try $
84+ (+) <$> (length <$> many spaceChar)
85+ <*> (length <$> listMarker <* many1 spaceChar)
86+
87+orderedListStart :: OrgParser Int
88+orderedListStart = genericListStart orderedListMarker
89+ -- Ordered list markers allowed in org-mode
90+ where orderedListMarker = mappend <$> many1 digit <*> (pure <$> oneOf ".)")
91+
92+drawerStart :: OrgParser String
93+drawerStart = try $
94+ skipSpaces *> drawerName <* skipSpaces <* newline
95+ where drawerName = char ':' *> manyTill nonspaceChar (char ':')
96+
97+metaLineStart :: OrgParser ()
98+metaLineStart = try $ skipSpaces <* string "#+"
99+
100+commentLineStart :: OrgParser ()
101+commentLineStart = try $ skipSpaces <* string "# "
102+
103+exampleLineStart :: OrgParser ()
104+exampleLineStart = () <$ try (skipSpaces *> string ": ")
105+
106+noteMarker :: OrgParser String
107+noteMarker = try $ do
108+ char '['
109+ choice [ many1Till digit (char ']')
110+ , (++) <$> string "fn:"
111+ <*> many1Till (noneOf "\n\r\t ") (char ']')
112+ ]
src/Text/Pandoc/Readers/Org/Blocks.hsnew file+891
@@ -0,0 +1,891 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-
3+Copyright (C) 2014-2016 Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
4+
5+This program is free software; you can redistribute it and/or modify
6+it under the terms of the GNU General Public License as published by
7+the Free Software Foundation; either version 2 of the License, or
8+(at your option) any later version.
9+
10+This program is distributed in the hope that it will be useful,
11+but WITHOUT ANY WARRANTY; without even the implied warranty of
12+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+GNU General Public License for more details.
14+
15+You should have received a copy of the GNU General Public License
16+along with this program; if not, write to the Free Software
17+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18+-}
19+
20+{- |
21+ Module : Text.Pandoc.Readers.Org.Options
22+ Copyright : Copyright (C) 2014-2016 Albert Krewinkel
23+ License : GNU GPL, version 2 or above
24+
25+ Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
26+
27+Parsers for Org-mode block elements.
28+-}
29+module Text.Pandoc.Readers.Org.Blocks
30+ ( blockList
31+ , meta
32+ ) where
33+
34+import Text.Pandoc.Readers.Org.BlockStarts
35+import Text.Pandoc.Readers.Org.Inlines
36+import Text.Pandoc.Readers.Org.ParserState
37+import Text.Pandoc.Readers.Org.Parsing
38+
39+import qualified Text.Pandoc.Builder as B
40+import Text.Pandoc.Builder ( Inlines, Blocks )
41+import Text.Pandoc.Definition
42+import Text.Pandoc.Compat.Monoid ((<>))
43+import Text.Pandoc.Options
44+import Text.Pandoc.Shared ( compactify', compactify'DL )
45+
46+import Control.Arrow ( first )
47+import Control.Monad ( foldM, guard, mzero )
48+import Data.Char ( toLower, toUpper)
49+import Data.List ( foldl', intersperse, isPrefixOf )
50+import qualified Data.Map as M
51+import Data.Maybe ( fromMaybe, isNothing )
52+import Network.HTTP ( urlEncode )
53+
54+
55+--
56+-- parsing blocks
57+--
58+
59+-- | Get a list of blocks.
60+blockList :: OrgParser [Block]
61+blockList = do
62+ blocks' <- blocks
63+ st <- getState
64+ return . B.toList $ runF blocks' st
65+
66+-- | Get the meta information safed in the state.
67+meta :: OrgParser Meta
68+meta = do
69+ st <- getState
70+ return $ runF (orgStateMeta' st) st
71+
72+blocks :: OrgParser (F Blocks)
73+blocks = mconcat <$> manyTill block eof
74+
75+block :: OrgParser (F Blocks)
76+block = choice [ mempty <$ blanklines
77+ , table
78+ , orgBlock
79+ , figure
80+ , example
81+ , genericDrawer
82+ , specialLine
83+ , header
84+ , horizontalRule
85+ , list
86+ , latexFragment
87+ , noteBlock
88+ , paraOrPlain
89+ ] <?> "block"
90+
91+
92+--
93+-- Block Attributes
94+--
95+
96+-- | Attributes that may be added to figures (like a name or caption).
97+data BlockAttributes = BlockAttributes
98+ { blockAttrName :: Maybe String
99+ , blockAttrCaption :: Maybe (F Inlines)
100+ , blockAttrKeyValues :: [(String, String)]
101+ }
102+
103+stringyMetaAttribute :: (String -> Bool) -> OrgParser (String, String)
104+stringyMetaAttribute attrCheck = try $ do
105+ metaLineStart
106+ attrName <- map toUpper <$> many1Till nonspaceChar (char ':')
107+ guard $ attrCheck attrName
108+ skipSpaces
109+ attrValue <- anyLine
110+ return (attrName, attrValue)
111+
112+blockAttributes :: OrgParser BlockAttributes
113+blockAttributes = try $ do
114+ kv <- many (stringyMetaAttribute attrCheck)
115+ let caption = foldl' (appendValues "CAPTION") Nothing kv
116+ let kvAttrs = foldl' (appendValues "ATTR_HTML") Nothing kv
117+ let name = lookup "NAME" kv
118+ caption' <- maybe (return Nothing)
119+ (fmap Just . parseFromString parseInlines)
120+ caption
121+ kvAttrs' <- parseFromString keyValues . (++ "\n") $ fromMaybe mempty kvAttrs
122+ return $ BlockAttributes
123+ { blockAttrName = name
124+ , blockAttrCaption = caption'
125+ , blockAttrKeyValues = kvAttrs'
126+ }
127+ where
128+ attrCheck :: String -> Bool
129+ attrCheck attr =
130+ case attr of
131+ "NAME" -> True
132+ "CAPTION" -> True
133+ "ATTR_HTML" -> True
134+ _ -> False
135+
136+ appendValues :: String -> Maybe String -> (String, String) -> Maybe String
137+ appendValues attrName accValue (key, value) =
138+ if key /= attrName
139+ then accValue
140+ else case accValue of
141+ Just acc -> Just $ acc ++ ' ':value
142+ Nothing -> Just value
143+
144+keyValues :: OrgParser [(String, String)]
145+keyValues = try $
146+ manyTill ((,) <$> key <*> value) newline
147+ where
148+ key :: OrgParser String
149+ key = try $ skipSpaces *> char ':' *> many1 nonspaceChar
150+
151+ value :: OrgParser String
152+ value = skipSpaces *> manyTill anyChar endOfValue
153+
154+ endOfValue :: OrgParser ()
155+ endOfValue =
156+ lookAhead $ (() <$ try (many1 spaceChar <* key))
157+ <|> () <$ newline
158+
159+
160+--
161+-- Org Blocks (#+BEGIN_... / #+END_...)
162+--
163+
164+type BlockProperties = (Int, String) -- (Indentation, Block-Type)
165+
166+updateIndent :: BlockProperties -> Int -> BlockProperties
167+updateIndent (_, blkType) indent = (indent, blkType)
168+
169+orgBlock :: OrgParser (F Blocks)
170+orgBlock = try $ do
171+ blockAttrs <- blockAttributes
172+ blockProp@(_, blkType) <- blockHeaderStart
173+ ($ blockProp) $
174+ case blkType of
175+ "comment" -> withRaw' (const mempty)
176+ "html" -> withRaw' (return . (B.rawBlock blkType))
177+ "latex" -> withRaw' (return . (B.rawBlock blkType))
178+ "ascii" -> withRaw' (return . (B.rawBlock blkType))
179+ "example" -> withRaw' (return . exampleCode)
180+ "quote" -> withParsed (fmap B.blockQuote)
181+ "verse" -> verseBlock
182+ "src" -> codeBlock blockAttrs
183+ _ -> withParsed (fmap $ divWithClass blkType)
184+
185+blockHeaderStart :: OrgParser (Int, String)
186+blockHeaderStart = try $ (,) <$> indentation <*> blockType
187+ where
188+ blockType = map toLower <$> (stringAnyCase "#+begin_" *> orgArgWord)
189+
190+indentation :: OrgParser Int
191+indentation = try $ do
192+ tabStop <- getOption readerTabStop
193+ s <- many spaceChar
194+ return $ spaceLength tabStop s
195+
196+spaceLength :: Int -> String -> Int
197+spaceLength tabStop s = (sum . map charLen) s
198+ where
199+ charLen ' ' = 1
200+ charLen '\t' = tabStop
201+ charLen _ = 0
202+
203+withRaw' :: (String -> F Blocks) -> BlockProperties -> OrgParser (F Blocks)
204+withRaw' f blockProp = (ignHeaders *> (f <$> rawBlockContent blockProp))
205+
206+withParsed :: (F Blocks -> F Blocks) -> BlockProperties -> OrgParser (F Blocks)
207+withParsed f blockProp = (ignHeaders *> (f <$> parsedBlockContent blockProp))
208+
209+ignHeaders :: OrgParser ()
210+ignHeaders = (() <$ newline) <|> (() <$ anyLine)
211+
212+divWithClass :: String -> Blocks -> Blocks
213+divWithClass cls = B.divWith ("", [cls], [])
214+
215+verseBlock :: BlockProperties -> OrgParser (F Blocks)
216+verseBlock blkProp = try $ do
217+ ignHeaders
218+ content <- rawBlockContent blkProp
219+ fmap B.para . mconcat . intersperse (pure B.linebreak)
220+ <$> mapM (parseFromString parseInlines) (map (++ "\n") . lines $ content)
221+
222+exportsCode :: [(String, String)] -> Bool
223+exportsCode attrs = not (("rundoc-exports", "none") `elem` attrs
224+ || ("rundoc-exports", "results") `elem` attrs)
225+
226+exportsResults :: [(String, String)] -> Bool
227+exportsResults attrs = ("rundoc-exports", "results") `elem` attrs
228+ || ("rundoc-exports", "both") `elem` attrs
229+
230+followingResultsBlock :: OrgParser (Maybe (F Blocks))
231+followingResultsBlock =
232+ optionMaybe (try $ blanklines *> stringAnyCase "#+RESULTS:"
233+ *> blankline
234+ *> block)
235+
236+codeBlock :: BlockAttributes -> BlockProperties -> OrgParser (F Blocks)
237+codeBlock blockAttrs blkProp = do
238+ skipSpaces
239+ (classes, kv) <- codeHeaderArgs <|> (mempty <$ ignHeaders)
240+ leadingIndent <- lookAhead indentation
241+ content <- rawBlockContent (updateIndent blkProp leadingIndent)
242+ resultsContent <- followingResultsBlock
243+ let id' = fromMaybe mempty $ blockAttrName blockAttrs
244+ let includeCode = exportsCode kv
245+ let includeResults = exportsResults kv
246+ let codeBlck = B.codeBlockWith ( id', classes, kv ) content
247+ let labelledBlck = maybe (pure codeBlck)
248+ (labelDiv codeBlck)
249+ (blockAttrCaption blockAttrs)
250+ let resultBlck = fromMaybe mempty resultsContent
251+ return $ (if includeCode then labelledBlck else mempty)
252+ <> (if includeResults then resultBlck else mempty)
253+ where
254+ labelDiv blk value =
255+ B.divWith nullAttr <$> (mappend <$> labelledBlock value
256+ <*> pure blk)
257+ labelledBlock = fmap (B.plain . B.spanWith ("", ["label"], []))
258+
259+rawBlockContent :: BlockProperties -> OrgParser String
260+rawBlockContent (indent, blockType) = try $
261+ unlines . map commaEscaped <$> manyTill indentedLine blockEnder
262+ where
263+ indentedLine = try $ ("" <$ blankline) <|> (indentWith indent *> anyLine)
264+ blockEnder = try $ skipSpaces *> stringAnyCase ("#+end_" <> blockType)
265+
266+parsedBlockContent :: BlockProperties -> OrgParser (F Blocks)
267+parsedBlockContent blkProps = try $ do
268+ raw <- rawBlockContent blkProps
269+ parseFromString blocks (raw ++ "\n")
270+
271+-- indent by specified number of spaces (or equiv. tabs)
272+indentWith :: Int -> OrgParser String
273+indentWith num = do
274+ tabStop <- getOption readerTabStop
275+ if num < tabStop
276+ then count num (char ' ')
277+ else choice [ try (count num (char ' '))
278+ , try (char '\t' >> count (num - tabStop) (char ' ')) ]
279+
280+type SwitchOption = (Char, Maybe String)
281+
282+-- | Parse code block arguments
283+-- TODO: We currently don't handle switches.
284+codeHeaderArgs :: OrgParser ([String], [(String, String)])
285+codeHeaderArgs = try $ do
286+ language <- skipSpaces *> orgArgWord
287+ _ <- skipSpaces *> (try $ switch `sepBy` (many1 spaceChar))
288+ parameters <- manyTill blockOption newline
289+ let pandocLang = translateLang language
290+ return $
291+ if hasRundocParameters parameters
292+ then ( [ pandocLang, rundocBlockClass ]
293+ , map toRundocAttrib (("language", language) : parameters)
294+ )
295+ else ([ pandocLang ], parameters)
296+ where
297+ hasRundocParameters = not . null
298+ toRundocAttrib = first ("rundoc-" ++)
299+
300+
301+switch :: OrgParser SwitchOption
302+switch = try $ simpleSwitch <|> lineNumbersSwitch
303+ where
304+ simpleSwitch = (\c -> (c, Nothing)) <$> (oneOf "-+" *> letter)
305+ lineNumbersSwitch = (\ls -> ('l', Just ls)) <$>
306+ (string "-l \"" *> many1Till nonspaceChar (char '"'))
307+
308+translateLang :: String -> String
309+translateLang "C" = "c"
310+translateLang "C++" = "cpp"
311+translateLang "emacs-lisp" = "commonlisp" -- emacs lisp is not supported
312+translateLang "js" = "javascript"
313+translateLang "lisp" = "commonlisp"
314+translateLang "R" = "r"
315+translateLang "sh" = "bash"
316+translateLang "sqlite" = "sql"
317+translateLang cs = cs
318+
319+-- | Prefix used for Rundoc classes and arguments.
320+rundocPrefix :: String
321+rundocPrefix = "rundoc-"
322+
323+-- | The class-name used to mark rundoc blocks.
324+rundocBlockClass :: String
325+rundocBlockClass = rundocPrefix ++ "block"
326+
327+blockOption :: OrgParser (String, String)
328+blockOption = try $ do
329+ argKey <- orgArgKey
330+ paramValue <- option "yes" orgParamValue
331+ return (argKey, paramValue)
332+
333+orgParamValue :: OrgParser String
334+orgParamValue = try $
335+ skipSpaces
336+ *> notFollowedBy (char ':' )
337+ *> many1 (noneOf "\t\n\r ")
338+ <* skipSpaces
339+
340+commaEscaped :: String -> String
341+commaEscaped (',':cs@('*':_)) = cs
342+commaEscaped (',':cs@('#':'+':_)) = cs
343+commaEscaped cs = cs
344+
345+example :: OrgParser (F Blocks)
346+example = try $ do
347+ return . return . exampleCode =<< unlines <$> many1 exampleLine
348+
349+exampleCode :: String -> Blocks
350+exampleCode = B.codeBlockWith ("", ["example"], [])
351+
352+exampleLine :: OrgParser String
353+exampleLine = try $ exampleLineStart *> anyLine
354+
355+horizontalRule :: OrgParser (F Blocks)
356+horizontalRule = return B.horizontalRule <$ try hline
357+
358+
359+--
360+-- Drawers
361+--
362+
363+-- | A generic drawer which has no special meaning for org-mode.
364+-- Whether or not this drawer is included in the output depends on the drawers
365+-- export setting.
366+genericDrawer :: OrgParser (F Blocks)
367+genericDrawer = try $ do
368+ name <- map toUpper <$> drawerStart
369+ content <- manyTill drawerLine (try drawerEnd)
370+ state <- getState
371+ -- Include drawer if it is explicitly included in or not explicitly excluded
372+ -- from the list of drawers that should be exported. PROPERTIES drawers are
373+ -- never exported.
374+ case (exportDrawers . orgStateExportSettings $ state) of
375+ _ | name == "PROPERTIES" -> return mempty
376+ Left names | name `elem` names -> return mempty
377+ Right names | name `notElem` names -> return mempty
378+ _ -> drawerDiv name <$> parseLines content
379+ where
380+ parseLines :: [String] -> OrgParser (F Blocks)
381+ parseLines = parseFromString blocks . (++ "\n") . unlines
382+
383+ drawerDiv :: String -> F Blocks -> F Blocks
384+ drawerDiv drawerName = fmap $ B.divWith (mempty, [drawerName, "drawer"], mempty)
385+
386+drawerLine :: OrgParser String
387+drawerLine = anyLine
388+
389+drawerEnd :: OrgParser String
390+drawerEnd = try $
391+ skipSpaces *> stringAnyCase ":END:" <* skipSpaces <* newline
392+
393+-- | Read a :PROPERTIES: drawer and return the key/value pairs contained
394+-- within.
395+propertiesDrawer :: OrgParser [(String, String)]
396+propertiesDrawer = try $ do
397+ drawerType <- drawerStart
398+ guard $ map toUpper drawerType == "PROPERTIES"
399+ manyTill property (try drawerEnd)
400+ where
401+ property :: OrgParser (String, String)
402+ property = try $ (,) <$> key <*> value
403+
404+ key :: OrgParser String
405+ key = try $ skipSpaces *> char ':' *> many1Till nonspaceChar (char ':')
406+
407+ value :: OrgParser String
408+ value = try $ skipSpaces *> manyTill anyChar (try $ skipSpaces *> newline)
409+
410+keyValuesToAttr :: [(String, String)] -> Attr
411+keyValuesToAttr kvs =
412+ let
413+ lowerKvs = map (\(k, v) -> (map toLower k, v)) kvs
414+ id' = fromMaybe mempty . lookup "custom_id" $ lowerKvs
415+ cls = fromMaybe mempty . lookup "class" $ lowerKvs
416+ kvs' = filter (flip notElem ["custom_id", "class"] . fst) lowerKvs
417+ in
418+ (id', words cls, kvs')
419+
420+
421+--
422+-- Figures
423+--
424+
425+-- | Figures (Image on a line by itself, preceded by name and/or caption)
426+figure :: OrgParser (F Blocks)
427+figure = try $ do
428+ figAttrs <- blockAttributes
429+ src <- skipSpaces *> selfTarget <* skipSpaces <* newline
430+ guard . not . isNothing . blockAttrCaption $ figAttrs
431+ guard (isImageFilename src)
432+ let figName = fromMaybe mempty $ blockAttrName figAttrs
433+ let figCaption = fromMaybe mempty $ blockAttrCaption figAttrs
434+ let figKeyVals = blockAttrKeyValues figAttrs
435+ let attr = (mempty, mempty, figKeyVals)
436+ return $ (B.para . B.imageWith attr src (withFigPrefix figName) <$> figCaption)
437+ where
438+ withFigPrefix :: String -> String
439+ withFigPrefix cs =
440+ if "fig:" `isPrefixOf` cs
441+ then cs
442+ else "fig:" ++ cs
443+
444+ selfTarget :: OrgParser String
445+ selfTarget = try $ char '[' *> linkTarget <* char ']'
446+
447+
448+--
449+-- Comments, Options and Metadata
450+--
451+
452+addLinkFormat :: String
453+ -> (String -> String)
454+ -> OrgParser ()
455+addLinkFormat key formatter = updateState $ \s ->
456+ let fs = orgStateLinkFormatters s
457+ in s{ orgStateLinkFormatters = M.insert key formatter fs }
458+
459+specialLine :: OrgParser (F Blocks)
460+specialLine = fmap return . try $ metaLine <|> commentLine
461+
462+-- The order, in which blocks are tried, makes sure that we're not looking at
463+-- the beginning of a block, so we don't need to check for it
464+metaLine :: OrgParser Blocks
465+metaLine = mempty <$ metaLineStart <* (optionLine <|> declarationLine)
466+
467+commentLine :: OrgParser Blocks
468+commentLine = commentLineStart *> anyLine *> pure mempty
469+
470+declarationLine :: OrgParser ()
471+declarationLine = try $ do
472+ key <- metaKey
473+ inlinesF <- metaInlines
474+ updateState $ \st ->
475+ let meta' = B.setMeta <$> pure key <*> inlinesF <*> pure nullMeta
476+ in st { orgStateMeta' = orgStateMeta' st <> meta' }
477+ return ()
478+
479+metaInlines :: OrgParser (F MetaValue)
480+metaInlines = fmap (MetaInlines . B.toList) <$> inlinesTillNewline
481+
482+metaKey :: OrgParser String
483+metaKey = map toLower <$> many1 (noneOf ": \n\r")
484+ <* char ':'
485+ <* skipSpaces
486+
487+optionLine :: OrgParser ()
488+optionLine = try $ do
489+ key <- metaKey
490+ case key of
491+ "link" -> parseLinkFormat >>= uncurry addLinkFormat
492+ "options" -> () <$ sepBy spaces exportSetting
493+ _ -> mzero
494+
495+--
496+-- Export Settings
497+--
498+
499+-- | Read and process org-mode specific export options.
500+exportSetting :: OrgParser ()
501+exportSetting = choice
502+ [ booleanSetting "^" setExportSubSuperscripts
503+ , ignoredSetting "'"
504+ , ignoredSetting "*"
505+ , ignoredSetting "-"
506+ , ignoredSetting ":"
507+ , ignoredSetting "<"
508+ , ignoredSetting "\\n"
509+ , ignoredSetting "arch"
510+ , ignoredSetting "author"
511+ , ignoredSetting "c"
512+ , ignoredSetting "creator"
513+ , complementableListSetting "d" setExportDrawers
514+ , ignoredSetting "date"
515+ , ignoredSetting "e"
516+ , ignoredSetting "email"
517+ , ignoredSetting "f"
518+ , ignoredSetting "H"
519+ , ignoredSetting "inline"
520+ , ignoredSetting "num"
521+ , ignoredSetting "p"
522+ , ignoredSetting "pri"
523+ , ignoredSetting "prop"
524+ , ignoredSetting "stat"
525+ , ignoredSetting "tags"
526+ , ignoredSetting "tasks"
527+ , ignoredSetting "tex"
528+ , ignoredSetting "timestamp"
529+ , ignoredSetting "title"
530+ , ignoredSetting "toc"
531+ , ignoredSetting "todo"
532+ , ignoredSetting "|"
533+ ] <?> "export setting"
534+
535+booleanSetting :: String -> ExportSettingSetter Bool -> OrgParser ()
536+booleanSetting settingIdentifier setter = try $ do
537+ string settingIdentifier
538+ char ':'
539+ value <- elispBoolean
540+ updateState $ modifyExportSettings setter value
541+
542+-- | Read an elisp boolean. Only NIL is treated as false, non-NIL values are
543+-- interpreted as true.
544+elispBoolean :: OrgParser Bool
545+elispBoolean = try $ do
546+ value <- many1 nonspaceChar
547+ return $ case map toLower value of
548+ "nil" -> False
549+ "{}" -> False
550+ "()" -> False
551+ _ -> True
552+
553+-- | A list or a complement list (i.e. a list starting with `not`).
554+complementableListSetting :: String
555+ -> ExportSettingSetter (Either [String] [String])
556+ -> OrgParser ()
557+complementableListSetting settingIdentifier setter = try $ do
558+ _ <- string settingIdentifier <* char ':'
559+ value <- choice [ Left <$> complementStringList
560+ , Right <$> stringList
561+ , (\b -> if b then Left [] else Right []) <$> elispBoolean
562+ ]
563+ updateState $ modifyExportSettings setter value
564+ where
565+ -- Read a plain list of strings.
566+ stringList :: OrgParser [String]
567+ stringList = try $
568+ char '('
569+ *> sepBy elispString spaces
570+ <* char ')'
571+
572+ -- Read an emacs lisp list specifying a complement set.
573+ complementStringList :: OrgParser [String]
574+ complementStringList = try $
575+ string "(not "
576+ *> sepBy elispString spaces
577+ <* char ')'
578+
579+ elispString :: OrgParser String
580+ elispString = try $
581+ char '"'
582+ *> manyTill alphaNum (char '"')
583+
584+ignoredSetting :: String -> OrgParser ()
585+ignoredSetting s = try (() <$ string s <* char ':' <* many1 nonspaceChar)
586+
587+
588+parseLinkFormat :: OrgParser ((String, String -> String))
589+parseLinkFormat = try $ do
590+ linkType <- (:) <$> letter <*> many (alphaNum <|> oneOf "-_") <* skipSpaces
591+ linkSubst <- parseFormat
592+ return (linkType, linkSubst)
593+
594+-- | An ad-hoc, single-argument-only implementation of a printf-style format
595+-- parser.
596+parseFormat :: OrgParser (String -> String)
597+parseFormat = try $ do
598+ replacePlain <|> replaceUrl <|> justAppend
599+ where
600+ -- inefficient, but who cares
601+ replacePlain = try $ (\x -> concat . flip intersperse x)
602+ <$> sequence [tillSpecifier 's', rest]
603+ replaceUrl = try $ (\x -> concat . flip intersperse x . urlEncode)
604+ <$> sequence [tillSpecifier 'h', rest]
605+ justAppend = try $ (++) <$> rest
606+
607+ rest = manyTill anyChar (eof <|> () <$ oneOf "\n\r")
608+ tillSpecifier c = manyTill (noneOf "\n\r") (try $ string ('%':c:""))
609+
610+--
611+-- Headers
612+--
613+
614+-- | Headers
615+header :: OrgParser (F Blocks)
616+header = try $ do
617+ level <- headerStart
618+ title <- manyTill inline (lookAhead $ optional headerTags <* newline)
619+ tags <- option [] headerTags
620+ newline
621+ propAttr <- option nullAttr (keyValuesToAttr <$> propertiesDrawer)
622+ inlines <- runF (tagTitle title tags) <$> getState
623+ attr <- registerHeader propAttr inlines
624+ return $ pure (B.headerWith attr level inlines)
625+ where
626+ tagTitle :: [F Inlines] -> [String] -> F Inlines
627+ tagTitle title tags = trimInlinesF . mconcat $ title <> map tagToInlineF tags
628+
629+ tagToInlineF :: String -> F Inlines
630+ tagToInlineF t = return $ B.spanWith ("", ["tag"], [("data-tag-name", t)]) mempty
631+
632+ headerTags :: OrgParser [String]
633+ headerTags = try $
634+ let tag = many1 (alphaNum <|> oneOf "@%#_") <* char ':'
635+ in skipSpaces
636+ *> char ':'
637+ *> many1 tag
638+ <* skipSpaces
639+
640+
641+--
642+-- Tables
643+--
644+
645+data OrgTableRow = OrgContentRow (F [Blocks])
646+ | OrgAlignRow [Alignment]
647+ | OrgHlineRow
648+
649+-- OrgTable is strongly related to the pandoc table ADT. Using the same
650+-- (i.e. pandoc-global) ADT would mean that the reader would break if the
651+-- global structure was to be changed, which would be bad. The final table
652+-- should be generated using a builder function. Column widths aren't
653+-- implemented yet, so they are not tracked here.
654+data OrgTable = OrgTable
655+ { orgTableAlignments :: [Alignment]
656+ , orgTableHeader :: [Blocks]
657+ , orgTableRows :: [[Blocks]]
658+ }
659+
660+table :: OrgParser (F Blocks)
661+table = try $ do
662+ blockAttrs <- blockAttributes
663+ lookAhead tableStart
664+ do
665+ rows <- tableRows
666+ let caption = fromMaybe (return mempty) $ blockAttrCaption blockAttrs
667+ return $ (<$> caption) . orgToPandocTable . normalizeTable =<< rowsToTable rows
668+
669+orgToPandocTable :: OrgTable
670+ -> Inlines
671+ -> Blocks
672+orgToPandocTable (OrgTable aligns heads lns) caption =
673+ B.table caption (zip aligns $ repeat 0) heads lns
674+
675+tableRows :: OrgParser [OrgTableRow]
676+tableRows = try $ many (tableAlignRow <|> tableHline <|> tableContentRow)
677+
678+tableContentRow :: OrgParser OrgTableRow
679+tableContentRow = try $
680+ OrgContentRow . sequence <$> (tableStart *> many1Till tableContentCell newline)
681+
682+tableContentCell :: OrgParser (F Blocks)
683+tableContentCell = try $
684+ fmap B.plain . trimInlinesF . mconcat <$> manyTill inline endOfCell
685+
686+tableAlignRow :: OrgParser OrgTableRow
687+tableAlignRow = try $ do
688+ tableStart
689+ cells <- many1Till tableAlignCell newline
690+ -- Empty rows are regular (i.e. content) rows, not alignment rows.
691+ guard $ any (/= AlignDefault) cells
692+ return $ OrgAlignRow cells
693+
694+tableAlignCell :: OrgParser Alignment
695+tableAlignCell =
696+ choice [ try $ emptyCell *> return AlignDefault
697+ , try $ skipSpaces
698+ *> char '<'
699+ *> tableAlignFromChar
700+ <* many digit
701+ <* char '>'
702+ <* emptyCell
703+ ] <?> "alignment info"
704+ where emptyCell = try $ skipSpaces *> endOfCell
705+
706+tableAlignFromChar :: OrgParser Alignment
707+tableAlignFromChar = try $
708+ choice [ char 'l' *> return AlignLeft
709+ , char 'c' *> return AlignCenter
710+ , char 'r' *> return AlignRight
711+ ]
712+
713+tableHline :: OrgParser OrgTableRow
714+tableHline = try $
715+ OrgHlineRow <$ (tableStart *> char '-' *> anyLine)
716+
717+endOfCell :: OrgParser Char
718+endOfCell = try $ char '|' <|> lookAhead newline
719+
720+rowsToTable :: [OrgTableRow]
721+ -> F OrgTable
722+rowsToTable = foldM rowToContent emptyTable
723+ where emptyTable = OrgTable mempty mempty mempty
724+
725+normalizeTable :: OrgTable -> OrgTable
726+normalizeTable (OrgTable aligns heads rows) = OrgTable aligns' heads rows
727+ where
728+ refRow = if heads /= mempty
729+ then heads
730+ else if rows == mempty then mempty else head rows
731+ cols = length refRow
732+ fillColumns base padding = take cols $ base ++ repeat padding
733+ aligns' = fillColumns aligns AlignDefault
734+
735+-- One or more horizontal rules after the first content line mark the previous
736+-- line as a header. All other horizontal lines are discarded.
737+rowToContent :: OrgTable
738+ -> OrgTableRow
739+ -> F OrgTable
740+rowToContent orgTable row =
741+ case row of
742+ OrgHlineRow -> return singleRowPromotedToHeader
743+ OrgAlignRow as -> return . setAligns $ as
744+ OrgContentRow cs -> appendToBody cs
745+ where
746+ singleRowPromotedToHeader :: OrgTable
747+ singleRowPromotedToHeader = case orgTable of
748+ OrgTable{ orgTableHeader = [], orgTableRows = b:[] } ->
749+ orgTable{ orgTableHeader = b , orgTableRows = [] }
750+ _ -> orgTable
751+
752+ setAligns :: [Alignment] -> OrgTable
753+ setAligns aligns = orgTable{ orgTableAlignments = aligns }
754+
755+ appendToBody :: F [Blocks] -> F OrgTable
756+ appendToBody frow = do
757+ newRow <- frow
758+ let oldRows = orgTableRows orgTable
759+ -- NOTE: This is an inefficient O(n) operation. This should be changed
760+ -- if performance ever becomes a problem.
761+ return orgTable{ orgTableRows = oldRows ++ [newRow] }
762+
763+
764+--
765+-- LaTeX fragments
766+--
767+latexFragment :: OrgParser (F Blocks)
768+latexFragment = try $ do
769+ envName <- latexEnvStart
770+ content <- mconcat <$> manyTill anyLineNewline (latexEnd envName)
771+ return . return $ B.rawBlock "latex" (content `inLatexEnv` envName)
772+ where
773+ c `inLatexEnv` e = mconcat [ "\\begin{", e, "}\n"
774+ , c
775+ , "\\end{", e, "}\n"
776+ ]
777+
778+latexEnd :: String -> OrgParser ()
779+latexEnd envName = try $
780+ () <$ skipSpaces
781+ <* string ("\\end{" ++ envName ++ "}")
782+ <* blankline
783+
784+
785+--
786+-- Footnote defintions
787+--
788+noteBlock :: OrgParser (F Blocks)
789+noteBlock = try $ do
790+ ref <- noteMarker <* skipSpaces
791+ content <- mconcat <$> blocksTillHeaderOrNote
792+ addToNotesTable (ref, content)
793+ return mempty
794+ where
795+ blocksTillHeaderOrNote =
796+ many1Till block (eof <|> () <$ lookAhead noteMarker
797+ <|> () <$ lookAhead headerStart)
798+
799+-- Paragraphs or Plain text
800+paraOrPlain :: OrgParser (F Blocks)
801+paraOrPlain = try $ do
802+ ils <- parseInlines
803+ nl <- option False (newline *> return True)
804+ -- Read block as paragraph, except if we are in a list context and the block
805+ -- is directly followed by a list item, in which case the block is read as
806+ -- plain text.
807+ try (guard nl
808+ *> notFollowedBy (inList *> (() <$ orderedListStart <|> bulletListStart))
809+ *> return (B.para <$> ils))
810+ <|> (return (B.plain <$> ils))
811+
812+inlinesTillNewline :: OrgParser (F Inlines)
813+inlinesTillNewline = trimInlinesF . mconcat <$> manyTill inline newline
814+
815+
816+--
817+-- list blocks
818+--
819+
820+list :: OrgParser (F Blocks)
821+list = choice [ definitionList, bulletList, orderedList ] <?> "list"
822+
823+definitionList :: OrgParser (F Blocks)
824+definitionList = try $ do n <- lookAhead (bulletListStart' Nothing)
825+ fmap B.definitionList . fmap compactify'DL . sequence
826+ <$> many1 (definitionListItem $ bulletListStart' (Just n))
827+
828+bulletList :: OrgParser (F Blocks)
829+bulletList = try $ do n <- lookAhead (bulletListStart' Nothing)
830+ fmap B.bulletList . fmap compactify' . sequence
831+ <$> many1 (listItem (bulletListStart' $ Just n))
832+
833+orderedList :: OrgParser (F Blocks)
834+orderedList = fmap B.orderedList . fmap compactify' . sequence
835+ <$> many1 (listItem orderedListStart)
836+
837+bulletListStart' :: Maybe Int -> OrgParser Int
838+-- returns length of bulletList prefix, inclusive of marker
839+bulletListStart' Nothing = do ind <- length <$> many spaceChar
840+ oneOf (bullets $ ind == 0)
841+ skipSpaces1
842+ return (ind + 1)
843+bulletListStart' (Just n) = do count (n-1) spaceChar
844+ oneOf (bullets $ n == 1)
845+ many1 spaceChar
846+ return n
847+
848+-- Unindented lists are legal, but they can't use '*' bullets.
849+-- We return n to maintain compatibility with the generic listItem.
850+bullets :: Bool -> String
851+bullets unindented = if unindented then "+-" else "*+-"
852+
853+definitionListItem :: OrgParser Int
854+ -> OrgParser (F (Inlines, [Blocks]))
855+definitionListItem parseMarkerGetLength = try $ do
856+ markerLength <- parseMarkerGetLength
857+ term <- manyTill (noneOf "\n\r") (try definitionMarker)
858+ line1 <- anyLineNewline
859+ blank <- option "" ("\n" <$ blankline)
860+ cont <- concat <$> many (listContinuation markerLength)
861+ term' <- parseFromString parseInlines term
862+ contents' <- parseFromString blocks $ line1 ++ blank ++ cont
863+ return $ (,) <$> term' <*> fmap (:[]) contents'
864+ where
865+ definitionMarker =
866+ spaceChar *> string "::" <* (spaceChar <|> lookAhead newline)
867+
868+
869+-- parse raw text for one list item, excluding start marker and continuations
870+listItem :: OrgParser Int
871+ -> OrgParser (F Blocks)
872+listItem start = try . withContext ListItemState $ do
873+ markerLength <- try start
874+ firstLine <- anyLineNewline
875+ blank <- option "" ("\n" <$ blankline)
876+ rest <- concat <$> many (listContinuation markerLength)
877+ parseFromString blocks $ firstLine ++ blank ++ rest
878+
879+-- continuation of a list item - indented and separated by blankline or endline.
880+-- Note: nested lists are parsed as continuations.
881+listContinuation :: Int
882+ -> OrgParser String
883+listContinuation markerLength = try $
884+ notFollowedBy' blankline
885+ *> (mappend <$> (concat <$> many1 listLine)
886+ <*> many blankline)
887+ where listLine = try $ indentWith markerLength *> anyLineNewline
888+
889+-- | Parse any line, include the final newline in the output.
890+anyLineNewline :: OrgParser String
891+anyLineNewline = (++ "\n") <$> anyLine
src/Text/Pandoc/Readers/Org/Inlines.hsnew file+715
@@ -0,0 +1,715 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-
3+Copyright (C) 2014-2016 Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
4+
5+This program is free software; you can redistribute it and/or modify
6+it under the terms of the GNU General Public License as published by
7+the Free Software Foundation; either version 2 of the License, or
8+(at your option) any later version.
9+
10+This program is distributed in the hope that it will be useful,
11+but WITHOUT ANY WARRANTY; without even the implied warranty of
12+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+GNU General Public License for more details.
14+
15+You should have received a copy of the GNU General Public License
16+along with this program; if not, write to the Free Software
17+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18+-}
19+
20+{- |
21+ Module : Text.Pandoc.Readers.Org.Options
22+ Copyright : Copyright (C) 2014-2016 Albert Krewinkel
23+ License : GNU GPL, version 2 or above
24+
25+ Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
26+
27+Parsers for Org-mode inline elements.
28+-}
29+module Text.Pandoc.Readers.Org.Inlines
30+ ( inline
31+ , addToNotesTable
32+ , parseInlines
33+ , isImageFilename
34+ , linkTarget
35+ ) where
36+
37+import Text.Pandoc.Readers.Org.BlockStarts
38+import Text.Pandoc.Readers.Org.ParserState
39+import Text.Pandoc.Readers.Org.Parsing
40+
41+import qualified Text.Pandoc.Builder as B
42+import Text.Pandoc.Builder ( Inlines )
43+import Text.Pandoc.Definition
44+import Text.Pandoc.Compat.Monoid ( (<>) )
45+import Text.Pandoc.Options
46+import Text.Pandoc.Readers.LaTeX ( inlineCommand, rawLaTeXInline )
47+import Text.TeXMath ( readTeX, writePandoc, DisplayType(..) )
48+import qualified Text.TeXMath.Readers.MathML.EntityMap as MathMLEntityMap
49+
50+import Control.Arrow ( first )
51+import Control.Monad ( guard, mplus, mzero, when )
52+import Data.Char ( isAlphaNum, isSpace )
53+import Data.List ( isPrefixOf, isSuffixOf )
54+import Data.Maybe ( fromMaybe )
55+import qualified Data.Map as M
56+
57+-- | Prefix used for Rundoc classes and arguments.
58+rundocPrefix :: String
59+rundocPrefix = "rundoc-"
60+
61+-- | The class-name used to mark rundoc blocks.
62+rundocBlockClass :: String
63+rundocBlockClass = rundocPrefix ++ "block"
64+
65+toRundocAttrib :: (String, String) -> (String, String)
66+toRundocAttrib = first ("rundoc-" ++)
67+
68+translateLang :: String -> String
69+translateLang "C" = "c"
70+translateLang "C++" = "cpp"
71+translateLang "emacs-lisp" = "commonlisp" -- emacs lisp is not supported
72+translateLang "js" = "javascript"
73+translateLang "lisp" = "commonlisp"
74+translateLang "R" = "r"
75+translateLang "sh" = "bash"
76+translateLang "sqlite" = "sql"
77+translateLang cs = cs
78+
79+--
80+-- Functions acting on the parser state
81+--
82+recordAnchorId :: String -> OrgParser ()
83+recordAnchorId i = updateState $ \s ->
84+ s{ orgStateAnchorIds = i : (orgStateAnchorIds s) }
85+
86+pushToInlineCharStack :: Char -> OrgParser ()
87+pushToInlineCharStack c = updateState $ \s ->
88+ s{ orgStateEmphasisCharStack = c:orgStateEmphasisCharStack s }
89+
90+popInlineCharStack :: OrgParser ()
91+popInlineCharStack = updateState $ \s ->
92+ s{ orgStateEmphasisCharStack = drop 1 . orgStateEmphasisCharStack $ s }
93+
94+surroundingEmphasisChar :: OrgParser [Char]
95+surroundingEmphasisChar =
96+ take 1 . drop 1 . orgStateEmphasisCharStack <$> getState
97+
98+startEmphasisNewlinesCounting :: Int -> OrgParser ()
99+startEmphasisNewlinesCounting maxNewlines = updateState $ \s ->
100+ s{ orgStateEmphasisNewlines = Just maxNewlines }
101+
102+decEmphasisNewlinesCount :: OrgParser ()
103+decEmphasisNewlinesCount = updateState $ \s ->
104+ s{ orgStateEmphasisNewlines = (\n -> n - 1) <$> orgStateEmphasisNewlines s }
105+
106+newlinesCountWithinLimits :: OrgParser Bool
107+newlinesCountWithinLimits = do
108+ st <- getState
109+ return $ ((< 0) <$> orgStateEmphasisNewlines st) /= Just True
110+
111+resetEmphasisNewlines :: OrgParser ()
112+resetEmphasisNewlines = updateState $ \s ->
113+ s{ orgStateEmphasisNewlines = Nothing }
114+
115+addToNotesTable :: OrgNoteRecord -> OrgParser ()
116+addToNotesTable note = do
117+ oldnotes <- orgStateNotes' <$> getState
118+ updateState $ \s -> s{ orgStateNotes' = note:oldnotes }
119+
120+-- | Parse a single Org-mode inline element
121+inline :: OrgParser (F Inlines)
122+inline =
123+ choice [ whitespace
124+ , linebreak
125+ , cite
126+ , footnote
127+ , linkOrImage
128+ , anchor
129+ , inlineCodeBlock
130+ , str
131+ , endline
132+ , emph
133+ , strong
134+ , strikeout
135+ , underline
136+ , code
137+ , math
138+ , displayMath
139+ , verbatim
140+ , subscript
141+ , superscript
142+ , inlineLaTeX
143+ , smart
144+ , symbol
145+ ] <* (guard =<< newlinesCountWithinLimits)
146+ <?> "inline"
147+
148+parseInlines :: OrgParser (F Inlines)
149+parseInlines = trimInlinesF . mconcat <$> many1 inline
150+
151+-- treat these as potentially non-text when parsing inline:
152+specialChars :: [Char]
153+specialChars = "\"$'()*+-,./:<=>[\\]^_{|}~"
154+
155+
156+whitespace :: OrgParser (F Inlines)
157+whitespace = pure B.space <$ skipMany1 spaceChar
158+ <* updateLastPreCharPos
159+ <* updateLastForbiddenCharPos
160+ <?> "whitespace"
161+
162+linebreak :: OrgParser (F Inlines)
163+linebreak = try $ pure B.linebreak <$ string "\\\\" <* skipSpaces <* newline
164+
165+str :: OrgParser (F Inlines)
166+str = return . B.str <$> many1 (noneOf $ specialChars ++ "\n\r ")
167+ <* updateLastStrPos
168+
169+-- | An endline character that can be treated as a space, not a structural
170+-- break. This should reflect the values of the Emacs variable
171+-- @org-element-pagaraph-separate@.
172+endline :: OrgParser (F Inlines)
173+endline = try $ do
174+ newline
175+ notFollowedBy blankline
176+ notFollowedBy' exampleLineStart
177+ notFollowedBy' hline
178+ notFollowedBy' noteMarker
179+ notFollowedBy' tableStart
180+ notFollowedBy' drawerStart
181+ notFollowedBy' headerStart
182+ notFollowedBy' metaLineStart
183+ notFollowedBy' latexEnvStart
184+ notFollowedBy' commentLineStart
185+ notFollowedBy' bulletListStart
186+ notFollowedBy' orderedListStart
187+ decEmphasisNewlinesCount
188+ guard =<< newlinesCountWithinLimits
189+ updateLastPreCharPos
190+ return . return $ B.softbreak
191+
192+cite :: OrgParser (F Inlines)
193+cite = try $ do
194+ guardEnabled Ext_citations
195+ (cs, raw) <- withRaw normalCite
196+ return $ (flip B.cite (B.text raw)) <$> cs
197+
198+normalCite :: OrgParser (F [Citation])
199+normalCite = try $ char '['
200+ *> skipSpaces
201+ *> citeList
202+ <* skipSpaces
203+ <* char ']'
204+
205+citeList :: OrgParser (F [Citation])
206+citeList = sequence <$> sepBy1 citation (try $ char ';' *> skipSpaces)
207+
208+citation :: OrgParser (F Citation)
209+citation = try $ do
210+ pref <- prefix
211+ (suppress_author, key) <- citeKey
212+ suff <- suffix
213+ return $ do
214+ x <- pref
215+ y <- suff
216+ return $ Citation{ citationId = key
217+ , citationPrefix = B.toList x
218+ , citationSuffix = B.toList y
219+ , citationMode = if suppress_author
220+ then SuppressAuthor
221+ else NormalCitation
222+ , citationNoteNum = 0
223+ , citationHash = 0
224+ }
225+ where
226+ prefix = trimInlinesF . mconcat <$>
227+ manyTill inline (char ']' <|> (']' <$ lookAhead citeKey))
228+ suffix = try $ do
229+ hasSpace <- option False (notFollowedBy nonspaceChar >> return True)
230+ skipSpaces
231+ rest <- trimInlinesF . mconcat <$>
232+ many (notFollowedBy (oneOf ";]") *> inline)
233+ return $ if hasSpace
234+ then (B.space <>) <$> rest
235+ else rest
236+
237+footnote :: OrgParser (F Inlines)
238+footnote = try $ inlineNote <|> referencedNote
239+
240+inlineNote :: OrgParser (F Inlines)
241+inlineNote = try $ do
242+ string "[fn:"
243+ ref <- many alphaNum
244+ char ':'
245+ note <- fmap B.para . trimInlinesF . mconcat <$> many1Till inline (char ']')
246+ when (not $ null ref) $
247+ addToNotesTable ("fn:" ++ ref, note)
248+ return $ B.note <$> note
249+
250+referencedNote :: OrgParser (F Inlines)
251+referencedNote = try $ do
252+ ref <- noteMarker
253+ return $ do
254+ notes <- asksF orgStateNotes'
255+ case lookup ref notes of
256+ Nothing -> return $ B.str $ "[" ++ ref ++ "]"
257+ Just contents -> do
258+ st <- askF
259+ let contents' = runF contents st{ orgStateNotes' = [] }
260+ return $ B.note contents'
261+
262+linkOrImage :: OrgParser (F Inlines)
263+linkOrImage = explicitOrImageLink
264+ <|> selflinkOrImage
265+ <|> angleLink
266+ <|> plainLink
267+ <?> "link or image"
268+
269+explicitOrImageLink :: OrgParser (F Inlines)
270+explicitOrImageLink = try $ do
271+ char '['
272+ srcF <- applyCustomLinkFormat =<< possiblyEmptyLinkTarget
273+ title <- enclosedRaw (char '[') (char ']')
274+ title' <- parseFromString (mconcat <$> many inline) title
275+ char ']'
276+ return $ do
277+ src <- srcF
278+ if isImageFilename title
279+ then pure $ B.link src "" $ B.image title mempty mempty
280+ else linkToInlinesF src =<< title'
281+
282+selflinkOrImage :: OrgParser (F Inlines)
283+selflinkOrImage = try $ do
284+ src <- char '[' *> linkTarget <* char ']'
285+ return $ linkToInlinesF src (B.str src)
286+
287+plainLink :: OrgParser (F Inlines)
288+plainLink = try $ do
289+ (orig, src) <- uri
290+ returnF $ B.link src "" (B.str orig)
291+
292+angleLink :: OrgParser (F Inlines)
293+angleLink = try $ do
294+ char '<'
295+ link <- plainLink
296+ char '>'
297+ return link
298+
299+linkTarget :: OrgParser String
300+linkTarget = enclosedByPair '[' ']' (noneOf "\n\r[]")
301+
302+possiblyEmptyLinkTarget :: OrgParser String
303+possiblyEmptyLinkTarget = try linkTarget <|> ("" <$ string "[]")
304+
305+applyCustomLinkFormat :: String -> OrgParser (F String)
306+applyCustomLinkFormat link = do
307+ let (linkType, rest) = break (== ':') link
308+ return $ do
309+ formatter <- M.lookup linkType <$> asksF orgStateLinkFormatters
310+ return $ maybe link ($ drop 1 rest) formatter
311+
312+-- | Take a link and return a function which produces new inlines when given
313+-- description inlines.
314+linkToInlinesF :: String -> Inlines -> F Inlines
315+linkToInlinesF linkStr =
316+ case linkStr of
317+ "" -> pure . B.link mempty "" -- wiki link (empty by convention)
318+ ('#':_) -> pure . B.link linkStr "" -- document-local fraction
319+ _ -> case cleanLinkString linkStr of
320+ (Just cleanedLink) -> if isImageFilename cleanedLink
321+ then const . pure $ B.image cleanedLink "" ""
322+ else pure . B.link cleanedLink ""
323+ Nothing -> internalLink linkStr -- other internal link
324+
325+-- | Cleanup and canonicalize a string describing a link. Return @Nothing@ if
326+-- the string does not appear to be a link.
327+cleanLinkString :: String -> Maybe String
328+cleanLinkString s =
329+ case s of
330+ '/':_ -> Just $ "file://" ++ s -- absolute path
331+ '.':'/':_ -> Just s -- relative path
332+ '.':'.':'/':_ -> Just s -- relative path
333+ -- Relative path or URL (file schema)
334+ 'f':'i':'l':'e':':':s' -> Just $ if ("//" `isPrefixOf` s') then s else s'
335+ _ | isUrl s -> Just s -- URL
336+ _ -> Nothing
337+ where
338+ isUrl :: String -> Bool
339+ isUrl cs =
340+ let (scheme, path) = break (== ':') cs
341+ in all (\c -> isAlphaNum c || c `elem` (".-"::String)) scheme
342+ && not (null path)
343+
344+isImageFilename :: String -> Bool
345+isImageFilename filename =
346+ any (\x -> ('.':x) `isSuffixOf` filename) imageExtensions &&
347+ (any (\x -> (x++":") `isPrefixOf` filename) protocols ||
348+ ':' `notElem` filename)
349+ where
350+ imageExtensions = [ "jpeg" , "jpg" , "png" , "gif" , "svg" ]
351+ protocols = [ "file", "http", "https" ]
352+
353+internalLink :: String -> Inlines -> F Inlines
354+internalLink link title = do
355+ anchorB <- (link `elem`) <$> asksF orgStateAnchorIds
356+ if anchorB
357+ then return $ B.link ('#':link) "" title
358+ else return $ B.emph title
359+
360+-- | Parse an anchor like @<<anchor-id>>@ and return an empty span with
361+-- @anchor-id@ set as id. Legal anchors in org-mode are defined through
362+-- @org-target-regexp@, which is fairly liberal. Since no link is created if
363+-- @anchor-id@ contains spaces, we are more restrictive in what is accepted as
364+-- an anchor.
365+
366+anchor :: OrgParser (F Inlines)
367+anchor = try $ do
368+ anchorId <- parseAnchor
369+ recordAnchorId anchorId
370+ returnF $ B.spanWith (solidify anchorId, [], []) mempty
371+ where
372+ parseAnchor = string "<<"
373+ *> many1 (noneOf "\t\n\r<>\"' ")
374+ <* string ">>"
375+ <* skipSpaces
376+
377+-- | Replace every char but [a-zA-Z0-9_.-:] with a hypen '-'. This mirrors
378+-- the org function @org-export-solidify-link-text@.
379+
380+solidify :: String -> String
381+solidify = map replaceSpecialChar
382+ where replaceSpecialChar c
383+ | isAlphaNum c = c
384+ | c `elem` ("_.-:" :: String) = c
385+ | otherwise = '-'
386+
387+-- | Parses an inline code block and marks it as an babel block.
388+inlineCodeBlock :: OrgParser (F Inlines)
389+inlineCodeBlock = try $ do
390+ string "src_"
391+ lang <- many1 orgArgWordChar
392+ opts <- option [] $ enclosedByPair '[' ']' inlineBlockOption
393+ inlineCode <- enclosedByPair '{' '}' (noneOf "\n\r")
394+ let attrClasses = [translateLang lang, rundocBlockClass]
395+ let attrKeyVal = map toRundocAttrib (("language", lang) : opts)
396+ returnF $ B.codeWith ("", attrClasses, attrKeyVal) inlineCode
397+ where
398+ inlineBlockOption :: OrgParser (String, String)
399+ inlineBlockOption = try $ do
400+ argKey <- orgArgKey
401+ paramValue <- option "yes" orgInlineParamValue
402+ return (argKey, paramValue)
403+
404+ orgInlineParamValue :: OrgParser String
405+ orgInlineParamValue = try $
406+ skipSpaces
407+ *> notFollowedBy (char ':')
408+ *> many1 (noneOf "\t\n\r ]")
409+ <* skipSpaces
410+
411+
412+
413+enclosedByPair :: Char -- ^ opening char
414+ -> Char -- ^ closing char
415+ -> OrgParser a -- ^ parser
416+ -> OrgParser [a]
417+enclosedByPair s e p = char s *> many1Till p (char e)
418+
419+emph :: OrgParser (F Inlines)
420+emph = fmap B.emph <$> emphasisBetween '/'
421+
422+strong :: OrgParser (F Inlines)
423+strong = fmap B.strong <$> emphasisBetween '*'
424+
425+strikeout :: OrgParser (F Inlines)
426+strikeout = fmap B.strikeout <$> emphasisBetween '+'
427+
428+-- There is no underline, so we use strong instead.
429+underline :: OrgParser (F Inlines)
430+underline = fmap B.strong <$> emphasisBetween '_'
431+
432+verbatim :: OrgParser (F Inlines)
433+verbatim = return . B.code <$> verbatimBetween '='
434+
435+code :: OrgParser (F Inlines)
436+code = return . B.code <$> verbatimBetween '~'
437+
438+subscript :: OrgParser (F Inlines)
439+subscript = fmap B.subscript <$> try (char '_' *> subOrSuperExpr)
440+
441+superscript :: OrgParser (F Inlines)
442+superscript = fmap B.superscript <$> try (char '^' *> subOrSuperExpr)
443+
444+math :: OrgParser (F Inlines)
445+math = return . B.math <$> choice [ math1CharBetween '$'
446+ , mathStringBetween '$'
447+ , rawMathBetween "\\(" "\\)"
448+ ]
449+
450+displayMath :: OrgParser (F Inlines)
451+displayMath = return . B.displayMath <$> choice [ rawMathBetween "\\[" "\\]"
452+ , rawMathBetween "$$" "$$"
453+ ]
454+
455+updatePositions :: Char
456+ -> OrgParser (Char)
457+updatePositions c = do
458+ when (c `elem` emphasisPreChars) updateLastPreCharPos
459+ when (c `elem` emphasisForbiddenBorderChars) updateLastForbiddenCharPos
460+ return c
461+
462+symbol :: OrgParser (F Inlines)
463+symbol = return . B.str . (: "") <$> (oneOf specialChars >>= updatePositions)
464+
465+emphasisBetween :: Char
466+ -> OrgParser (F Inlines)
467+emphasisBetween c = try $ do
468+ startEmphasisNewlinesCounting emphasisAllowedNewlines
469+ res <- enclosedInlines (emphasisStart c) (emphasisEnd c)
470+ isTopLevelEmphasis <- null . orgStateEmphasisCharStack <$> getState
471+ when isTopLevelEmphasis
472+ resetEmphasisNewlines
473+ return res
474+
475+verbatimBetween :: Char
476+ -> OrgParser String
477+verbatimBetween c = try $
478+ emphasisStart c *>
479+ many1TillNOrLessNewlines 1 (noneOf "\n\r") (emphasisEnd c)
480+
481+-- | Parses a raw string delimited by @c@ using Org's math rules
482+mathStringBetween :: Char
483+ -> OrgParser String
484+mathStringBetween c = try $ do
485+ mathStart c
486+ body <- many1TillNOrLessNewlines mathAllowedNewlines
487+ (noneOf (c:"\n\r"))
488+ (lookAhead $ mathEnd c)
489+ final <- mathEnd c
490+ return $ body ++ [final]
491+
492+-- | Parse a single character between @c@ using math rules
493+math1CharBetween :: Char
494+ -> OrgParser String
495+math1CharBetween c = try $ do
496+ char c
497+ res <- noneOf $ c:mathForbiddenBorderChars
498+ char c
499+ eof <|> () <$ lookAhead (oneOf mathPostChars)
500+ return [res]
501+
502+rawMathBetween :: String
503+ -> String
504+ -> OrgParser String
505+rawMathBetween s e = try $ string s *> manyTill anyChar (try $ string e)
506+
507+-- | Parses the start (opening character) of emphasis
508+emphasisStart :: Char -> OrgParser Char
509+emphasisStart c = try $ do
510+ guard =<< afterEmphasisPreChar
511+ guard =<< notAfterString
512+ char c
513+ lookAhead (noneOf emphasisForbiddenBorderChars)
514+ pushToInlineCharStack c
515+ return c
516+
517+-- | Parses the closing character of emphasis
518+emphasisEnd :: Char -> OrgParser Char
519+emphasisEnd c = try $ do
520+ guard =<< notAfterForbiddenBorderChar
521+ char c
522+ eof <|> () <$ lookAhead acceptablePostChars
523+ updateLastStrPos
524+ popInlineCharStack
525+ return c
526+ where acceptablePostChars =
527+ surroundingEmphasisChar >>= \x -> oneOf (x ++ emphasisPostChars)
528+
529+mathStart :: Char -> OrgParser Char
530+mathStart c = try $
531+ char c <* notFollowedBy' (oneOf (c:mathForbiddenBorderChars))
532+
533+mathEnd :: Char -> OrgParser Char
534+mathEnd c = try $ do
535+ res <- noneOf (c:mathForbiddenBorderChars)
536+ char c
537+ eof <|> () <$ lookAhead (oneOf mathPostChars)
538+ return res
539+
540+
541+enclosedInlines :: OrgParser a
542+ -> OrgParser b
543+ -> OrgParser (F Inlines)
544+enclosedInlines start end = try $
545+ trimInlinesF . mconcat <$> enclosed start end inline
546+
547+enclosedRaw :: OrgParser a
548+ -> OrgParser b
549+ -> OrgParser String
550+enclosedRaw start end = try $
551+ start *> (onSingleLine <|> spanningTwoLines)
552+ where onSingleLine = try $ many1Till (noneOf "\n\r") end
553+ spanningTwoLines = try $
554+ anyLine >>= \f -> mappend (f <> " ") <$> onSingleLine
555+
556+-- | Like many1Till, but parses at most @n+1@ lines. @p@ must not consume
557+-- newlines.
558+many1TillNOrLessNewlines :: Int
559+ -> OrgParser Char
560+ -> OrgParser a
561+ -> OrgParser String
562+many1TillNOrLessNewlines n p end = try $
563+ nMoreLines (Just n) mempty >>= oneOrMore
564+ where
565+ nMoreLines Nothing cs = return cs
566+ nMoreLines (Just 0) cs = try $ (cs ++) <$> finalLine
567+ nMoreLines k cs = try $ (final k cs <|> rest k cs)
568+ >>= uncurry nMoreLines
569+ final _ cs = (\x -> (Nothing, cs ++ x)) <$> try finalLine
570+ rest m cs = (\x -> (minus1 <$> m, cs ++ x ++ "\n")) <$> try (manyTill p newline)
571+ finalLine = try $ manyTill p end
572+ minus1 k = k - 1
573+ oneOrMore cs = guard (not $ null cs) *> return cs
574+
575+-- Org allows customization of the way it reads emphasis. We use the defaults
576+-- here (see, e.g., the Emacs Lisp variable `org-emphasis-regexp-components`
577+-- for details).
578+
579+-- | Chars allowed to occur before emphasis (spaces and newlines are ok, too)
580+emphasisPreChars :: [Char]
581+emphasisPreChars = "\t \"'({"
582+
583+-- | Chars allowed at after emphasis
584+emphasisPostChars :: [Char]
585+emphasisPostChars = "\t\n !\"'),-.:;?\\}"
586+
587+-- | Chars not allowed at the (inner) border of emphasis
588+emphasisForbiddenBorderChars :: [Char]
589+emphasisForbiddenBorderChars = "\t\n\r \"',"
590+
591+-- | The maximum number of newlines within
592+emphasisAllowedNewlines :: Int
593+emphasisAllowedNewlines = 1
594+
595+-- LaTeX-style math: see `org-latex-regexps` for details
596+
597+-- | Chars allowed after an inline ($...$) math statement
598+mathPostChars :: [Char]
599+mathPostChars = "\t\n \"'),-.:;?"
600+
601+-- | Chars not allowed at the (inner) border of math
602+mathForbiddenBorderChars :: [Char]
603+mathForbiddenBorderChars = "\t\n\r ,;.$"
604+
605+-- | Maximum number of newlines in an inline math statement
606+mathAllowedNewlines :: Int
607+mathAllowedNewlines = 2
608+
609+-- | Whether we are right behind a char allowed before emphasis
610+afterEmphasisPreChar :: OrgParser Bool
611+afterEmphasisPreChar = do
612+ pos <- getPosition
613+ lastPrePos <- orgStateLastPreCharPos <$> getState
614+ return . fromMaybe True $ (== pos) <$> lastPrePos
615+
616+-- | Whether the parser is right after a forbidden border char
617+notAfterForbiddenBorderChar :: OrgParser Bool
618+notAfterForbiddenBorderChar = do
619+ pos <- getPosition
620+ lastFBCPos <- orgStateLastForbiddenCharPos <$> getState
621+ return $ lastFBCPos /= Just pos
622+
623+-- | Read a sub- or superscript expression
624+subOrSuperExpr :: OrgParser (F Inlines)
625+subOrSuperExpr = try $
626+ choice [ id <$> charsInBalanced '{' '}' (noneOf "\n\r")
627+ , enclosing ('(', ')') <$> charsInBalanced '(' ')' (noneOf "\n\r")
628+ , simpleSubOrSuperString
629+ ] >>= parseFromString (mconcat <$> many inline)
630+ where enclosing (left, right) s = left : s ++ [right]
631+
632+simpleSubOrSuperString :: OrgParser String
633+simpleSubOrSuperString = try $ do
634+ state <- getState
635+ guard . exportSubSuperscripts . orgStateExportSettings $ state
636+ choice [ string "*"
637+ , mappend <$> option [] ((:[]) <$> oneOf "+-")
638+ <*> many1 alphaNum
639+ ]
640+
641+inlineLaTeX :: OrgParser (F Inlines)
642+inlineLaTeX = try $ do
643+ cmd <- inlineLaTeXCommand
644+ maybe mzero returnF $
645+ parseAsMath cmd `mplus` parseAsMathMLSym cmd `mplus` parseAsInlineLaTeX cmd
646+ where
647+ parseAsMath :: String -> Maybe Inlines
648+ parseAsMath cs = B.fromList <$> texMathToPandoc cs
649+
650+ parseAsInlineLaTeX :: String -> Maybe Inlines
651+ parseAsInlineLaTeX cs = maybeRight $ runParser inlineCommand state "" cs
652+
653+ parseAsMathMLSym :: String -> Maybe Inlines
654+ parseAsMathMLSym cs = B.str <$> MathMLEntityMap.getUnicode (clean cs)
655+ -- drop initial backslash and any trailing "{}"
656+ where clean = dropWhileEnd (`elem` ("{}" :: String)) . drop 1
657+
658+ state :: ParserState
659+ state = def{ stateOptions = def{ readerParseRaw = True }}
660+
661+ texMathToPandoc :: String -> Maybe [Inline]
662+ texMathToPandoc cs = (maybeRight $ readTeX cs) >>= writePandoc DisplayInline
663+
664+maybeRight :: Either a b -> Maybe b
665+maybeRight = either (const Nothing) Just
666+
667+inlineLaTeXCommand :: OrgParser String
668+inlineLaTeXCommand = try $ do
669+ rest <- getInput
670+ case runParser rawLaTeXInline def "source" rest of
671+ Right (RawInline _ cs) -> do
672+ -- drop any trailing whitespace, those are not be part of the command as
673+ -- far as org mode is concerned.
674+ let cmdNoSpc = dropWhileEnd isSpace cs
675+ let len = length cmdNoSpc
676+ count len anyChar
677+ return cmdNoSpc
678+ _ -> mzero
679+
680+-- Taken from Data.OldList.
681+dropWhileEnd :: (a -> Bool) -> [a] -> [a]
682+dropWhileEnd p = foldr (\x xs -> if p x && null xs then [] else x : xs) []
683+
684+smart :: OrgParser (F Inlines)
685+smart = do
686+ getOption readerSmart >>= guard
687+ doubleQuoted <|> singleQuoted <|>
688+ choice (map (return <$>) [orgApostrophe, orgDash, orgEllipses])
689+ where
690+ orgDash = dash <* updatePositions '-'
691+ orgEllipses = ellipses <* updatePositions '.'
692+ orgApostrophe =
693+ (char '\'' <|> char '\8217') <* updateLastPreCharPos
694+ <* updateLastForbiddenCharPos
695+ *> return (B.str "\x2019")
696+
697+singleQuoted :: OrgParser (F Inlines)
698+singleQuoted = try $ do
699+ singleQuoteStart
700+ updatePositions '\''
701+ withQuoteContext InSingleQuote $
702+ fmap B.singleQuoted . trimInlinesF . mconcat <$>
703+ many1Till inline (singleQuoteEnd <* updatePositions '\'')
704+
705+-- doubleQuoted will handle regular double-quoted sections, as well
706+-- as dialogues with an open double-quote without a close double-quote
707+-- in the same paragraph.
708+doubleQuoted :: OrgParser (F Inlines)
709+doubleQuoted = try $ do
710+ doubleQuoteStart
711+ updatePositions '"'
712+ contents <- mconcat <$> many (try $ notFollowedBy doubleQuoteEnd >> inline)
713+ (withQuoteContext InDoubleQuote $ (doubleQuoteEnd <* updateLastForbiddenCharPos) >> return
714+ (fmap B.doubleQuoted . trimInlinesF $ contents))
715+ <|> (return $ return (B.str "\8220") <> contents)
src/Text/Pandoc/Readers/Org/ParserState.hs+5−2
@@ -29,9 +29,11 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2929Define the Org-mode parser state.
3030-}
3131module Text.Pandoc.Readers.Org.ParserState
32− ( OrgParserState(..)
33− , OrgParserLocal(..)
32+ ( OrgParserState (..)
33+ , OrgParserLocal (..)
3434 , OrgNoteRecord
35+ , HasReaderOptions (..)
36+ , HasQuoteContext (..)
3537 , F(..)
3638 , askF
3739 , asksF
@@ -184,6 +186,7 @@ modifyExportSettings :: ExportSettingSetter a -> a -> OrgParserState -> OrgParse
184186modifyExportSettings setter val state =
185187 state { orgStateExportSettings = setter val . orgStateExportSettings $ state }
186188
189+
187190--
188191-- Parser state reader
189192--
src/Text/Pandoc/Readers/Org/Parsing.hsnew file+201
@@ -0,0 +1,201 @@
1+{-
2+Copyright (C) 2014-2016 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+
19+{- |
20+ Module : Text.Pandoc.Readers.Org.Options
21+ Copyright : Copyright (C) 2014-2016 Albert Krewinkel
22+ License : GNU GPL, version 2 or above
23+
24+ Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
25+
26+Org-mode parsing utilities.
27+
28+Most functions are simply re-exports from @Text.Pandoc.Parsing@, some
29+functions are adapted to Org-mode specific functionality.
30+-}
31+module Text.Pandoc.Readers.Org.Parsing
32+ ( OrgParser
33+ , anyLine
34+ , blanklines
35+ , newline
36+ , parseFromString
37+ , skipSpaces1
38+ , inList
39+ , withContext
40+ , updateLastForbiddenCharPos
41+ , updateLastPreCharPos
42+ , orgArgKey
43+ , orgArgWord
44+ , orgArgWordChar
45+ -- * Re-exports from Text.Pandoc.Parser
46+ , ParserContext (..)
47+ , many1Till
48+ , notFollowedBy'
49+ , spaceChar
50+ , nonspaceChar
51+ , skipSpaces
52+ , blankline
53+ , enclosed
54+ , stringAnyCase
55+ , charsInBalanced
56+ , uri
57+ , withRaw
58+ , readWithM
59+ , guardEnabled
60+ , updateLastStrPos
61+ , notAfterString
62+ , ParserState (..)
63+ , registerHeader
64+ , QuoteContext (..)
65+ , singleQuoteStart
66+ , singleQuoteEnd
67+ , doubleQuoteStart
68+ , doubleQuoteEnd
69+ , dash
70+ , ellipses
71+ , citeKey
72+ -- * Re-exports from Text.Pandoc.Parsec
73+ , runParser
74+ , getInput
75+ , char
76+ , letter
77+ , digit
78+ , alphaNum
79+ , skipMany1
80+ , spaces
81+ , anyChar
82+ , string
83+ , count
84+ , eof
85+ , noneOf
86+ , oneOf
87+ , lookAhead
88+ , notFollowedBy
89+ , many
90+ , many1
91+ , manyTill
92+ , (<|>)
93+ , (<?>)
94+ , choice
95+ , try
96+ , sepBy
97+ , sepBy1
98+ , option
99+ , optional
100+ , optionMaybe
101+ , getState
102+ , updateState
103+ , SourcePos
104+ , getPosition
105+ ) where
106+
107+import Text.Pandoc.Readers.Org.ParserState
108+
109+import qualified Text.Pandoc.Parsing as P
110+import Text.Pandoc.Parsing hiding ( anyLine, blanklines, newline
111+ , parseFromString )
112+
113+import Control.Monad ( guard )
114+import Control.Monad.Reader ( Reader )
115+
116+-- | The parser used to read org files.
117+type OrgParser = ParserT [Char] OrgParserState (Reader OrgParserLocal)
118+
119+--
120+-- Adaptions and specializations of parsing utilities
121+--
122+
123+-- | Parse any line of text
124+anyLine :: OrgParser String
125+anyLine =
126+ P.anyLine
127+ <* updateLastPreCharPos
128+ <* updateLastForbiddenCharPos
129+
130+-- The version Text.Pandoc.Parsing cannot be used, as we need additional parts
131+-- of the state saved and restored.
132+parseFromString :: OrgParser a -> String -> OrgParser a
133+parseFromString parser str' = do
134+ oldLastPreCharPos <- orgStateLastPreCharPos <$> getState
135+ updateState $ \s -> s{ orgStateLastPreCharPos = Nothing }
136+ result <- P.parseFromString parser str'
137+ updateState $ \s -> s{ orgStateLastPreCharPos = oldLastPreCharPos }
138+ return result
139+
140+-- | Skip one or more tab or space characters.
141+skipSpaces1 :: OrgParser ()
142+skipSpaces1 = skipMany1 spaceChar
143+
144+-- | Like @Text.Parsec.Char.newline@, but causes additional state changes.
145+newline :: OrgParser Char
146+newline =
147+ P.newline
148+ <* updateLastPreCharPos
149+ <* updateLastForbiddenCharPos
150+
151+-- | Like @Text.Parsec.Char.blanklines@, but causes additional state changes.
152+blanklines :: OrgParser [Char]
153+blanklines =
154+ P.blanklines
155+ <* updateLastPreCharPos
156+ <* updateLastForbiddenCharPos
157+
158+-- | Succeeds when we're in list context.
159+inList :: OrgParser ()
160+inList = do
161+ ctx <- orgStateParserContext <$> getState
162+ guard (ctx == ListItemState)
163+
164+-- | Parse in different context
165+withContext :: ParserContext -- ^ New parser context
166+ -> OrgParser a -- ^ Parser to run in that context
167+ -> OrgParser a
168+withContext context parser = do
169+ oldContext <- orgStateParserContext <$> getState
170+ updateState $ \s -> s{ orgStateParserContext = context }
171+ result <- parser
172+ updateState $ \s -> s{ orgStateParserContext = oldContext }
173+ return result
174+
175+--
176+-- Parser state update functions
177+--
178+
179+-- | Set the current position as the last position at which a forbidden char
180+-- was found (i.e. a character which is not allowed at the inner border of
181+-- markup).
182+updateLastForbiddenCharPos :: OrgParser ()
183+updateLastForbiddenCharPos = getPosition >>= \p ->
184+ updateState $ \s -> s{ orgStateLastForbiddenCharPos = Just p}
185+
186+-- | Set the current parser position as the position at which a character was
187+-- seen which allows inline markup to follow.
188+updateLastPreCharPos :: OrgParser ()
189+updateLastPreCharPos = getPosition >>= \p ->
190+ updateState $ \s -> s{ orgStateLastPreCharPos = Just p}
191+
192+orgArgKey :: OrgParser String
193+orgArgKey = try $
194+ skipSpaces *> char ':'
195+ *> many1 orgArgWordChar
196+
197+orgArgWord :: OrgParser String
198+orgArgWord = many1 orgArgWordChar
199+
200+orgArgWordChar :: OrgParser Char
201+orgArgWordChar = alphaNum <|> oneOf "-_"