docling-project / docling-project/docling

MSWord: outlineLvl fallback classifies numbered clause bodies as SectionHeaderItem (regression from #3961)

Open
#4,106 1 comment 0 reactions 0 assignees View on GitHub
bug docx
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.