Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .spell-dict
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ unescaping
unittest
unordered
unsanitized
unterminated
untrusted
UTF
uTidylib
Expand Down
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ See the [Contributing Guide](contributing.md) for details.
* Update serializer to be non-recursive (#1644).
* Improve ancestor handling in the inline `Treeprocessor` (#1646).
* Fix issue where inline HTML attributes were rejected if they had `<` or `>` in the attribute (#1647).
* Fix issue where an unterminated end tag (`</foo`) could cause all remaining content to be dropped (#1651).

## [3.11.0] - 2026-09-25

Expand Down
6 changes: 4 additions & 2 deletions markdown/htmlparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,12 @@ class _HTMLParser(htmlparser.HTMLParser):
def parse_endtag(self, i):
start = self.rawdata[i:i+3]
c = ord(start[-1])
if len(start) < 3 or not (65 <= c <= 90 or 97 <= c <= 122):
is_data = len(start) < 3 or not (65 <= c <= 90 or 97 <= c <= 122)
pos = super().parse_endtag(i) if not is_data else -1
if pos == -1:
self.handle_data(self.rawdata[i:i + 2])
return i + 2
return super().parse_endtag(i)
return pos

def parse_starttag(self, i: int) -> int: # pragma: no cover
# Treat `</>` as normal data as it is not a real tag.
Expand Down
57 changes: 57 additions & 0 deletions tests/test_syntax/blocks/test_html_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1787,3 +1787,60 @@ def test_unclosed_endtag(self):
'''
)
)

def test_unterminated_endtag_in_code_span(self):
"""Ensure an unterminated end tag does not swallow the remaining content."""

# https://github.com/Python-Markdown/markdown/issues/1651
self.assertMarkdownRenders(
'Run `cmd 2</dev/null` here.\n\nThis paragraph should not disappear.',
self.dedent(
'''
<p>Run <code>cmd 2&lt;/dev/null</code> here.</p>
<p>This paragraph should not disappear.</p>
'''
)
)

def test_additional_tail_cases(self):
"""Ensure various tail cases survive."""

# https://github.com/Python-Markdown/markdown/issues/1651
self.assertMarkdownRenders(
self.dedent(
"""
`</dev` z > q

`</ab c` z

`</dev/` > q

`</dev/null` z > q

`</ab c>` z
"""
),
self.dedent(
"""
<p><code>&lt;/dev</code> z &gt; q</p>
<p><code>&lt;/ab c</code> z</p>
<p><code>&lt;/dev/</code> &gt; q</p>
<p><code>&lt;/dev/null</code> z &gt; q</p>
<p><code>&lt;/ab c&gt;</code> z</p>
"""
)
)

def test_unterminated_endtag(self):
"""Ensure an unterminated end tag is treated as text."""

# https://github.com/Python-Markdown/markdown/issues/1651
self.assertMarkdownRenders(
'foo </bar\n\nbaz',
self.dedent(
'''
<p>foo &lt;/bar</p>
<p>baz</p>
'''
)
)
Loading