git.iko.soy/typeable/pandoc
on master help sign in
Smarter smart quotes.

Treat a leading " with no closing " as a left curly quote. This supports the practice, in fiction, of continuing paragraphs quoting the same speaker without an end quote. It also helps with quotes that break over lines in line blocks. Closes #7216.

commit80e2e88287f43d88ea92a77779b25e161c81f67b authorJohn MacFarlane <jgm@berkeley.edu> dateWed 28 Apr 2021, 23:30 (6 years ago) parent8fe7e8d
7 files changed+67−70
src/Text/Pandoc/Parsing.hs+29−16 src/Text/Pandoc/Readers/Markdown.hs+10−8 src/Text/Pandoc/Readers/RST.hs+1−18 src/Text/Pandoc/Readers/TWiki.hs+1−21 test/Tests/Readers/Markdown.hs+1−1 test/command/7216.md+19 test/command/pandoc-citeproc-320a.md+6−6
src/Text/Pandoc/Parsing.hs+29−16
@@ -105,8 +105,9 @@ module Text.Pandoc.Parsing ( take1WhileP,
105105 singleQuoteEnd,
106106 doubleQuoteStart,
107107 doubleQuoteEnd,
108− ellipses,
109108 apostrophe,
109+ doubleCloseQuote,
110+ ellipses,
110111 dash,
111112 nested,
112113 citeKey,
@@ -1398,10 +1399,7 @@ smartPunctuation :: (HasReaderOptions st, HasLastStrPosition st, HasQuoteContext
13981399 -> ParserT s st m Inlines
13991400smartPunctuation inlineParser = do
14001401 guardEnabled Ext_smart
1401− choice [ quoted inlineParser, apostrophe, dash, ellipses ]
1402−
1403−apostrophe :: Stream s m Char => ParserT s st m Inlines
1404−apostrophe = (char '\'' <|> char '\8217') >> return (B.str "\x2019")
1402+ choice [ quoted inlineParser, apostrophe, doubleCloseQuote, dash, ellipses ]
14051403
14061404quoted :: (HasLastStrPosition st, HasQuoteContext st m, Stream s m Char)
14071405 => ParserT s st m Inlines
@@ -1411,16 +1409,22 @@ quoted inlineParser = doubleQuoted inlineParser <|> singleQuoted inlineParser
14111409singleQuoted :: (HasLastStrPosition st, HasQuoteContext st m, Stream s m Char)
14121410 => ParserT s st m Inlines
14131411 -> ParserT s st m Inlines
1414−singleQuoted inlineParser = try $ B.singleQuoted . mconcat
1415− <$ singleQuoteStart
1416− <*> withQuoteContext InSingleQuote (many1Till inlineParser singleQuoteEnd)
1417−
1418−doubleQuoted :: (HasQuoteContext st m, Stream s m Char)
1412+singleQuoted inlineParser = do
1413+ singleQuoteStart
1414+ (B.singleQuoted . mconcat <$>
1415+ try
1416+ (withQuoteContext InSingleQuote (many1Till inlineParser singleQuoteEnd)))
1417+ <|> pure "\8217"
1418+
1419+doubleQuoted :: (HasQuoteContext st m, HasLastStrPosition st, Stream s m Char)
14191420 => ParserT s st m Inlines
14201421 -> ParserT s st m Inlines
1421−doubleQuoted inlineParser = try $ B.doubleQuoted . mconcat
1422− <$ doubleQuoteStart
1423− <*> withQuoteContext InDoubleQuote (manyTill inlineParser doubleQuoteEnd)
1422+doubleQuoted inlineParser = do
1423+ doubleQuoteStart
1424+ (B.doubleQuoted . mconcat <$>
1425+ try
1426+ (withQuoteContext InDoubleQuote (manyTill inlineParser doubleQuoteEnd)))
1427+ <|> pure (B.str "\8220")
14241428
14251429failIfInQuoteContext :: (HasQuoteContext st m, Stream s m t)
14261430 => QuoteContext
@@ -1443,7 +1447,7 @@ singleQuoteStart = do
14431447 guard =<< notAfterString
14441448 try $ do
14451449 charOrRef "'\8216\145"
1446− notFollowedBy (oneOf [' ', '\t', '\n'])
1450+ notFollowedBy (satisfy isSpaceChar)
14471451
14481452singleQuoteEnd :: Stream s m Char
14491453 => ParserT s st m ()
@@ -1451,17 +1455,26 @@ singleQuoteEnd = try $ do
14511455 charOrRef "'\8217\146"
14521456 notFollowedBy alphaNum
14531457
1454−doubleQuoteStart :: (HasQuoteContext st m, Stream s m Char)
1458+doubleQuoteStart :: (HasLastStrPosition st,
1459+ HasQuoteContext st m,
1460+ Stream s m Char)
14551461 => ParserT s st m ()
14561462doubleQuoteStart = do
14571463 failIfInQuoteContext InDoubleQuote
1464+ guard =<< notAfterString
14581465 try $ do charOrRef "\"\8220\147"
1459− notFollowedBy (oneOf [' ', '\t', '\n'])
1466+ notFollowedBy (satisfy isSpaceChar)
14601467
14611468doubleQuoteEnd :: Stream s m Char
14621469 => ParserT s st m ()
14631470doubleQuoteEnd = void (charOrRef "\"\8221\148")
14641471
1472+apostrophe :: Stream s m Char => ParserT s st m Inlines
1473+apostrophe = (char '\'' <|> char '\8217') >> return (B.str "\8217")
1474+
1475+doubleCloseQuote :: Stream s m Char => ParserT s st m Inlines
1476+doubleCloseQuote = B.str "\8221" <$ char '"'
1477+
14651478ellipses :: Stream s m Char
14661479 => ParserT s st m Inlines
14671480ellipses = try (string "..." >> return (B.str "\8230"))
src/Text/Pandoc/Readers/Markdown.hs+10−8
@@ -2199,25 +2199,27 @@ citation = try $ do
21992199smart :: PandocMonad m => MarkdownParser m (F Inlines)
22002200smart = do
22012201 guardEnabled Ext_smart
2202− doubleQuoted <|> singleQuoted <|>
2203− choice (map (return <$>) [apostrophe, dash, ellipses])
2202+ doubleQuoted <|> singleQuoted <|> (return <$> doubleCloseQuote) <|>
2203+ (return <$> apostrophe) <|> (return <$> dash) <|> (return <$> ellipses)
22042204
22052205singleQuoted :: PandocMonad m => MarkdownParser m (F Inlines)
2206−singleQuoted = try $ do
2206+singleQuoted = do
22072207 singleQuoteStart
2208− withQuoteContext InSingleQuote $
2208+ (try (withQuoteContext InSingleQuote $
22092209 fmap B.singleQuoted . trimInlinesF . mconcat <$>
2210− many1Till inline singleQuoteEnd
2210+ many1Till inline singleQuoteEnd))
2211+ <|> (return (return (B.str "\8217")))
22112212
22122213-- doubleQuoted will handle regular double-quoted sections, as well
22132214-- as dialogues with an open double-quote without a close double-quote
22142215-- in the same paragraph.
22152216doubleQuoted :: PandocMonad m => MarkdownParser m (F Inlines)
2216−doubleQuoted = try $ do
2217+doubleQuoted = do
22172218 doubleQuoteStart
2218− withQuoteContext InDoubleQuote $
2219+ (try (withQuoteContext InDoubleQuote $
22192220 fmap B.doubleQuoted . trimInlinesF . mconcat <$>
2220− many1Till inline doubleQuoteEnd
2221+ many1Till inline doubleQuoteEnd))
2222+ <|> (return (return (B.str "\8220")))
22212223
22222224toRow :: [Blocks] -> Row
22232225toRow = Row nullAttr . map B.simpleCell
src/Text/Pandoc/Readers/RST.hs+1−18
@@ -1658,21 +1658,4 @@ note = try $ do
16581658 return $ B.note contents
16591659
16601660smart :: PandocMonad m => RSTParser m Inlines
1661−smart = do
1662− guardEnabled Ext_smart
1663− doubleQuoted <|> singleQuoted <|>
1664− choice [apostrophe, dash, ellipses]
1665−
1666−singleQuoted :: PandocMonad m => RSTParser m Inlines
1667−singleQuoted = try $ do
1668− singleQuoteStart
1669− withQuoteContext InSingleQuote $
1670− B.singleQuoted . trimInlines . mconcat <$>
1671− many1Till inline singleQuoteEnd
1672−
1673−doubleQuoted :: PandocMonad m => RSTParser m Inlines
1674−doubleQuoted = try $ do
1675− doubleQuoteStart
1676− withQuoteContext InDoubleQuote $
1677− B.doubleQuoted . trimInlines . mconcat <$>
1678− many1Till inline doubleQuoteEnd
1661+smart = smartPunctuation inline
src/Text/Pandoc/Readers/TWiki.hs+1−21
@@ -469,27 +469,7 @@ symbol :: PandocMonad m => TWParser m B.Inlines
469469symbol = B.str <$> countChar 1 nonspaceChar
470470
471471smart :: PandocMonad m => TWParser m B.Inlines
472−smart = do
473− guardEnabled Ext_smart
474− doubleQuoted <|> singleQuoted <|>
475− choice [ apostrophe
476− , dash
477− , ellipses
478− ]
479−
480−singleQuoted :: PandocMonad m => TWParser m B.Inlines
481−singleQuoted = try $ do
482− singleQuoteStart
483− withQuoteContext InSingleQuote
484− (B.singleQuoted . B.trimInlines . mconcat <$> many1Till inline singleQuoteEnd)
485−
486−doubleQuoted :: PandocMonad m => TWParser m B.Inlines
487−doubleQuoted = try $ do
488− doubleQuoteStart
489− contents <- mconcat <$> many (try $ notFollowedBy doubleQuoteEnd >> inline)
490− withQuoteContext InDoubleQuote (doubleQuoteEnd >>
491− return (B.doubleQuoted $ B.trimInlines contents))
492− <|> return (B.str "\8220" B.<> contents)
472+smart = smartPunctuation inline
493473
494474link :: PandocMonad m => TWParser m B.Inlines
495475link = try $ do
test/Tests/Readers/Markdown.hs+1−1
@@ -358,7 +358,7 @@ tests = [ testGroup "inline code"
358358 para (text "The value of the " <> math "x" <> text "\8217s and the systems\8217 condition.")
359359 , test markdownSmart "unclosed double quote"
360360 ("**this should \"be bold**"
361− =?> para (strong "this should \"be bold"))
361+ =?> para (strong "this should \8220be bold"))
362362 ]
363363 , testGroup "footnotes"
364364 [ "indent followed by newline and flush-left text" =:
test/command/7216.mdnew file+19
@@ -0,0 +1,19 @@
1+```
2+pandoc -t latex
3+"This is some text in quotes. Another paragraph by the same speaker follows. The first paragraph should have no close quote.
4+
5+"The second paragraph should have open and close quotes."
6+
7+| "Open quote on this line,
8+| Close quote on the next line."
9+| "Quotes on the same line."
10+^D
11+``This is some text in quotes. Another paragraph by the same speaker
12+follows. The first paragraph should have no close quote.
13+
14+``The second paragraph should have open and close quotes.''
15+
16+``Open quote on this line,\\
17+Close quote on the next line.''\\
18+``Quotes on the same line.''
19+```
test/command/pandoc-citeproc-320a.md+6−6
@@ -49,17 +49,13 @@ references:
4949Foo [@item1; @item2; @item3; @item4; @item5; @item6; @item7; @item8].
5050^D
5151Foo (al-ʾUdhrī, n.d.; al-ʿUdhrī, n.d.; al-\'Udhrī, n.d.; al-'Udhrī,
52−n.d.; al-'Udhrī, n.d.; Uch, n.d.; Uebel, n.d.; Zzz, n.d.).
52+n.d.a, n.d.b; Uch, n.d.; Uebel, n.d.; Zzz, n.d.).
5353
5454::: {#refs .references .csl-bib-body .hanging-indent}
5555::: {#ref-item6 .csl-entry}
5656Uch, Ann. n.d.
5757:::
5858
59−::: {#ref-item4 .csl-entry}
60−'Udhrī, Jamīl al-. n.d.
61−:::
62−
6359::: {#ref-item1 .csl-entry}
6460ʾUdhrī, Jamīl al-. n.d.
6561:::
@@ -72,8 +68,12 @@ Uch, Ann. n.d.
7268\'Udhrī, Jamīl al-. n.d.
7369:::
7470
71+::: {#ref-item4 .csl-entry}
72+'Udhrī, Jamīl al-. n.d.a.
73+:::
74+
7575::: {#ref-item5 .csl-entry}
76−'Udhrī, Jamīl al-. n.d.
76+---------. n.d.b.
7777:::
7878
7979::: {#ref-item7 .csl-entry}