Repository: pulldown-cmark/pulldown-cmark (rust)

Issue:
`Options::ENABLE_DEFINITION_LIST` changes the parsing of lazy continuation lines
Given the following input:

```markdown
> text in blockquote
:lazy continuation line
```

Running `cargo run -- -e` returns these events:

```
0..44: Start(BlockQuote(None))
2..44: Start(Paragraph)
2..20: Text(Borrowed("text in blockquote"))
20..21: SoftBreak
21..43: Text(Borrowed(":not a definition list"))
2..44: End(Paragraph)
0..44: End(BlockQuote(None))
EOF
```

Running `cargo run -- -e --enable-definition-list ` changes how the lazy continuation line is parsed. My guess is the `:` at the start of the line makes the parser think it needs to close the preceding blockquote and paragraph.

```markdown
0..21: Start(BlockQuote(None))
2..21: Start(Paragraph)
2..20: Text(Borrowed("text in blockquote"))
2..21: End(Paragraph)
0..21: End(BlockQuote(None))
21..44: Start(Paragraph)
21..43: Text(Borrowed(":not a definition list"))
21..44: End(Paragraph)
EOF
```

Pull request description:
Prevent definition list defs from interrupting non-paragraphs
Fixes #995

Patch:
diff --git a/pulldown-cmark/specs/definition_lists.txt b/pulldown-cmark/specs/definition_lists.txt
index 8a8a2bd..1290063 100644
--- a/pulldown-cmark/specs/definition_lists.txt
+++ b/pulldown-cmark/specs/definition_lists.txt
@@ -251,6 +251,61 @@ fruit</p>
 </dl>
 ````````````````````````````````
 
+A lazy continuation may start with a `:`, if it has enough indent.
+
+```````````````````````````````` example
+apple
+   : > computer company
+     : red fruit
+
+orange
+   : > telecom company
+   : orange fruit
+
+chili's
+   : > restaurant company
+ : spicy fruit
+.
+<dl>
+<dt>apple</dt>
+<dd><blockquote>
+<p>computer company
+: red fruit</p>
+</blockquote>
+</dd>
+<dt>orange</dt>
+<dd><blockquote>
+<p>telecom company</p>
+</blockquote>
+</dd>
+<dd>orange fruit</dd>
+<dt>chili's</dt>
+<dd><blockquote>
+<p>restaurant company</p>
+</blockquote>
+</dd>
+<dd>spicy fruit</dd>
+</dl>
+````````````````````````````````
+
+It may not, however, lazily act as a definition in a list.
+
+```````````````````````````````` example
+> cherry
+> : keyboard company
+> pomegranate
+: tart fruit
+.
+<blockquote>
+<dl>
+<dt>cherry</dt>
+<dd>keyboard company
+pomegranate
+: tart fruit</dd>
+</dl>
+</blockquote>
+````````````````````````````````
+
 Definition terms may span multiple lines:
 
 ```````````````````````````````` example
@@ -437,9 +492,10 @@ Test|Table
 ----|-----
 : first
 .
-<table><thead><tr><th>Test</th><th>Table</th></tr></thead><tbody></tbody>
+<table><thead><tr><th>Test</th><th>Table</th></tr></thead><tbody>
+<tr><td>: first</td><td></td></tr>
+</tbody>
 </table>
-<p>: first</p>
 ````````````````````````````````
 
 ```````````````````````````````` example
@@ -454,9 +510,10 @@ Test|Table
 <dt>first</dt>
 <dd>second</dd>
 </dl>
-<table><thead><tr><th>Test</th><th>Table</th></tr></thead><tbody></tbody>
+<table><thead><tr><th>Test</th><th>Table</th></tr></thead><tbody>
+<tr><td>: fourth</td><td></td></tr>
+</tbody>
 </table>
-<p>: fourth</p>
 ````````````````````````````````
 
 
@@ -511,6 +568,19 @@ first
 ````````````````````````````````
 
 
+Definition list titles can't be block quotes
+
+```````````````````````````````` example
+> first
+: second
+.
+<blockquote>
+<p>first
+: second</p>
+</blockquote>
+````````````````````````````````
+
+
 Definition titles can't end with hard line breaks.
 
 ```````````````````````````````` example
diff --git a/pulldown-cmark/src/firstpass.rs b/pulldown-cmark/src/firstpass.rs
index 5fa1258..2c21bab 100644
--- a/pulldown-cmark/src/firstpass.rs
+++ b/pulldown-cmark/src/firstpass.rs
@@ -248,13 +248,14 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                     // and break out if we can't re-scan all of them
                     let ix = start_ix + line_start.bytes_scanned();
                     let mut lazy_line_start = LineStart::new(&bytes[ix..]);
