Skip to content

fix(docx): lower a Word list's item Word sets high by the space above it, its marker with its text - #886

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

DemchaAV merged 4 commits into
2.5-devfrom
fix/docx-list-item-lower

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Why

Word draws a Word list's marker from the paragraph's mark, and LibreOffice does not move that marker by the mark's w:position. So since #883 a Word list's item takes no position, and one that Word sets high by half a point or more stayed high.

In the corpus that is MidnightNavy's and NavySidebar's two-line duties. A wrapped item takes the list's lineSpacing into its exact line and off its space above, and Word seats that taller line a fifth of the gap higher than the page sets the text. Measured against the page:

  • MidnightNavy: 0.70–1.06pt high in Word, 0.82–1.04pt in LibreOffice;
  • NavySidebar: 0.30–0.47pt high.

What changed

  • seatTheItem lowers such an item by growing its w:before (lowerIntoTheSpaceAbove). The marker and the text move together in both editors.

    • It applies only where a position would have moved the item: the seat finds it high by half a point or more.
    • It applies only where its space above stays above it on its page, as for a raise.
    • It applies in the flow, in cells and in layer stacks of one layer, which the writer sets in the flow; MidnightNavy and NavySidebar wrap each job in one.
    • It does not apply in an overlay that lays one thing over another, or to an item opening a card, whose top border is taken inside that space.
  • The lowering is given back below. itemsLowered records each grown space. The next item gives it back from its own space above, first in first out, before its own raise. An item standing as high takes it back by its own lowering. An item that cannot give back all it is owed, and stands less high itself, has the items above take the rest back, last lowered first. It then stands where it would have, and nothing under it moves down.

  • What the list's last items still owe hangs below the list (hangingBelow), with one HungLowering per list: its spaces, how far it still hangs, and the page the list ends on.

    • The hang is taken as any hanging line is: by the next paragraph or table, by a band's opening space, by what a cell owes under its content, by the room under a cell in its row, or by a card's padding.
    • Each gives its share to every list's hang, so lists side by side in a row are settled each on its own.
  • Where the hang is let go, the lowering is taken back (letTheLoweringGo, last grown first). The items then stand as Word sets them, and what follows stands where it did. That happens:

    • below a table's composed cell or a layer stack's column, whose writers read nothing of a cell's overhang;
    • in a row cell whose room the layout does not report;
    • in a panel piece that goes on onto the next page;
    • under a column's later layer, which resumes the page's distance (resumeHere);
    • at forgetTheHang;
    • at a block on a later page than the list, whose space above Word drops (takeTheLowering reads the block's start page); such a block takes nothing of the hang, as a block moved to a new page does;
    • at an item the list sets on a later page;
    • at the section's end, and where the space a cell of the last row owes under its own last paragraph, written or all taken, is left out there along with what it gave back (dropTheSpaceBelow, givenByTheSpaceBelow).

    Otherwise the page above would run that much longer.

  • Past the list, on the same page, a block whose space above and own top edge cannot take the hang is stood lower and named (space above), as for any line hanging below a block. The items above it keep their lowering. In the corpus every list's leftover is taken whole.

  • listOpensItsPage sees a list opening a page inside a one-layer stack too. Before, it was detected only at overlayDepth == 0. A raise is still not made in overlays, so this affects only the lowering.

Corpus

The DOCX files of cv-midnight_navy and cv-navy_sidebar change; the other 60 are byte-identical, and the report's 969 notes are unchanged. No lowering in the corpus is taken back: every list's leftover is taken by the block, room or space under it.

pt, + below the page Word 16 LibreOffice, Windows LibreOffice 24.2 (container)
MidnightNavy duties, before −1.06 … −0.70 −1.04 … −0.82 −1.04 … −0.82
MidnightNavy duties, after −0.10 … +0.20 −0.14 … +0.08 −0.14 … +0.08
NavySidebar duties, before −0.45 … −0.30 −0.47 … −0.45 −0.47 … −0.45
NavySidebar duties, after +0.03 … +0.19 +0.03 … +0.05 +0.03 … +0.05
  • By median, 2 documents moved nearer the page in each editor and none moved further. Every line after the lists stands where it did.
  • Read from the PDFs' glyphs, the bullets stand on their text's baseline in all three editors.
  • The Word and LibreOffice-on-Windows baselines are rewritten from this measurement, with only these two documents' rows changed.
  • The Linux LibreOffice baseline is rewritten from this PR's CI artifact, in a commit of its own. Against fix(docx): write a list composed in a table cell on the lines its table laid out #884's run, LibreOffice 24.2 moved only these two documents, 2 nearer and none further, matching the container to the hundredth; 52 of 62 documents still pass.

Not changed

A Word-list item still set low by half a point or more after its raise takes no position, and its remainder is not handed to the cell's padding. The corpus has none: after the raise, 0 of its 140 Word-list items stay that low.

Verification

  • ./mvnw -B -ntp clean verify → BUILD SUCCESS: render-docx 1466 (+31), qa 1820.

  • DocxListItemLowerTest (new, 29 tests) covers:

    • lowering by the space above, with no position on the text or the mark;
    • the chain under it, pinned to Word's seat of a two-line item, an item set higher than the one above lowered by the difference, and an item that cannot give it back having it taken back above;
    • giving back before a raise;
    • what the next paragraph, table, band, cell, row room or card padding takes;
    • the take-back below a composed cell, at the end of the document, at a row ending the document whose cell's space below gave it back (written, or all taken), before a paragraph or list opening a page (in the flow and inside a one-layer stack), above a band with no space above it, and below a row or card whose hang is let go;
    • a block on a later page keeping its space above whole, with no note;
    • lists side by side, each taken back;
    • the exclusions: an item opening its page or a card, a list over two pages in a cell, a stack over a drawing, an item high by under half a point.

    Most take-back tests carry a control showing the same setting lowered where something gives it back.

  • DocxLayerColumnsTest: a column ending with a list, and a later layer under one, take the lowering back; each has a lowered control.

  • DocxComposedListTest: the wrapped item in a cell is now lowered and given back by the item under it, pinned to Word's seat.

  • DocxListItemSeatTest: the item whose marker is its text gets a paragraph under it. A lowering it must not get would otherwise be taken back at the document's end, unseen.

  • Undone one at a time, each of 31 parts of the change fails at least one of these tests: the lowering, its give-back and take-back within the list, each take-back site listed above but two, the per-list and page checks, the exclusions.

  • Two take-back sites have no test of their own: a panel piece the panel goes on from on the next page, and a row cell whose room under it the layout does not report.

  • Not covered: in a row ending a section, a lowering the room under its cell gave back, where that room is the space below a cell beside it, which the section's end leaves out too. The row then stands up to the lowering taller, never lower than the page's. The corpus has none.

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

Word draws a Word list's marker from the paragraph's mark, and
LibreOffice does not move it by the mark's position, so such an item is
given no position, and one Word set high by half a point or more stayed
there: MidnightNavy's two-line duties stood up to 1.06pt high in Word
and 1.04pt in LibreOffice, NavySidebar's up to 0.47pt.

Such an item is now lowered by growing the space written above it,
which moves its marker and its text together in both editors. The items
under it give that back from the space above them, before their own
raise, one standing as high taking it back by its own lowering. What
the list's last items still owe hangs below the list, each list's on
its own, as a line hanging below a block does: the space above what
follows takes it, and at a cell's end the space the cell owes, its
row's room or a card's padding. Where that hang is let go - below a
table's composed cell or a layer stack's column, under a column's later
layer, at a block on a later page, at a section's end - the lowering is
taken back, and the items stand as before. Not an item the layout opens
a page with, one of a list set over two pages in a cell, one opening a
card, nor one in an overlay laying one thing over another; a stack of
one layer is in the flow.

The duties now stand within 0.2pt in Word and LibreOffice, everything
after them where it stood; the other 60 corpus documents are
byte-identical, the report's notes unchanged, and the Word and
LibreOffice-on-Windows baselines change only there.
…hange's CI run

- Taken from the DOCX Fidelity artifact of this change's CI run. Against
  the run before it, only cv-midnight_navy and cv-navy_sidebar moved:
  their duties, 0.82 to 1.04pt and 0.45 to 0.47pt high, now stand 0.14pt
  high to 0.08pt low and 0.03 to 0.05pt low, every other line where it
  stood; 52 of 62 documents still pass.
- Without it, the gate's half point a line would let those lines slide
  back to where they stood.
…or the space ending a section, cannot give it back

- An item that cannot give back from the space above it what the items
  above it were lowered by, and stands less high itself, has them take
  the rest back, the last lowered first: it stands where it would have,
  and nothing under it is pushed down. Every item settles the lowering
  above it, its marker drawn by Word or not.
- What the space below a cell's last paragraph gave back of a lowering
  is recorded with that paragraph. Where the section's end leaves that
  space out of its last table's last row, the lowering is taken back
  too, and the row stands as tall as before.
… where the section's end leaves it out, written or all taken

- Where the space a cell owes under its last paragraph gave all of it
  to a list's lowering, nothing is written below the paragraph. Leaving
  the section's last row's space out then skipped its record, and the row
  stood the lowering taller. The record is now taken back wherever the
  section's end reaches that paragraph, and is removed once read.
@DemchaAV
DemchaAV merged commit 30c5889 into 2.5-dev Oct 12, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-list-item-lower branch October 12, 2026 19:23
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