Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 15 additions & 16 deletions docling/backend/md_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"<pre>\s*<code>\s*{re.escape(_START_MARKER)}"
rf"(.*?){re.escape(_STOP_MARKER)}\s*</code>\s*</pre>"
rf"(\d+){re.escape(_STOP_MARKER)}\s*</code>\s*</pre>"
)
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)
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions tests/test_backend_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 `&amp;lt;` in the text; double unescaping turns
`&lt;b&gt;` into `<b>`.
"""
markdown = "Intro.\n\n<div>Write &amp;lt;b&amp;gt; for bold &amp; more</div>\n"
conv_result = get_converter().convert_string(markdown, format=InputFormat.MD)
assert conv_result.status == ConversionStatus.SUCCESS

texts = [item.text for item in conv_result.document.texts]
assert texts == ["Intro.", "Write &lt;b&gt; for bold & more"]


def test_standard_ordered_list_still_starts_at_one():
"""Ordinary 1-based ordered lists must continue to export as 1-based."""
markdown = "1. alpha\n2. beta\n3. gamma\n"
Expand Down
6 changes: 3 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading