From 6ce2cc2a5f3b1654fa7a5e67975fadf0137e3a08 Mon Sep 17 00:00:00 2001 From: Ryan Chou <88779759+ryanchou1994@users.noreply.github.com> Date: Wed, 9 Sep 2026 04:07:56 +0800 Subject: [PATCH] fix: ignore comment-separated whitespace at block boundaries --- markdownify/__init__.py | 19 ++++++++++++---- tests/test_advanced.py | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/markdownify/__init__.py b/markdownify/__init__.py index 28cdaf6..e32c87d 100644 --- a/markdownify/__init__.py +++ b/markdownify/__init__.py @@ -240,7 +240,7 @@ def process_tag(self, node, parent_tags=None): # adjacent to the inner/outer boundaries of block elements. should_remove_inside = should_remove_whitespace_inside(node) - def _can_ignore(el): + def _can_ignore(el, previous_content, next_content): if isinstance(el, Tag): # Tags are always processed. return False @@ -252,10 +252,10 @@ def _can_ignore(el): if six.text_type(el).strip() != '': # Non-whitespace text nodes are always processed. return False - elif should_remove_inside and (not el.previous_sibling or not el.next_sibling): + elif should_remove_inside and (previous_content is None or next_content is None): # Inside block elements (excluding
), ignore adjacent whitespace elements.
                     return True
-                elif should_remove_whitespace_outside(el.previous_sibling) or should_remove_whitespace_outside(el.next_sibling):
+                elif should_remove_whitespace_outside(previous_content) or should_remove_whitespace_outside(next_content):
                     # Outside block elements (including 
), ignore adjacent whitespace elements.
                     return True
                 else:
@@ -265,7 +265,18 @@ def _can_ignore(el):
             else:
                 raise ValueError('Unexpected element type: %s' % type(el))
 
-        children_to_convert = [el for el in node.children if not _can_ignore(el)]
+        # Ignore comments and whitespace when locating block boundaries. Advance
+        # through content siblings once, rather than rescanning long comment runs.
+        content_siblings = (el for el in node.children if _is_block_content_element(el))
+        previous_content = None
+        next_content = next(content_siblings, None)
+        children_to_convert = []
+        for el in node.children:
+            if el is next_content:
+                previous_content = el
+                next_content = next(content_siblings, None)
+            if not _can_ignore(el, previous_content, next_content):
+                children_to_convert.append(el)
 
         # Create a copy of this tag's parent context, then update it to include this tag
         # to propagate down into the children.
diff --git a/tests/test_advanced.py b/tests/test_advanced.py
index 6123d8c..e6cd97c 100644
--- a/tests/test_advanced.py
+++ b/tests/test_advanced.py
@@ -1,3 +1,7 @@
+import pytest
+from bs4 import BeautifulSoup, Comment
+from markdownify import MarkdownConverter
+
 from .utils import md
 
 
@@ -27,6 +31,50 @@ def test_ignore_comments_with_other_tags():
     assert text == "[example link](http://example.com/)"
 
 
+@pytest.mark.parametrize('separator', ['\n', ' ', '\t'])
+@pytest.mark.parametrize('count', [1, 2, 4])
+def test_ignore_whitespace_between_comments_and_blocks(separator, count):
+    comments = separator + ('' + separator) * count
+    html = '

line 1

' + comments + '

line 2

' + assert MarkdownConverter().convert(html) == 'line 1\n\nline 2' + + +def test_ignore_comment_whitespace_inside_block_boundaries(): + html = '
\n

text

\n
' + assert md(html) == '\n\ntext\n\n' + + +@pytest.mark.parametrize('html, expected', [ + ('one two', 'one **two**'), + ('one a b two', 'one `a b` two'), +]) +def test_comment_whitespace_between_inline_content(html, expected): + assert md(html) == expected + + +@pytest.mark.parametrize('code', [False, True]) +def test_comment_whitespace_in_pre(code): + content = ' a\t\n\n\n b ' + if code: + content = '' + content + '' + assert md('
' + content + '
', strip_pre=None) == '\n\n```\n a\t\n\n\n b \n```\n\n' + + +def test_comment_whitespace_conversion_preserves_soup(): + soup = BeautifulSoup('

one

\n\n\n

two

', 'html.parser') + original = str(soup) + comments = soup.find_all(string=lambda node: isinstance(node, Comment)) + positions = [(node.parent, node.previous_sibling, node.next_sibling) for node in comments] + converter = MarkdownConverter() + assert converter.convert_soup(soup) == 'one\n\ntwo' + assert converter.convert_soup(soup) == 'one\n\ntwo' + assert str(soup) == original + for node, (parent, previous, following) in zip(comments, positions): + assert node.parent is parent + assert node.previous_sibling is previous + assert node.next_sibling is following + + def test_code_with_tricky_content(): assert md('>') == "`>`" assert md('/home/username') == "`/home/`**username**"