Skip to content

fix(docx): stand a nested list item with no marker where the page sets its text - #840

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

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

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Why

ParagraphPrimitive.writeBulletedPair sets a project's name and its description as one hangingIndent(true) list: the name a bulleted item, the description a nested item with no marker (markerFor(1, none())). Both are rich, so the DOCX export writes them as paragraphs.

  • It wrote a nested item two spaces a level in (" ".repeat(depth)), with no indent. In Word every such description in Panel, Executive, ModernProfessional, MinimalUnderlined and BoxedSections started up to 3.4pt short of the page's and wrapped back 6 to 7.9pt short, at the list's edge.
  • In LibreOffice, the foot of MinimalUnderlined's first page stood 14pt high under them.
  • A list that nests kept its top level at a space after the marker (fix(docx): give a hanging list's top level the page's marker column, text markers too #836 measured no nested level), so the name started after the bullet and a space, not at the page's column.

What changed

  • DocxLayoutMetrics.itemLines reports each item's text as well as its lines: ItemText(lines, x), x being where its text fragment starts (fragment x plus the payload's left padding). For an item laid out with its marker in a column, the text is the fragment to the right, or the later one where the two start together.
  • indentAsTheItemIs. A nested rich item with no marker stands where the layout set its text. Its left indent is the insets plus its distance past the list's edge (listTextLeft, the list's placement plus its padding), written as applyInset writes insets, and the spaces are dropped.
  • Measure. At the page's place the item has the page's measure exactly, and Word sets a size the page gives to the tenth at the half point. Its lines are weighed as a paragraph's are (wordsMeasure(List<ParagraphLine>, room), factored out of wordsMeasure(ParagraphNode, room), whose behaviour is unchanged), and the right indent gives or takes the difference. Measured at the page's place alone, Panel's magazine description broke its first line a word sooner in Word than the page does.
  • nestsBesideItsText. A list of paragraphs that nests only such items, with its items matched to the layout's, sets its top level at the page's column too (setInAColumn): marker, tab, and lines hung there, as fix(docx): give a hanging list's top level the page's marker column, text markers too #836 does for a list that does not nest. Kept at a space, the name started the gap less a space left of the description now set at the page's place.
  • What keeps the spaces: a nested item with a marker, one of plain text, one the layout sets no further in than the list's edge, and every item of a list the layout did not place or whose items it does not report one by one (an item split across pages). The top level of a list that nests any of them keeps its space.
  • Docs. Recipe (what a list becomes), capability matrix (list row), CHANGELOG.

Verification

  • Word corpus. scripts/docx-visual/word-fidelity.ps1 -Update (Word 16.0.20430) → BUILD SUCCESS.
    • Lines past 2pt across: 49 → 23.
    • 38 lines moved, all across; the down column is unchanged in every row.
    • Every description now stands within 0.07pt: Panel −3.17/−7.75, Executive −3.36/−7.92, ModernProfessional −0.48/−6.00, MinimalUnderlined −1.78/−6.34, BoxedSections −1.86/−6.42 → all within 0.07. A wrapped line of a Panel project name, −7.75 → +0.07.
    • Panel finds one line more (74 of 79). No line lost.
  • LibreOffice (Windows).
    • Lines past 2pt across: 54 → 29.
    • The foot of MinimalUnderlined's first page, 14pt high, comes within 1.4pt; lines past 2pt down: 127 → 118.
    • Three lines more are found, none lost.
  • LibreOffice (Linux). The baseline is this PR's CI artifact (run 37199619566): past 2pt across 335 → 310, past 2pt down 245 → 236.
  • Tests. DocxListMarkerGapTest, three new:
    • a nested markerless rich description at 8.6pt has no spaces ahead of it, a left indent of the bullet's width and its gap, and a right indent that measures it at Word's size; its name reads bullet, tab, name, tabbed to where the description stands;
    • a nested item with a marker keeps its spaces;
    • a list that nests an item with a marker keeps its top level at a space.
  • Sabotage. Each rule fails its test:
    • no indent: the text starts with spaces;
    • no measure: the item has no right indent;
    • the top level kept at a space whenever the list nests (nests()): the name test fails;
    • the top level tabbed whatever the list nests: the two "keeps its space" tests fail.
  • render-docx. 890 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 → 3/3 green.

Notes

  • The test's left bound is loose. It checks that the indent lies between the gap and the gap plus a size, not the layout's exact place; the corpus measures that (within 0.07pt in Word).
  • Plain-text descriptions keep the old form. A nested item of plain text goes through writeListLine, which writes it two spaces a level in when the list is not a Word list.

Lane: shared-engine (render-docx).

…s its text

A project's description under its name is a nested rich item with no
marker; the export wrote it two spaces a level in with no indent, so in
Word it started up to 3.4pt short of the page's and wrapped back 6 to
7.9pt short. Where the layout's items are matched to the list's, it now
stands every line as far past the list's edge as the layout set its text,
and its lines are measured at Word's half-point sizes as a paragraph's
are, so it breaks at the page's words.

Lines more than 2pt off across fall from 49 to 24 in Word and from 54 to
30 in LibreOffice; LibreOffice's lines past 2pt down fall from 127 to 118.
// that is never below its edge, and an item past it there still stands its distance in.
indent.setLeft(BigInteger.valueOf(leftIndentTwips(insetLeft + cellTextShift + past)));
double room = availableWidth() - past;
double measure = Double.isFinite(room) && room > 0 ? wordsMeasure(laidOut.lines(), room) : Double.NaN;
… only items at their text

A list that nests only rich items with no marker, each now standing where
the layout set its text, sets its top level at the page's column too: kept
at a space, a project's name started the gap less a space left of the
description under it. A list that nests any other item keeps the space.
itemLines keeps the later fragment where marker and text start together.
The Linux LibreOffice baseline is this branch's CI measure; the prose
names the first page's foot and the cases that keep the spaces exactly.
@DemchaAV
DemchaAV merged commit 5dd581d into 2.5-dev Oct 4, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-horizontal-offsets-6 branch October 4, 2026 12:00
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.

2 participants