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

Issue:
Support for Description Lists
Some parsers have an extension to support description/definition lists. In https://talk.commonmark.org/t/description-list/289 there is a proposal to add it to CommonMark. A common syntax is to put a colon at the beginning of the line.

An example from the [Pandoc manual](http://pandoc.org/MANUAL.html#definition-lists):

```
Term 1

:   Definition 1

Term 2 with *inline markup*

:   Definition 2

        { some code, part of Definition 2 }

    Third paragraph of definition 2.
```


Pull request:
Implement commonmark-hs compatible definition lists
Fixes #67 

Patch:
diff --git a/pulldown-cmark/examples/parser-map-tag-print.rs b/pulldown-cmark/examples/parser-map-tag-print.rs
index 30b1919..886befa 100644
--- a/pulldown-cmark/examples/parser-map-tag-print.rs
+++ b/pulldown-cmark/examples/parser-map-tag-print.rs
@@ -60,6 +60,9 @@ fn main() {
                     "List ordered_list_first_item_number: {:?}",
                     ordered_list_first_item_number
                 ),
+                Tag::DefinitionList => println!("Definition list"),
+                Tag::DefinitionListTitle => println!("Definition title (definition list item)"),
+                Tag::DefinitionListDefinition => println!("Definition (definition list item)"),
                 Tag::Item => println!("Item (this is a list item)"),
                 Tag::Emphasis => println!("Emphasis (this is a span tag)"),
                 Tag::Strong => println!("Strong (this is a span tag)"),
diff --git a/pulldown-cmark/specs/definition_lists.txt b/pulldown-cmark/specs/definition_lists.txt
new file mode 100644
index 0000000..c41e5d1
--- /dev/null
+++ b/pulldown-cmark/specs/definition_lists.txt
@@ -0,0 +1,522 @@
+## Definition lists
+
+Based on https://github.com/jgm/commonmark-hs/blob/e3747fd282c806cd2f8e83226e85f1b83ee889c3/commonmark-extensions/test/definition_lists.md
+
+The term is given on a line by itself, followed by
+one or more definitions. Each definition must begin
+with `:` (after 0-2 spaces); subsequent lines must
+be indented unless they are lazy paragraph
+continuations.
+
+Commonmark-HS allows definition lists to use `~`.
+We can't do that, because we already use `~` for strikethrough.
+
+The list is tight if there is no blank line between
+the term and the first definition, otherwise loose.
+
+```````````````````````````````` example
+apple
+:   red fruit
+
+orange
+:   orange fruit
+.
+<dl>
+<dt>apple</dt>
+<dd>red fruit</dd>
+<dt>orange</dt>
+<dd>orange fruit</dd>
+</dl>
+````````````````````````````````
+
+Loose:
+
+```````````````````````````````` example
+apple
+
+:   red fruit
+
+orange
+
+:   orange fruit
+.
+<dl>
+<dt>apple</dt>
+<dd>
+<p>red fruit</p>
+</dd>
+<dt>orange</dt>
+<dd>
+<p>orange fruit</p>
+</dd>
+</dl>
+````````````````````````````````
+
+Also loose:
+
+```````````````````````````````` example
+apple
+
+:   red fruit
+.
+<dl>
+<dt>apple</dt>
+<dd>
+<p>red fruit</p>
+</dd>
+</dl>
+````````````````````````````````
+
+Indented marker:
+
+```````````````````````````````` example
+apple
+  : red fruit
+
+orange
+  : orange fruit
+.
+<dl>
+<dt>apple</dt>
+<dd>red fruit</dd>
+<dt>orange</dt>
+<dd>orange fruit</dd>
+</dl>
+````````````````````````````````
+
+```````````````````````````````` example
+apple
+
+ : red fruit
+
+orange
+
+ : orange fruit
+.
+<dl>
+<dt>apple</dt>
+<dd>
+<p>red fruit</p>
+</dd>
+<dt>orange</dt>
+<dd>
+<p>orange fruit</p>
+</dd>
+</dl>
+````````````````````````````````
+
+Multiple blocks in a definition:
+
+```````````````````````````````` example
+*apple*
+
+:   red fruit
+
+    contains seeds,
+    crisp, pleasant to taste
+
+*orange*
+
+:   orange fruit
+
+        { orange code block }
+
+    > orange block quote
+.
+<dl>
+<dt><em>apple</em></dt>
+<dd>
+<p>red fruit</p>
+<p>contains seeds,
+crisp, pleasant to taste</p>
+</dd>
+<dt><em>orange</em></dt>
+<dd>
+<p>orange fruit</p>
+<pre><code>{ orange code block }
+</code></pre>
+<blockquote>
+<p>orange block quote</p>
+</blockquote>
+</dd>
+</dl>
+````````````````````````````````
+
+Nested lists:
+
+```````````````````````````````` example
+term
+
+:   1. Para one
+
+       Para two
+.
+<dl>
+<dt>term</dt>
+<dd>
+<ol>
+<li><p>Para one</p>
+<p>Para two</p></li>
+</ol>
+</dd>
+</dl>
+````````````````````````````````
+
+Multiple definitions, tight:
+
+```````````````````````````````` example
+apple
+:   red fruit
+:   computer company
+
+orange
+:   orange fruit
+:   telecom company
+.
+<dl>
+<dt>apple</dt>
+<dd>red fruit</dd>
+<dd>computer company</dd>
+<dt>orange</dt>
+<dd>orange fruit</dd>
+<dd>telecom company</dd>
+</dl>
+````````````````````````````````
+
+Multiple definitions, loose:
+
+```````````````````````````````` example
+apple
+
+:   red fruit
+
+:   computer company
+
+orange
+
+:   orange fruit
+:   telecom company
+.
+<dl>
+<dt>apple</dt>
+<dd>
+<p>red fruit</p>
+</dd>
+<dd>
+<p>computer company</p>
+</dd>
+<dt>orange</dt>
+<dd>
+<p>orange fruit</p>
+</dd>
+<dd>
+<p>telecom company</p>
+</dd>
+</dl>
+````````````````````````````````
+
+Lazy line continuations:
+
+```````````````````````````````` example
+apple
+
+:   red fruit
+
+:   computer
+company
+
+orange
+
+:   orange
+fruit
+:   telecom company
+.
+<dl>
+<dt>apple</dt>
+<dd>
+<p>red fruit</p>
+</dd>
+<dd>
+<p>computer
+company</p>
+</dd>
+<dt>orange</dt>
+<dd>
+<p>orange
+fruit</p>
+</dd>
+<dd>
+<p>telecom company</p>
+</dd>
+</dl>
+````````````````````````````````
+
+Definition terms may span multiple lines:
+
+```````````````````````````````` example
+a
+b\
+c
+
+:   foo
+.
+<dl>
+<dt>a
+b<br />
+c</dt>
+<dd>
+<p>foo</p>
+</dd>
+</dl>
+````````````````````````````````
+
+Definition list with preceding paragraph
+(<https://github.com/jgm/commonmark-hs/issues/35>):
+
+```````````````````````````````` example
+Foo
+
+bar
+:   baz
+
+bim
+:   bor
+.
+<p>Foo</p>
+<dl>
+<dt>bar</dt>
+<dd>baz</dd>
+<dt>bim</dt>
+<dd>bor</dd>
+</dl>
+````````````````````````````````
+
+Definition list followed by paragraph.
+
+```````````````````````````````` example
+bar
+:   baz
+
+bim
+:   bor
+
+Bloze
+.
+<dl>
+<dt>bar</dt>
+<dd>baz</dd>
+<dt>bim</dt>
+<dd>bor</dd>
+</dl>
+<p>Bloze</p>
+````````````````````````````````
+
+The total indentation for a definition list cannot exceed four.
+This means you can't have one start with four spaces.
+
+```````````````````````````````` example
+bar
+    :baz
+
+Bloze
+.
+<p>bar
+:baz</p>
+<p>Bloze</p>
+````````````````````````````````
+
+You can follow one with four spaces,
+because it'll "eat" the three spaces after it.
+
+```````````````````````````````` example
+bar
+:    baz
+
+Bloze
+.
+<dl>
+<dt>bar</dt>
+<dd>baz</dd>
+</dl>
+<p>Bloze</p>
+````````````````````````````````
+
+To use an indented code block inside of a definition,
+you need to have a total of eight spaces of indentation.
+This can be done by having a colon followed by seven spaces,
+or three spaces followed by four.
+
+```````````````````````````````` example
+bar
+:       baz
+
+bar
+ :      baz
+
+bar
+  :     baz
+
+bar
+   :    baz
+
+bar
+    :   baz
+.
+<dl>
+<dt>bar</dt>
+<dd><pre><code>baz
+</code></pre></dd>
+<dt>bar</dt>
+<dd><pre><code>baz
+</code></pre></dd>
+<dt>bar</dt>
+<dd><pre><code>baz
+</code></pre></dd>
+<dt>bar</dt>
+<dd><pre><code>baz
+</code></pre></dd>
+</dl>
+<p>bar
+:   baz</p>
+````````````````````````````````
+
+
+Definition titles can't be tables.
+
+```````````````````````````````` example
+Test|Table
+----|-----
+: first
+.
+<table><thead><tr><th>Test</th><th>Table</th></tr></thead><tbody></tbody>
+</table>
+<p>: first</p>
+````````````````````````````````
+
+```````````````````````````````` example
+first
+: second
+
+Test|Table
+----|-----
+: fourth
+.
+<dl>
+<dt>first</dt>
+<dd>second</dd>
+</dl>
+<table><thead><tr><th>Test</th><th>Table</th></tr></thead><tbody></tbody>
+</table>
+<p>: fourth</p>
+````````````````````````````````
+
+
+Definition titles can't be headers
+
+```````````````````````````````` example
+My section
+==========
+: first
+.
+<h1>My section</h1>
+<p>: first</p>
+````````````````````````````````
+
+```````````````````````````````` example
+first
+: second
+
+My section
+==========
+: fourth
+.
+<dl>
+<dt>first</dt>
+<dd>second</dd>
+</dl>
+<h1>My section</h1>
+<p>: fourth</p>
+````````````````````````````````
+
+```````````````````````````````` example
+## My subsection
+: first
+.
+<h2>My subsection</h2>
+<p>: first</p>
+````````````````````````````````
+
+```````````````````````````````` example
+first
+: second
+
+## My subsection
+: fourth
+.
+<dl>
+<dt>first</dt>
+<dd>second</dd>
+</dl>
+<h2>My subsection</h2>
+<p>: fourth</p>
+````````````````````````````````
+
+
+Definition titles can't end with hard line breaks.
+
+```````````````````````````````` example
+first\
+: second
+
+third  
+: fourth
+.
+<dl>
+<dt>first\</dt>
+<dd>second</dd>
+<dt>third</dt>
+<dd>fourth</dd>
+</dl>
+````````````````````````````````
+
+
+Definition titles can't be HTML blocks, but inline's fine.
+
+```````````````````````````````` example
+<div>first</div>
+: second
+
+first
+: second
+<div>third</div>
+: fourth
+.
+<div>first</div>
+: second
+<dl>
+<dt>first</dt>
+<dd>second</dd>
+</dl>
+<div>third</div>
+: fourth
+````````````````````````````````
+
+```````````````````````````````` example
+<span>first</span>
+: second
+
+third
+: fourth
+
+<span>fifth</span>
+: sixth
+.
+<dl>
+<dt><span>first</span></dt>
+<dd>second</dd>
+<dt>third</dt>
+<dd>fourth</dd>
+<dt><span>fifth</span></dt>
+<dd>sixth</dd>
+</dl>
+````````````````````````````````
diff --git a/pulldown-cmark/src/firstpass.rs b/pulldown-cmark/src/firstpass.rs
index 89eb3e0..b1548d0 100644
--- a/pulldown-cmark/src/firstpass.rs
+++ b/pulldown-cmark/src/firstpass.rs
@@ -165,6 +165,66 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                             body: ItemBody::TaskListMarker(is_checked),
                         });
                 }
