Skip to content

HTML backend: text after a nested list inside <li> is merged into the item and moved before the nested items #4422

Open

Description

@morten-lagabote

Blocked by #4526 (fix in docling-project/docling-core#830).

Bug

In the HTML backend, a paragraph that follows a nested list inside the same <li> is merged into that list item's text. The nested list items are emitted after it. Reading order therefore changes: text that the source places after the nested enumeration appears before it, glued to the list item's opening text.

Input:

<ol>
  <li>
    <p>Intro:</p>
    <ol>
      <li><p>First nested.</p></li>
      <li><p>Second nested.</p></li>
    </ol>
    <p>After the nested list.</p>
  </li>
</ol>
<p>Next paragraph.</p>

Actual (label:text in iterate_items() order):

list_item: Intro: After the nested list.
list_item: First nested.
list_item: Second nested.
text: Next paragraph.

Expected: After the nested list. comes after the nested items and stays within the outer list item, for example:

list_item: Intro:
list_item: First nested.
list_item: Second nested.
text: After the nested list.   (child of the outer list item)
text: Next paragraph.

The same happens with text after a nested <table>, and inside <dd>:

<ul><li>Intro<table><tr><td>A</td></tr></table>After the table.</li></ul>
<dl><dt>Term</dt><dd>Intro<ul><li>x</li></ul>After the list.</dd></dl>
list_item: Intro After the table.
table
list_item: Term
list_item: Intro After the list.
list_item: x

The list item text is extracted from all the text in the item, skipping nested lists and tables, which are then emitted after it (_add_list_item_with_content and _process_list_item_nested_content).

This pattern is common in documentation generated from reStructuredText or Markdown. For example, https://docs.python.org/3/whatsnew/3.13.html has 25 such list items. In the one for PythonFinalizationError, the attribution after the nested list is moved into the introduction (Markdown export, links removed):

- Add PythonFinalizationError , a new exception derived from RuntimeError and used to signal when operations are blocked during finalization . The following callables now raise PythonFinalizationError , instead of RuntimeError : (Contributed by Victor Stinner in gh-114570 .)
    - _thread.start_new_thread()
    - os.fork()
    - os.forkpty()
    - subprocess.Popen

Steps to reproduce

from io import BytesIO

from docling.datamodel.base_models import DocumentStream, InputFormat
from docling.document_converter import DocumentConverter

html = """<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
<ol><li><p>Intro:</p>
<ol><li><p>First nested.</p></li><li><p>Second nested.</p></li></ol>
<p>After the nested list.</p></li></ol>
<p>Next paragraph.</p>
</body></html>"""

converter = DocumentConverter(allowed_formats=[InputFormat.HTML])
document = converter.convert(DocumentStream(name="nested.html", stream=BytesIO(html.encode()))).document
for item, _ in document.iterate_items():
    print(f"{item.label.value}: {getattr(item, 'text', '')}")

Docling version

2.130.0 (also reproduced with 2.124.0); docling-core 2.99.0; beautifulsoup4 4.15.0

Python version

Python 3.13.9

Activity

  1. morten-lagabote commented on Sep 29, 2026

    @morten-lagabote
    ContributorAuthor

    @ceberam I have a fix for this ready. It builds on #4440, which changes the same loop in _handle_list, so I'll open it once #4440 is reviewed, to keep one PR at a time on this code.

  2. ceberam commented on Sep 29, 2026

    @ceberam
    Member

    Thanks @morten-lagabote . I think it makes sense to tackle both issues together. I will provide my feedback shortly.

  3. added
    markdownissue related to markdown backend
    htmlissue related to html backend
    and removed
    markdownissue related to markdown backend
    on Sep 29, 2026
  4. morten-lagabote commented on Sep 30, 2026

    @morten-lagabote
    ContributorAuthor

    @ceberam I have the fix for this ready on top of #4440, but I'm holding it back. The content after a nested block becomes an InlineGroup child of the ListItem, after the nested ListGroup, and MarkdownListSerializer appends any InlineGroup part to the previous line (my_parts[-1].text = f"{my_parts[-1].text}{p.text}"). So <p>(Contributed by … <a>gh-114570</a>.)</p> after a sub-list renders as - subprocess.Popen(Contributed by …. On https://docs.python.org/3/whatsnew/3.13.html this hits all 25 list items with content after a nested list. The tree is right, the Markdown is worse than today.

    I think the serializer should only append an InlineGroup to the previous part when it holds the text of the same ListItem, i.e. when it is its first child. Would you prefer that as a docling-core issue and PR first, with this one following the release, or is it covered by docling-core#693 / #3891?

  5. ceberam commented on Oct 1, 2026

    @ceberam
    Member

    Thanks @morten-lagabote for already drafting a PR for this issue. What you spotted seems indeed a weakness of the Markdown serializer. It does not make sense to attach a nested block as an InlineGroup. The effort in docling-project/docling-core#693 and #3891 is independent from this bug.

    I would then suggest the following:

    Once the fix in docling-core is merged and we make a new release, we can unblock this issue.

  6. morten-lagabote commented on Oct 9, 2026

    @morten-lagabote
    ContributorAuthor

    Three public documents where this changes the reading order of legal text (regjeringen.no, converted on main at 8a6155b):

    Document On main
    NOU 2016: 27, ch. 19 (Regulation (EU) 2015/847, Art. 2(3)) However, this Regulation shall apply … is in the text of item 3, before its points (a) and (b). 3 list items on the page.
    Prop. 71 L (2021–2022), ch. 14 q) «kompetent institusjon» eller eller eller, then the points i) to iv). In the source, each eller stands between two points. 15 list items on the page.
    Prop. 1 S (2015–2016), ch. 5 The paragraph Merinntekt som gir grunnlag … comes before the table that it follows in the source.

    The fix is ready and rebased on main. It now also covers the third shape, where the text of the item only follows the nested block. With it, the three pages keep the source order, and no text is added or lost.

    @ceberam, a question on the order of the PRs. docling-project/docling-core#830 has no review yet, and docling-project/docling-core#844 changes the same serializer. In these three documents, no text after a nested block has formatting or a link, so their Markdown is correct without the docling-core fix. Only such a text with formatting or a link is appended to the previous line (#4526). Can I open the PR for this issue now, with that limit stated, or do you prefer to wait for docling-core?

    Note: I cannot set the formal blocked by relation on this issue (it needs triage rights), so the first line of the description states it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

htmlissue related to html backend

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions