From add83e81698a3468b4fde02f3af3c42d5c56f790 Mon Sep 17 00:00:00 2001 From: John MacFarlane Date: Mon, 2 Jun 2025 23:29:47 -0700 Subject: [PATCH] Markdown reader: make definition lists behave like other lists. If the `four_space_rule` extension is not enabled, figure out the indentation needed for child blocks dynamically, by looking at the first nonspace content after the `:` marker. Previously the four-space rule was always obeyed. Remove the old `compact_definition_lists` extension. This was neded to preserve backwards compatibility after pandoc 1.12 was released, but at this point we can get rid of it. T.P.Extensions: remove `Ext_compact_definition_lists` constructor for `Extension` [API change]. Fix tight/loose detection for definition lists, to conform to the documentation. Closes #10889. --- MANUAL.txt | 30 ---------- src/Text/Pandoc/Extensions.hs | 3 - src/Text/Pandoc/Readers/Markdown.hs | 74 ++++++++----------------- src/Text/Pandoc/Writers/Markdown.hs | 16 ++---- test/Tests/Readers/Markdown.hs | 22 ++------ test/command/10889.md | 16 ++++++ test/command/lists-inside-definition.md | 1 + 7 files changed, 51 insertions(+), 111 deletions(-) create mode 100644 test/command/10889.md diff --git a/MANUAL.txt b/MANUAL.txt index 6198cbe05..39355c424 100644 --- a/MANUAL.txt +++ b/MANUAL.txt @@ -4502,9 +4502,6 @@ definition: ~ Definition 2b Note that space between items in a definition list is required. -(A variant that loosens this requirement, but disallows "lazy" -hard wrapping, can be activated with the [`compact_definition_lists` -extension][Extension: `compact_definition_lists`].) [^3]: I have been influenced by the suggestions of [David Wheeler](https://justatheory.com/2009/02/modest-markdown-proposal/). @@ -6109,33 +6106,6 @@ and image references. This extension should not be confused with the Parses MultiMarkdown-style heading identifiers (in square brackets, after the heading but before any trailing `#`s in an ATX heading). -### Extension: `compact_definition_lists` ### - -Activates the definition list syntax of pandoc 1.12.x and earlier. -This syntax differs from the one described above under [Definition lists] -in several respects: - - - No blank line is required between consecutive items of the - definition list. - - To get a "tight" or "compact" list, omit space between consecutive - items; the space between a term and its definition does not affect - anything. - - Lazy wrapping of paragraphs is not allowed: the entire definition must - be indented four spaces.[^6] - -[^6]: To see why laziness is incompatible with relaxing the requirement - of a blank line between items, consider the following example: - - bar - : definition - foo - : definition - - Is this a single list item with two definitions of "bar," the first of - which is lazily wrapped, or two list items? To remove the ambiguity - we must either disallow lazy wrapping or require a blank line between - list items. - ### Extension: `gutenberg` ### Use [Project Gutenberg] conventions for `plain` output: diff --git a/src/Text/Pandoc/Extensions.hs b/src/Text/Pandoc/Extensions.hs index 7f1b35c17..f1c9c0f37 100644 --- a/src/Text/Pandoc/Extensions.hs +++ b/src/Text/Pandoc/Extensions.hs @@ -59,8 +59,6 @@ data Extension = | Ext_blank_before_header -- ^ Require blank line before a header | Ext_bracketed_spans -- ^ Bracketed spans with attributes | Ext_citations -- ^ Pandoc/citeproc citations - | Ext_compact_definition_lists -- ^ Definition lists without space between items, - -- and disallow laziness | Ext_definition_lists -- ^ Definition lists as in pandoc, mmd, php | Ext_east_asian_line_breaks -- ^ Newlines in paragraphs are ignored between -- East Asian wide characters. Note: this extension @@ -511,7 +509,6 @@ getAllExtensions f = universalExtensions <> getAll f , Ext_mark , Ext_mmd_link_attributes , Ext_mmd_header_identifiers - , Ext_compact_definition_lists , Ext_gutenberg , Ext_smart , Ext_literate_haskell diff --git a/src/Text/Pandoc/Readers/Markdown.hs b/src/Text/Pandoc/Readers/Markdown.hs index a4677ec07..d81c4e3c1 100644 --- a/src/Text/Pandoc/Readers/Markdown.hs +++ b/src/Text/Pandoc/Readers/Markdown.hs @@ -845,7 +845,9 @@ orderedListStart mbstydelim = try $ do return (num, style, delim)) listStart :: PandocMonad m => MarkdownParser m () -listStart = bulletListStart <|> Control.Monad.void (orderedListStart Nothing) +listStart = bulletListStart + <|> Control.Monad.void (orderedListStart Nothing) + <|> defListStart listLine :: PandocMonad m => Int -> MarkdownParser m Text listLine continuationIndent = try $ do @@ -967,67 +969,39 @@ bulletList = do -- definition lists -defListMarker :: PandocMonad m => MarkdownParser m () -defListMarker = do - sps <- nonindentSpaces +defListStart :: PandocMonad m => MarkdownParser m () +defListStart = do + nonindentSpaces char ':' <|> char '~' - tabStop <- getOption readerTabStop - let remaining = tabStop - (T.length sps + 1) - if remaining > 0 - then try (count remaining (char ' ')) <|> string "\t" <|> many1 spaceChar - else mzero - return () + gobbleSpaces 1 <|> () <$ lookAhead newline + try (gobbleAtMostSpaces 3 >> notFollowedBy spaceChar) <|> return () -definitionListItem :: PandocMonad m => Bool -> MarkdownParser m (F (Inlines, [Blocks])) -definitionListItem compact = try $ do +definitionListItem :: PandocMonad m => MarkdownParser m (F (Inlines, [Blocks])) +definitionListItem = try $ do rawLine' <- anyLine - raw <- many1 $ defRawBlock compact term <- parseFromString' (trimInlinesF <$> inlines) rawLine' - contents <- mapM (parseFromString' parseBlocks . (<> "\n")) raw + isTight <- (False <$ blanklines) <|> pure True + fourSpaceRule <- (True <$ guardEnabled Ext_four_space_rule) <|> pure False + contents <- many1 $ listItem fourSpaceRule defListStart optional blanklines - return $ liftM2 (,) term (sequence contents) + return $ liftM2 (,) + term + ((if isTight + then fmap (fmap (fmap paraToPlain)) + else id) (sequence contents)) -defRawBlock :: PandocMonad m => Bool -> MarkdownParser m Text -defRawBlock compact = try $ do - hasBlank <- option False $ blankline >> return True - defListMarker - firstline <- anyLineNewline - let dline = try - ( do notFollowedBy blankline - notFollowedByHtmlCloser - notFollowedByDivCloser - if compact -- laziness not compatible with compact - then () <$ indentSpaces - else (() <$ indentSpaces) - <|> notFollowedBy defListMarker - anyLine ) - rawlines <- many dline - cont <- fmap T.concat $ many $ try $ do - trailing <- option "" blanklines - ln <- indentSpaces >> notFollowedBy blankline >> anyLine - lns <- many dline - return $ trailing <> T.unlines (ln:lns) - return $ trimr (firstline <> T.unlines rawlines <> cont) <> - if hasBlank || not (T.null cont) then "\n\n" else "" +paraToPlain :: Block -> Block +paraToPlain (Para ils) = Plain ils +paraToPlain x = x definitionList :: PandocMonad m => MarkdownParser m (F Blocks) definitionList = try $ do + guardEnabled Ext_definition_lists lookAhead (anyLine >> optional (blankline >> notFollowedBy (Control.Monad.void table)) >> -- don't capture table caption as def list! - defListMarker) - compactDefinitionList <|> normalDefinitionList - -compactDefinitionList :: PandocMonad m => MarkdownParser m (F Blocks) -compactDefinitionList = do - guardEnabled Ext_compact_definition_lists - items <- fmap sequence $ many1 $ definitionListItem True - return $ B.definitionList <$> fmap compactifyDL items - -normalDefinitionList :: PandocMonad m => MarkdownParser m (F Blocks) -normalDefinitionList = do - guardEnabled Ext_definition_lists - items <- fmap sequence $ many1 $ definitionListItem False + defListStart) + items <- fmap sequence $ many1 definitionListItem return $ B.definitionList <$> items -- diff --git a/src/Text/Pandoc/Writers/Markdown.hs b/src/Text/Pandoc/Writers/Markdown.hs index 9adff3597..9bada680d 100644 --- a/src/Text/Pandoc/Writers/Markdown.hs +++ b/src/Text/Pandoc/Writers/Markdown.hs @@ -860,17 +860,11 @@ definitionListItemToMarkdown opts (label, defs) = do let isTight = case defs of ((Plain _ : _): _) -> True _ -> False - if isEnabled Ext_compact_definition_lists opts - then do - let contents = vcat $ map (\d -> hang tabStop (leader <> sps) - $ vcat d <> cr) defs' - return $ nowrap labelText <> cr <> contents <> cr - else do - let contents = (if isTight then vcat else vsep) $ map - (\d -> hang tabStop (leader <> sps) $ vcat d) - defs' - return $ blankline <> nowrap labelText $$ - (if isTight then empty else blankline) <> contents <> blankline + let contents = (if isTight then vcat else vsep) $ map + (\d -> hang tabStop (leader <> sps) $ vcat d) + defs' + return $ blankline <> nowrap labelText $$ + (if isTight then empty else blankline) <> contents <> blankline else return $ nowrap (chomp labelText <> literal " " <> cr) <> vsep (map vsep defs') <> blankline diff --git a/test/Tests/Readers/Markdown.hs b/test/Tests/Readers/Markdown.hs index ed0d59843..20ab99e48 100644 --- a/test/Tests/Readers/Markdown.hs +++ b/test/Tests/Readers/Markdown.hs @@ -29,10 +29,6 @@ markdownSmart :: Text -> Pandoc markdownSmart = purely $ readMarkdown def { readerExtensions = enableExtension Ext_smart pandocExtensions } -markdownCDL :: Text -> Pandoc -markdownCDL = purely $ readMarkdown def { readerExtensions = enableExtension - Ext_compact_definition_lists pandocExtensions } - markdownGH :: Text -> Pandoc markdownGH = purely $ readMarkdown def {readerExtensions = enableExtension Ext_wikilinks_title_before_pipe githubMarkdownExtensions } @@ -461,14 +457,14 @@ tests = [ testGroup "inline code" , "blank space before first def" =: "foo1\n\n : bar\n\nfoo2\n\n : bar2\n : bar3\n" =?> definitionList [ (text "foo1", [para (text "bar")]) - , (text "foo2", [para (text "bar2"), - plain (text "bar3")]) + , (text "foo2", [plain (text "bar2"), + para (text "bar3")]) ] , "blank space before second def" =: "foo1\n : bar\n\nfoo2\n : bar2\n\n : bar3\n" =?> definitionList [ (text "foo1", [plain (text "bar")]) , (text "foo2", [plain (text "bar2"), - para (text "bar3")]) + plain (text "bar3")]) ] , "laziness" =: "foo1\n : bar\nbaz\n : bar2\n" =?> @@ -478,8 +474,8 @@ tests = [ testGroup "inline code" ] , "no blank space before first of two paragraphs" =: "foo1\n : bar\n\n baz\n" =?> - definitionList [ (text "foo1", [para (text "bar") <> - para (text "baz")]) + definitionList [ (text "foo1", [plain (text "bar") <> + plain (text "baz")]) ] , "first line not indented" =: "foo\n: bar\n" =?> @@ -492,14 +488,6 @@ tests = [ testGroup "inline code" divWith nullAttr (definitionList [ (text "foo", [bulletList [plain (text "bar")]]) ]) ] - , testGroup "+compact_definition_lists" - [ test markdownCDL "basic compact list" $ - "foo1\n: bar\n baz\nfoo2\n: bar2\n" =?> - definitionList [ (text "foo1", [plain (text "bar" <> softbreak <> - text "baz")]) - , (text "foo2", [plain (text "bar2")]) - ] - ] , testGroup "lists" [ "issue #1154" =: " -
\n first div breaks\n
\n\n \n\n
\n with this div too.\n
\n" diff --git a/test/command/10889.md b/test/command/10889.md new file mode 100644 index 000000000..8f99685ce --- /dev/null +++ b/test/command/10889.md @@ -0,0 +1,16 @@ +``` +% pandoc +apple + +: pomaceous + + fruit +^D +
+
apple
+
+

pomaceous

+

fruit

+
+
+``` diff --git a/test/command/lists-inside-definition.md b/test/command/lists-inside-definition.md index 731cd10d3..de2c72c94 100644 --- a/test/command/lists-inside-definition.md +++ b/test/command/lists-inside-definition.md @@ -33,6 +33,7 @@ Definition 2. list ^D \begin{description} +\tightlist \item[Definition] Foo