Skip to content

fix(docx): seat a list's items where the page sets them, as a paragraph's - #883

Merged
DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-list-item-seat
Oct 12, 2026
Merged

DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-list-item-seat

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Why

A paragraph's exact line is seated on the page's baseline. Word and LibreOffice stand it four fifths of the way down the line, the page at the face's ascent, and the export moves the line up into the space above it, or by its position. A list item's line got none of that: writeListLine and writeRichListLine never called the seat, so every item stood where Word put it.

By the export's own measure of Word's seat, 68 items of the corpus stood low by under half a point (median 0.22pt) and 16 high by half a point and more. Measured, they stood about where it put them. BoxedSections' skills, 0.18pt low by it, stood 0.23–0.28pt low in Word and LibreOffice. MidnightNavy's two-line duties, 0.9pt high by it, stood 0.76–1.06pt high.

What changed

  • seatTheItem seats an item through the paragraph's seat, from the lines the layout set it on (ItemText) and the list's line gap:
    • A line set low rises into the gap above it as far as the gap goes and owes as much below. The item under it rises into what it owes, so a run of items is seated by the space above the first.
    • Under half a point, an item opening a cell takes what the gap cannot give from the cell's padding, or from the space above its row of columns. These are the rules fix(docx): raise a cell's first line into its padding, and a row's out of the space above it #882 gave a paragraph.
    • What is left where the whole difference is half a point or more goes to the text's position.
  • Some items are not raised into the gap, though their position still moves where they take one.
    • An item the layout opens a page with. That is the first item of a list it starts on a new page in the flow, or an item on a later page than the one before it. Word drops the space above at the top of a page, so what the item owed would stand the rest lower.
    • An item of a list the layout sets over two pages in a cell. There, what an item owed would become the space above the next page's first item, inside the cell. Word is not known to drop that space.
    • ItemText now carries the page the item was set on.
  • An item whose marker Word draws gets no position. Word sets a Word list's marker from the paragraph's mark, and LibreOffice does not move that marker by the mark's w:position.
    • With −1pt on both the text and the mark, MidnightNavy's bullet stood at y=248.20 in LibreOffice and its text at 249.35. In Word both stood at 249.14 (PDF glyphs).
    • Such an item keeps only the raise into the space above, which moves marker and text together in both editors.
    • So MidnightNavy's and NavySidebar's duties, set high, stand as before.
  • One seat for both. shiftToThePagesBaseline now takes the lines and their gap, and raiseIntoTheSpaceAbove takes whether the line stays on its page instead of the node. The paragraph overload computes both exactly as before, so a paragraph's own seat is unchanged. A paragraph after a raised item now takes back what the item owes above it, as it does after a raised paragraph.
  • Items without matched layout lines are not seated. SlateOrange's certifications are an example: a list in a table's composed cell. They are written without a line height at all, which is a separate defect.

Corpus

Word and LibreOffice on Windows measured against the committed baselines; LibreOffice 24.2 is this PR's CI run against the previous head's:

median nearer / further lines nearer / further (≥0.05pt)
Word 16.0 5 / 0 75 / 1 (one step of Word's 0.12pt grid)
LibreOffice, Windows 3 / 0 76 / 0
LibreOffice 24.2 (CI) 3 / 0 76 / 0

How far the items moved:

Document Items Word LibreOffice
ClassicInvoice notes up 0.24–0.36pt, to within 0.05pt of the page up 0.3pt, to within 0.05pt
BoxedSections, ModernProfessional skills and projects up 0.12–0.24pt up 0.2pt
Executive project descriptions (a list Word does not number) down 0.48pt, to within 0.06pt down 0.55pt, to within 0.06pt

The Word and LibreOffice-on-Windows baselines are rewritten from this measurement, and change only in the eight documents that moved. The Linux LibreOffice baseline is rewritten from this PR's CI artifact (a commit of its own), which also takes in the lines the two seats before this one moved inside the gate's half point; 52 of 62 documents still pass. The report's notes are unchanged: 969, byte-identical.

assets/readme/examples/word-export-companion.docx is re-rendered: its lists move.

Verification

  • ./mvnw -B -ntp clean verify → BUILD SUCCESS.
  • DocxListItemSeatTest (new, 13 tests), raising into the space above:
    • an item rises into the gap above it, and the next item into what the first owes;
    • a Word list's item low by more than its gap rises that far, with no position on its runs or its mark;
    • an item opening a panel rises into the panel's padding;
    • an item opening a row's column rises out of the space above its table;
    • an item of runs, its marker written as text in a column, is seated like a plain one.
  • DocxListItemSeatTest, position:
    • an item whose marker is written as text is lowered by its position, every run with it;
    • a Word list's item set high keeps no position;
    • an item's two lines are seated at their middle one.
  • DocxListItemSeatTest, not raised:
    • an item on a later page keeps the space above it, and the next item on that page rises;
    • the first item of a list the layout starts on a new page keeps its space above;
    • the items of a list the layout sets over two pages in a panel keep the space above them;
    • an item in a layer stack keeps the space above it;
    • an item of a list in a table's composed cell, which lays out no lines of its own, is left as Word sets it.
  • DocxVerticalSpacingTest: the list's items now count their raise.
  • Each part of the seat, undone, fails at least one of these tests.

Lane: shared-engine — DOCX semantic backend (render-docx), no public API change.

…ph's

- A list item's exact lines were never seated: Word stands the baseline
  four fifths of the way down, the page its face's ascent down, and an
  item's text stood where Word put it. seatTheItem runs it through the
  paragraph's seat: a line set low rises into the gap above it as far
  as the gap goes and owes as much below, the item under it rising into
  that; under half a point, one opening a cell takes the rest from the
  cell's padding or the space above its row of columns; what is left of
  half a point and more is the text's position.
- Not raised into the gap: an item the layout opens a page with, the
  first of a list it starts on a new page or one on a later page than
  the item before it, and an item of a list in a cell broken over a
  page. ItemText carries the page it was set on.
- An item whose marker Word draws keeps no position: LibreOffice does
  not move a Word list's marker by the mark's position, and the text
  would stand off its bullet there.
- shiftToThePagesBaseline takes the lines and their gap, and
  raiseIntoTheSpaceAbove whether the line stays on its page, so a
  paragraph and an item share one seat.
- Baselines: Word and LibreOffice on Windows rewritten from the corpus;
  the Word export companion re-rendered.
- Tests: DocxListItemSeatTest; DocxVerticalSpacingTest's list counts its
  items' raise.
…hange's CI run

- The Linux baseline was last written for the composed chip's label
  seat. The text cells beside composed ones, the first lines of cells
  and now the list items have moved lines since, each inside the gate's
  half point, so it held them to their old places.
- Taken from the DOCX Fidelity artifact of this change's CI run.
  Against the run before it, 3 documents moved nearer by median, none
  further; 76 lines nearer, none further; 52 of 62 still pass.
… and say so

- A list's item in a cell is raised into the space above it where its
  list is on one page; whether its row is kept whole adds nothing, as
  a row of columns never crosses a page and a list in a table's
  composed cell lays out no lines to seat it by.
- Javadoc and docs: the items not raised into the space above still
  take a position where they take one; the list, not its cell, is what
  the layout sets over two pages; what is left goes to the position
  where the whole difference is half a point or more.
- Tests: a list in a composed cell is held to having no exact line, an
  item of runs to being tabbed to its text's column.
@DemchaAV
DemchaAV merged commit 279477d into 2.5-dev Oct 12, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-list-item-seat branch October 12, 2026 10:09
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