Skip to content

fix(docx): give a hanging list's top level the page's marker column, text markers too - #836

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-horizontal-offsets-2
Oct 4, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-horizontal-offsets-2

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Why

The DOCX export placed list items in two ways.

Either way, an item's wrapped lines came back under its marker, while the page hangs them at the text. Panel's skills stood 7.8pt left of the page's on their second lines in both editors.

That had been a recorded decision ("hangingIndent changes nothing in DOCX", pinned by DocxHangingIndentIsIgnoredTest). It rested on the export not knowing the marker's width as Word sets it. Two things have changed:

  • Measured column. The layout now reaches the export and sets the marker in a column of its own.
  • Same widths. Where the export embeds the page's face, or names a standard face Word sets in the same widths, Word's marker is the page's grown to its half-point size.

What changed

For the top level of a hangingIndent(true) list that does not nest, DocxSemanticBackend takes the layout's marker column: the marker's width and markerGap, from DocxLayoutMetrics.markerToText(list, letters). A text marker's fragment is found by its letters.

  • Word lists. numberingFor sets level 0's indent and hanging to that column, and names the list's face and size in the level's w:rPr. Word draws a level's marker in the paragraph mark's style, and an item at an exact line leaves its mark the document's. Without that, a list set smaller than the body would get a bullet wider than the one its column was measured against. indentListItemInside uses the same column when a container's inset puts w:ind on the paragraph.
  • Lists of paragraphs. writeRichListLine writes the marker, a tab, and hangFrom(): left = start + column, hanging = column, with a left stop there. This covers rich items, drawn markers, and now plain items too, routed via setInAColumn. Picture markers from fix(docx): start a picture-marked list item's text where the page does, a tab past the marker and its gap #835 now hang as well.
  • When the column applies. Only where the gap covers what Word may set the marker wider:
  • When the stated column stays:
    • a list that nests items (nests) keeps the stated columns at every level. Only level 0 is measured, and a nested level kept stated could stand left of its parent's text past a 10pt gap;
    • nested levels;
    • a marker the gap does not clear;
    • a list without the flag.
  • Tests and docs. The test that pinned the old decision is renamed DocxListHangingIndentTest and holds the new rule. ListBuilder.hangingIndent / markerGap Javadoc (core; no signature change), both recipes, the capability matrix and CHANGELOG are updated.

Verification

  • Word corpus. scripts/docx-visual/word-fidelity.ps1 (Word 16.0.20430) → BUILD SUCCESS, baseline rewritten with -Update.
    • Only Panel (4 lines) and SerifHeadline (2) moved, all across: from −7.76 / +2.25pt to within 0.04pt.
    • Nothing moved down, nothing was lost.
    • Lines past 2pt across: 136 → 130.
  • LibreOffice (Windows). The same 6 lines moved, to within 0.15pt; nothing else. Lines past 2pt across: 102 → 96. Baseline rewritten.
  • LibreOffice (Linux). The baseline is this PR's CI artifact (docx-fidelity-37164256524): Panel 4 and SerifHeadline 2 lines moved; lines past 2pt across: 388 → 382.
  • Level style. After naming the list's style on the level, both corpora measured line for line as their baselines: no list in the corpus is set apart from its document's size and face.
  • The nesting rule's cost. Panel's nesting projects list keeps its lines where they are on 2.5-dev: 7 at −7.75pt and 3 at −3.17pt across in Word.
  • Tests:
    • DocxListMarkerGapTest: 13 tests, 7 of them new —
      • a Word list takes the layout's column;
      • an 8pt list in an 11pt document names its own size on the level;
      • a nesting list keeps its space at the top level too, for a drawn marker and for rich items;
      • a Word list in a padded column keeps its inset plus the column;
      • too little gap keeps the stated column (Word list) or the space (paragraphs);
      • a rich item hangs at the column.
    • DocxListHangingIndentTest (was …IsIgnoredTest):
      • the gap sets the level's column where it covers the marker, and never the text;
      • the flag changes only the level's column, nothing on the paragraph;
      • a nesting list keeps the stated columns at every level.
    • DocxSemanticBackendTest: the rich-list tests expect the tab and the same runs otherwise.
  • Sabotage. Each rule fails its test:
  • render-docx. 878 tests green.
  • Full reactor gate. ./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose-qa,:graph-compose-coverage -am → BUILD SUCCESS (qa 1820 tests).
  • Examples. CommittedAssetDriftTest in examples → 3/3 green.

Notes

  • Word measures the marker. It sets it in its own widths. The column relies on those being the page's, grown to the half point, which holds for an embedded face and for the standard faces Word substitutes width-for-width. A face whose licence forbids embedding is substituted by the editor, and its marker may set wider or narrower than the half-point clearance allows.
  • Siblings with their own markers. A top-level item carrying a marker of its own, in a list whose others carry the list's, keeps the space while its siblings tab. The page does not line them up either.

Lane: shared-engine (render-docx), with a Javadoc correction in core.

… and size

Word draws a level's marker in the paragraph mark's style, and an item at
an exact line leaves its mark the document's: a list set smaller than the
body would get a bullet wider than the column measured for it. The level
now names the list's face and size.

A nesting list keeps its space at the top level on the paragraph path too,
for a drawn marker and for rich items; tests pin both. The Linux
LibreOffice baseline is this branch's CI measure, and the docs state the
embedding condition the column relies on.
@DemchaAV
DemchaAV merged commit b92754b into 2.5-dev Oct 4, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-horizontal-offsets-2 branch October 4, 2026 06:32
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