Repository: Python-Markdown/markdown (python)

Issue:
Incorrect HTML output for code span containing `</>`
In markdown==3.8, a code span containing the string "\`</>\`" is incorrectly rendered. Instead of producing the expected HTML output with the characters escaped, it strips the content entirely:

```pycon
>>> import markdown
>>> markdown.__version__
'3.8'

>>> markdown.markdown('`</>`')
'<p>``</p>'
```

**Expected result:** 

```python
`'<p><code>&lt;/&gt;</code></p>'`
```

Other examples that work correctly:

```pycon
>>> markdown.markdown('`<`')
✅ '<p><code>&lt;</code></p>'
>>> markdown.markdown('`</`')
✅ '<p><code>&lt;/</code></p>'
>>> markdown.markdown('`</>`')
❌ '<p>``</p>'
>>> markdown.markdown('`/>`')
✅ '<p><code>/&gt;</code></p>'
>>> markdown.markdown('`>`')
✅ '<p><code>&gt;</code></p>'
```

This issue appears specific to the combination `</>`. Would appreciate any insight into whether this is a known bug, an edge case in the parser, or an intentional (though surprising) behavior. Thanks!

Pull request description:
Fix HTML handling of `</>`
Fixes #1528

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 7c96a6f..9f972a3 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -14,6 +14,7 @@ See the [Contributing Guide](contributing.md) for details.
 ### Fixed
 
 * Fixed dropped content in `md_in_html` (#1526).
+* Fixed HTML handling corner case that prevented some content from not being rendered (#1528).
 
 ## [3.8.0] - 2025-04-09
 
diff --git a/markdown/htmlparser.py b/markdown/htmlparser.py
index 33b918d..1528325 100644
--- a/markdown/htmlparser.py
+++ b/markdown/htmlparser.py
@@ -41,6 +41,10 @@ htmlparser = importlib.util.module_from_spec(spec)
 spec.loader.exec_module(htmlparser)
 sys.modules['htmlparser'] = htmlparser
 
+# This is a hack. We are sneaking in `</>` so we can capture it without the HTML parser
+# throwing it away. When we see it, we will process it as data.
+htmlparser.starttagopen = re.compile('<[a-zA-Z]|</>')
+
 # Monkeypatch `HTMLParser` to only accept `?>` to close Processing Instructions.
 htmlparser.piclose = re.compile(r'\?>')
 # Monkeypatch `HTMLParser` to only recognize entity references with a closing semicolon.
@@ -297,6 +301,11 @@ class HTMLExtractor(htmlparser.HTMLParser):
         return self.__starttag_text
 
     def parse_starttag(self, i: int) -> int:  # pragma: no cover
+        # Treat `</>` as normal data as it is not a real tag.
+        if self.rawdata[i:i + 3] == '</>':
+            self.handle_data(self.rawdata[i:i + 3])
+            return i + 3
+
         self.__starttag_text = None
         endpos = self.check_for_whole_start_tag(i)
         if endpos < 0:


Test patch:
diff --git a/tests/test_syntax/blocks/test_html_blocks.py b/tests/test_syntax/blocks/test_html_blocks.py
index 33375d3..8ea3904 100644
--- a/tests/test_syntax/blocks/test_html_blocks.py
+++ b/tests/test_syntax/blocks/test_html_blocks.py
@@ -1643,3 +1643,21 @@ class TestHTMLBlocks(TestCase):
         placeholder = md.htmlStash.get_placeholder(md.htmlStash.html_counter + 1)
         result = md.postprocessors['raw_html'].run(placeholder)
         self.assertEqual(placeholder, result)
+
+    def test_noname_tag(self):
+        self.assertMarkdownRenders(
+            self.dedent(
+                """
+                <div>
+                </>
+                </div>
+                """
+            ),
+            self.dedent(
+                """
+                <div>
+                </>
+                </div>
+                """
+            )
+        )
diff --git a/tests/test_syntax/extensions/test_md_in_html.py b/tests/test_syntax/extensions/test_md_in_html.py
index 5ef860d..903766f 100644
--- a/tests/test_syntax/extensions/test_md_in_html.py
+++ b/tests/test_syntax/extensions/test_md_in_html.py
@@ -1538,6 +1538,24 @@ class TestMdInHTML(TestCase):
             extensions=['md_in_html']
         )
 
+    def test_noname_tag(self):
+        self.assertMarkdownRenders(
+            self.dedent(
+                """
+                <div markdown>
+                </>
+                </div>
+                """
+            ),
+            self.dedent(
+                """
+                <div>
+                <p>&lt;/&gt;</p>
+                </div>
+                """
+            )
+        )
+
 
 def load_tests(loader, tests, pattern):
     """ Ensure `TestHTMLBlocks` doesn't get run twice by excluding it here. """
diff --git a/tests/test_syntax/inline/test_code.py b/tests/test_syntax/inline/test_code.py
index 9fc3790..c7ccc4b 100644
--- a/tests/test_syntax/inline/test_code.py
+++ b/tests/test_syntax/inline/test_code.py
@@ -62,3 +62,20 @@ class TestCode(TestCase):
                 """
             )
         )
+
+    def test_noname_tag(self):
+        # Browsers ignore `</>`, but a Markdown parser should not, and should treat it as data
+        # but not a tag.
+
+        self.assertMarkdownRenders(
+            self.dedent(
+                """
+                `</>`
+                """
+            ),
+            self.dedent(
+                """
+                <p><code>&lt;/&gt;</code></p>
+                """
+            )
+        )
diff --git a/tests/test_syntax/inline/test_raw_html.py b/tests/test_syntax/inline/test_raw_html.py
index f165f75..57d725e 100644
--- a/tests/test_syntax/inline/test_raw_html.py
+++ b/tests/test_syntax/inline/test_raw_html.py
@@ -31,3 +31,6 @@ class TestRawHtml(TestCase):
 
     def test_inline_html_backslashes(self):
         self.assertMarkdownRenders('<img src="..\\..\\foo.png">', '<p><img src="..\\..\\foo.png"></p>')
+
+    def test_noname_tag(self):
+        self.assertMarkdownRenders('<span></></span>', '<p><span>&lt;/&gt;</span></p>')