From ebd57923e821fb20cca56693ea1bbc0a9a3de4e1 Mon Sep 17 00:00:00 2001 From: Apoorv Darshan Date: Mon, 6 Jul 2026 15:22:05 +0530 Subject: [PATCH] Fix #249: preserve whitespace around stripped block-level elements Whitespace immediately outside a block-level element is collapsed because a converted block already provides its own line separation. But when a block element such as
is stripped (or excluded via convert=[...]), it is emitted inline, so removing the surrounding whitespace runs the neighbouring words together (e.g. 'a
b
c
' with strip=['div'] became 'ab c' -> 'abc'). Only treat a block-level sibling as a whitespace boundary when it is actually being converted; a stripped sibling now keeps the separating whitespace. Added a regression test. --- markdownify/__init__.py | 16 ++++++++++++++-- tests/test_args.py | 8 ++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/markdownify/__init__.py b/markdownify/__init__.py index 28cdaf6..3386715 100644 --- a/markdownify/__init__.py +++ b/markdownify/__init__.py @@ -362,17 +362,29 @@ def process_text(self, el, parent_tags=None): # remove leading whitespace at the start or just after a # block-level element; remove traliing whitespace at the end # or just before a block-level element. - if (should_remove_whitespace_outside(el.previous_sibling) + if (self._removes_adjacent_whitespace(el.previous_sibling) or (should_remove_whitespace_inside(el.parent) and not el.previous_sibling)): text = text.lstrip(' \t\r\n') - if (should_remove_whitespace_outside(el.next_sibling) + if (self._removes_adjacent_whitespace(el.next_sibling) or (should_remove_whitespace_inside(el.parent) and not el.next_sibling)): text = text.rstrip() return text + def _removes_adjacent_whitespace(self, el): + """Return whether a sibling element absorbs the whitespace next to it. + + Block-level elements normally do, but a block-level element that is + stripped (or otherwise not converted) is emitted inline, so the + whitespace that separates it from its neighbours must be preserved + instead of being collapsed away (see #249). + """ + if not should_remove_whitespace_outside(el): + return False + return self.should_convert_tag(el.name.lower()) + def get_conv_fn_cached(self, tag_name): """Given a tag name, return the conversion function using the cache.""" # If conversion function is not in cache, add it diff --git a/tests/test_args.py b/tests/test_args.py index 838ef9d..bc6732a 100644 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -16,6 +16,14 @@ def test_do_not_strip(): assert text == '[Some Text](https://github.com/matthewwithanm)' +def test_strip_block_element_preserves_surrounding_whitespace(): + # A stripped block-level element is rendered inline, so the whitespace + # separating it from its neighbours must be preserved (see #249). + html = 'Ignored
Still ignored
tag.
' + assert md(html, strip=['div']) == 'Ignored Still ignored tag.' + assert md(html, convert=['span']) == 'Ignored Still ignored tag.' + + def test_convert(): text = md('Some Text', convert=['a']) assert text == '[Some Text](https://github.com/matthewwithanm)'