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\n
note
\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![alt]({_png_data_uri(7, 5)})\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" + "
H1H2
12
\n\n" + "
Moredetail
\n\n" + f'

logo

\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' + "

x

y

z
\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"]