Skip to content

feat(docx): give a paragraph the heading role the document declared - #708

Merged
DemchaAV merged 2 commits into
2.5-devfrom
feature/docx-headings
Sep 22, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
feature/docx-headings

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Why

A twenty-page export opened as one flat run of paragraphs. Nothing in the Navigation Pane,
nothing in the outline view, and a table of contents generated in Word that came back
empty. Word builds all three from heading styles, and the export wrote none — so a
document handed over to be worked on had no structure to move around in, only text.

The document already says where its headings are: bookmark(...) carries a
DocumentBookmarkOptions with an outline level, which the PDF backend turns into its
outline tree. On the way to Word that level was read and dropped.

What changed

A stated outline level becomes Word's own HeadingN style. applyHeadingRole sets
w:pStyle from DocumentBookmarkOptions.level(), and writeHeadingStyle defines the style
it names. Word recognises its built-in headings by a pair — the id Heading1 and the name
heading 1 — so both are written; with only one of them the file has a custom style that
happens to be called Heading and the Navigation Pane stays empty.

The style carries the outline level and nothing else. No run properties, no spacing,
basedOn Normal. The paragraph already has the look its author gave it, and a heading style
that also set a font and a size would restyle every heading on the way out — the export
describes the page rather than redesigning it.

Only the levels the document uses are defined. Nine heading styles in a document with
one heading is eight entries in Word's gallery that nothing refers to, so headingLevelsIn
walks the graph first. A level past Word's nine is clamped rather than written as a style
that does not exist, which would leave the paragraph with no role at all.

The role is never inferred from type size. A heading guessed from a large first line
turns a subtitle into a chapter and leaves a small real heading as body text — both wrong in
a document someone then edits. It comes from what the document declared or not at all.

The capability matrix and the DOCX recipe said the outline is not written. Both now say what
it is.

Verification

./mvnw -B -ntp clean verify over the seven-module gate → BUILD SUCCESS, 1784 tests.
./mvnw -B -ntp test -f examples/pom.xml93 tests, BUILD SUCCESS; no committed preview
drifted.

render-docx goes from 193 to 199 tests. DocxHeadingRoleTest covers a level becoming the
matching style, the id/name pair and outline level Word needs, a heading style with no
formatting of its own, only the used levels being defined, a level past nine being clamped,
and a 28pt paragraph beside a 9pt one producing no heading at all.

Checked through Word 16.0 rather than by reading the XML back: the exported document reports
style=Heading 1 outlineLevel=1 for the chapter, Heading 2 / 2 for the section and
Normal / 10 for body text, and Word's own GetCrossReferenceItems(wdRefTypeHeading)
returns the heading list with its nesting — which is the list the Navigation Pane and a
generated table of contents are built from.

Lane: canonical — document.backend.semantic.docx only; no engine, layout or pagination
internals are touched.

…lared

A twenty-page export opened as one flat run of paragraphs: nothing in the
Navigation Pane, nothing in the outline view, and a generated table of
contents that came back empty. Word builds all three from heading styles,
and the export wrote none.

The outline level a document states when it declares a bookmark now becomes
Word's own HeadingN style. The style carries the outline level and nothing
else — the paragraph already has the look its author gave it, and a heading
style that also set a font and a size would restyle every heading on the way
out. Word knows its built-in headings by the pair HeadingN and "heading N",
so both are written; with one of them it is a custom style that happens to be
called Heading and the Navigation Pane stays empty. Only the levels a document
uses are defined, and a level past Word's nine is clamped.

The role is read from what the document declared and never inferred from type
size: a heading guessed from a large first line turns a subtitle into a
chapter and leaves a small real heading as body text.
@DemchaAV
DemchaAV merged commit 55dacc9 into 2.5-dev Sep 22, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the feature/docx-headings branch September 22, 2026 19:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant