docling-project / docling-project/docling
MSWord: outlineLvl fallback classifies numbered clause bodies as SectionHeaderItem (regression from #3961)
- Dominant language
- Python
- Stars
- 66.4k
- Forks
- 4.8k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 84
Description
### Bug
Since #3961 (which fixed #3959), the `w:outlineLvl` fallback classifies paragraphs of ordinary
prose as `SectionHeaderItem`, when the style carrying the outline level is applied to the text
itself rather than to a short label. Legal templates do this routinely for numbered clauses.
I filed #3959, and the fix is right for its motivating case — localized heading styles genuinely
cannot be found by name. This is only to report that the new signal is broader than that case.
### Reproduction
```python
import io
from docx import Document
from docx.enum.style import WD_STYLE_TYPE
from docx.oxml.ns import qn
from docling.datamodel.base_models import DocumentStream
from docling.document_converter import DocumentConverter
doc = Document()
# A style with no "heading" in its name, carrying an outline level.
style = doc.styles.add_style("Level3", WD_STYLE_TYPE.PARAGRAPH)
ppr = style.element.get_or_add_pPr()
ppr.append(ppr.makeelement(qn("w:outlineLvl"), {qn("w:val"): "2"}))
doc.add_paragraph("1.1 Definitions", style="Level3")
doc.add_paragraph(
'"Work" means any result of the Supplier\'s activity created in the performance of the '
"Modifications under this Agreement which meets the criteria of a work protected by the "
"Copyright Act, including all documentation and source code relating to it.",
style="Level3",
)
doc.add_paragraph("Plain body paragraph.")
buf = io.BytesIO()
doc.save(buf)
result = DocumentConverter().convert(
DocumentStream(name="c.docx", stream=io.BytesIO(buf.getvalue()))
)
for item, _ in result.document.iterate_items(with_groups=False):
text = (getattr(item, "text", "") or "").strip()
print(f"{type(item).__name__:19} ({len(text):3} chars) {text[:60]}")
```
**2.117.0**
```
TextItem ( 15 chars) 1.1 Definitions
TextItem (246 chars) "Work" means any result of the Supplier's activity created i
TextItem ( 21 chars) Plain body paragraph.
```
**2.123.1**
```
SectionHeaderItem ( 15 chars) 1.1 Definitions
SectionHeaderItem (246 chars) "Work" means any result of the Supplier's activity created i
TextItem ( 21 chars) Plain body paragraph.
```
### Why this matters
A 246-character section header is not a heading. On a real contract of ours the same styles turn
363 of 875 items into headings, the longest 906 characters. `export_to_markdown()` is almost
unchanged, so no text is lost inside docling — but consumers that treat a section header as a
breadcrumb rather than as content drop it, silently.
`w:outlineLvl` 0-8 does genuinely mean "participates in the outline", and Word will list these
paragraphs in its navigation pane, so the current behaviour is defensible. The observation is only
that outline participation and "is a heading" are different predicates, and the new path cannot
tell them apart.
If a discriminator is wanted, the one that held across our corpus was **adjacency**: a style
applied to two consecutive paragraphs is being used as body text, since a heading is rarely
followed immediately by another heading of the same style. Length would be cruder but catches the
same files. Either could apply only to the name-independent path, leaving name-detected headings
untouched.
### Versions
`docling-slim` 2.117.0 (expected) vs 2.123.1 (actual), Python 3.14, macOS.
Contributor guide
Research direction
Start with the DocumentConverter reproduction in the issue and trace the name-independent w:outlineLvl fallback that changed between 2.117.0 and 2.123.1. Compare its SectionHeaderItem classification with the unchanged export_to_markdown() behavior; done means numbered clause prose remains content while genuine headings still classify correctly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100