Repository: matthewwithanm/python-markdownify
Pull request #119 (2024-04-04 20:11:21)

Special-case use of HTML tags for converting `<sub>` / `<sup>`
Special-case use of HTML tags for `sub_symbol` and `sup_symbol`. In particular, this allows setting `sub_symbol='<sub>'`, `sup_symbol='<sup>'` to use raw HTML in the output when converting subscripts and superscripts.

chrispy-snps: @jsm28 - would you consider removing the `*_after` configuration options and automatically using the closing tag variant if the opening option is an HTML tag?
jsm28: I've revised things as suggested to handle HTML tags automatically instead of having separate configuration options.

Patch:
diff --git a/MANIFEST.in b/MANIFEST.in
index 9561fb1..70656c8 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,1 +1,2 @@
 include README.rst
+prune tests
diff --git a/README.rst b/README.rst
index 35d58fd..55ea7cf 100644
--- a/README.rst
+++ b/README.rst
@@ -87,7 +87,11 @@ strong_em_symbol
 sub_symbol, sup_symbol
   Define the chars that surround ``<sub>`` and ``<sup>`` text. Defaults to an
   empty string, because this is non-standard behavior. Could be something like
-  ``~`` and ``^`` to result in ``~sub~`` and ``^sup^``.
+  ``~`` and ``^`` to result in ``~sub~`` and ``^sup^``.  If the value starts
+  with ``<`` and ends with ``>``, it is treated as an HTML tag and a ``/`` is
+  inserted after the ``<`` in the string used after the text; this allows
+  specifying ``<sub>`` to use raw HTML in the output for subscripts, for
+  example.
 
 newline_style
   Defines the style of marking linebreaks (``<br>``) in markdown. The default
diff --git a/markdownify/__init__.py b/markdownify/__init__.py
index eaa6ded..d7bd780 100644
--- a/markdownify/__init__.py
+++ b/markdownify/__init__.py
@@ -43,17 +43,22 @@ def abstract_inline_conversion(markup_fn):
     """
     This abstracts all simple inline tags like b, em, del, ...
     Returns a function that wraps the chomped text in a pair of the string
-    that is returned by markup_fn. markup_fn is necessary to allow for
+    that is returned by markup_fn, with '/' inserted in the string used after
+    the text if it looks like an HTML tag. markup_fn is necessary to allow for
     references to self.strong_em_symbol etc.
     """
     def implementation(self, el, text, convert_as_inline):
         markup = markup_fn(self)
+        if markup.startswith('<') and markup.endswith('>'):
+            markup_after = '</' + markup[1:]
+        else:
+            markup_after = markup
         if el.find_parent(['pre', 'code', 'kbd', 'samp']):
             return text
         prefix, suffix, text = chomp(text)
         if not text:
             return ''
-        return '%s%s%s%s%s' % (prefix, markup, text, markup, suffix)
+        return '%s%s%s%s%s' % (prefix, markup, text, markup_after, suffix)
     return implementation
 
 
@@ -383,13 +388,13 @@ class MarkdownConverter(object):
 
     def convert_td(self, el, text, convert_as_inline):
         colspan = 1
-        if 'colspan' in el.attrs:
+        if 'colspan' in el.attrs and el['colspan'].isdigit():
             colspan = int(el['colspan'])
         return ' ' + text.strip().replace("\n", " ") + ' |' * colspan
 
     def convert_th(self, el, text, convert_as_inline):
         colspan = 1
-        if 'colspan' in el.attrs:
+        if 'colspan' in el.attrs and el['colspan'].isdigit():
             colspan = int(el['colspan'])
         return ' ' + text.strip().replace("\n", " ") + ' |' * colspan
 
@@ -406,7 +411,7 @@ class MarkdownConverter(object):
             # first row and is headline: print headline underline
             full_colspan = 0
             for cell in cells:
-                if "colspan" in cell.attrs:
+                if 'colspan' in cell.attrs and cell['colspan'].isdigit():
                     full_colspan += int(cell["colspan"])
                 else:
                     full_colspan += 1


Test patch:
diff --git a/tests/test_conversions.py b/tests/test_conversions.py
index 9652143..a35b982 100644
--- a/tests/test_conversions.py
+++ b/tests/test_conversions.py
@@ -268,11 +268,13 @@ def test_strong_em_symbol():
 def test_sub():
     assert md('<sub>foo</sub>') == 'foo'
     assert md('<sub>foo</sub>', sub_symbol='~') == '~foo~'
+    assert md('<sub>foo</sub>', sub_symbol='<sub>') == '<sub>foo</sub>'
 
 
 def test_sup():
     assert md('<sup>foo</sup>') == 'foo'
     assert md('<sup>foo</sup>', sup_symbol='^') == '^foo^'
+    assert md('<sup>foo</sup>', sup_symbol='<sup>') == '<sup>foo</sup>'
 
 
 def test_lang():
diff --git a/tests/test_tables.py b/tests/test_tables.py
index 9120c29..594e5bf 100644
--- a/tests/test_tables.py
+++ b/tests/test_tables.py
@@ -215,7 +215,7 @@ table_with_colspan = """<table>
         <th>Age</th>
     </tr>
     <tr>
-        <td>Jill</td>
+        <td colspan="1">Jill</td>
         <td>Smith</td>
         <td>50</td>
     </tr>
@@ -226,6 +226,17 @@ table_with_colspan = """<table>
     </tr>
 </table>"""
 
+table_with_undefined_colspan = """<table>
+    <tr>
+        <th colspan="undefined">Name</th>
+        <th>Age</th>
+    </tr>
+    <tr>
+        <td colspan="-1">Jill</td>
+        <td>Smith</td>
+    </tr>
+</table>"""
+
 
 def test_table():
     assert md(table) == '\n\n| Firstname | Lastname | Age |\n| --- | --- | --- |\n| Jill | Smith | 50 |\n| Eve | Jackson | 94 |\n\n'
@@ -240,3 +251,4 @@ def test_table():
     assert md(table_body) == '\n\n| Firstname | Lastname | Age |\n| --- | --- | --- |\n| Jill | Smith | 50 |\n| Eve | Jackson | 94 |\n\n'
     assert md(table_with_caption) == 'TEXT\n\nCaption\n| Firstname | Lastname | Age |\n| --- | --- | --- |\n\n'
     assert md(table_with_colspan) == '\n\n| Name | | Age |\n| --- | --- | --- |\n| Jill | Smith | 50 |\n| Eve | Jackson | 94 |\n\n'
+    assert md(table_with_undefined_colspan) == '\n\n| Name | Age |\n| --- | --- |\n| Jill | Smith |\n\n'