You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
List Item Serialization Policy: Data Model & Serialization Rules
#4837
There has been several Git issues and PRs related to the serialization of lists from a DoclingDocument recently. I thought it would be useful to share this summary of the serialization policy.
A. The DoclingDocument Tree for List Items
In DoclingDocument, a ListItem owns both its primary text and any continuation blocks (loose list content, nested sub-lists, tables, code blocks):
An InlineGroup child of a ListItem serves two distinct purposes:
Role A (Item Leading Text): When the list item's primary text has formatting or links (for example, <li><b>Bold</b> text</li>), backends set ListItem.text = "" and place the InlineGroup as the first child (item.children[0]).
Role B (Continuation Block): When a subsequent paragraph has formatting/links (or trailing text following a sub-list/table), it is also created as an InlineGroup under ListItem.children.
The Definitive Disambiguation Rule
An InlineGroup represents the item's primary line text if and only if item.text == "" and item.children[0].cref == group.self_ref (it is the first child).
Any other InlineGroup child is a block-level continuation (like a TextItem).
C. HTML vs Markdown Serializers
HTMLTextSerializer handles this correctly:
If has_inline_repr (item.text == "" and len(children) == 1 and isinstance(children[0], InlineGroup)), it renders the InlineGroup as item text.
It then calls doc_serializer.get_parts(item=item, visited=my_visited), serializing all block children inside <li>...</li>. Because my_visited is updated, the outer walk never duplicates them.
MarkdownTextSerializer historically did not recurse into item.children:
ListItem.children were yielded to MarkdownListSerializer, which attempted to append every InlineGroup to the previous text line (my_parts[-1].text = f"{my_parts[-1].text}{p.text}"), corrupting trailing text and dropping loose list indentation.
D. The Markdown Serialization Specification
1. Concepts: Continuation Blocks and Loose Lists
In CommonMark and standard Markdown:
Tight List: A list where each item consists solely of a single inline paragraph without blank lines separating the elements.
Loose List: A list where at least one item contains multiple block-level elements or is separated from its siblings by blank lines.
Continuation Block: Any secondary block-level element (such as an additional paragraph, a fenced code block, a block quote, or a table) that belongs to an existing list item and follows its opening header line.
To maintain list membership in CommonMark, every line of a continuation block must be indented to the item's content column (the indentation level immediately following the list marker and its trailing space), separated from previous blocks by blank lines.
Content Column Calculation
The content column indentation width is defined as:
content_column = len(marker) + 1
For - or * : len("-") + 1 = 2 spaces.
For 1. : len("1.") + 1 = 3 spaces.
For 10. : len("10.") + 1 = 4 spaces.
2. Concrete Example
Input DoclingDocument Hierarchy:
ListGroup
├── ListItem(marker="1.", text="Install the package.")
├── ListItem(marker="2.", text="Set the API key.")
│ ├── TextItem("Use the key from the dashboard.")
│ └── CodeItem("export API_KEY=...")
└── ListItem(marker="3.", text="Run the import.")
Serialized Markdown Output:
1. Install the package.
2. Set the API key.
Use the key from the dashboard.
export API_KEY=...
3. Run the import.
If the continuation blocks (TextItem and CodeItem) were emitted without the 3-space indentation, a Markdown parser would interpret the code block as exiting the list to the document body, breaking the list into two disconnected lists.
3. Serialization Algorithm in MarkdownTextSerializer
When serializing a ListItem in MarkdownTextSerializer.serialize():
Primary Header Line:
If has_inline_repr (or empty item.text with first-child InlineGroup), serialize that InlineGroup as the item text line.
Otherwise, serialize item.text with the marker (- , 1. , etc.).
Call doc_serializer.get_parts(item=item, visited=my_visited). (If the first child was used for has_inline_repr, mark it visited so it is not re-serialized).
Re-indent every continuation block line by content_column spaces.
Separate continuation blocks with blank lines (\n\n) to form valid CommonMark loose lists.
Emit a trailing blank line before the next sibling list item.
MarkdownListSerializer Simplification:
Because MarkdownTextSerializer now consumes and visits all child blocks, MarkdownListSerializer only receives top-level ListItem items. The fragile InlineGroup append heuristic in MarkdownListSerializer can be safely removed or guarded.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
There has been several Git issues and PRs related to the serialization of lists from a
DoclingDocumentrecently. I thought it would be useful to share this summary of the serialization policy.A. The
DoclingDocumentTree for List ItemsIn
DoclingDocument, aListItemowns both its primary text and any continuation blocks (loose list content, nested sub-lists, tables, code blocks):B. The Dual Role of
InlineGroupunder aListItemAn
InlineGroupchild of aListItemserves two distinct purposes:<li><b>Bold</b> text</li>), backends setListItem.text = ""and place theInlineGroupas the first child (item.children[0]).InlineGroupunderListItem.children.The Definitive Disambiguation Rule
An
InlineGrouprepresents the item's primary line text if and only ifitem.text == ""anditem.children[0].cref == group.self_ref(it is the first child).Any other
InlineGroupchild is a block-level continuation (like aTextItem).C. HTML vs Markdown Serializers
HTMLTextSerializerhandles this correctly:has_inline_repr(item.text == ""andlen(children) == 1andisinstance(children[0], InlineGroup)), it renders theInlineGroupas item text.doc_serializer.get_parts(item=item, visited=my_visited), serializing all block children inside<li>...</li>. Becausemy_visitedis updated, the outer walk never duplicates them.MarkdownTextSerializerhistorically did not recurse intoitem.children:ListItem.childrenwere yielded toMarkdownListSerializer, which attempted to append everyInlineGroupto the previous text line (my_parts[-1].text = f"{my_parts[-1].text}{p.text}"), corrupting trailing text and dropping loose list indentation.D. The Markdown Serialization Specification
1. Concepts: Continuation Blocks and Loose Lists
In CommonMark and standard Markdown:
To maintain list membership in CommonMark, every line of a continuation block must be indented to the item's content column (the indentation level immediately following the list marker and its trailing space), separated from previous blocks by blank lines.
Content Column Calculation
The content column indentation width is defined as:
content_column = len(marker) + 1-or*:len("-") + 1 = 2spaces.1.:len("1.") + 1 = 3spaces.10.:len("10.") + 1 = 4spaces.2. Concrete Example
Input
DoclingDocumentHierarchy:Serialized Markdown Output:
export API_KEY=...
If the continuation blocks (
TextItemandCodeItem) were emitted without the 3-space indentation, a Markdown parser would interpret the code block as exiting the list to the document body, breaking the list into two disconnected lists.3. Serialization Algorithm in
MarkdownTextSerializerWhen serializing a
ListIteminMarkdownTextSerializer.serialize():has_inline_repr(or emptyitem.textwith first-childInlineGroup), serialize thatInlineGroupas the item text line.item.textwith the marker (-,1., etc.)._serialize_list_continuations):doc_serializer.get_parts(item=item, visited=my_visited). (If the first child was used forhas_inline_repr, mark it visited so it is not re-serialized).content_columnspaces.\n\n) to form valid CommonMark loose lists.MarkdownListSerializerSimplification:MarkdownTextSerializernow consumes and visits all child blocks,MarkdownListSerializeronly receives top-levelListItemitems. The fragileInlineGroupappend heuristic inMarkdownListSerializercan be safely removed or guarded.All reactions