From 7f5ac2d202d0cbda22907b5b362f027d0074ca49 Mon Sep 17 00:00:00 2001 From: Casper Verbuyst Date: Wed, 21 Oct 2026 20:48:06 +0200 Subject: [PATCH] Fix TOC labels for quoted HTML angle brackets The TOC tag stripper stops at the first greater-than sign, including one inside a quoted attribute. Attribute text then enters heading IDs and TOC labels. Reuse the inline parser's HTML pattern to strip the complete tag in both heading content and custom labels, preserving HTML entities. Add regression coverage for both quote styles, embedded quotes, multiline attributes, generated IDs, TOC tokens, and custom labels. Assisted by Codex with investigation, implementation, tests, changelog, validation, and this commit message. Signed-off-by: Casper Verbuyst --- docs/changelog.md | 2 + markdown/extensions/toc.py | 5 +- tests/test_syntax/extensions/test_toc.py | 59 ++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 94b4a2f5..6f27060e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -16,6 +16,8 @@ See the [Contributing Guide](contributing.md) for details. ### Fixed +* Strip complete HTML tags with quoted `>` characters from `toc` labels and + generated heading IDs. * Fix quadratic-time regex backtracking in `attr_list` when a block ends with an unclosed attribute list followed by a long run of spaces, e.g. a line consisting only of `{` and some text with many trailing spaces (#1656). diff --git a/markdown/extensions/toc.py b/markdown/extensions/toc.py index 06812549..e0c4c74e 100644 --- a/markdown/extensions/toc.py +++ b/markdown/extensions/toc.py @@ -24,6 +24,7 @@ from ..util import parseBoolValue, AMP_SUBSTITUTE, deprecated, HTML_PLACEHOLDER_RE, AtomicString from ..treeprocessors import UnescapeTreeprocessor from ..serializers import RE_AMP +from ..inlinepatterns import HTML_RE import re import html import unicodedata @@ -108,8 +109,8 @@ def strip_tags(text: str) -> str: while (start := text.find('', start)) != -1: text = f'{text[:start]}{text[end + 3:]}' - while (start := text.find('<')) != -1 and (end := text.find('>', start)) != -1: - text = f'{text[:start]}{text[end + 1:]}' + # Use the inline HTML pattern to respect quoted attribute values. + text = re.sub(HTML_RE, '', text, flags=re.DOTALL) # Collapse whitespace text = ' '.join(text.split()) diff --git a/tests/test_syntax/extensions/test_toc.py b/tests/test_syntax/extensions/test_toc.py index 372ba09c..07df442b 100644 --- a/tests/test_syntax/extensions/test_toc.py +++ b/tests/test_syntax/extensions/test_toc.py @@ -155,6 +155,53 @@ def testTOCReset(self): self.assertEqual(md.toc, '') self.assertEqual(md.toc_tokens, []) + def testHTMLAttributeWithAngleBracket(self): + for quote in ['"', "'"]: + with self.subTest(quote=quote): + heading = f' 3{quote}>Python support' + self.assertMarkdownRenders( + f'# {heading}', + f'

{heading}

', + expected_attrs={ + 'toc': ( + '
\n\n
\n' + ), + 'toc_tokens': [{ + 'level': 1, + 'id': 'python-support', + 'name': 'Python support', + 'html': heading, + 'data-toc-label': '', + 'children': [] + }] + } + ) + + def testCustomLabelAttributeWithAngleBracket(self): + label = 'Python & support' + self.assertMarkdownRenders( + f"# Python support {{data-toc-label='{label}'}}", + '

Python support

', + expected_attrs={ + 'toc': ( + '
\n\n
\n' + ), + 'toc_tokens': [{ + 'level': 1, + 'id': 'python-support', + 'name': 'Python & support', + 'html': 'Python support', + 'data-toc-label': label, + 'children': [] + }] + }, + extensions=[TocExtension(), 'attr_list'] + ) + def testUniqueIds(self): self.assertMarkdownRenders( self.dedent( @@ -1454,6 +1501,18 @@ def testStripElementWithNewlines(self): 'foo bar' ) + def testStripElementWithQuotedBrackets(self): + for tag in [ + '', + "", + '', + '', + '', + "", + ]: + with self.subTest(tag=tag): + self.assertEqual(strip_tags(f'foo {tag}bar'), 'foo bar') + def testStripComment(self): self.assertEqual( strip_tags('foo bar'),