Skip to content
Merged
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
44 changes: 44 additions & 0 deletions docling/backend/md_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,9 @@ def _iterate_elements( # noqa: C901
if element in visited:
return

# The payloads below this size were pushed by enclosing elements.
creation_stack_size = len(creation_stack)

# A line break only joins runs of the same paragraph. When no text run
# follows the break inside it (for example, the paragraph ends in inline
# HTML or a code span, or the next lines are table rows), the pending
Expand Down Expand Up @@ -819,6 +822,23 @@ def _iterate_elements( # noqa: C901
elif isinstance(element, marko.inline.Image):
_log.debug(" - Image with alt: %s, url: %s", element.title, element.dest)

# An image without alt text emits no RawText. If it is the content of
# a list item or heading, create that item first, with empty text, so
# that the picture goes under the list item, as in the HTML backend.
if creation_stack and not element.children:
parent_item = self._flush_creation_stack(
doc=doc,
creation_stack=creation_stack,
snippet_text="",
parent_item=parent_item,
list_ordered_flag_by_ref=list_ordered_flag_by_ref,
list_start_by_ref=list_start_by_ref,
list_item_counter_by_ref=list_item_counter_by_ref,
list_last_item_by_ref=list_last_item_by_ref,
formatting=formatting,
hyperlink=hyperlink,
)

fig_caption: TextItem | None = None
if element.title is not None and element.title != "":
title = unescape(element.title)
Expand Down Expand Up @@ -1054,6 +1074,30 @@ def _iterate_elements( # noqa: C901
hyperlink=hyperlink,
)

# The content of a list item or heading had no text run, e.g.
# only inline HTML. Create the item with empty text now, or the
# next text run of the document, in a later block, would take it.
if len(creation_stack) > creation_stack_size and isinstance(
element,
marko.block.ListItem
| marko.block.Heading
| marko.block.SetextHeading,
):
pending = creation_stack[creation_stack_size:]
del creation_stack[creation_stack_size:]
self._flush_creation_stack(
doc=doc,
creation_stack=pending,
snippet_text="",
parent_item=parent_item,
list_ordered_flag_by_ref=list_ordered_flag_by_ref,
list_start_by_ref=list_start_by_ref,
list_item_counter_by_ref=list_item_counter_by_ref,
list_last_item_by_ref=list_last_item_by_ref,
formatting=formatting,
hyperlink=hyperlink,
)

def _get_image_loader(self) -> ImageResourceLoader:
"""Lazily build the shared image-resource loader.

Expand Down
60 changes: 59 additions & 1 deletion tests/test_backend_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
from pathlib import Path

import pytest
from docling_core.types.doc import CodeItem, CodeLanguageLabel, PictureItem
from docling_core.types.doc import (
CodeItem,
CodeLanguageLabel,
DocItemLabel,
ListItem,
PictureItem,
)
from PIL import Image

from docling.backend.md_backend import MarkdownDocumentBackend
Expand Down Expand Up @@ -136,6 +142,58 @@ def test_convert_list_item_codespan_only():
assert "- raw\\_ops.Abs" in pred_md


def test_list_item_with_only_an_image_keeps_its_place():
"""An image without alt text emits no text run. Its list item is still
created, with the picture under it, so the next item does not take its place.
"""
doc = (
get_converter()
.convert_string(
"1. ![](a.png)\n2. Second step\n3. Third step\n", format=InputFormat.MD
)
.document
)

items = [item for item in doc.texts if isinstance(item, ListItem)]
assert [(item.marker, item.text) for item in items] == [
("1.", ""),
("2.", "Second step"),
("3.", "Third step"),
]
assert len({item.parent.cref for item in items}) == 1
assert doc.pictures[0].parent == items[0].get_ref()


def test_sub_list_of_an_image_only_item_is_not_duplicated():
doc = (
get_converter()
.convert_string("- ![](a.png)\n - nested\n- next\n", format=InputFormat.MD)
.document
)

items = {item.text: item for item in doc.texts if isinstance(item, ListItem)}
assert [item.text for item in doc.texts] == ["", "nested", "next"]
assert items["nested"].parent.resolve(doc).parent == items[""].get_ref()


@pytest.mark.parametrize(
"heading",
["# ![](logo.png)", '# <img src="logo.png" width="80">', "![](logo.png)\n==="],
ids=["image", "inline-html", "setext-image"],
)
def test_heading_without_text_does_not_take_the_next_paragraph(heading: str):
doc = (
get_converter()
.convert_string(f"{heading}\n\nIntro paragraph.\n", format=InputFormat.MD)
.document
)

assert [(item.label, item.text) for item in doc.texts] == [
(DocItemLabel.TITLE, ""),
(DocItemLabel.TEXT, "Intro paragraph."),
]


def _convert_markdown(
markdown: str, options: MarkdownBackendOptions
) -> DoclingDocument:
Expand Down
Loading