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

Org reader: Support more inline/display math variants

commit3e147199b86fbe42eb6b993e748dd82ab2050da9 authorJohn MacFarlane <jgm@berkeley.edu> dateThu 10 Apr 2014, 11:02 (13 years ago) parenta9eb0caace8837
2 files changed+80−28
src/Text/Pandoc/Readers/Org.hs+50−22 tests/Tests/Readers/Org.hs+30−6
src/Text/Pandoc/Readers/Org.hs+50−22
@@ -44,11 +44,14 @@ import Data.List (foldl', isPrefixOf, isSuffixOf)
4444import Data.Maybe (listToMaybe, fromMaybe)
4545import Data.Monoid (mconcat, mempty, mappend)
4646
47+-- Ignore HLint warnings to use String instead of [Char]
48+{-# ANN module ("HLint: ignore Use String" :: String) #-}
49+
4750-- | Parse org-mode string and return a Pandoc document.
4851readOrg :: ReaderOptions -- ^ Reader options
4952 -> String -- ^ String to parse (assuming @'\n'@ line endings)
5053 -> Pandoc
51−readOrg opts s = (readWith parseOrg) def{ orgStateOptions = opts } (s ++ "\n\n")
54+readOrg opts s = readWith parseOrg def{ orgStateOptions = opts } (s ++ "\n\n")
5255
5356type OrgParser = Parser [Char] OrgParserState
5457
@@ -111,7 +114,7 @@ updateLastPreCharPos = getPosition >>= \p ->
111114
112115pushToInlineCharStack :: Char -> OrgParser ()
113116pushToInlineCharStack c = updateState $ \st ->
114− st { orgStateEmphasisCharStack = c:(orgStateEmphasisCharStack st) }
117+ st { orgStateEmphasisCharStack = c:orgStateEmphasisCharStack st }
115118
116119popInlineCharStack :: OrgParser ()
117120popInlineCharStack = updateState $ \st ->
@@ -176,7 +179,7 @@ orgBlock = try $ do
176179 "comment" -> return mempty
177180 "src" -> return $ B.codeBlockWith ("", classArgs, []) blockStr
178181 _ -> B.divWith ("", [blockType], [])
179− <$> (parseFromString parseBlocks blockStr)
182+ <$> parseFromString parseBlocks blockStr
180183
181184blockHeader :: OrgParser (Int, String, [String])
182185blockHeader = (,,) <$> blockIndent
@@ -199,7 +202,7 @@ rawBlockContent indent blockType =
199202indentWith :: Int -> OrgParser String
200203indentWith num = do
201204 tabStop <- getOption readerTabStop
202− if (num < tabStop)
205+ if num < tabStop
203206 then count num (char ' ')
204207 else choice [ try (count num (char ' '))
205208 , try (char '\t' >> count (num - tabStop) (char ' ')) ]
@@ -242,7 +245,7 @@ drawerStart = try $
242245 <|> stringAnyCase "LOGBOOK"
243246
244247drawerLine :: OrgParser String
245−drawerLine = try $ anyLine
248+drawerLine = try anyLine
246249
247250drawerEnd :: OrgParser String
248251drawerEnd = try $
@@ -276,7 +279,7 @@ declarationLine = try $ do
276279metaValue :: OrgParser MetaValue
277280metaValue = MetaInlines . B.toList . trimInlines <$> restOfLine
278281
279−metaKey :: OrgParser [Char]
282+metaKey :: OrgParser String
280283metaKey = map toLower <$> many1 (noneOf ": \n\r")
281284 <* char ':'
282285 <* skipSpaces
@@ -350,7 +353,7 @@ tableAlignRow = try $
350353
351354tableAlignCell :: OrgParser Alignment
352355tableAlignCell =
353− choice [ try $ emptyCell *> return (AlignDefault)
356+ choice [ try $ emptyCell *> return AlignDefault
354357 , try $ skipSpaces
355358 *> char '<'
356359 *> tableAlignFromChar
@@ -381,8 +384,8 @@ normalizeTable (OrgTable cols aligns heads lns) =
381384 let aligns' = fillColumns aligns AlignDefault
382385 heads' = if heads == mempty
383386 then mempty
384− else fillColumns heads (B.plain mempty)
385− lns' = map (flip fillColumns (B.plain mempty)) lns
387+ else fillColumns heads (B.plain mempty)
388+ lns' = map (`fillColumns` B.plain mempty) lns
386389 fillColumns base padding = take cols $ base ++ repeat padding
387390 in OrgTable cols aligns' heads' lns'
388391
@@ -512,6 +515,7 @@ inline =
512515 , underline
513516 , code
514517 , math
518+ , displayMath
515519 , verbatim
516520 , subscript
517521 , superscript
@@ -564,13 +568,14 @@ explicitOrImageLink = try $ do
564568 title <- enclosedRaw (char '[') (char ']')
565569 title' <- parseFromString (mconcat <$> many inline) title
566570 char ']'
567− return $ if (isImageFilename src) && (isImageFilename title)
568− then B.link src "" (B.image title "" "")
569− else B.link src "" title'
571+ return . B.link src ""
572+ $ if isImageFilename src && isImageFilename title
573+ then B.image title "" ""
574+ else title'
570575
571576selflinkOrImage :: OrgParser Inlines
572577selflinkOrImage = try $ do
573− src <- (char '[') *> linkTarget <* char ']'
578+ src <- char '[' *> linkTarget <* char ']'
574579 return $ if isImageFilename src
575580 then B.image src "" ""
576581 else B.link src "" (B.str src)
@@ -607,13 +612,21 @@ verbatim :: OrgParser Inlines
607612verbatim = B.rawInline "" <$> verbatimBetween '~'
608613
609614math :: OrgParser Inlines
610−math = B.math <$> mathStringBetween '$'
615+math = B.math <$> choice [ math1CharBetween '$'
616+ , mathStringBetween '$'
617+ , rawMathBetween "\\(" "\\)"
618+ ]
619+
620+displayMath :: OrgParser Inlines
621+displayMath = B.displayMath <$> choice [ rawMathBetween "\\[" "\\]"
622+ , rawMathBetween "$$" "$$"
623+ ]
611624
612625subscript :: OrgParser Inlines
613−subscript = B.subscript <$> (try $ char '_' *> maybeGroupedByBraces)
626+subscript = B.subscript <$> try (char '_' *> maybeGroupedByBraces)
614627
615628superscript :: OrgParser Inlines
616−superscript = B.superscript <$> (try $ char '^' *> maybeGroupedByBraces)
629+superscript = B.superscript <$> try (char '^' *> maybeGroupedByBraces)
617630
618631maybeGroupedByBraces :: OrgParser Inlines
619632maybeGroupedByBraces = try $
@@ -655,6 +668,21 @@ mathStringBetween c = try $ do
655668 final <- mathEnd c
656669 return $ body ++ [final]
657670
671+-- | Parse a single character between @c@ using math rules
672+math1CharBetween :: Char
673+ -> OrgParser String
674+math1CharBetween c = try $ do
675+ char c
676+ res <- noneOf $ c:mathForbiddenBorderChars
677+ char c
678+ eof <|> lookAhead (oneOf mathPostChars) *> return ()
679+ return [res]
680+
681+rawMathBetween :: String
682+ -> String
683+ -> OrgParser String
684+rawMathBetween s e = try $ string s *> manyTill anyChar (try $ string e)
685+
658686-- | Parses the start (opening character) of emphasis
659687emphasisStart :: Char -> OrgParser Char
660688emphasisStart c = try $ do
@@ -678,14 +706,14 @@ emphasisEnd c = try $ do
678706 return c
679707
680708mathStart :: Char -> OrgParser Char
681−mathStart c = try $ do
709+mathStart c = try $
682710 char c <* notFollowedBy' (oneOf (c:mathForbiddenBorderChars))
683711
684712mathEnd :: Char -> OrgParser Char
685713mathEnd c = try $ do
686714 res <- noneOf (c:mathForbiddenBorderChars)
687715 char c
688− eof <|> (lookAhead $ oneOf mathPostChars *> pure ())
716+ eof <|> lookAhead (oneOf mathPostChars *> pure ())
689717 return res
690718
691719
@@ -717,8 +745,8 @@ many1TillNOrLessNewlines n p end = try $
717745 nMoreLines (Just 0) cs = try $ (cs ++) <$> finalLine
718746 nMoreLines k cs = try $ (final k cs <|> rest k cs)
719747 >>= uncurry nMoreLines
720− final _ cs = (\x -> (Nothing, cs ++ x)) <$> (try finalLine)
721− rest m cs = (\x -> (minus1 <$> m, cs ++ x ++ "\n")) <$> (try $ manyTill p P.newline)
748+ final _ cs = (\x -> (Nothing, cs ++ x)) <$> try finalLine
749+ rest m cs = (\x -> (minus1 <$> m, cs ++ x ++ "\n")) <$> try (manyTill p P.newline)
722750 finalLine = try $ manyTill p end
723751 minus1 k = k - 1
724752 oneOrMore cs = guard (not $ null cs) *> return cs
@@ -747,7 +775,7 @@ emphasisAllowedNewlines = 1
747775
748776-- | Chars allowed after an inline ($...$) math statement
749777mathPostChars :: [Char]
750−mathPostChars = "\t\n \"',-.:;?"
778+mathPostChars = "\t\n \"'),-.:;?"
751779
752780-- | Chars not allowed at the (inner) border of math
753781mathForbiddenBorderChars :: [Char]
@@ -762,7 +790,7 @@ afterEmphasisPreChar :: OrgParser Bool
762790afterEmphasisPreChar = do
763791 pos <- getPosition
764792 lastPrePos <- orgStateLastPreCharPos <$> getState
765− return $ lastPrePos == Nothing || lastPrePos == Just pos
793+ return . fromMaybe True $ (== pos) <$> lastPrePos
766794
767795-- | Whether we are right after the end of a string
768796notAfterString :: OrgParser Bool
tests/Tests/Readers/Org.hs+30−6
@@ -54,14 +54,26 @@ tests =
5454 "=Robot.rock()=" =?>
5555 para (code "Robot.rock()")
5656
57− , "Math" =:
58− "$E=mc^2$" =?>
59− para (math "E=mc^2")
60−
6157 , "Verbatim" =:
6258 "~word for word~" =?>
6359 para (rawInline "" "word for word")
6460
61+ , "Math $..$" =:
62+ "$E=mc^2$" =?>
63+ para (math "E=mc^2")
64+
65+ , "Math $$..$$" =:
66+ "$$E=mc^2$$" =?>
67+ para (displayMath "E=mc^2")
68+
69+ , "Math \\[..\\]" =:
70+ "\\[E=ℎν\\]" =?>
71+ para (displayMath "E=ℎν")
72+
73+ , "Math \\(..\\)" =:
74+ "\\(σ_x σ_p ≥ \\frac{ℏ}{2}\\)" =?>
75+ para (math "σ_x σ_p ≥ \\frac{ℏ}{2}")
76+
6577 , "Symbol" =:
6678 "A * symbol" =?>
6779 para (str "A" <> space <> str "*" <> space <> "symbol")
@@ -86,14 +98,19 @@ tests =
8698 unlines [ "this+that+ +so+on"
8799 , "seven*eight* nine*"
88100 , "+not+funny+"
89− , "this == self"
90101 ] =?>
91102 para (spcSep [ "this+that+", "+so+on"
92103 , "seven*eight*", "nine*"
93104 , strikeout "not+funny"
94− , "this" <> space <> "==" <> space <> "self"
95105 ])
96106
107+ , "No empty markup" =:
108+ -- FIXME: __ is erroneously parsed as subscript "_"
109+ -- "// ** __ ++ == ~~ $$" =?>
110+ -- para (spcSep [ "//", "**", "__", "++", "==", "~~", "$$" ])
111+ "// ** ++ == ~~ $$" =?>
112+ para (spcSep [ "//", "**", "++", "==", "~~", "$$" ])
113+
97114 , "Adherence to Org's rules for markup borders" =:
98115 "/t/& a/ / ./r/ (*l*) /e/! /b/." =?>
99116 para (spcSep [ emph $ "t/&" <> space <> "a"
@@ -109,6 +126,13 @@ tests =
109126 para ((math "a\nb\nc") <> space <>
110127 spcSep [ "$d", "e", "f", "g$" ])
111128
129+ , "Single-character math" =:
130+ "$a$ $b$! $c$?" =?>
131+ para (spcSep [ math "a"
132+ , "$b$!"
133+ , (math "c") <> "?"
134+ ])
135+
112136 , "Markup may not span more than two lines" =:
113137 unlines [ "/this *is +totally", "nice+ not*", "emph/" ] =?>
114138 para (spcSep [ "/this"