From 99b69de9da0e8020868e08db46448be34bff76fc Mon Sep 17 00:00:00 2001 From: wolfgang-aura <169568318+wolfgang-aura@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:52:06 +0800 Subject: [PATCH 1/2] Keep a comment inside raw inline HTML in the same paragraph --- docs/changelog.md | 2 + markdown/extensions/md_in_html.py | 2 + markdown/htmlparser.py | 38 ++++- tests/test_syntax/blocks/test_html_blocks.py | 145 +++++++++++++++++++ 4 files changed, 186 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index f20687c0a..4df6240b6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -16,6 +16,8 @@ See the [Contributing Guide](contributing.md) for details. * Update serializer to be non-recursive (#1644). * Improve ancestor handling in the inline `Treeprocessor` (#1646). +* Keep a raw HTML comment inside an inline HTML element that closes in the + same paragraph, instead of splitting the paragraph around it (#1643). ## [3.11.0] - 2026-09-25 diff --git a/markdown/extensions/md_in_html.py b/markdown/extensions/md_in_html.py index d02d7a483..1a4ec0e2b 100644 --- a/markdown/extensions/md_in_html.py +++ b/markdown/extensions/md_in_html.py @@ -152,6 +152,7 @@ def handle_starttag(self, tag, attrs): self.handle_data(self.md.htmlStash.store(text)) else: self.handle_data(text) + self.open_inline_tag(tag) if tag in self.CDATA_CONTENT_ELEMENTS: # This is presumably a standalone tag in a code span (see #1036). self.clear_cdata_mode() @@ -242,6 +243,7 @@ def handle_endtag(self, tag): self.handle_data(self.md.htmlStash.store(text)) else: self.handle_data(text) + self.close_inline_tag(tag) def handle_startendtag(self, tag, attrs): if tag in self.empty_tags: diff --git a/markdown/htmlparser.py b/markdown/htmlparser.py index 488da0333..1bce32ea6 100644 --- a/markdown/htmlparser.py +++ b/markdown/htmlparser.py @@ -92,6 +92,14 @@ # The newlines may be preceded by additional whitespace. blank_line_re = re.compile(r'^([ ]*\n){2}') +# Match a blank line anywhere in a run of text. +inner_blank_line_re = re.compile(r'\n[ \t]*\n') + +# Elements which never have an end tag and so cannot contain a comment. +void_tags = frozenset([ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' +]) + class _HTMLParser(htmlparser.HTMLParser): """Handle special start and end tags.""" @@ -144,6 +152,7 @@ def reset(self): self.inraw = False self.intail = False self.stack: list[str] = [] # When `inraw==True`, stack contains a list of tags + self.inline_stack: list[str] = [] # Unclosed raw inline tags in the current paragraph self._cache: list[str] = [] self.cleandoc: list[str] = [] self.lineno_start_cache = [0] @@ -223,6 +232,7 @@ def handle_starttag(self, tag: str, attrs: Sequence[tuple[str, str]]): self._cache.append(text) else: self.cleandoc.append(text) + self.open_inline_tag(tag) if tag in self.CDATA_CONTENT_ELEMENTS: # This is presumably a standalone tag in a code span (see #1036). self.clear_cdata_mode() @@ -253,6 +263,26 @@ def handle_endtag(self, tag: str): self._cache = [] else: self.cleandoc.append(text) + self.close_inline_tag(tag) + + def open_inline_tag(self, tag: str): + """ Track an unclosed raw inline tag so a comment inside it is not treated as a block. """ + if not self.md.is_block_level(tag) and tag not in void_tags: + self.inline_stack.append(tag) + + def close_inline_tag(self, tag: str): + """ Stop tracking an inline tag and any unclosed inline tags opened after it. """ + if tag in self.inline_stack: + while self.inline_stack: + if self.inline_stack.pop() == tag: + break + + def inline_close_follows(self, text: str) -> bool: + """ Return `True` if an open inline tag is closed in the same paragraph after the comment `text`. """ + if not self.inline_stack: + return False + rest = inner_blank_line_re.split(self.rawdata[self.line_offset + self.offset + len(text):], 1)[0] + return any(re.search(r'\s*{}\s*>'.format(re.escape(tag)), rest, re.I) for tag in self.inline_stack) def handle_data(self, data: str): if self.intail and '\n' in data: @@ -260,6 +290,9 @@ def handle_data(self, data: str): if self.inraw: self._cache.append(data) else: + if inner_blank_line_re.search(data): + # A blank line ends the paragraph, so open inline tags no longer enclose what follows. + self.inline_stack = [] self.cleandoc.append(data) def handle_empty_tag(self, data: str, is_block: bool): @@ -296,7 +329,10 @@ def handle_entityref(self, name: str): def handle_comment(self, data: str): # Check if the comment is unclosed, if so, we need to override position - self.handle_empty_tag(''.format(data), is_block=True) + text = ''.format(data) + # A comment inside a raw inline element is part of that inline content, unless it contains a blank line. + is_block = bool(inner_blank_line_re.search(data)) or not self.inline_close_follows(text) + self.handle_empty_tag(text, is_block=is_block) def handle_decl(self, data: str): self.handle_empty_tag(''.format(data), is_block=True) diff --git a/tests/test_syntax/blocks/test_html_blocks.py b/tests/test_syntax/blocks/test_html_blocks.py index ecc10f456..e05471722 100644 --- a/tests/test_syntax/blocks/test_html_blocks.py +++ b/tests/test_syntax/blocks/test_html_blocks.py @@ -752,6 +752,151 @@ def test_comment_in_code_span(self): '
<!-- *foo* -->
+ +
+ """ + ) + ) + + def test_comment_in_inline_html_own_line_before_endtag(self): + self.assertMarkdownRenders( + self.dedent( + """ + + + """ + ), + self.dedent( + """ ++
+ """ + ) + ) + + def test_comment_in_inline_html_after_blank_line(self): + self.assertMarkdownRenders( + self.dedent( + """ + + + + + + """ + ), + self.dedent( + """ ++ + + + """ + ) + ) + + def test_comment_after_closed_inline_html(self): + self.assertMarkdownRenders( + self.dedent( + """ + foo + + """ + ), + self.dedent( + """ +
foo
+ + """ + ) + ) + + def test_comment_after_unclosed_inline_tag_in_code_span(self): + self.assertMarkdownRenders( + self.dedent( + """ + Use `` for bold. + + """ + ), + self.dedent( + """ +Use <b> for bold.
foo
+ + """ + ) + ) + + def test_comment_with_blank_line_in_inline_html(self): + self.assertMarkdownRenders( + self.dedent( + """ + + + + """ + ), + self.dedent( + """ ++ + + """ + ) + ) + + def test_comment_after_void_inline_html(self): + self.assertMarkdownRenders( + self.dedent( + """ +
+
+ """
+ ),
+ self.dedent(
+ """
+ 
+
+ """ + ) + ) + def test_comment_in_inline_html_own_line_before_endtag(self): self.assertMarkdownRenders( self.dedent(