From f6a5758b8ce48e6ba9decbf26484257b63623545 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:28:46 +0800 Subject: [PATCH] fix(html2text): thread escape_backslash into escape_md_section() call Sibling flags (escape_dot/escape_plus/escape_dash) are all forwarded from the HTML2Text instance into escape_md_section() at its call site in handle_data(), but escape_backslash was never passed. As a result, escape_md_section()'s own hardcoded default of True always wins, so backslashes are escaped/doubled in generated markdown regardless of what the instance (or CustomHTML2Text's own escape_backslash=False default, used by the default markdown-generation pipeline) configures. Fix: pass escape_backslash=self.escape_backslash alongside the other escape_* flags, the same way the siblings are threaded. Co-Authored-By: Claude Opus 4.8 --- crawl4ai/html2text/__init__.py | 1 + tests/unit/test_html2text_escape_backslash.py | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 tests/unit/test_html2text_escape_backslash.py diff --git a/crawl4ai/html2text/__init__.py b/crawl4ai/html2text/__init__.py index ff39fb211..2b77ea865 100644 --- a/crawl4ai/html2text/__init__.py +++ b/crawl4ai/html2text/__init__.py @@ -952,6 +952,7 @@ def handle_data(self, data: str, entity_char: bool = False) -> None: data = escape_md_section( data, snob=self.escape_snob, + escape_backslash=self.escape_backslash, escape_dot=self.escape_dot, escape_plus=self.escape_plus, escape_dash=self.escape_dash, diff --git a/tests/unit/test_html2text_escape_backslash.py b/tests/unit/test_html2text_escape_backslash.py new file mode 100644 index 000000000..7f15092cc --- /dev/null +++ b/tests/unit/test_html2text_escape_backslash.py @@ -0,0 +1,35 @@ +"""Unit tests for CustomHTML2Text's escape_backslash config flag. + +escape_backslash is threaded from config.py -> HTML2Text.__init__ -> instance +attribute, and sibling flags (escape_dot/escape_plus/escape_dash) are all +forwarded into escape_md_section() at its call site in handle_data(). This +tests that escape_backslash is forwarded the same way, so that setting it to +False actually disables backslash escaping in the generated markdown. +""" +from crawl4ai.html2text import CustomHTML2Text + + +def _convert(html: str, escape_backslash: bool) -> str: + h = CustomHTML2Text() + h.body_width = 0 + h.escape_backslash = escape_backslash + return h.handle(html) + + +# RE_MD_BACKSLASH_MATCHER (html2text/config.py) only fires on a backslash +# followed by a markdown-special char (`*_{}[]()#+-.!), so the repro text +# must contain a literal backslash immediately before one of those chars. +HTML_WITH_BACKSLASH = r"

Use \* for multiplication

" + + +class TestEscapeBackslashConfig: + def test_escape_backslash_false_leaves_backslashes_alone(self): + """escape_backslash=False must not double the literal backslash.""" + out = _convert(HTML_WITH_BACKSLASH, escape_backslash=False) + assert r"\*" in out + assert r"\\*" not in out + + def test_escape_backslash_true_still_escapes(self): + """escape_backslash=True (the escape_md_section default) must still escape.""" + out = _convert(HTML_WITH_BACKSLASH, escape_backslash=True) + assert r"\\*" in out