diff --git a/docling/backend/md_backend.py b/docling/backend/md_backend.py index b7c2f03961..4e608bb16e 100644 --- a/docling/backend/md_backend.py +++ b/docling/backend/md_backend.py @@ -268,7 +268,7 @@ def __init__( self._pending_hard_line_break = False self._pending_soft_line_break = False - self._html_blocks: int = 0 + self._html_blocks: list[str] = [] self._image_loader: ImageResourceLoader | None = None # A leading BOM is dropped. Kept, it prefixes the first line, so a @@ -998,15 +998,17 @@ def _iterate_elements( # noqa: C901 self._pending_hard_line_break = True elif isinstance(element, marko.block.HTMLBlock): - self._html_blocks += 1 _log.debug("HTML Block: %s", element) if ( len(element.body) > 0 ): # If Marko doesn't return any content for HTML block, skip it - html_block = element.body.strip() + self._html_blocks.append(element.body.strip()) - # wrap in markers to enable post-processing in convert() - text_to_add = f"{_START_MARKER}{html_block}{_STOP_MARKER}" + # Keep only an index in the code text: the HTML serializer may + # escape code text, so convert() splices the raw block back. + text_to_add = ( + f"{_START_MARKER}{len(self._html_blocks) - 1}{_STOP_MARKER}" + ) doc.add_code( parent=parent_item, text=text_to_add, @@ -1182,30 +1184,27 @@ def convert(self) -> DoclingDocument: list_last_item_by_ref={}, ) - if self._html_blocks > 0: + if self._html_blocks: html_backend_cls = MarkdownHTMLDocumentBackend html_payload = serialize_markdown_for_html( doc, source_hash=self.document_hash ) - html_str = html_payload.text - - # Restore only the marked HTML blocks. The HTML serializer - # escapes code content, so decode each block before parsing it. + html_blocks = self._html_blocks + # restore the raw HTML blocks in place of their marked code blocks pattern = ( rf"
\s*\s*{re.escape(_START_MARKER)}"
- rf"(.*?){re.escape(_STOP_MARKER)}\s*\s*"
+ rf"(\d+){re.escape(_STOP_MARKER)}\s*\s*"
)
html_str, count = re.subn(
pattern,
- lambda match: unescape(match.group(1)),
- html_str,
- flags=re.DOTALL,
+ lambda match: html_blocks[int(match.group(1))],
+ html_payload.text,
)
- if count != self._html_blocks:
+ if count != len(html_blocks):
raise RuntimeError(
"An internal error has occurred during Markdown conversion."
)
- self._html_blocks = 0
+ self._html_blocks = []
# delegate to HTML backend
stream = BytesIO(bytes(html_str, encoding="utf-8"))
md_options = cast(MarkdownBackendOptions, self.options)
diff --git a/pyproject.toml b/pyproject.toml
index 8a73a417fe..a9efb8af72 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -48,7 +48,7 @@ requires-python = '>=3.10,<4.0'
# MINIMAL BASE (8 packages) - ~50MB
dependencies = [
'pydantic>=2.0.0,<3.0.0',
- 'docling-core>=2.103.1,<3.0.0',
+ 'docling-core>=2.104.0,<3.0.0',
'pydantic-settings>=2.3.0,<3.0.0',
'filetype>=1.2.0,<2.0.0',
'requests>=2.32.2,<3.0.0',
@@ -288,7 +288,7 @@ models-vlm-inline = [
feat-chunking = [
# >=2.94.1 relaxed its transformers pin; older releases cap transformers<5.9.0
# on macOS, which conflicts with the mlx-vlm floor in models-vlm-inline.
- 'docling-core[chunking]>=2.103.1,<3.0.0',
+ 'docling-core[chunking]>=2.104.0,<3.0.0',
]
service-client = [
@@ -340,7 +340,7 @@ typecheck = [
]
pr-fast-checks = [
{ include-group = "typecheck" },
- "docling-core[chunking] (>=2.103.1,<3.0.0)",
+ "docling-core[chunking] (>=2.104.0,<3.0.0)",
"pydantic (>=2.0.0,<3.0.0)",
"pydantic-settings (>=2.3.0,<3.0.0)",
"ruff==0.15.12",
diff --git a/tests/test_backend_markdown.py b/tests/test_backend_markdown.py
index f766e58833..d1f975b8de 100644
--- a/tests/test_backend_markdown.py
+++ b/tests/test_backend_markdown.py
@@ -1067,6 +1067,21 @@ def test_ordered_list_numbers_survive_html_block():
)
+def test_html_block_entities_are_decoded_exactly_once():
+ """Raw HTML must reach the HTML backend unchanged, whether or not the
+ docling-core HTML serializer escapes code text.
+
+ Double escaping keeps `<` in the text; double unescaping turns
+ `<b>` into ``.
+ """
+ markdown = "Intro.\n\n