Skip to content

fix(docx): add use_outline_level_for_headings option to backend - #4480

Open
ceberam wants to merge 1 commit into
mainfrom
fix/docx-outline
Open

ceberam wants to merge 1 commit into
mainfrom
fix/docx-outline

Conversation

@ceberam

@ceberam ceberam commented Oct 1, 2026

Copy link
Copy Markdown
Member

Problem

Since #3961, the DOCX backend uses w:outlineLvl as a fallback heading signal for styles that cannot be identified by name — the main motivation being localized heading styles produced by LibreOffice (e.g. Czech Nadpis1). This fallback is correct for that case, but w:outlineLvl is not reserved exclusively for structural headings in OOXML: legal and regulatory templates routinely assign an outline level to paragraph styles that are used for numbered clause bodies, so that those paragraphs participate in the document outline and TOC without being structural headings. When such a document is converted, every paragraph of those styles is promoted to a SectionHeaderItem, which causes consumers that treat section headers as breadcrumbs rather than content to silently drop large portions of the document body.

Solution

A new boolean option use_outline_level_for_headings (default True) is added to MsWordBackendOptions. When set to False, the w:outlineLvl-only fallback path in _get_label_and_level is skipped entirely. Name-based heading detection — which covers the built-in English styles and any style whose name or id contains the substring "heading" — is unaffected regardless of the option value.

The default is kept at True to preserve the existing behaviour for localized documents that depend on the fallback. Users who know their document uses w:outlineLvl on non-heading styles can opt out explicitly.

The known limitation (a style used for both outline participation and body prose cannot be distinguished from a true heading style using OOXML signals alone) is documented in the Field description and in the _get_label_and_level docstring.

Changes

  • docling/datamodel/backend_options.py — adds use_outline_level_for_headings: bool = True to MsWordBackendOptions, using the Annotated + Field pattern with a description that explains the purpose and known limitation.
  • docling/backend/msword_backend.py — gates the w:outlineLvl-only fallback on the new option; documents the known limitation in the _get_label_and_level docstring.
  • tests/test_backend_msword_outline.py — adds a mixed_heading_docx pytest fixture that builds a single document containing a named heading (Heading 1), two outline-level-only paragraphs (Level3 style), and a plain body paragraph; two tests exercise use_outline_level_for_headings=True and =False against that same fixture.

Testing

uv run pytest tests/test_backend_msword_outline.py
# 6 passed

Closes #4106.

Checklist:

  • Documentation has been updated, if necessary.
  • Examples have been added, if necessary.
  • Tests have been added, if necessary.

Signed-off-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>
@ceberam ceberam added bug Something isn't working docx issue related to docx backend labels Oct 1, 2026
@mergify

mergify Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @ceberam, all your commits are properly signed off. 🎉

@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ceberam
ceberam requested a review from DanielNg0729 October 1, 2026 16:39

@DanielNg0729 DanielNg0729 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Dr @ceberam, thank you very much for this fix. I agree that the opt-out is a cleaner approach than the heuristic I proposed in #4124.

I tried the change locally with @kubasamanek's fixture from #4106. With the default setting, the output is the same as on main. With use_outline_level_for_headings=False, the clause bodies come out as TextItem, as intended. The new tests also pass for me, and the disabled-case test fails on main, so it protects the fix well.

While reading the change, I noticed a few small things. Please feel free to ignore them if I have misunderstood something:

  1. I may be wrong, but I think isinstance(self.options, MsWordBackendOptions) and self.options.use_outline_level_for_headings becomes False when the backend gets a different options object. I tried it with DeclarativeBackendOptions(), and the fallback was turned off even though the default is True. Would it make sense to write it as not isinstance(self.options, MsWordBackendOptions) or self.options.use_outline_level_for_headings, so the default is kept in that case?
  2. When the option is False, styles detected by name still take their level from w:outlineLvl. Would a short note in the field description help, so the option name doesn't surprise users?
  3. One small idea, only if you think it's useful: a test with the localized Nadpis 1 style from the reporter's fixture would show that those headings also become text when the option is off.

These are only small suggestions, and the PR looks good to me. Thank you very much again for your guidance on this issue!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working docx issue related to docx backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants