diff --git a/docling/backend/html_backend.py b/docling/backend/html_backend.py
index 9794a8718f..132c4af35e 100644
--- a/docling/backend/html_backend.py
+++ b/docling/backend/html_backend.py
@@ -928,7 +928,11 @@ def convert(self) -> DoclingDocument:
binary_hash=self.document_hash,
)
doc = DoclingDocument(name=self.file.stem or "file", origin=origin)
+ self.convert_into(doc)
+ return doc
+ def convert_into(self, doc: DoclingDocument) -> None:
+ """Append the converted content to the body of `doc`."""
if cast(HTMLBackendOptions, self.options).render_page:
self._render_with_browser()
if self._rendered_html:
@@ -1011,7 +1015,6 @@ def convert(self) -> DoclingDocument:
self.ctx = _Context()
self._render_visibility_cache.clear()
self._walk(content, doc)
- return doc
@staticmethod
def _is_header_in_cell_or_item(header: Tag) -> bool:
@@ -3333,7 +3336,7 @@ def _get_list_style_marker(li: Tag) -> str:
Docling's HTML export writes the original marker of a list item this way,
so reading it back keeps markers such as `3.` when the HTML is converted
- again (the Markdown backend does this for files with HTML blocks).
+ again.
"""
style = li.get("style")
if not isinstance(style, str):
diff --git a/docling/backend/md_backend.py b/docling/backend/md_backend.py
index b7c2f03961..bcefeeb0bf 100644
--- a/docling/backend/md_backend.py
+++ b/docling/backend/md_backend.py
@@ -37,11 +37,8 @@
from docling.backend.abstract_backend import (
DeclarativeDocumentBackend,
)
+from docling.backend.html_backend import HTMLDocumentBackend
from docling.backend.utils.image_resource_loader import ImageResourceLoader
-from docling.backend.utils.markdown_html import (
- MarkdownHTMLDocumentBackend,
- serialize_markdown_for_html,
-)
from docling.datamodel.backend_options import (
HTMLBackendOptions,
MarkdownBackendOptions,
@@ -108,10 +105,6 @@ class _GfmStrikethrough(marko.inline.InlineElement):
"Install it with `pip install 'docling-slim[format-markdown]'`."
)
-_MARKER_BODY = "DOCLING_DOC_MD_HTML_EXPORT"
-_START_MARKER = f"#_#_{_MARKER_BODY}_START_#_#"
-_STOP_MARKER = f"#_#_{_MARKER_BODY}_STOP_#_#"
-
class _PendingCreationType(str, Enum):
"""CoordOrigin."""
@@ -268,7 +261,6 @@ def __init__(
self._pending_hard_line_break = False
self._pending_soft_line_break = False
- self._html_blocks: int = 0
self._image_loader: ImageResourceLoader | None = None
# A leading BOM is dropped. Kept, it prefixes the first line, so a
@@ -998,21 +990,9 @@ 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()
-
- # wrap in markers to enable post-processing in convert()
- text_to_add = f"{_START_MARKER}{html_block}{_STOP_MARKER}"
- doc.add_code(
- parent=parent_item,
- text=text_to_add,
- formatting=formatting,
- hyperlink=hyperlink,
- )
+ if html_block := element.body.strip():
+ self._add_html_block(doc, parent_item, html_block)
elif isinstance(element, _gfm_el.Table):
_log.debug(" - GFM Table")
@@ -1137,6 +1117,38 @@ def _load_image_ref(self, dest: str) -> ImageRef | None:
warnings.warn(f"Could not process an image from {dest}: {exc}")
return None
+ def _add_html_block(
+ self, doc: DoclingDocument, parent_item: NodeItem | None, html_block: str
+ ) -> None:
+ """Convert one HTML block with the HTML backend into `parent_item`."""
+ md_options = cast(MarkdownBackendOptions, self.options)
+ html_options = HTMLBackendOptions(
+ enable_local_fetch=md_options.enable_local_fetch,
+ enable_remote_fetch=md_options.enable_remote_fetch,
+ fetch_images=md_options.fetch_images,
+ source_uri=md_options.source_uri,
+ infer_furniture=False,
+ add_title=False,
+ )
+ stream = BytesIO(html_block.encode("utf-8"))
+ in_doc = InputDocument(
+ path_or_stream=stream,
+ format=InputFormat.HTML,
+ backend=HTMLDocumentBackend,
+ filename=self.file.name,
+ backend_options=html_options,
+ )
+ first_new = len(doc.body.children)
+ HTMLDocumentBackend(
+ in_doc=in_doc, path_or_stream=stream, options=html_options
+ ).convert_into(doc)
+ if parent_item is not None:
+ new_refs = doc.body.children[first_new:]
+ del doc.body.children[first_new:]
+ for ref in new_refs:
+ ref.resolve(doc).parent = parent_item.get_ref()
+ parent_item.children.extend(new_refs)
+
def is_valid(self) -> bool:
return self.valid
@@ -1181,56 +1193,6 @@ def convert(self) -> DoclingDocument:
list_item_counter_by_ref={},
list_last_item_by_ref={},
)
-
- if self._html_blocks > 0:
- 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.
- pattern = (
- rf"
\s*\s*{re.escape(_START_MARKER)}"
- rf"(.*?){re.escape(_STOP_MARKER)}\s*\s*
"
- )
- html_str, count = re.subn(
- pattern,
- lambda match: unescape(match.group(1)),
- html_str,
- flags=re.DOTALL,
- )
- if count != self._html_blocks:
- raise RuntimeError(
- "An internal error has occurred during Markdown conversion."
- )
- self._html_blocks = 0
- # delegate to HTML backend
- stream = BytesIO(bytes(html_str, encoding="utf-8"))
- md_options = cast(MarkdownBackendOptions, self.options)
- html_options = HTMLBackendOptions(
- enable_local_fetch=md_options.enable_local_fetch,
- enable_remote_fetch=md_options.enable_remote_fetch,
- fetch_images=md_options.fetch_images,
- source_uri=md_options.source_uri,
- infer_furniture=False,
- add_title=False,
- )
- in_doc = InputDocument(
- path_or_stream=stream,
- format=InputFormat.HTML,
- backend=html_backend_cls,
- filename=self.file.name,
- backend_options=html_options,
- )
- html_backend_obj = html_backend_cls(
- in_doc=in_doc,
- path_or_stream=stream,
- options=html_options,
- picture_refs=html_payload.picture_refs,
- )
- doc = html_backend_obj.convert()
else:
raise RuntimeError(
f"Cannot convert md with {self.document_hash} because the backend failed to init."
diff --git a/docling/backend/utils/markdown_html.py b/docling/backend/utils/markdown_html.py
deleted file mode 100644
index 9e41e2b3bf..0000000000
--- a/docling/backend/utils/markdown_html.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# SPDX-FileCopyrightText: The Docling Contributors
-# SPDX-License-Identifier: MIT
-
-"""Keep Markdown pictures through the internal HTML round trip."""
-
-import html
-from dataclasses import dataclass
-from io import BytesIO
-from pathlib import Path
-from typing import Any
-
-from docling_core.transforms.serializer.base import (
- BaseDocSerializer,
- SerializationResult,
-)
-from docling_core.transforms.serializer.common import create_ser_result
-from docling_core.transforms.serializer.html import (
- HTMLDocSerializer,
- HTMLParams,
- HTMLPictureSerializer,
-)
-from docling_core.types.doc import DoclingDocument, ImageRef, PictureItem
-from docling_core.types.doc.common.constants import DOCUMENT_TOKENS_EXPORT_LABELS
-from docling_core.types.doc.common.content_layer import DEFAULT_CONTENT_LAYERS
-from typing_extensions import override
-
-from docling.backend.html_backend import HTMLDocumentBackend
-from docling.datamodel.backend_options import HTMLBackendOptions
-from docling.datamodel.document import InputDocument
-
-
-@dataclass(frozen=True)
-class MarkdownHTMLPayload:
- text: str
- picture_refs: dict[str, ImageRef | None]
-
-
-class _MarkdownPictureSerializer(HTMLPictureSerializer):
- def __init__(self, reference_prefix: str) -> None:
- self._reference_prefix = reference_prefix
-
- @override
- def serialize(
- self,
- *,
- item: PictureItem,
- doc_serializer: BaseDocSerializer,
- doc: DoclingDocument,
- **kwargs: Any,
- ) -> SerializationResult:
- # The HTML backend must see an img even when fetching was disabled or
- # failed. Its source is a document-local key, not a new fetch request.
- reference = html.escape(self._reference_prefix + item.self_ref, quote=True)
- parts = [
- doc_serializer.serialize_captions(item=item, tag="figcaption", **kwargs),
- create_ser_result(text=f'
', span_source=item),
- ]
- text = "".join(part.text for part in parts)
- return create_ser_result(text=f"{text}", span_source=parts)
-
-
-class MarkdownHTMLDocumentBackend(HTMLDocumentBackend):
- def __init__(
- self,
- in_doc: InputDocument,
- *,
- path_or_stream: BytesIO | Path,
- options: HTMLBackendOptions | None,
- picture_refs: dict[str, ImageRef | None] | None = None,
- ) -> None:
- # InputDocument constructs a backend to inspect it before conversion;
- # that instance does not yet need the already parsed Markdown pictures.
- super().__init__(in_doc=in_doc, path_or_stream=path_or_stream, options=options)
- self._picture_refs = picture_refs if picture_refs is not None else {}
-
- @override
- def _create_image_ref(self, src_url: str) -> ImageRef | None:
- if src_url in self._picture_refs:
- return self._picture_refs[src_url]
- # Images in the original HTML retain the normal loader and its guards.
- return super()._create_image_ref(src_url)
-
-
-def serialize_markdown_for_html(
- doc: DoclingDocument, *, source_hash: str
-) -> MarkdownHTMLPayload:
- # Include the source hash so original HTML cannot collide with a reference
- # merely by using the same picture index. The namespace is deterministic.
- prefix = f"data:application/x-docling-md-picture,{source_hash}/"
- picture_refs = {
- prefix + picture.self_ref: picture.image for picture in doc.pictures
- }
- serializer = HTMLDocSerializer(
- doc=doc,
- params=HTMLParams(
- labels=DOCUMENT_TOKENS_EXPORT_LABELS,
- layers=DEFAULT_CONTENT_LAYERS,
- ),
- picture_serializer=_MarkdownPictureSerializer(prefix),
- )
- return MarkdownHTMLPayload(serializer.serialize().text, picture_refs)
diff --git a/tests/data/md/groundtruth/escaped_characters.md.json b/tests/data/md/groundtruth/escaped_characters.md.json
index 826912662f..7d59b0633f 100644
--- a/tests/data/md/groundtruth/escaped_characters.md.json
+++ b/tests/data/md/groundtruth/escaped_characters.md.json
@@ -3,8 +3,8 @@
"version": "1.10.0",
"name": "escaped_characters",
"origin": {
- "mimetype": "text/html",
- "binary_hash": 3824621944112159534,
+ "mimetype": "text/markdown",
+ "binary_hash": 14089559831613693230,
"filename": "escaped_characters.md"
},
"furniture": {
@@ -20,20 +20,53 @@
{
"$ref": "#/texts/0"
},
+ {
+ "$ref": "#/texts/1"
+ },
+ {
+ "$ref": "#/texts/2"
+ },
{
"$ref": "#/texts/3"
},
+ {
+ "$ref": "#/groups/0"
+ },
+ {
+ "$ref": "#/groups/1"
+ },
{
"$ref": "#/texts/6"
},
+ {
+ "$ref": "#/texts/7"
+ },
{
"$ref": "#/texts/8"
},
+ {
+ "$ref": "#/texts/9"
+ },
{
"$ref": "#/texts/10"
},
+ {
+ "$ref": "#/tables/0"
+ },
+ {
+ "$ref": "#/groups/2"
+ },
{
"$ref": "#/texts/14"
+ },
+ {
+ "$ref": "#/texts/15"
+ },
+ {
+ "$ref": "#/texts/16"
+ },
+ {
+ "$ref": "#/texts/17"
}
],
"content_layer": "body",
@@ -44,7 +77,7 @@
{
"self_ref": "#/groups/0",
"parent": {
- "$ref": "#/texts/3"
+ "$ref": "#/body"
},
"children": [
{
@@ -52,13 +85,13 @@
}
],
"content_layer": "body",
- "name": "ordered list",
+ "name": "list",
"label": "list"
},
{
"self_ref": "#/groups/1",
"parent": {
- "$ref": "#/texts/3"
+ "$ref": "#/body"
},
"children": [
{
@@ -72,7 +105,7 @@
{
"self_ref": "#/groups/2",
"parent": {
- "$ref": "#/texts/10"
+ "$ref": "#/body"
},
"children": [
{
@@ -96,11 +129,7 @@
"parent": {
"$ref": "#/body"
},
- "children": [
- {
- "$ref": "#/texts/1"
- }
- ],
+ "children": [],
"content_layer": "body",
"label": "title",
"prov": [],
@@ -110,13 +139,9 @@
{
"self_ref": "#/texts/1",
"parent": {
- "$ref": "#/texts/0"
+ "$ref": "#/body"
},
- "children": [
- {
- "$ref": "#/texts/2"
- }
- ],
+ "children": [],
"content_layer": "body",
"label": "section_header",
"prov": [],
@@ -127,7 +152,7 @@
{
"self_ref": "#/texts/2",
"parent": {
- "$ref": "#/texts/1"
+ "$ref": "#/body"
},
"children": [],
"content_layer": "body",
@@ -141,14 +166,7 @@
"parent": {
"$ref": "#/body"
},
- "children": [
- {
- "$ref": "#/groups/0"
- },
- {
- "$ref": "#/groups/1"
- }
- ],
+ "children": [],
"content_layer": "body",
"label": "title",
"prov": [],
@@ -188,11 +206,7 @@
"parent": {
"$ref": "#/body"
},
- "children": [
- {
- "$ref": "#/texts/7"
- }
- ],
+ "children": [],
"content_layer": "body",
"label": "title",
"prov": [],
@@ -202,7 +216,7 @@
{
"self_ref": "#/texts/7",
"parent": {
- "$ref": "#/texts/6"
+ "$ref": "#/body"
},
"children": [],
"content_layer": "body",
@@ -220,11 +234,7 @@
"parent": {
"$ref": "#/body"
},
- "children": [
- {
- "$ref": "#/texts/9"
- }
- ],
+ "children": [],
"content_layer": "body",
"label": "title",
"prov": [],
@@ -234,7 +244,7 @@
{
"self_ref": "#/texts/9",
"parent": {
- "$ref": "#/texts/8"
+ "$ref": "#/body"
},
"children": [],
"content_layer": "body",
@@ -252,14 +262,7 @@
"parent": {
"$ref": "#/body"
},
- "children": [
- {
- "$ref": "#/tables/0"
- },
- {
- "$ref": "#/groups/2"
- }
- ],
+ "children": [],
"content_layer": "body",
"label": "title",
"prov": [],
@@ -311,14 +314,7 @@
"parent": {
"$ref": "#/body"
},
- "children": [
- {
- "$ref": "#/texts/15"
- },
- {
- "$ref": "#/texts/16"
- }
- ],
+ "children": [],
"content_layer": "body",
"label": "title",
"prov": [],
@@ -328,7 +324,7 @@
{
"self_ref": "#/texts/15",
"parent": {
- "$ref": "#/texts/14"
+ "$ref": "#/body"
},
"children": [],
"content_layer": "body",
@@ -340,13 +336,9 @@
{
"self_ref": "#/texts/16",
"parent": {
- "$ref": "#/texts/14"
+ "$ref": "#/body"
},
- "children": [
- {
- "$ref": "#/texts/17"
- }
- ],
+ "children": [],
"content_layer": "body",
"label": "section_header",
"prov": [],
@@ -357,7 +349,7 @@
{
"self_ref": "#/texts/17",
"parent": {
- "$ref": "#/texts/16"
+ "$ref": "#/body"
},
"children": [],
"content_layer": "body",
@@ -373,7 +365,7 @@
{
"self_ref": "#/tables/0",
"parent": {
- "$ref": "#/texts/10"
+ "$ref": "#/body"
},
"children": [],
"content_layer": "body",
diff --git a/tests/data/md/groundtruth/mixed_without_h1.md.yaml b/tests/data/md/groundtruth/mixed_without_h1.md.yaml
index b3037f9d61..79f4c612a8 100644
--- a/tests/data/md/groundtruth/mixed_without_h1.md.yaml
+++ b/tests/data/md/groundtruth/mixed_without_h1.md.yaml
@@ -1,7 +1,9 @@
body:
children:
- $ref: '#/texts/0'
+ - $ref: '#/texts/1'
- $ref: '#/groups/0'
+ - $ref: '#/texts/9'
content_layer: body
label: unspecified
name: _root_
@@ -14,14 +16,6 @@ furniture:
name: _root_
self_ref: '#/furniture'
groups:
-- children:
- - $ref: '#/texts/1'
- content_layer: body
- label: section
- name: header-1
- parent:
- $ref: '#/body'
- self_ref: '#/groups/0'
- children:
- $ref: '#/texts/2'
- $ref: '#/texts/4'
@@ -30,8 +24,8 @@ groups:
label: list
name: list
parent:
- $ref: '#/texts/1'
- self_ref: '#/groups/1'
+ $ref: '#/body'
+ self_ref: '#/groups/0'
- children:
- $ref: '#/texts/3'
content_layer: body
@@ -39,13 +33,23 @@ groups:
name: list
parent:
$ref: '#/texts/2'
+ self_ref: '#/groups/1'
+- children:
+ - $ref: '#/texts/6'
+ - $ref: '#/texts/7'
+ - $ref: '#/texts/8'
+ content_layer: body
+ label: inline
+ name: group
+ parent:
+ $ref: '#/texts/5'
self_ref: '#/groups/2'
key_value_items: []
name: mixed_without_h1
origin:
- binary_hash: 11100987453172996924
+ binary_hash: 407652015547384603
filename: mixed_without_h1.md
- mimetype: text/html
+ mimetype: text/markdown
pages: {}
pictures: []
schema_name: DoclingDocument
@@ -60,27 +64,25 @@ texts:
prov: []
self_ref: '#/texts/0'
text: Content before first heading
-- children:
- - $ref: '#/groups/1'
- - $ref: '#/texts/6'
+- children: []
content_layer: body
label: section_header
level: 1
orig: Some heading
parent:
- $ref: '#/groups/0'
+ $ref: '#/body'
prov: []
self_ref: '#/texts/1'
text: Some heading
- children:
- - $ref: '#/groups/2'
+ - $ref: '#/groups/1'
content_layer: body
enumerated: false
label: list_item
marker: ''
orig: A. first
parent:
- $ref: '#/groups/1'
+ $ref: '#/groups/0'
prov: []
self_ref: '#/texts/2'
text: A. first
@@ -91,7 +93,7 @@ texts:
marker: ''
orig: subitem
parent:
- $ref: '#/groups/2'
+ $ref: '#/groups/1'
prov: []
self_ref: '#/texts/3'
text: subitem
@@ -102,28 +104,56 @@ texts:
marker: ''
orig: B. second
parent:
- $ref: '#/groups/1'
+ $ref: '#/groups/0'
prov: []
self_ref: '#/texts/4'
text: B. second
-- children: []
+- children:
+ - $ref: '#/groups/2'
content_layer: body
enumerated: false
label: list_item
marker: ''
- orig: 2 . strange
+ orig: ''
parent:
- $ref: '#/groups/1'
+ $ref: '#/groups/0'
prov: []
self_ref: '#/texts/5'
- text: 2 . strange
+ text: ''
- children: []
content_layer: body
label: text
- orig: The end!
+ orig: '2'
parent:
- $ref: '#/texts/1'
+ $ref: '#/groups/2'
prov: []
self_ref: '#/texts/6'
+ text: '2'
+- children: []
+ content_layer: body
+ label: text
+ orig: .
+ parent:
+ $ref: '#/groups/2'
+ prov: []
+ self_ref: '#/texts/7'
+ text: .
+- children: []
+ content_layer: body
+ label: text
+ orig: strange
+ parent:
+ $ref: '#/groups/2'
+ prov: []
+ self_ref: '#/texts/8'
+ text: strange
+- children: []
+ content_layer: body
+ label: text
+ orig: The end!
+ parent:
+ $ref: '#/body'
+ prov: []
+ self_ref: '#/texts/9'
text: The end!
version: 1.10.0
diff --git a/tests/data/md/groundtruth/signature_stamp_01.md.json b/tests/data/md/groundtruth/signature_stamp_01.md.json
index 513db072c3..24a6aea4e3 100644
--- a/tests/data/md/groundtruth/signature_stamp_01.md.json
+++ b/tests/data/md/groundtruth/signature_stamp_01.md.json
@@ -3,8 +3,8 @@
"version": "1.10.0",
"name": "signature_stamp_01",
"origin": {
- "mimetype": "text/html",
- "binary_hash": 3703610756227907514,
+ "mimetype": "text/markdown",
+ "binary_hash": 5357081687404952837,
"filename": "signature_stamp_01.md"
},
"furniture": {
diff --git a/tests/test_backend_markdown.py b/tests/test_backend_markdown.py
index f766e58833..37835335ab 100644
--- a/tests/test_backend_markdown.py
+++ b/tests/test_backend_markdown.py
@@ -1052,10 +1052,7 @@ def test_ordered_list_split_by_prose_preserves_numbers():
def test_ordered_list_numbers_survive_html_block():
- """A Markdown file with an HTML block is converted again through HTML.
-
- The list numbers must survive that round trip, like in a file without HTML.
- """
+ """The list numbers are the same as in a file without HTML blocks."""
markdown = "5. foo\n6. bar\n\nRestart the shell.\n\n7. baz\n\nnote
\n"
conv_result = get_converter().convert_string(markdown, format=InputFormat.MD)
assert conv_result.status == ConversionStatus.SUCCESS
diff --git a/tests/test_backend_markdown_html_blocks.py b/tests/test_backend_markdown_html_blocks.py
new file mode 100644
index 0000000000..556a3acd21
--- /dev/null
+++ b/tests/test_backend_markdown_html_blocks.py
@@ -0,0 +1,158 @@
+# SPDX-FileCopyrightText: The Docling Contributors
+# SPDX-License-Identifier: MIT
+
+import pytest
+from docling_core.types.doc import CodeItem, DoclingDocument, GroupItem, RichTableCell
+
+from docling.datamodel.backend_options import MarkdownBackendOptions
+from tests.test_backend_markdown import _convert_markdown, _png_data_uri
+
+pytestmark = pytest.mark.cross_platform
+
+
+_HTML_BLOCK_BODY = """\
+```cpp
+#include
+Dict d;
+printf("");
+```
+
+```bash
+conda create -n
+conda activate
+```
+
+Use `` here.
+
+| a | b |
+|---|---|
+| x | 2 |
+
+See [one](https://a.example) and **bold**
+See [two](https://b.example) and *italic*
+
+5. first
+6. second
+"""
+
+
+def _items(doc: DoclingDocument) -> list[tuple[str, str]]:
+ return [(str(t.label), t.text) for t in doc.texts]
+
+
+@pytest.mark.parametrize(
+ ("before", "after"),
+ [
+ ("", ""),
+ ("\n\n", ""),
+ ("", "\n\n"),
+ ("\n\n", "\n\n\n
\n"),
+ ],
+ ids=["no_html_block", "leading", "trailing", "both"],
+)
+def test_html_comment_does_not_change_surrounding_content(before: str, after: str):
+ """An HTML block leaves the Markdown content around it unchanged."""
+ options = MarkdownBackendOptions()
+ expected = _convert_markdown(_HTML_BLOCK_BODY, options)
+ doc = _convert_markdown(f"{before}{_HTML_BLOCK_BODY}{after}", options)
+
+ codes = [t.text for t in doc.texts if isinstance(t, CodeItem)]
+ assert codes == [
+ '#include \nDict d;\nprintf("");',
+ "conda create -n
\nconda activate ",
+ "",
+ ]
+ assert [c.text for c in doc.tables[0].data.table_cells] == ["a", "b", "x", "2"]
+ assert [t.marker for t in doc.texts if t.label == "list_item"] == ["5.", "6."]
+ assert _items(doc) == _items(expected)
+ assert doc.export_to_markdown() == expected.export_to_markdown()
+
+
+def test_adjacent_paragraphs_stay_separate_around_html_block():
+ markdown = (
+ "\n\n"
+ "See [one](https://a.example) and **bold**\n\n"
+ "See [two](https://b.example)\n"
+ )
+ doc = _convert_markdown(markdown, MarkdownBackendOptions())
+
+ assert [
+ [child.resolve(doc).text for child in group.children] for group in doc.groups
+ ] == [["See", "one", "and", "bold"], ["See", "two"]]
+
+
+def test_markdown_picture_survives_html_block():
+ markdown = f"\n\n})\n"
+ doc = _convert_markdown(markdown, MarkdownBackendOptions(fetch_images=True))
+
+ assert len(doc.pictures) == 1
+ image = doc.pictures[0].get_image(doc)
+ assert image is not None
+ assert image.size == (7, 5)
+
+
+def test_html_block_content_is_converted():
+ markdown = (
+ "# Title\n\n"
+ "Intro ``\n\n"
+ "Section
\n\n"
+ "\n\n"
+ "More
detail \n\n"
+ f'})
\n\n'
+ "open
tag\n\n"
+ "after\n"
+ )
+ doc = _convert_markdown(markdown, MarkdownBackendOptions(fetch_images=True))
+
+ assert [(str(t.label), t.text) for t in doc.texts] == [
+ ("title", "Title"),
+ ("text", "Intro"),
+ ("code", ""),
+ ("section_header", "Section"),
+ ("text", "More"),
+ ("text", "detail"),
+ ("caption", "logo"),
+ ("text", "open"),
+ ("text", "tag"),
+ ("text", "after"),
+ ]
+ assert [[c.text for c in row] for row in doc.tables[0].data.grid] == [
+ ["H1", "H2"],
+ ["1", "2"],
+ ]
+ assert len(doc.pictures) == 1
+ assert doc.pictures[0].image is not None
+
+
+def test_html_block_inside_list_item():
+ markdown = "- a\n block
\n- b\n\n \n- ``\n"
+ doc = _convert_markdown(markdown, MarkdownBackendOptions())
+
+ assert _items(doc) == [
+ ("list_item", "a"),
+ ("text", "block"),
+ ("list_item", "b"),
+ ("list_item", ""),
+ ]
+ assert len(doc.groups) == 1
+ assert doc.groups[0].label == "list"
+
+
+def test_html_block_references_stay_valid():
+ """Items an HTML block refers to by reference still resolve after conversion."""
+ markdown = (
+ "first\n\nsecond\n\n"
+ '
Cap\n\n'
+ "\n"
+ )
+ doc = _convert_markdown(markdown, MarkdownBackendOptions())
+
+ assert doc.validate_tree(doc.body)
+ assert [c.resolve(doc).text for c in doc.pictures[0].captions] == ["Cap"]
+ table = doc.tables[0]
+ rich_cells = [c for c in table.data.table_cells if isinstance(c, RichTableCell)]
+ assert len(rich_cells) == 1
+ cell_group = rich_cells[0].ref.resolve(doc)
+ assert isinstance(cell_group, GroupItem)
+ assert cell_group.parent == table.get_ref()
+ assert [c.resolve(doc).text for c in cell_group.children] == ["x", "y"]