List Marker Normalization#
List marker normalization is the process of detecting hierarchical or compound list-item markers — such as 9a., 3.a., 1.2.1, or 5.1 — and moving them out of the item's text field into the dedicated marker field of a ListItem, while setting enumerated = True. When this step is missed, the marker stays fused in the item text, producing doubled output in Markdown and other exports (e.g., 9a. 9a. Algorithm step).
This concern spans two independent pipelines:
| Pipeline | Implementation layer |
|---|---|
| PDF / image | ListItemMarkerProcessor in docling-ibm-models; optional post-processing in docling |
| DOCX | Native marker construction in msword_backend.py |
The ListItem data model stores normalized markers in the marker field alongside the text and enumerated flag .
PDF Pipeline: ListItemMarkerProcessor#
The primary rule-based classifier for PDF list markers lives in docling_ibm_models/list_item_normalizer/list_marker_processor.py. The ListItemMarkerProcessor class recognizes two families of patterns compiled at init time :
- Bullet patterns — Unicode and ASCII bullet symbols
- Numbered patterns — simple single-level formats:
\d+\.,\d+\),(1),[1], Roman numerals, single lettersa./A.
Gap: none of these patterns cover compound markers like 9a., 3.a., or 1.2.1. Items with such markers pass through undetected, leaving the marker text fused in ListItem.text. A development branch (feat/compound-list-item-markers) in a fork of docling-ibm-models adds the missing patterns, and PR #3849 temporarily pins Docling's dependency to that branch while the fix is upstreamed.
The processor's main entry point is process_document() , which calls update_list_items_in_place() and optionally merge_markers_and_text_items_into_list_items() to combine standalone marker-only TextItems with their following content items.
PDF Pipeline: Compound Marker Recovery#
PR #3815 proposed an optional ListNormalizationModel post-processing stage to recover compound markers that slip past ListItemMarkerProcessor. It was closed in favor of fixing the patterns upstream (PR #3849), but its design documents the normalization logic:
- Pattern:
_COMPOUND_ORDEREDregex captures dotted-decimal (1.1,1.1.1) and digit+letter (9a,9a.,3.a) prefixes at the start ofListItem.text. - Conservative detection: only processes items whose
markerfield is empty (already-handled items are untouched). Triggers only in a numbered context — i.e., when the enclosing list group already has a recognized numbered marker or at least two sibling items match compound patterns . - Normalization: sets
it.markerto the extracted prefix, strips the marker fromit.text, and setsit.enumerated = True. - Opt-in: would have been enabled via
PdfPipelineOptions().list_normalization_options.enabled = True.
The preferred resolution (PR #3849) adds compound patterns directly to ListItemMarkerProcessor in docling-ibm-models, avoiding post-processing overhead. Ground-truth impact from that change: markers like 3.a., 9a.–9d. and dotted-decimal section identifiers (5.1, 5.2) move from item text into the marker field across affected documents .
DOCX Pipeline: Multi-Level Marker Construction#
The DOCX backend (docling/backend/msword_backend.py) builds hierarchical markers natively from Word's numbering.xml, without a separate normalization stage. PR #2815 introduced the full multi-level support.
Numbering resolution (_get_numId_and_ilvl()): reads <w:numPr> directly from the paragraph XML; if absent, walks the paragraph's style inheritance chain to find an inherited numPr. Defaults ilvl to 0 when numId is found but ilvl is unspecified .
Abstract numbering lookup (_get_level_element()): given (numId, ilvl), walks numbering.xml through the <w:num> → <w:abstractNumId> → <w:abstractNum> → <w:lvl> chain to retrieve the level element .
Counter tracking (_get_list_counter()): maintains a dict[tuple[int,int], int] keyed by (numId, ilvl). When a parent level counter advances, all sub-level counters for that numId are reset .
Marker rendering (_build_enum_marker()): reads the w:lvlText template (e.g., "%1.%2") and substitutes %N placeholders with actual counter values. Falls back to a dotted-decimal 1.2.3. pattern for plain numeric formats . Templates with non-trivial prefix/suffix text (e.g., "Proposal %1:") are rendered verbatim .
Key Source References#
| File / PR | Role |
|---|---|
docling_ibm_models/list_item_normalizer/list_marker_processor.py | Primary PDF list marker classifier; ListItemMarkerProcessor |
docling/backend/msword_backend.py — lines 905–1060 | DOCX numbering resolution and hierarchical marker rendering |
| PR #3849 | Temporary dependency pin adding compound-marker patterns to docling-ibm-models |
| PR #3815 | Closed proposal for PDF post-processing recovery stage; documents the _COMPOUND_ORDERED regex and conservative detection logic |
| PR #2815 | Merged fix enabling inherited numPr and lvlText-based multi-level markers in DOCX |