From 7c88cb21a7f7ff53e1beadbe177766f0f8118e51 Mon Sep 17 00:00:00 2001
From: Patrick Creighton
Date: Tue, 6 Oct 2026 10:19:14 -0700
Subject: [PATCH] Keep hard line breaks that the output normalization strips
markdownify writes
as two trailing spaces, which MarkItDown.convert()
then strips from every output line, so Word line breaks and HTML
became soft breaks that render as a space. A break with text or an image
on both sides of it in its parent element is now written as a backslash
hard break, which survives the normalization. Other breaks, and those in
table cells, headings, and preformatted text, keep markdownify's handling.
Tests that recorded the old converter output, including the OCR
plugin's, now expect the backslash.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../tests/test_docx_converter.py | 2 +-
.../tests/test_docx_inheritance.py | 8 +--
.../tests/test_pptx_converter.py | 2 +-
.../tests/test_pptx_inheritance.py | 12 ++---
.../tests/test_xlsx_converter.py | 2 +-
.../tests/test_xlsx_inheritance.py | 14 +++---
.../src/markitdown/converters/_markdownify.py | 39 +++++++++++++++
packages/markitdown/tests/test_docx.py | 8 +++
packages/markitdown/tests/test_html.py | 49 ++++++++++++++++++-
packages/markitdown/tests/test_rss.py | 2 +-
packages/markitdown/tests/test_xlsx.py | 2 +-
11 files changed, 116 insertions(+), 24 deletions(-)
diff --git a/packages/markitdown-ocr/tests/test_docx_converter.py b/packages/markitdown-ocr/tests/test_docx_converter.py
index fc359d6c52..1093cc695d 100644
--- a/packages/markitdown-ocr/tests/test_docx_converter.py
+++ b/packages/markitdown-ocr/tests/test_docx_converter.py
@@ -28,7 +28,7 @@
TEST_DATA_DIR = Path(__file__).parent / "ocr_test_data"
_MOCK_TEXT = "MOCK_OCR_TEXT_12345"
-_MOCK_BLOCK = "*[Image OCR] \nMOCK\\_OCR\\_TEXT\\_12345 \n[End OCR]*"
+_MOCK_BLOCK = "*[Image OCR]\\\nMOCK\\_OCR\\_TEXT\\_12345\\\n[End OCR]*"
class MockOCRService:
diff --git a/packages/markitdown-ocr/tests/test_docx_inheritance.py b/packages/markitdown-ocr/tests/test_docx_inheritance.py
index 7cf02d705d..a2cea38a22 100644
--- a/packages/markitdown-ocr/tests/test_docx_inheritance.py
+++ b/packages/markitdown-ocr/tests/test_docx_inheritance.py
@@ -104,7 +104,7 @@ def test_plugin_registration_full_trip(monkeypatch: pytest.MonkeyPatch) -> None:
assert result.markdown == (
"# Heading\n\nBefore\n\n"
- "*[Image OCR]\nRecognized\\_text\n[End OCR]*\n\n"
+ "*[Image OCR]\\\nRecognized\\_text\\\n[End OCR]*\n\n"
"After\n\n**Native content**"
)
client.chat.completions.create.assert_called_once()
@@ -145,8 +145,8 @@ def test_recognition_cache_is_local_and_service_override_is_preserved() -> None:
overridden = _convert(converter, data, ocr_service=second)
again = _convert(converter, data)
- assert default.count("*[Image OCR] \nfirst \n[End OCR]*") == 12
- assert overridden.count("*[Image OCR] \nsecond \n[End OCR]*") == 12
+ assert default.count("*[Image OCR]\\\nfirst\\\n[End OCR]*") == 12
+ assert overridden.count("*[Image OCR]\\\nsecond\\\n[End OCR]*") == 12
assert again == default
assert first.extract_text.call_count == 2
second.extract_text.assert_called_once()
@@ -189,7 +189,7 @@ def test_ocr_text_is_escaped_and_line_endings_are_preserved_as_html_breaks(
"
final
[End OCR]
"
) in convert_html.call_args.args[0]
assert (
- "*[Image OCR] \nA\\_B \\*literal\\* \n" " & value \nfinal \n[End OCR]*"
+ "*[Image OCR]\\\nA\\_B \\*literal\\*\\\n" " & value\\\nfinal\\\n[End OCR]*"
) in result
assert "data-markitdown-image-" not in convert_html.call_args.args[0]
diff --git a/packages/markitdown-ocr/tests/test_pptx_converter.py b/packages/markitdown-ocr/tests/test_pptx_converter.py
index 683a688511..5a01ca9c91 100644
--- a/packages/markitdown-ocr/tests/test_pptx_converter.py
+++ b/packages/markitdown-ocr/tests/test_pptx_converter.py
@@ -34,7 +34,7 @@
TEST_DATA_DIR = Path(__file__).parent / "ocr_test_data"
_MOCK_TEXT = "MOCK_OCR_TEXT_12345"
-_OCR_BLOCK = "*[Image OCR] \nMOCK\\_OCR\\_TEXT\\_12345 \n[End OCR]*"
+_OCR_BLOCK = "*[Image OCR]\\\nMOCK\\_OCR\\_TEXT\\_12345\\\n[End OCR]*"
class MockOCRService:
diff --git a/packages/markitdown-ocr/tests/test_pptx_inheritance.py b/packages/markitdown-ocr/tests/test_pptx_inheritance.py
index 6ee3b89d22..0fe26753aa 100644
--- a/packages/markitdown-ocr/tests/test_pptx_inheritance.py
+++ b/packages/markitdown-ocr/tests/test_pptx_inheritance.py
@@ -103,8 +103,8 @@ def test_ocr_cache_is_document_local_and_honors_service_override() -> None:
overridden = _convert(converter, data, ocr_service=second)
again = _convert(converter, data)
- assert original.count("*[Image OCR] \nfirst \n[End OCR]*") == 12
- assert overridden.count("*[Image OCR] \nsecond \n[End OCR]*") == 12
+ assert original.count("*[Image OCR]\\\nfirst\\\n[End OCR]*") == 12
+ assert overridden.count("*[Image OCR]\\\nsecond\\\n[End OCR]*") == 12
assert again == original
assert first.extract_text.call_count == 2
second.extract_text.assert_called_once()
@@ -148,7 +148,7 @@ def test_ocr_text_is_escaped_with_normalized_html_breaks_and_html_options(
"
final
[End OCR]",
)
assert (
- "*[Image OCR] \nA\\_B \\*literal\\* \n" " & value \nfinal \n[End OCR]*"
+ "*[Image OCR]\\\nA\\_B \\*literal\\*\\\n" " & value\\\nfinal\\\n[End OCR]*"
) in result
assert "*literal*" in _convert(
converter, data, escape_asterisks=False, escape_underscores=False
@@ -213,7 +213,7 @@ def recognize(stream):
)
assert len(seen) == 1 and b"