-                    let current_container =
-                        scan_containers(&self.tree, &mut lazy_line_start, self.options)
-                            == self.tree.spine_len();
+                    let tree_position =
+                        scan_containers(&self.tree, &mut lazy_line_start, self.options);
+                    let current_container = tree_position == self.tree.spine_len();
                     if !lazy_line_start.scan_space(4)
                         && self.scan_paragraph_interrupt(
                             &bytes[ix + lazy_line_start.bytes_scanned()..],
                             current_container,
+                            tree_position,
                         )
                     {
                         return ix;
@@ -420,11 +421,14 @@ impl<'a, 'b> FirstPass<'a, 'b> {
         bytes: &'input [u8],
     ) -> Option<LineStart<'input>> {
         let mut line_start = LineStart::new(bytes);
-        let current_container =
-            scan_containers(&self.tree, &mut line_start, self.options) == self.tree.spine_len();
+        let tree_position = scan_containers(&self.tree, &mut line_start, self.options);
+        let current_container = tree_position == self.tree.spine_len();
         if !line_start.scan_space(4)
-            && self
-                .scan_paragraph_interrupt(&bytes[line_start.bytes_scanned()..], current_container)
+            && self.scan_paragraph_interrupt(
+                &bytes[line_start.bytes_scanned()..],
+                current_container,
+                tree_position,
+            )
         {
             None
         } else {
@@ -554,8 +558,8 @@ impl<'a, 'b> FirstPass<'a, 'b> {
     ) -> Option<(usize, TreeIndex)> {
         let bytes = self.text.as_bytes();
         let mut line_start = LineStart::new(&bytes[ix..]);
-        let current_container =
-            scan_containers(&self.tree, &mut line_start, self.options) == self.tree.spine_len();
+        let tree_position = scan_containers(&self.tree, &mut line_start, self.options);
+        let current_container = tree_position == self.tree.spine_len();
         if !current_container {
             return None;
         }
@@ -567,6 +571,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
             self.options.contains(Options::ENABLE_FOOTNOTES),
             self.options.contains(Options::ENABLE_DEFINITION_LIST),
             &self.tree,
+            tree_position,
         ) {
             return None;
         }
@@ -642,8 +647,8 @@ impl<'a, 'b> FirstPass<'a, 'b> {
 
             ix = next_ix;
             let mut line_start = LineStart::new(&bytes[ix..]);
-            let current_container =
-                scan_containers(&self.tree, &mut line_start, self.options) == self.tree.spine_len();
+            let tree_position = scan_containers(&self.tree, &mut line_start, self.options);
+            let current_container = tree_position == self.tree.spine_len();
             let trailing_backslash_pos = match brk {
                 Some(Item {
                     start,
@@ -670,7 +675,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                 }
                 // first check for non-empty lists, then for other interrupts
                 let suffix = &bytes[ix_new..];
-                if self.scan_paragraph_interrupt(suffix, current_container) {
+                if self.scan_paragraph_interrupt(suffix, current_container, tree_position) {
                     if let Some(pos) = trailing_backslash_pos {
                         self.tree.append_text(pos, pos + 1, false);
                     }
@@ -1741,14 +1746,14 @@ impl<'a, 'b> FirstPass<'a, 'b> {
             &self.text[start..],
             &|bytes| {
                 let mut line_start = LineStart::new(bytes);
-                let current_container = scan_containers(&self.tree, &mut line_start, self.options)
-                    == self.tree.spine_len();
+                let tree_position = scan_containers(&self.tree, &mut line_start, self.options);
+                let current_container = tree_position == self.tree.spine_len();
                 if line_start.scan_space(4) {
                     return Some(line_start.bytes_scanned());
                 }
                 let bytes_scanned = line_start.bytes_scanned();
                 let suffix = &bytes[bytes_scanned..];
-                if self.scan_paragraph_interrupt(suffix, current_container)
+                if self.scan_paragraph_interrupt(suffix, current_container, tree_position)
                     || (current_container && scan_setext_heading(suffix).is_some())
                 {
                     None
@@ -1792,11 +1797,11 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                 break;
             }
             let mut line_start = LineStart::new(&bytes[i..]);
-            let current_container =
-                scan_containers(&self.tree, &mut line_start, self.options) == self.tree.spine_len();
+            let tree_position = scan_containers(&self.tree, &mut line_start, self.options);
+            let current_container = tree_position == self.tree.spine_len();
             if !line_start.scan_space(4) {
                 let suffix = &bytes[i + line_start.bytes_scanned()..];
-                if self.scan_paragraph_interrupt(suffix, current_container)
+                if self.scan_paragraph_interrupt(suffix, current_container, tree_position)
                     || scan_setext_heading(suffix).is_some()
                 {
                     return None;
@@ -1851,12 +1856,11 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                         bytecount += 1;
                     }
                     let mut line_start = LineStart::new(&bytes[bytecount..]);
-                    let current_container =
-                        scan_containers(&self.tree, &mut line_start, self.options)
-                            == self.tree.spine_len();
+                    let tree_position = scan_containers(&self.tree, &mut line_start, self.options);
+                    let current_container = tree_position == self.tree.spine_len();
                     if !line_start.scan_space(4) {
                         let suffix = &bytes[bytecount + line_start.bytes_scanned()..];
-                        if self.scan_paragraph_interrupt(suffix, current_container)
+                        if self.scan_paragraph_interrupt(suffix, current_container, tree_position)
                             || scan_setext_heading(suffix).is_some()
                         {
                             return None;
@@ -1957,13 +1961,19 @@ impl<'a, 'b> FirstPass<'a, 'b> {
     }
 
     /// Checks whether we should break a paragraph on the given input.
-    fn scan_paragraph_interrupt(&self, bytes: &[u8], current_container: bool) -> bool {
+    fn scan_paragraph_interrupt(
+        &self,
+        bytes: &[u8],
+        current_container: bool,
+        tree_position: usize,
+    ) -> bool {
         if scan_paragraph_interrupt_no_table(
             bytes,
             current_container,
             self.options.contains(Options::ENABLE_FOOTNOTES),
             self.options.contains(Options::ENABLE_DEFINITION_LIST),
             &self.tree,
+            tree_position,
         ) {
             return true;
         }
@@ -2117,6 +2127,7 @@ fn scan_paragraph_interrupt_no_table(
     has_footnote: bool,
     definition_list: bool,
     tree: &Tree<Item>,
+    tree_position: usize,
 ) -> bool {
     scan_eol(bytes).is_some()
         || scan_hrule(bytes).is_ok()
@@ -2133,7 +2144,20 @@ fn scan_paragraph_interrupt_no_table(
         })
         || bytes.starts_with(b"<")
             && (get_html_end_tag(&bytes[1..]).is_some() || starts_html_block_type_6(&bytes[1..]))
-        || definition_list && bytes.starts_with(b":")
+        || definition_list
+            && ((current_container
+                && tree.peek_up().map_or(false, |cur| {
+                    matches!(
+                        tree[cur].item.body,
+                        ItemBody::Paragraph
+                            | ItemBody::TightParagraph
+                            | ItemBody::MaybeDefinitionListTitle
+                    )
+                }))
+                || tree.walk_spine().nth(tree_position).map_or(false, |cur| {
+                    matches!(tree[*cur].item.body, ItemBody::DefinitionListDefinition(_))
+                }))
+            && bytes.starts_with(b":")
         || (has_footnote
             && bytes.starts_with(b"[^")
             && scan_link_label_rest(


Test patch:
diff --git a/pulldown-cmark/tests/suite/definition_lists.rs b/pulldown-cmark/tests/suite/definition_lists.rs
index b14855d..a572259 100644
--- a/pulldown-cmark/tests/suite/definition_lists.rs
+++ b/pulldown-cmark/tests/suite/definition_lists.rs
@@ -266,6 +266,65 @@ fruit</p>
 
 #[test]
 fn definition_lists_test_11() {
+    let original = r##"apple
+   : > computer company
+     : red fruit
+
+orange
+   : > telecom company
+   : orange fruit
+
+chili's
+   : > restaurant company
+ : spicy fruit
+"##;
+    let expected = r##"<dl>
+<dt>apple</dt>
+<dd><blockquote>
+<p>computer company
+: red fruit</p>
+</blockquote>
+</dd>
+<dt>orange</dt>
+<dd><blockquote>
+<p>telecom company</p>
+</blockquote>
+</dd>
+<dd>orange fruit</dd>
+<dt>chili's</dt>
+<dd><blockquote>
+<p>restaurant company</p>
+</blockquote>
+</dd>
+<dd>spicy fruit</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_12() {
+    let original = r##"> cherry
+> : keyboard company
+> pomegranate
+: tart fruit
+"##;
+    let expected = r##"<blockquote>
+<dl>
+<dt>cherry</dt>
+<dd>keyboard company
+pomegranate
+: tart fruit</dd>
+</dl>
+</blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_13() {
     let original = r##"a
 b\
 c
@@ -286,7 +345,7 @@ c</dt>
 }
 
 #[test]
-fn definition_lists_test_12() {
+fn definition_lists_test_14() {
     let original = r##"Foo
 
 bar
@@ -308,7 +367,7 @@ bim
 }
 
 #[test]
-fn definition_lists_test_13() {
+fn definition_lists_test_15() {
     let original = r##"bar
 :   baz
 
@@ -330,7 +389,7 @@ Bloze
 }
 
 #[test]
-fn definition_lists_test_14() {
+fn definition_lists_test_16() {
     let original = r##"bar
     :baz
 
@@ -345,7 +404,7 @@ Bloze
 }
 
 #[test]
-fn definition_lists_test_15() {
+fn definition_lists_test_17() {
     let original = r##"bar
 :    baz
 
@@ -362,7 +421,7 @@ Bloze
 }
 
 #[test]
-fn definition_lists_test_16() {
+fn definition_lists_test_18() {
     let original = r##"bar
 :       baz
 
@@ -405,7 +464,7 @@ bar
 }
 
 #[test]
-fn definition_lists_test_17() {
+fn definition_lists_test_19() {
     let original = r##"*orange*
 
 :   orange fruit
@@ -451,21 +510,22 @@ fn definition_lists_test_17() {
 }
 
 #[test]
-fn definition_lists_test_18() {
+fn definition_lists_test_20() {
     let original = r##"Test|Table
 ----|-----
 : first
 "##;
-    let expected = r##"<table><thead><tr><th>Test</th><th>Table</th></tr></thead><tbody></tbody>
+    let expected = r##"<table><thead><tr><th>Test</th><th>Table</th></tr></thead><tbody>
+<tr><td>: first</td><td></td></tr>
+</tbody>
 </table>
-<p>: first</p>
 "##;
 
     test_markdown_html(original, expected, false, false, false, false, false);
 }
 
 #[test]
-fn definition_lists_test_19() {
+fn definition_lists_test_21() {
     let original = r##"first
 : second
 
@@ -477,16 +537,17 @@ Test|Table
 <dt>first</dt>
 <dd>second</dd>
 </dl>
-<table><thead><tr><th>Test</th><th>Table</th></tr></thead><tbody></tbody>
+<table><thead><tr><th>Test</th><th>Table</th></tr></thead><tbody>
+<tr><td>: fourth</td><td></td></tr>
+</tbody>
 </table>
-<p>: fourth</p>
 "##;
 
     test_markdown_html(original, expected, false, false, false, false, false);
 }
 
 #[test]
-fn definition_lists_test_20() {
+fn definition_lists_test_22() {
     let original = r##"My section
 ==========
 : first
@@ -499,7 +560,7 @@ fn definition_lists_test_20() {
 }
 
 #[test]
-fn definition_lists_test_21() {
+fn definition_lists_test_23() {
     let original = r##"first
 : second
 
@@ -519,7 +580,7 @@ My section
 }
 
 #[test]
-fn definition_lists_test_22() {
+fn definition_lists_test_24() {
     let original = r##"## My subsection
 : first
 "##;
@@ -531,7 +592,7 @@ fn definition_lists_test_22() {
 }
 
 #[test]
-fn definition_lists_test_23() {
+fn definition_lists_test_25() {
     let original = r##"first
 : second
 
@@ -550,7 +611,21 @@ fn definition_lists_test_23() {
 }
 
 #[test]
-fn definition_lists_test_24() {
+fn definition_lists_test_26() {
+    let original = r##"> first
+: second
+"##;
+    let expected = r##"<blockquote>
+<p>first
+: second</p>
+</blockquote>
+"##;
+
+    test_markdown_html(original, expected, false, false, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_27() {
     let original = r##"first\
 : second
 
@@ -569,7 +644,7 @@ third
 }
 
 #[test]
-fn definition_lists_test_25() {
+fn definition_lists_test_28() {
     let original = r##"<div>first</div>
 : second
 
@@ -592,7 +667,7 @@ first
 }
 
 #[test]
-fn definition_lists_test_26() {
+fn definition_lists_test_29() {
     let original = r##"<span>first</span>
 : second
 
@@ -616,7 +691,7 @@ third
 }
 
 #[test]
-fn definition_lists_test_27() {
+fn definition_lists_test_30() {
     let original = r##"level one
 : l1
     level two