Repository: matthewwithanm/python-markdownify

Issue:
support HTML definition lists (`<dl>`, `<dt>`, and `<dd>`)
Support HTML definition lists, such as:

```html
<dl>
  <dt>term 1</dt>
  <dd>
    <p>This is a definition of term 1.</p>
  </dd>
  <dt>term 2</dt>
  <dd>
    <p>This is a definition of term 2.</p>
  </dd>
</dl>
```

The widely-used Pandoc utility supports definition lists:

https://pandoc.org/MANUAL.html#definition-lists

using the PHP Markdown Extra syntax:

https://michelf.ca/projects/php-markdown/extra/#def-list

Discussion:
ninsbl: I just tested the feature locally as I am interested in the functionality for markdown conversion. It is great to see this becoming available, I noticed that the result is one big text paragraph. I think an additional newline would be needed after the `<dd>` elements (for example).
chrispy-snps: @ninsbl - can you share a small testcase here for me to reproduce it?
ninsbl: Sure. We are currently looking into moving documentation for GRASS GIS from HTML to Markdown.

One manual page with _\<dl\>_, _\<dt\>_ and _\<dd\>_ is for example: https://grass.osgeo.org/grass84/manuals/grass.html

Here is the code used for translation to Markdown (with python-markdownify installed for the branch)

```
import bs4
import requests

from markdownify import markdownify, MarkdownConverter

resp = requests.get("https://grass.osgeo.org/grass84/manuals/grass.html")
soup = bs4.BeautifulSoup(resp.text, 'html5lib')
MarkdownConverter(
    **{
        "heading_style": "atx",
        "escape_misc": True,
        "code_language": "shell",
        "newline_style": "backslash",
        "wrap_width": 79
    }
).convert_soup(soup)
```

You can see resulting markdown here: https://github.com/ninsbl/grass/edit/md_test/md/grass.md. Please look at the "FLAGS" section...

Note the line break before the `:` (which becomes a white space) and only one linebreak after the _\<dd\>_ element becoming space as well...

Also, element content seems only parially wrapped...
chrispy-snps: @ninsbl - here is the "Flags" section from the Markdown file you referenced:

```markdown
### Flags:

**-h** \| **-help** \| **--help**
:   Prints a brief usage message and exits
**-v** \| **--version**
:   Prints the version of GRASS and exits
**-c XY**
:   Creates new GRASS project (location) without coordinate reference system in specified GISDBASE
**-c geofile**
:   Creates new GRASS project in specified GISDBASE with coordinate reference system based on georeferenced file
**-c EPSG:code**
:   Creates new GRASS project in specified GISDBASE with coordinate reference system defined by EPSG code
**-c EPSG:code:datum\_trans**
:   Creates new GRASS project in specified GISDBASE with coordinate reference system defined by EPSG code and datum transform parameters
**-e**
:   Exit after creation of project or mapset. Only with **-c** flag
**-f**
:   Forces removal of .gislock if exists (use with care!). Only with --text flag
**--text**
:   Indicates that Text-based User Interface should be used (skip welcome screen)
**--gtext**
:   Indicates that Text-based User Interface should be used (show welcome screen)
**--gui**
:   Indicates that Graphical User Interface
    (*[wxGUI](wxGUI.html)*) should be used
**--config**
:   Prints GRASS configuration parameters (options: arch, build, compiler, date, path, python\_path, revision, svn\_revision, version)
**--exec EXECUTABLE**
:   Execute GRASS module or script. The provided executable will be executed in a GRASS GIS non-interactive session.
**--tmp-project**
:   Run using a temporary project which is created based on the given
    coordinate reference system and deleted at the end of the execution
    (use with the --exec flag).
    The active mapset will be the PERMANENT mapset.
**--tmp-mapset**
:   Run using a temporary mapset which is created in the specified
    project and deleted at the end of the execution
    (use with the --exec flag).
```

To me, it looks like the code in the pull request is functioning properly. I am able to convert this Markdown back to HTML with Pandoc, and the definition list structures are converted correctly back to HTML.

I do see the wrapping behavior you mention, but that exists with and without this pull request and would be something to investigate separately.

Patch:
diff --git a/markdownify/__init__.py b/markdownify/__init__.py
index fd03569..ac53077 100644
--- a/markdownify/__init__.py
+++ b/markdownify/__init__.py
@@ -319,6 +319,38 @@ class MarkdownConverter(object):
 
     convert_kbd = convert_code
 
+    def convert_dd(self, el, text, convert_as_inline):
+        text = (text or '').strip()
+        if convert_as_inline:
+            return ' ' + text + ' '
+        if not text:
+            return '\n'
+
+        # indent definition content lines by four spaces
+        def _indent_for_dd(match):
+            line_content = match.group(1)
+            return '    ' + line_content if line_content else ''
+        text = line_with_content_re.sub(_indent_for_dd, text)
+
+        # insert definition marker into first-line indent whitespace
+        text = ':' + text[1:]
+
+        return '%s\n' % text
+
+    def convert_dt(self, el, text, convert_as_inline):
+        # remove newlines from term text
+        text = (text or '').strip()
+        text = all_whitespace_re.sub(' ', text)
+        if convert_as_inline:
+            return ' ' + text + ' '
+        if not text:
+            return '\n'
+
+        # TODO - format consecutive <dt> elements as directly adjacent lines):
+        #   https://michelf.ca/projects/php-markdown/extra/#def-list
+
+        return '\n%s\n' % text
+
     def _convert_hn(self, n, el, text, convert_as_inline):
         """ Method name prefixed with _ to prevent <hn> to call this """
         if convert_as_inline:


Test patch:
diff --git a/tests/test_conversions.py b/tests/test_conversions.py
index 868db7c..cc5ebc7 100644
--- a/tests/test_conversions.py
+++ b/tests/test_conversions.py
@@ -104,6 +104,16 @@ def test_code():
     assert md('<code>foo<sub>bar</sub>baz</code>', sub_symbol='^') == '`foobarbaz`'
 
 
+def test_dl():
+    assert md('<dl><dt>term</dt><dd>definition</dd></dl>') == '\nterm\n:   definition\n'
+    assert md('<dl><dt><p>te</p><p>rm</p></dt><dd>definition</dd></dl>') == '\nte rm\n:   definition\n'
+    assert md('<dl><dt>term</dt><dd><p>definition-p1</p><p>definition-p2</p></dd></dl>') == '\nterm\n:   definition-p1\n\n    definition-p2\n'
+    assert md('<dl><dt>term</dt><dd><p>definition 1</p></dd><dd><p>definition 2</p></dd></dl>') == '\nterm\n:   definition 1\n:   definition 2\n'
+    assert md('<dl><dt>term 1</dt><dd>definition 1</dd><dt>term 2</dt><dd>definition 2</dd></dl>') == '\nterm 1\n:   definition 1\nterm 2\n:   definition 2\n'
+    assert md('<dl><dt>term</dt><dd><blockquote><p>line 1</p><p>line 2</p></blockquote></dd></dl>') == '\nterm\n:   > line 1\n    >\n    > line 2\n'
+    assert md('<dl><dt>term</dt><dd><ol><li><p>1</p><ul><li>2a</li><li>2b</li></ul></li><li><p>3</p></li></ol></dd></dl>') == '\nterm\n:   1. 1\n\n       * 2a\n       * 2b\n    2. 3\n'
+
+
 def test_del():
     inline_tests('del', '~~')