+            } else if let Some((
+                indent,
+                child,
+                item,
+            )) = self
+                .options
+                .contains(Options::ENABLE_DEFINITION_LIST)
+                .then(||
+                    self.tree
+                        .cur()
+                        .map(|cur| (self.tree[cur].child, &mut self.tree[cur].item))
+                )
+                .flatten()
+                .filter(|(_, item)| matches!(item, Item {
+                    body: ItemBody::Paragraph | ItemBody::MaybeDefinitionListTitle | ItemBody::DefinitionListDefinition(_),
+                    ..
+                }))
+                .and_then(|item| Some((line_start.scan_definition_list_definition_marker()?, item.0, item.1)))
+            {
+                match item.body {
+                    ItemBody::Paragraph => {
+                        item.body = ItemBody::DefinitionList(true);
+                        let Item { start, end, .. } = *item;
+                        let list_idx = self.tree.cur().unwrap();
+                        let title_idx = self.tree.create_node(Item {
+                            start,
+                            end, // will get updated later if item not empty
+                            body: ItemBody::DefinitionListTitle,
+                        });
+                        self.tree[title_idx].child = child;
+                        self.tree[list_idx].child = Some(title_idx);
+                        self.tree.push();
+                    }
+                    ItemBody::MaybeDefinitionListTitle => {
+                        item.body = ItemBody::DefinitionListTitle;
+                    }
+                    ItemBody::DefinitionListDefinition(_) => {}
+                    _ => unreachable!(),
+                }
+                let after_marker_index = start_ix + line_start.bytes_scanned();
+                self.tree.append(Item {
+                    start: container_start,
+                    end: after_marker_index, // will get updated later if item not empty
+                    body: ItemBody::DefinitionListDefinition(indent),
+                });
+                if let Some(ItemBody::DefinitionList(ref mut is_tight)) =
+                    self.tree
+                        .peek_up()
+                        .map(|cur| &mut self.tree[cur].item.body)
+                {
+                    if self.last_line_blank {
+                        *is_tight = false;
+                        self.last_line_blank = false;
+                    }
+                }
+                self.tree.push();
+                if let Some(n) = scan_blank_line(&bytes[after_marker_index..]) {
+                    self.begin_list_item = Some(after_marker_index + n);
+                    return after_marker_index + n;
+                }
             } else if line_start.scan_blockquote_marker() {
                 let kind = if self.options.contains(Options::ENABLE_GFM) {
                     line_start.scan_blockquote_tag()
@@ -216,7 +276,8 @@ impl<'a, 'b> FirstPass<'a, 'b> {
             if let Some(node_ix) = self.tree.peek_up() {
                 match &mut self.tree[node_ix].item.body {
                     ItemBody::BlockQuote(..) => (),
-                    ItemBody::ListItem(indent) if self.begin_list_item.is_some() => {
+                    ItemBody::ListItem(indent)
+                    | ItemBody::DefinitionListDefinition(indent) if self.begin_list_item.is_some() => {
                         self.last_line_blank = true;
                         // This is a blank list item.
                         // While the list itself can be continued no matter how many blank lines
@@ -228,17 +289,18 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                         self.last_line_blank = true;
                     }
                 }
+            } else {
+                self.last_line_blank = true;
             }
             return ix + n;
         }
 
-        self.finish_list(start_ix);
-
         // Save `remaining_space` here to avoid needing to backtrack `line_start` for HTML blocks
         let remaining_space = line_start.remaining_space();
 
         let indent = line_start.scan_space_upto(4);
         if indent == 4 {
+            self.finish_list(start_ix);
             let ix = start_ix + line_start.bytes_scanned();
             let remaining_space = line_start.remaining_space();
             return self.parse_indented_code_block(ix, remaining_space);
@@ -255,6 +317,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                 self.options
                     .contains(Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS),
             ) {
+                self.finish_list(start_ix);
                 return self.parse_metadata_block(ix, metadata_block_ch);
             }
         }
@@ -264,6 +327,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
             // Types 1-5 are all detected by one function and all end with the same
             // pattern
             if let Some(html_end_tag) = get_html_end_tag(&bytes[(ix + 1)..]) {
+                self.finish_list(start_ix);
                 return self.parse_html_block_type_1_to_5(
                     ix,
                     html_end_tag,
@@ -274,24 +338,29 @@ impl<'a, 'b> FirstPass<'a, 'b> {
 
             // Detect type 6
             if starts_html_block_type_6(&bytes[(ix + 1)..]) {
+                self.finish_list(start_ix);
                 return self.parse_html_block_type_6_or_7(ix, remaining_space, indent);
             }
 
             // Detect type 7
             if let Some(_html_bytes) = scan_html_type_7(&bytes[ix..]) {
+                self.finish_list(start_ix);
                 return self.parse_html_block_type_6_or_7(ix, remaining_space, indent);
             }
         }
 
         if let Ok(n) = scan_hrule(&bytes[ix..]) {
+            self.finish_list(start_ix);
             return self.parse_hrule(n, ix);
         }
 
         if let Some(atx_size) = scan_atx_heading(&bytes[ix..]) {
+            self.finish_list(start_ix);
             return self.parse_atx_heading(ix, atx_size);
         }
 
         if let Some((n, fence_ch)) = scan_code_fence(&bytes[ix..]) {
+            self.finish_list(start_ix);
             return self.parse_fenced_code_block(ix, indent, fence_ch, n);
         }
 
@@ -331,6 +400,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                         current_container,
                     )
                 {
+                    self.finish_list(start_ix);
                     return ix;
                 } else {
                     line_start = lazy_line_start;
@@ -338,6 +408,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                     start_ix = ix;
                 }
             } else {
+                self.finish_list(start_ix);
                 return ix;
             }
         }
@@ -482,6 +553,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
             &bytes[ix..],
             current_container,
             self.options.has_gfm_footnotes(),
+            self.options.contains(Options::ENABLE_DEFINITION_LIST),
             &self.tree,
         ) {
             return None;
@@ -493,10 +565,18 @@ impl<'a, 'b> FirstPass<'a, 'b> {
 
     /// Returns offset of line start after paragraph.
     fn parse_paragraph(&mut self, start_ix: usize) -> usize {
+        let body = if let Some(ItemBody::DefinitionList(_)) = self.tree.peek_up().map(|idx| self.tree[idx].item.body) {
+            // blank lines between the previous definition and this one don't count
+            self.last_line_blank = false;
+            ItemBody::MaybeDefinitionListTitle
+        } else {
+            self.finish_list(start_ix);
+            ItemBody::Paragraph
+        };
         let node_ix = self.tree.append(Item {
             start: start_ix,
             end: 0, // will get set later
-            body: ItemBody::Paragraph,
+            body,
         });
         self.tree.push();
 
@@ -527,6 +607,9 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                 // be a cleaner way
                 self.tree[node_ix].child = None;
                 self.tree.pop();
+                if body == ItemBody::MaybeDefinitionListTitle {
+                    self.finish_list(ix);
+                }
                 self.tree.push();
                 if let Some(ix) = self.parse_table(table_cols, ix, next_ix) {
                     return ix;
@@ -557,8 +640,11 @@ impl<'a, 'b> FirstPass<'a, 'b> {
                         if let Some(pos) = trailing_backslash_pos {
                             self.tree.append_text(pos, pos + 1, false);
                         }
-                        ix = ix_setext;
-                        break;
+                        self.pop(ix_setext);
+                        if body == ItemBody::MaybeDefinitionListTitle {
+                            self.finish_list(ix);
+                        }
+                        return ix_setext;
                     }
                 }
                 // first check for non-empty lists, then for other interrupts
@@ -1401,7 +1487,11 @@ impl<'a, 'b> FirstPass<'a, 'b> {
     fn pop(&mut self, ix: usize) {
         let cur_ix = self.tree.pop().unwrap();
         self.tree[cur_ix].item.end = ix;
-        if let ItemBody::List(true, _, _) = self.tree[cur_ix].item.body {
+        if let ItemBody::DefinitionList(_) = self.tree[cur_ix].item.body {
+            fixup_end_of_definition_list(&mut self.tree, cur_ix);
+            self.begin_list_item = None;
+        }
+        if let ItemBody::List(true, _, _) | ItemBody::DefinitionList(true) = self.tree[cur_ix].item.body {
             surgerize_tight_list(&mut self.tree, cur_ix);
             self.begin_list_item = None;
         }
@@ -1412,13 +1502,13 @@ impl<'a, 'b> FirstPass<'a, 'b> {
     fn finish_list(&mut self, ix: usize) {
         self.finish_empty_list_item();
         if let Some(node_ix) = self.tree.peek_up() {
-            if let ItemBody::List(_, _, _) = self.tree[node_ix].item.body {
+            if let ItemBody::List(_, _, _) | ItemBody::DefinitionList(_) = self.tree[node_ix].item.body {
                 self.pop(ix);
             }
         }
         if self.last_line_blank {
             if let Some(node_ix) = self.tree.peek_grandparent() {
-                if let ItemBody::List(ref mut is_tight, _, _) = self.tree[node_ix].item.body {
+                if let ItemBody::List(ref mut is_tight, _, _) | ItemBody::DefinitionList(ref mut is_tight) = self.tree[node_ix].item.body {
                     *is_tight = false;
                 }
             }
@@ -1431,7 +1521,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
             if self.last_line_blank {
                 // A list item can begin with at most one blank line.
                 if let Some(node_ix) = self.tree.peek_up() {
-                    if let ItemBody::ListItem(_) = self.tree[node_ix].item.body {
+                    if let ItemBody::ListItem(_) | ItemBody::DefinitionListDefinition(_) = self.tree[node_ix].item.body {
                         self.pop(begin_list_item);
                     }
                 }
@@ -1854,7 +1944,13 @@ 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 {
         let gfm_footnote = self.options.has_gfm_footnotes();
-        if scan_paragraph_interrupt_no_table(bytes, current_container, gfm_footnote, &self.tree) {
+        if scan_paragraph_interrupt_no_table(
+            bytes,
+            current_container,
+            gfm_footnote,
+            self.options.contains(Options::ENABLE_DEFINITION_LIST),
+            &self.tree,
+        ) {
             return true;
         }
         // pulldown-cmark allows heavy tables, that have a `|` on the header row,
@@ -2010,6 +2106,7 @@ fn scan_paragraph_interrupt_no_table(
     bytes: &[u8],
     current_container: bool,
     gfm_footnote: bool,
+    definition_list: bool,
     tree: &Tree<Item>,
 ) -> bool {
     scan_eol(bytes).is_some()
@@ -2027,6 +2124,7 @@ 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":")
         || (gfm_footnote
             && bytes.starts_with(b"[^")
             && scan_link_label_rest(
@@ -2126,6 +2224,27 @@ fn surgerize_tight_list(tree: &mut Tree<Item>, list_ix: TreeIndex) {
     }
 }
 
+fn fixup_end_of_definition_list(tree: &mut Tree<Item>, list_ix: TreeIndex) {
+    let mut list_item = tree[list_ix].child;
+    let mut previous_list_item = None;
+    while let Some(listitem_ix) = list_item {
+        match &mut tree[listitem_ix].item.body {
+            ItemBody::DefinitionListTitle | ItemBody::DefinitionListDefinition(_) => {
+                previous_list_item = list_item;
+                list_item = tree[listitem_ix].next;
+            }
+            body @ ItemBody::MaybeDefinitionListTitle => {
+                *body = ItemBody::Paragraph;
+                break;
+            }
+            _ => break,
+        }
+    }
+    if let Some(previous_list_item) = previous_list_item {
+        tree.truncate_to_parent(previous_list_item);
+    }
+}
+
 /// Determines whether the delimiter run starting at given index is
 /// left-flanking, as defined by the commonmark spec (and isn't intraword
 /// for _ delims).
diff --git a/pulldown-cmark/src/html.rs b/pulldown-cmark/src/html.rs
index 54005bd..8560909 100644
--- a/pulldown-cmark/src/html.rs
+++ b/pulldown-cmark/src/html.rs
@@ -300,6 +300,27 @@ where
                     self.write("\n<li>")
                 }
             }
+            Tag::DefinitionList => {
+                if self.end_newline {
+                    self.write("<dl>\n")
+                } else {
+                    self.write("\n<dl>\n")
+                }
+            }
+            Tag::DefinitionListTitle => {
+                if self.end_newline {
+                    self.write("<dt>")
+                } else {
+                    self.write("\n<dt>")
+                }
+            }
+            Tag::DefinitionListDefinition => {
+                if self.end_newline {
+                    self.write("<dd>")
+                } else {
+                    self.write("\n<dd>")
+                }
+            }
             Tag::Emphasis => self.write("<em>"),
             Tag::Strong => self.write("<strong>"),
             Tag::Strikethrough => self.write("<del>"),
@@ -414,6 +435,15 @@ where
             TagEnd::Item => {
                 self.write("</li>\n")?;
             }
+            TagEnd::DefinitionList => {
+                self.write("</dl>\n")?;
+            }
+            TagEnd::DefinitionListTitle => {
+                self.write("</dt>\n")?;
+            }
+            TagEnd::DefinitionListDefinition => {
+                self.write("</dd>\n")?;
+            }
             TagEnd::Emphasis => {
                 self.write("</em>")?;
             }
diff --git a/pulldown-cmark/src/lib.rs b/pulldown-cmark/src/lib.rs
index 82c28fc..616d8b5 100644
--- a/pulldown-cmark/src/lib.rs
+++ b/pulldown-cmark/src/lib.rs
@@ -177,6 +177,10 @@ pub enum Tag<'a> {
     #[cfg_attr(feature = "serde", serde(borrow))]
     FootnoteDefinition(CowStr<'a>),
 
+    DefinitionList,
+    DefinitionListTitle,
+    DefinitionListDefinition,
+
     /// A table. Contains a vector describing the text-alignment for each of its columns.
     Table(Vec<Alignment>),
     /// A table header. Contains only `TableCell`s. Note that the table body starts immediately
@@ -235,6 +239,9 @@ impl<'a> Tag<'a> {
             Tag::Link { .. } => TagEnd::Link,
             Tag::Image { .. } => TagEnd::Image,
             Tag::MetadataBlock(kind) => TagEnd::MetadataBlock(*kind),
+            Tag::DefinitionList => TagEnd::DefinitionList,
+            Tag::DefinitionListTitle => TagEnd::DefinitionListTitle,
+            Tag::DefinitionListDefinition => TagEnd::DefinitionListDefinition,
         }
     }
 }
@@ -256,6 +263,10 @@ pub enum TagEnd {
     Item,
     FootnoteDefinition,
 
+    DefinitionList,
+    DefinitionListTitle,
+    DefinitionListDefinition,
+
     Table,
     TableHead,
     TableRow,
@@ -486,6 +497,15 @@ bitflags::bitflags! {
         /// The following features are currently behind this tag:
         /// - Blockquote tags ([!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION]).
         const ENABLE_GFM = 1 << 11;
+        /// Commonmark-HS-Extensions compatible definition lists.
+        ///
+        /// ```markdown
+        /// title 1
+        ///   : definition 1
+        /// title 2
+        ///   : definition 2
+        /// ```
+        const ENABLE_DEFINITION_LIST = 1 << 12;
     }
 }
 
diff --git a/pulldown-cmark/src/parse.rs b/pulldown-cmark/src/parse.rs
index 8fef665..6c08dff 100644
--- a/pulldown-cmark/src/parse.rs
+++ b/pulldown-cmark/src/parse.rs
@@ -104,6 +104,14 @@ pub(crate) enum ItemBody {
     FootnoteDefinition(CowIndex),
     MetadataBlock(MetadataBlockKind),
 
+    // Definition lists
+    DefinitionList(bool), // is_tight
+    // gets turned into either a paragraph or a definition list title,
+    // depending on whether there's a definition after it
+    MaybeDefinitionListTitle,
+    DefinitionListTitle,
+    DefinitionListDefinition(usize),
+
     // Tables
     Table(AlignmentIndex),
     TableHead,
@@ -1294,6 +1302,13 @@ pub(crate) fn scan_containers(
                     break;
                 }
             }
+            ItemBody::DefinitionListDefinition(indent) => {
+                let save = line_start.clone();
+                if !line_start.scan_space(indent) && !line_start.is_at_eol() {
+                    *line_start = save;
+                    break;
+                }
+            }
             ItemBody::FootnoteDefinition(..) if gfm_footnotes => {
                 let save = line_start.clone();
                 if !line_start.scan_space(4) && !line_start.is_at_eol() {
@@ -2042,6 +2057,9 @@ fn body_to_tag_end(body: &ItemBody) -> TagEnd {
         ItemBody::Table(..) => TagEnd::Table,
         ItemBody::FootnoteDefinition(..) => TagEnd::FootnoteDefinition,
         ItemBody::MetadataBlock(kind) => TagEnd::MetadataBlock(kind),
+        ItemBody::DefinitionList(_) => TagEnd::DefinitionList,
+        ItemBody::DefinitionListTitle => TagEnd::DefinitionListTitle,
+        ItemBody::DefinitionListDefinition(_) => TagEnd::DefinitionListDefinition,
         _ => panic!("unexpected item body {:?}", body),
     }
 }
@@ -2126,6 +2144,9 @@ fn item_to_event<'a>(item: Item, text: &'a str, allocs: &mut Allocations<'a>) ->
                 Event::InlineMath(allocs.take_cow(cow_ix))
             }
         }
+        ItemBody::DefinitionList(_) => Tag::DefinitionList,
+        ItemBody::DefinitionListTitle => Tag::DefinitionListTitle,
+        ItemBody::DefinitionListDefinition(_) => Tag::DefinitionListDefinition,
         _ => panic!("unexpected item body {:?}", item.body),
     };
 
diff --git a/pulldown-cmark/src/scanners.rs b/pulldown-cmark/src/scanners.rs
index 274e345..160d1c8 100644
--- a/pulldown-cmark/src/scanners.rs
+++ b/pulldown-cmark/src/scanners.rs
@@ -271,6 +271,29 @@ impl<'a> LineStart<'a> {
         }
     }
 
+    /// Scan a definition marker.
+    ///
+    /// Definition markers are single colons, preceded by at most three spaces
+    /// and followed by at most three spaces. The indentation of following
+    /// lines is equal to the whole size of the marker, including the colon.
+    ///
+    /// If one is found, it will make the preceding paragraph into a definition
+    /// list title.
+    ///
+    /// Return value is the amount of indentation, or `None` if it's not a
+    /// definition list marker.
+    pub(crate) fn scan_definition_list_definition_marker(&mut self) -> Option<usize> {
+        let save = self.clone();
+        let indent = self.scan_space_upto(4);
+        if indent < 4 && self.scan_ch(b':') {
+            let remaining = 4 - (indent + 1);
+            Some(indent + 1 + self.scan_space_upto(remaining))
+        } else {
+            *self = save;
+            None
+        }
+    }
+
     /// Scan a list marker.
     ///
     /// Return value is the character, the start index, and the indent in spaces.
diff --git a/pulldown-cmark/src/tree.rs b/pulldown-cmark/src/tree.rs
index 9b2ce20..cca6208 100644
--- a/pulldown-cmark/src/tree.rs
+++ b/pulldown-cmark/src/tree.rs
@@ -175,6 +175,21 @@ impl<T: Default> Tree<T> {
         self.cur = self[cur_ix].next;
         self.cur
     }
+
+    pub(crate) fn truncate_to_parent(&mut self, child_ix: TreeIndex) {
+        let next = self[child_ix].next;
+        self[child_ix].next = None;
+        if let Some(cur) = self.cur {
+            self[cur].next = next;
+        } else if let Some(&parent) = self.spine.last() {
+            self[parent].child = next;
+        }
+        if next.is_some() {
+            self.cur = next;
+        } else {
+            self.pop();
+        }
+    }
 }
 
 impl Tree<Item> {


Test patch:
diff --git a/pulldown-cmark/tests/lib.rs b/pulldown-cmark/tests/lib.rs
index 4c16ae0..e34cb2a 100644
--- a/pulldown-cmark/tests/lib.rs
+++ b/pulldown-cmark/tests/lib.rs
@@ -33,6 +33,7 @@ pub fn test_markdown_html(
         opts.insert(Options::ENABLE_SMART_PUNCTUATION);
     }
     opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
+    opts.insert(Options::ENABLE_DEFINITION_LIST);
 
     let p = Parser::new_ext(input, opts);
     pulldown_cmark::html::push_html(&mut s, p);
diff --git a/pulldown-cmark/tests/suite/definition_lists.rs b/pulldown-cmark/tests/suite/definition_lists.rs
new file mode 100644
index 0000000..408a334
--- /dev/null
+++ b/pulldown-cmark/tests/suite/definition_lists.rs
@@ -0,0 +1,565 @@
+// This file is auto-generated by the build script
+// Please, do not modify it manually
+
+use super::test_markdown_html;
+
+#[test]
+fn definition_lists_test_1() {
+    let original = r##"apple
+:   red fruit
+
+orange
+:   orange fruit
+"##;
+    let expected = r##"<dl>
+<dt>apple</dt>
+<dd>red fruit</dd>
+<dt>orange</dt>
+<dd>orange fruit</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_2() {
+    let original = r##"apple
+
+:   red fruit
+
+orange
+
+:   orange fruit
+"##;
+    let expected = r##"<dl>
+<dt>apple</dt>
+<dd>
+<p>red fruit</p>
+</dd>
+<dt>orange</dt>
+<dd>
+<p>orange fruit</p>
+</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_3() {
+    let original = r##"apple
+
+:   red fruit
+"##;
+    let expected = r##"<dl>
+<dt>apple</dt>
+<dd>
+<p>red fruit</p>
+</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_4() {
+    let original = r##"apple
+  : red fruit
+
+orange
+  : orange fruit
+"##;
+    let expected = r##"<dl>
+<dt>apple</dt>
+<dd>red fruit</dd>
+<dt>orange</dt>
+<dd>orange fruit</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_5() {
+    let original = r##"apple
+
+ : red fruit
+
+orange
+
+ : orange fruit
+"##;
+    let expected = r##"<dl>
+<dt>apple</dt>
+<dd>
+<p>red fruit</p>
+</dd>
+<dt>orange</dt>
+<dd>
+<p>orange fruit</p>
+</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_6() {
+    let original = r##"*apple*
+
+:   red fruit
+
+    contains seeds,
+    crisp, pleasant to taste
+
+*orange*
+
+:   orange fruit
+
+        { orange code block }
+
+    > orange block quote
+"##;
+    let expected = r##"<dl>
+<dt><em>apple</em></dt>
+<dd>
+<p>red fruit</p>
+<p>contains seeds,
+crisp, pleasant to taste</p>
+</dd>
+<dt><em>orange</em></dt>
+<dd>
+<p>orange fruit</p>
+<pre><code>{ orange code block }
+</code></pre>
+<blockquote>
+<p>orange block quote</p>
+</blockquote>
+</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_7() {
+    let original = r##"term
+
+:   1. Para one
+
+       Para two
+"##;
+    let expected = r##"<dl>
+<dt>term</dt>
+<dd>
+<ol>
+<li><p>Para one</p>
+<p>Para two</p></li>
+</ol>
+</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_8() {
+    let original = r##"apple
+:   red fruit
+:   computer company
+
+orange
+:   orange fruit
+:   telecom company
+"##;
+    let expected = r##"<dl>
+<dt>apple</dt>
+<dd>red fruit</dd>
+<dd>computer company</dd>
+<dt>orange</dt>
+<dd>orange fruit</dd>
+<dd>telecom company</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_9() {
+    let original = r##"apple
+
+:   red fruit
+
+:   computer company
+
+orange
+
+:   orange fruit
+:   telecom company
+"##;
+    let expected = r##"<dl>
+<dt>apple</dt>
+<dd>
+<p>red fruit</p>
+</dd>
+<dd>
+<p>computer company</p>
+</dd>
+<dt>orange</dt>
+<dd>
+<p>orange fruit</p>
+</dd>
+<dd>
+<p>telecom company</p>
+</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_10() {
+    let original = r##"apple
+
+:   red fruit
+
+:   computer
+company
+
+orange
+
+:   orange
+fruit
+:   telecom company
+"##;
+    let expected = r##"<dl>
+<dt>apple</dt>
+<dd>
+<p>red fruit</p>
+</dd>
+<dd>
+<p>computer
+company</p>
+</dd>
+<dt>orange</dt>
+<dd>
+<p>orange
+fruit</p>
+</dd>
+<dd>
+<p>telecom company</p>
+</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_11() {
+    let original = r##"a
+b\
+c
+
+:   foo
+"##;
+    let expected = r##"<dl>
+<dt>a
+b<br />
+c</dt>
+<dd>
+<p>foo</p>
+</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_12() {
+    let original = r##"Foo
+
+bar
+:   baz
+
+bim
+:   bor
+"##;
+    let expected = r##"<p>Foo</p>
+<dl>
+<dt>bar</dt>
+<dd>baz</dd>
+<dt>bim</dt>
+<dd>bor</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_13() {
+    let original = r##"bar
+:   baz
+
+bim
+:   bor
+
+Bloze
+"##;
+    let expected = r##"<dl>
+<dt>bar</dt>
+<dd>baz</dd>
+<dt>bim</dt>
+<dd>bor</dd>
+</dl>
+<p>Bloze</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_14() {
+    let original = r##"bar
+    :baz
+
+Bloze
+"##;
+    let expected = r##"<p>bar
+:baz</p>
+<p>Bloze</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_15() {
+    let original = r##"bar
+:    baz
+
+Bloze
+"##;
+    let expected = r##"<dl>
+<dt>bar</dt>
+<dd>baz</dd>
+</dl>
+<p>Bloze</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_16() {
+    let original = r##"bar
+:       baz
+
+bar
+ :      baz
+
+bar
+  :     baz
+
+bar
+   :    baz
+
+bar
+    :   baz
+"##;
+    let expected = r##"<dl>
+<dt>bar</dt>
+<dd><pre><code>baz
+</code></pre></dd>
+<dt>bar</dt>
+<dd><pre><code>baz
+</code></pre></dd>
+<dt>bar</dt>
+<dd><pre><code>baz
+</code></pre></dd>
+<dt>bar</dt>
+<dd><pre><code>baz
+</code></pre></dd>
+</dl>
+<p>bar
+:   baz</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_17() {
+    let original = r##"Test|Table
+----|-----
+: first
+"##;
+    let expected = r##"<table><thead><tr><th>Test</th><th>Table</th></tr></thead><tbody></tbody>
+</table>
+<p>: first</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_18() {
+    let original = r##"first
+: second
+
+Test|Table
+----|-----
+: fourth
+"##;
+    let expected = r##"<dl>
+<dt>first</dt>
+<dd>second</dd>
+</dl>
+<table><thead><tr><th>Test</th><th>Table</th></tr></thead><tbody></tbody>
+</table>
+<p>: fourth</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_19() {
+    let original = r##"My section
+==========
+: first
+"##;
+    let expected = r##"<h1>My section</h1>
+<p>: first</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_20() {
+    let original = r##"first
+: second
+
+My section
+==========
+: fourth
+"##;
+    let expected = r##"<dl>
+<dt>first</dt>
+<dd>second</dd>
+</dl>
+<h1>My section</h1>
+<p>: fourth</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_21() {
+    let original = r##"## My subsection
+: first
+"##;
+    let expected = r##"<h2>My subsection</h2>
+<p>: first</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_22() {
+    let original = r##"first
+: second
+
+## My subsection
+: fourth
+"##;
+    let expected = r##"<dl>
+<dt>first</dt>
+<dd>second</dd>
+</dl>
+<h2>My subsection</h2>
+<p>: fourth</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_23() {
+    let original = r##"first\
+: second
+
+third  
+: fourth
+"##;
+    let expected = r##"<dl>
+<dt>first\</dt>
+<dd>second</dd>
+<dt>third</dt>
+<dd>fourth</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_24() {
+    let original = r##"<div>first</div>
+: second
+
+first
+: second
+<div>third</div>
+: fourth
+"##;
+    let expected = r##"<div>first</div>
+: second
+<dl>
+<dt>first</dt>
+<dd>second</dd>
+</dl>
+<div>third</div>
+: fourth
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn definition_lists_test_25() {
+    let original = r##"<span>first</span>
+: second
+
+third
+: fourth
+
+<span>fifth</span>
+: sixth
+"##;
+    let expected = r##"<dl>
+<dt><span>first</span></dt>
+<dd>second</dd>
+<dt>third</dt>
+<dd>fourth</dd>
+<dt><span>fifth</span></dt>
+<dd>sixth</dd>
+</dl>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
diff --git a/pulldown-cmark/tests/suite/mod.rs b/pulldown-cmark/tests/suite/mod.rs
index 0f4a250..7a3fd14 100644
--- a/pulldown-cmark/tests/suite/mod.rs
+++ b/pulldown-cmark/tests/suite/mod.rs
@@ -4,6 +4,7 @@
 pub use super::test_markdown_html;
 
 mod blockquotes_tags;
+mod definition_lists;
 mod footnotes;
 mod gfm_strikethrough;
 mod gfm_table;