Skip to content

fix(docx): grow a Word list's item where its face holds its marker - #958

Merged
DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-least-list-items
Oct 25, 2026
Merged

DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-least-list-items

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Why

A line is written at a least height (w:lineRule="atLeast"), which Word grows when a reader makes the text larger, where it is at least as tall as its runs' own line (#955, #956, #957). A Word list's item, whose marker Word draws, was never read: DocxGrownLines.reachOf gave any paragraph with w:numPr no reach, so every item kept an exact line. With Normal two points larger, Word cut the tops off their letters on screen. The corpus has 140 such items, all in CV cells, 96 of them on lines at least as tall as their faces' own.

Measured in Word 16 and LibreOffice 24.2 (CI's build), on items of 8pt Lato and 9pt Fira Sans Condensed beside the same paragraph unnumbered, exact and least, at the face's own line and 1.25pt taller:

  • A marker in the item's face and size leaves the line as tall and the text where it stands, in both editors: a bullet, and the export's deeper levels' white bullet and small square in Lato, an en dash in Fira Sans Condensed; with a space or a tab after it, its face stated by its level or taken from the paragraph's mark, the mark bold or not.
  • A larger marker grows the first least line by its height in both (a 16pt bullet beside 8pt Lato: the first baseline 7.9pt lower). Word draws the marker as a run of the first line, in its level's properties over the mark's.
  • A marker its face holds no glyph for is set in another face: Word set an arrowhead beside 8pt Lato in Segoe UI Symbol, its least line 0.72pt taller; LibreOffice set it in DejaVu Sans, as tall.

So an item whose marker is set in a face that holds it stands its text where an unnumbered paragraph would.

What changed

  • DocxGrownLines.reachOf reads a Word list's item with its marker (withItsMarker): a run of the first line in the level's rPr over the paragraph mark's (Run.over), its letters the level's text. No reach where:
    • the item names no level of the numbering part, or its list carries a level override (w:lvlOverride) or links a numbering style (numStyleLink, styleLink);
    • the level is not a bullet, or writes a number (% in its text) or a picture bullet;
    • the marker's face is not shipped, is raised, or holds no glyph for one of its letters.
  • DocxFontTable.ShippedLines.holds asks a shipped face's Unicode character map whether it holds every letter of a text. The map is read with fontbox from the face's own file, the one the package embeds, the first time a marker asks it, and the file is let go then: 0.4 to 3ms a face (Barlow, Lato, Fira Sans Condensed), once an export.
  • The grown lines read the numbering part through a supplier (document::getNumbering), since the part is written as lists come, and keep each level read: an item's lines are read several times.
  • Not read: a run's own letters are not asked of its face. The corpus has no least-line run with a letter its face lacks (1967 runs checked).
  • Docs: the recipe's Line height row, the CHANGELOG, the older least-line entry's list clause, and the picture lines' entry, whose TealPulse sentence named a cause the export's reckoning does not bear out.

Verification

  • Probes (scratch, not committed), each in Word and LibreOffice 24.2: 28 cases a face and marker, five of them — numbered and not, exact and least, two heights, a space or a tab, the level's face or the mark's, a bold mark, a marker twice the size, one the face lacks.
  • Tests:
    • DocxGrownLinesTest, aWordListsItemIsReadWithItsMarkerInItsLevelsFaceOverItsMarks: a bullet in the text's face and size, or in the mark's, reaches as the unnumbered paragraph and may grow; a 20pt bullet reaches 20pt Lato's ascent and may not; a white bullet in a Barlow mark has no reach, in a Lato level over that mark it does.
    • aWordListsMarkerNotSetInItsOwnFaceIsNotRead: an arrowhead Lato lacks, a level's number, a decimal level, a bold face not shipped, a raise, a picture bullet, a depth the list does not define, an override, a numbering style linked either way, a list the part does not hold and no numbering part give no reach.
    • DocxGrownLinesExportTest: a Word list's item is written least with no position; one whose marker Lato lacks stays exact.
    • DocxComposedListTest: items composed in a cell are least, as in the flow; a wrapped item's gap comes off the space above it and nothing is lowered, while an exact one is lowered as before.
    • DocxListItemLowerTest, DocxLayerColumnsTest: the lowering of an item Word sets high is pinned on exact items, their marker one Lato and Spectral lack; their numbers are unchanged. anItemWhoseFaceHoldsItsBulletIsALeastLineAndNotLowered: the same item under Word's bullet is least, its space above as written, the paragraph under it taking nothing back.
  • Fail-closed: 11 sabotages, each run alone, all caught: no marker read; glyphs not asked; the level's or the mark's properties dropped; a level's number, a numbered level, an override, a picture bullet, the item's depth, a numbering style read; every face holding every letter.
  • Corpus: 8 of 62 documents change, 96 list items exact → least, every Word list item at least as tall as its faces' own line; the 44 still exact are set tighter. Five items no longer take the lowering their exact line needed from the space above them (MidnightNavy 2, NavySidebar 3); SlateOrange's four set high are lowered a fifth of a point by the space under them, as a line set high by under half a point is. The report changes only in their line height counts.
  • Word edit protocol, restyle-body-through-normal:
    • on the eight documents, squeezed paragraphs 222 → 140, and SerifHeadline passes;
    • corpus-wide 923 of 1832, from 1005, with 15 documents passing, from 14.
  • Word fidelity gate (scripts/docx-visual/word-fidelity.ps1): BUILD SUCCESS. On the eight documents:
    • lines: 53 nearer the page and 26 further, of 682;
    • steps from a line to the next off by more than 0.2pt: 80 of 456, from 95; p90 0.30, from 0.34.
  • LibreOffice 24.2 (CI's build, in a container): 53 nearer and 23 further, of 678. Steps off by more than 0.2pt: 88 of 482, from 107; p90 0.27, from 0.28.
  • Where it stands further: OrangeOps' duties (17 lines in Word, 16 in LibreOffice) and SerifHeadline's items (8 and 7), 8pt Lato on lines of 9.7 to 10.15pt in a table's cell, stand about 0.25pt lower in both editors, the lowest 0.42pt low in Word. The export reckons their least line within 0.06pt of the page (printed from letTheLinesGrow in a probe run), and Word and LibreOffice stand it about a quarter point lower. As exact lines the export reckoned them as much high, and they stood nearer the page, from 0.35pt high to 0.18pt low: the four-fifths seat of an exact line made up for the reckoning. The quarter-point check on a least line reads the same reckoning, so it does not hold these lines; each stands within half a point. TealPulse's duties, least since fix(docx): grow a line holding a picture where the picture stays within its text #957, are the same case (reckoned 0.04pt off, 0.29pt low in Word); that entry's wording is corrected here too. NavySidebar's, TerracottaRail's and ProfessionalSidebar's items stand nearer.
  • Gate:
    • ./mvnw -B -ntp clean verify -pl <core, render-pdf, render-docx, render-pptx, templates, testing, qa, coverage> -am → BUILD SUCCESS: core 823, render-pdf 339, render-docx 2182, testing 5, render-pptx 142, templates 127, qa 1830.
    • The JDK 17 knowledge checks pass with the API surfaces unchanged, and core javadoc has 0 warnings.
    • ./mvnw -f examples/pom.xml test → BUILD SUCCESS, 93 tests.

Lane: shared-engine (render-docx). No public API change. Word list items' line rules change in the file.

- DocxGrownLines reads a Word list's item with its marker, a run of its
  first line in its level's properties over its paragraph mark's, where
  the level writes letters its face holds: not a number, a picture, an
  overridden level or one a numbering style defines.
- DocxFontTable.ShippedLines.holds asks a shipped face's character map
  for a text's letters; Word sets a letter its face lacks in a face of
  its own, whose line is taller.
- Tests: an item grows with a marker in its text's face and size, in its
  mark's face, not with a larger one or one its face lacks; the lowering
  tests keep their items exact by a marker Lato and Spectral lack.
@DemchaAV
DemchaAV marked this pull request as draft October 25, 2026 09:03
- The CHANGELOG said OrangeOps', SerifHeadline's and TealPulse's least
  lines stood lower because the space above did not take their gap, and
  within the quarter point a least line is held to. The export reckons
  them within 0.06pt of the page, and Word and LibreOffice stand them
  about a quarter point lower, the lowest 0.42pt; as exact lines the
  reckoning was as far off the other way. The entries now say so.
- DocxGrownLines.levelOf's Javadoc names every level it does not read.
… stated

DocxGrownLines asked a list marker's letters of the Normal style's face
without checking that the document states one. A run naming no face is
read only when it does, so the call could not be reached without it;
the check now says so where the face is asked.
@DemchaAV
DemchaAV marked this pull request as ready for review October 25, 2026 09:18
@DemchaAV
DemchaAV merged commit a70f177 into 2.5-dev Oct 25, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-least-list-items branch October 25, 2026 09:37
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