Repository navigation
HTML backend: text after a nested list inside <li> is merged into the item and moved before the nested items #4422
Description
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
morten-lagabote commentedon Sep 29, 2026
@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.ceberam commentedon Sep 29, 2026
Thanks @morten-lagabote . I think it makes sense to tackle both issues together. I will provide my feedback shortly.
morten-lagabote commentedon Sep 30, 2026
@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
InlineGroupchild of theListItem, after the nestedListGroup, andMarkdownListSerializerappends anyInlineGrouppart 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
InlineGroupto the previous part when it holds the text of the sameListItem, 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?ceberam commentedon Oct 1, 2026
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.
morten-lagabote commentedon Oct 9, 2026
Three public documents where this changes the reading order of legal text (regjeringen.no, converted on
mainat 8a6155b):mainHowever, this Regulation shall apply …is in the text of item 3, before its points (a) and (b). 3 list items on the page.q) «kompetent institusjon» eller eller eller, then the points i) to iv). In the source, eachellerstands between two points. 15 list items on the page.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.