Skip to content

fix(docx): start a picture-marked list item's text where the page does, a tab past the marker and its gap - #835

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

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

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Why

A list item whose marker is a picture — a dot, an icon — was written in DOCX as the picture, a space and the text. The page leaves the list's markerGap after the marker; a space is a little over 2pt. TealPulse's skills, highlights and certifications (a 3.2pt dot, a 9.2pt gap) stood 6.3 to 6.7pt left of the page's in both editors, and OrangeOps' 6pt in Word. These were the corpus's largest horizontal offsets by line count, made visible by the gate's new horizontal check (#834).

markerGap had been left out of DOCX for two reasons:

  • the export had no measurement of the marker;
  • Word sets a marker in its own widths.

Both have changed for a marker that is a picture alone. The export now has the layout, which places the marker and its text as two fragments. And Word draws a picture at the size it is written.

What changed

  • DocxLayoutMetrics.markerToText(list). It gives how far right of its marker a list's first item's text starts: the text fragment's x less the marker fragment's x. That is the marker's width and the gap, as the layout resolved them.
  • DocxSemanticBackend.writeRichListLine. It writes a tab after a marker that is a picture alone, with a left tab stop that distance past the first line's start (tabTo). The start is the paragraph's left indent and first-line indent, the origin Word measures tab stops from in the body and in a cell. This applies only to a top-level item whose marker is the list's:
    • flat lists, through writeListItems;
    • tree lists — rich or nested items — through writeNestedItem at depth 0, when the item's marker equals the list's.
  • When the tab is not used.
    • A tab is written only when the stop stands past the picture, its edges included (drawnWidth, more than 0.1pt clear). Otherwise the tab would run on to Word's next default stop, half an inch on. A dot with markerGap(0) keeps its space.
    • A marker of text keeps its space: Word sets it in its own widths. So does a picture with a blank run beside it: the layout drops the blank, Word sets it, and the stop is not measured past it.
    • A nested item's or item-specific marker keeps its space.
    • Wrapped lines still start at the item's start, as before: the hanging-indent decision for DOCX stands.
  • Docs. ListBuilder.hangingIndent / markerGap Javadoc (core; no signature change), docs/recipes/docx-export.md, the capability-matrix rows for list hanging indent and inline shapes, and CHANGELOG say what DOCX now keeps.

Verification

  • Word corpus. scripts/docx-visual/word-fidelity.ps1 (Word 16.0.20430) → BUILD SUCCESS, baseline rewritten with -Update.
    • Only TealPulse (28 lines) and OrangeOps (11 lines) moved, all across: from −6.72…−5.98pt to within 0.1pt.
    • No line moved down, and none was lost.
    • Lines more than 2pt off across: 175 → 136.
  • LibreOffice (Windows). TealPulse's 28 lines moved from −6.6…−6.26 to within 0.08pt; nothing else moved. Lines past 2pt across: 130 → 102. Baseline rewritten. The Linux baseline is this PR's CI artifact docx-fidelity-37159605087: the same 39 lines (TealPulse 28, OrangeOps 11) nearer across, none worse or lost; Linux lines past 2pt across 427 → 388.
  • Changes after review. The edge clearance and the tree-list path changed no corpus line in either editor.
  • DocxListMarkerGapTest (new, 6 tests):
    • a dot marker's items tab to its width and gap past a padded column's indent;
    • a text marker keeps its space;
    • a dot with no gap to clear keeps its space;
    • a dot with a blank run beside it keeps its space;
    • a top-level rich item in a row cell tabs from the cell's text edge;
    • a nested item with a drawn marker keeps its space.
  • Existing test. DocxSemanticBackendTest.aDrawnListMarkerIsItsPicture… now expects the tab and checks its stop at the dot's width plus the default gap, where it pinned the space.
  • Sabotage. Each rule fails its test:
    • a space for every marker fails the indent test;
    • a stop from the column's edge rather than the indent fails it too;
    • no edge clearance fails the no-gap test;
    • judging "picture alone" by its plain text fails the blank-run test;
    • no tree path fails the rich-item test;
    • applying to nested items fails the nested test.
  • 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

  • The tab is edited text. A reader who types before the item's text types after the tab, and the text stays at the stop. A reader who deletes the tab gets the text against the marker.
  • Docs. docs/recipes/lists.md and the Javadoc of DocxHangingIndentIsIgnoredTest now name the exception.
  • One measure per list. The offset is the list's first item's. Every top-level item carrying the list's marker shares the marker's width and gap (ListMarkerGeometry); ListBuilder.build() gives each the list's marker instance. A hand-built ListNode whose first item draws a marker of its own could lend its offset to the others; the DSL builds no such list.

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

…inux LibreOffice baseline from CI, and name the drawn-marker exception in the list docs
@DemchaAV
DemchaAV merged commit ba0e898 into 2.5-dev Oct 3, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-horizontal-offsets branch October 3, 2026 23:15
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