Three measurement bugs in columnsFlow, each worth a page on its own:
rects became lines by their top, so a raised run counted as a line of
its own (192 for 111); the split probed `left + 1`, which is a line's
*end* right-to-left, so it kept a word too many and the flow then shaved
one line per pass; and one median advance under-measured a paragraph
whose lines differ in height (2834px real against 2453 estimated), by
enough to budget 74 lines into room for 64. The chain used to flip
between one fragment and two forever; it now settles after one split.
Brings the ribbon chrome, its tabs and controls, the Paragraph/Tabs dialogs
and the shared selection reader over from word_ui. No file overlap with
main's cover-graphic fix, so the merge is clean.
Two compounding bugs on a title/back-page background image:
- its host paragraph picks up position:relative from the line-height
rule, becoming the frame's containing block instead of the page and
offsetting it by a full page margin — now forced back to static.
- the frame always painted behind text (z-index -1); a graphic that
declares ODF's style:run-through="foreground" now paints in front,
as LibreOffice draws it, via a new inFront attr round-tripped through
ODF export/import.
Confirmed against LibreOffice's own render: the duplicated, ~2cm
displaced cover graphic now matches exactly.
Clipboard, Font, Paragraph, a style gallery of live tiles, and Find/Replace —
which had no button in either chrome before. So did change case, underline and
strikethrough line styles: the engine has carried them all along.
Two things the fixed-height band forced. Its own overflow clips a dropdown, so
a menu leaves the flow and pins itself under its trigger. And with one shared
open-menu id, each wrapper must name the menu it owns, or every sibling closes
it on mousedown and the click on a row never lands.
A sweep of the whole tree: 27 blocks ran to four lines or more. Trimmed
to what the code can't say, with two of them split back onto the
function each was really about.
Word paints a link through its Hyperlink character style, never for
being one, so the editor's own blue overwrote both the file's #0000FF
and the plain black of a link carrying no style at all. The DOCX
importer marks those `plain` and the CSS leaves them alone; LibreOffice
paints every ODF text:a, so those keep the visual.
The dot advance was probed once, on the first row, and reused for every
level below it — so a level with a smaller style ran ~9% short, 11mm of
bare gap before the page number. Probed per font now (a handful of
measurements, not one per row). A fixture's contents: 122/84/72 dots per row,
exactly what LibreOffice draws.
Collapsed borders paint a shared edge once, so the spacer <tr> bridging
the gap took the row separator with it and left one fragment open —
visible in a fixture as tables missing their frame on a page. The spacer now
draws the line LibreOffice draws there: the separator the break falls on,
or the table's own box where the rows carry none. Absolutely positioned,
since a collapsed border on the spacer shifts every row below by half.
A book's chapter heading is "Chapter 1" and its title on two lines, and both
LibreOffice and Word copy that break into the index. We joined it into one line,
so a fixture's contents came out 18 rows short. The leader and page
number ride the last of an entry's lines (align-items: last baseline).
.tiptap's padding draws one pair of side margins for the whole document, so a
title page declaring 3.18cm left sat 3.85mm off. The difference now rides a
--sec-inset-left/-right node decoration on the section's top-level blocks, and
everything that writes a block margin adds it in — an indent, a named style's
and a table's alignment all compose with it instead of losing to it.
Page geometry was document-wide, so a title page declaring 1cm started 1cm too
low. w:pgMar / the section's ODF page layout now ride its HfSet and feed the
per-page content area; an ODF master handing over (style:next-style-name) gives
its own layout to the section's first page and the successor's to the rest.
A spacer can only push down, so a smaller margin than the document's needs the
opposite: the section's first block gets a lifting spacer where nothing was
pushed onto its page ahead of it.
The zone is one paragraph, so its mark sizes every line of the band. Runs
that agree on a font now set that strut (applyUniformRunFont, as the body
already does), the zone schema carries blockFontSize so it renders, and
hfReachPx measures a line by the family's natural height instead of a flat
1.15. A 10pt footer reserved 12pt lines and sat 1.6mm high on every page.
`wrapDist` carries Word's distL/distR and ODF's fo:margin-left/-right onto
the frame; a file that declares none wraps flush, which is what LibreOffice
draws (probed: text runs to the frame edge, and 0.5cm of declared margin
stops it exactly 0.5cm short). The hardcoded 14px was 2px more than Word's
own 0.32cm and broke a line one word early.
Whole pixels cost a page-wide picture 0.3mm of height, and the column cap
then scaled that loss into the width too. Sizes stay fractional (snapped
back to a whole pixel inside the export's own 0.001cm quantization), and a
sub-pixel overhang trims the width without touching the height — which is
what LibreOffice draws.
A block spaced above single is drawn half a leading above where it flows,
and offsetTop reports the drawn position — so every block a break pushed
to a page top landed that half low. Take the shift back off where the
document is measured.
The no-descent rule matched a block holding nothing but frames, so the
image-break-caption idiom kept a text descent under its picture — 0.9mm
per figure, and LibreOffice's own guides stack dozens of them in tables.
Test the run between two hard breaks instead and mark the frame itself.
ODF style:may-break-between-rows="false" — probed: LibreOffice leaves four
lines of a page empty rather than split such a table, and splits the identical
one when the attribute says "true". pageBreaks now emits a single atomic leaf
for the wrapper unless the table is taller than a page, where the rule cannot
be met. Imported, exported and round-tripped; Word has no equivalent.
Probed: an as-char frame alone in a paragraph makes a line exactly as tall as
itself whatever style:vertical-pos/-rel says — only from-top's offset still
lengthens it. The node view cannot see its siblings, so the block's class
overrides the alignment it wrote.
LibreOffice sizes such a line max(frame, line-height) — nothing of the text
sits under the frame (probed: an image-only paragraph is exactly as tall as
its image, down to a small one, where the text line wins). Our baseline
alignment left the strut's descent below it, ~1.1mm per formula. Whitespace
beside the frame counts as nothing, which CSS cannot ask, so image.ts marks
the block.
fo:min-height is a floor, not the band: LibreOffice grows it for the zone's
own lines and, in a footer, for the first paragraph's space above (probed —
a header's space below only hangs into the body). The zone paragraph now
carries those margins, so the reach counts them at its real font size and a
page with no zone of its own keeps the floor.
ODF style:vertical-pos/-rel on an inline frame, which every LibreOffice
formula image carries: baseline, character area, or a plain offset from the
baseline. The default moves from the line-box bottom to the baseline, so a
line with a picture in it keeps the descent below it, as LibreOffice and
Word both do.
LibreOffice drops it at the page bottom, as it drops space above at a page
top (probed: a paragraph 8mm clear of the bottom keeps its 2cm below and
stays). Counting it pushed the last block of a page down a page too early.
A paragraph with fewer lines than orphan and widow control demand together
has no legal break point, so it moves to the next page whole — and the
heading kept with it has to follow. Measuring only its first two lines left
the heading stranded at the page foot.
The 6px we put over and under every wrapped frame is a default only this editor
has: LibreOffice and Word wrap flush unless the file declares a distance. It
made a fixture's figure boxes 3mm too tall each.
One fixture: 120 → 100 parity issues, the DOCX fixtures unchanged.
The atomic leaf a text box becomes carried neither its space above nor its space
below, so the fit test ignored both and a page top never dropped the space above
the way it does for a paragraph. The box's space rides --space-top, which the
page-top decoration zeroes — the rotor centres itself in the padding box and has
to follow it.
An as-char figure frame sits in a paragraph of its own; the importer lifts the
box to top level and dropped that paragraph whole, so its space above and below
went with it — every figure in a fixture sat 2.1mm too tight. The box now carries
them (ParagraphSpacing covers textBox), space above as padding so it adds to the
block above, and the export mints an anchor style to write both back.
One fixture: 142 → 100 parity issues.
A heading only moved down when its successor's *first* line no longer fit. But
widow-orphan control carries a lone first line over anyway, so the heading was
left stranded at the page foot — LibreOffice moves the pair. The fit test now
asks for MIN_KEPT_LINES of the successor.
Two ways a table's height was off. Its space above was a margin, so it
collapsed against the paragraph above instead of adding to it — now it rides
--space-before like a paragraph's. And every cell border paints a whole pixel
however thin the file declares it, where LibreOffice reserves the declared
width; the excess now comes off the cell's padding-bottom, 0.25mm a row.
A character style is a stylesheet rule, direct formatting an inline style on the
run's span. The charStyle mark ranked below TextStyle, so its span sat *inside*
and the rule won over the ancestor's inline style — a 9pt style silently undid a
run's own 10pt. Priority 102 lifts it above TextStyle's 101.
One fixture: 259 → 172 parity issues, lineEnd 49 → 7.
ODF puts fo:margin-top/-bottom on the table style and LibreOffice honours
them like a paragraph's; we dropped both, so every callout table in a fixture sat
half a line too tight and the document ran a page short. With them the Math
Guide paginates to LibreOffice's 63 pages exactly.
Only the ordinary-block push site added the space the page-top rule had
dropped above it; the index, text box, columns and table-row sites subtracted
the spacers alone, so each of those measured too high by whatever the pages
above had swallowed and got an equally oversized spacer — a fixture's figure
frames landed 16mm down their page. One naturalTopOf() now serves all six.
Two measured deviations, both worth ~1mm a row on a fixture: a cell's first block
got the space above that LibreOffice adds nowhere in a cell, and a repeating
header row rendered centred and bold because it is a <th> and the UA says so.
Pagination's page-top-drop detection now skips cell leaves, or the space that
is never there would read as a decoration to add back.
The TOC rendered every level with our 0.5cm-per-level indent, an editor-only
1.5 line height and no space above — so a fixture's contents drifted 7.6mm off
LibreOffice down one page and pushed entries onto the wrong one. The per-level
entry template names a paragraph style; the rows now carry it as data-style
and the document stylesheet does the rest. Our own indents stay the fallback
for an index whose file names none.
A manual's footer carries text:chapter, which we imported as the string the
producer happened to cache — so every page of a fixture claimed the last chapter
of the book. It is now a live field: the layer resolves the last heading at
or above its outline level to have started by that page. Round-trips to ODF
text:chapter and Word STYLEREF; PDF keeps the cached name (Chrome's print
engine has no named strings).
A table-of-content entry template names the stop its page numbers are set
against — the deputy-standards guidance puts it 45mm inside the column,
and every number of its index hung out at the margin instead.
A table-of-content entry template that declares its tab stop without a
style:leader-char means the gap is empty — the deputy-standards guidance
has one, and got a row of dots it never asked for. Round-trips.
An as-char figure frame is centred by the paragraph it sits in, and the
importer lifts it out of that paragraph — so a frame whose own style says
"from-left" landed at the margin instead of the middle of the column.
ODF style:vertical-align (middle/bottom) on a table cell, which every
LibreOffice callout table uses for its Tip/Note label. Import and render
only: odf-kit dedupes cell styles by property, so writing it back needs a
pass of its own — noted in docs/architecture/tables.md.
The inset ring was a fixed 0.15cm the editor imposed on every box, so a
figure frame — which declares 0 — sat 1.5mm right of where LibreOffice
draws it and stood 3mm taller, on every figure of the document.
An as-char text box takes its centring from the paragraph it is anchored
in, and the importer lifts it out of that paragraph — so every figure in
a fixture sat at the left margin. Carry style:horizontal-pos on the
box (an image needs none: with no side to float to it centres anyway).
Every row carried 200 dots — clipped on screen, but the clipboard, the
PDF and any measurement got all of them. Cut each to what its gap holds,
and step the levels by 0.5cm, which is what LibreOffice's Contents styles
use. The parity runner now waits for the page numbers to settle too.
Three faults in the zone layout: the wrap marker's value is '', so every
pass nested another span around the tab and added its advance again; a
stop past the line (18cm in a 17cm footer) hung the page number off the
page; and a segment measured after it had wrapped reads short by the
space the break swallowed, which kept it wrapped.
A block at a page top loses its space above, which shortens it, which
moves what follows — so the next pass read a layout its own decoration
had made, the two states alternated, and the ping-pong damper froze
whichever came first. Add the dropped space back when measuring and take
it off the flow below instead: same page, but the same one every time.
The default master was the one literally named Standard, which a document
need never reference: a fixture's body runs on OOoPageStyle, so its
footer band went unreserved and every page held ~15mm too much text.
An index longer than a page has no inner document positions for a
pagination spacer, so it now breaks itself between entries.
Two causes: rangeWidth added a rect per inline box *and* per text node, so
a segment inside a span measured double; and the pen was read back from
the DOM, where it already carried the previous pass's advance. Carrying
the pen along the line settles both, and the layout no longer depends on
how many frames pagination takes.
The rule read the padding it had just zeroed, so it flipped every pass and
the ping-pong damper froze it on whichever state came first. Read the
block's own --space-before, which the decoration doesn't touch.
ODF text:anchor-type="page" is a cover graphic or a watermark: out of
flow, placed from its page's corner, behind the text. In flow, the two
full-page frames of a fixture's covers cost three pages.
A word processor's paragraph frame includes the space below it, so a
block whose text fits but whose spacing doesn't moves to the next page —
where the space above is then dropped, which the page-top rule missed
for a block pushed by its own spacer (its measured top still excludes
that push). Keep-with-next counts the same pair of spacings. Both probed
against LibreOffice; the OPG fixture's pages 3-6 now match it line for
line.
LibreOffice adds the upper block's space-below to the lower one's
space-above, and drops the space-above where a page break put the block
at a page top — but for a Word document it takes only the larger of the
two (all probed against soffice). CSS margins can only collapse, so
space-before rides --space-before and editor.css renders it as padding
('add') or margin ('max'); the model is read from settings.xml, assumed
for DOCX, and written back on ODF export.
A positioned frame in a header is out of flow — the title-page background or
watermark. It keeps its wrap and its offset from the page corner, and the
layer paints it per page under the body instead of flowing it inline, where
it reserved a page-tall line of body space.
The page surface moves from .tiptap to .paper so the image has a layer
between the sheet and the text; ODF export writes it back page-anchored.