Commit Graph
391 Commits
Author SHA1 Message Date
Steffen Becker fe55aec95f fix: the page draws its text black, as a word processor does
The sheet took the app's slate (#1e293b), so every uncoloured run
differed from LibreOffice — and from a run the same document colours
#000000 explicitly. Light and dark themes now paint the page black on
white; allBlack still inverts it, and the PDF export no longer has to
override anything.
2026-08-11 21:27:39 +00:00
Steffen Becker f055fc4850 docs: every comment back inside the three-line rule
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.
2026-08-11 21:22:13 +00:00
Steffen Becker 2a8f0675bc fix: Word's hollow bullet is the letter it is
A Courier New "o" was mapped to ◦ for looks. LibreOffice and Word both
draw the glyph the numbering names, so the nested lists of the gravity
summary now read like theirs.
2026-08-11 21:22:13 +00:00
Steffen Becker 5969795d04 fix: a header or footer takes the document's font, not the editor's
The zone was pinned to Liberation Serif 12pt, so a run whose font the
file leaves to the default — every page-number field in a fixture
fixture — rendered serif inside an Arial footer. Both formats base the
Header/Footer style on the default paragraph style, so the zone now
takes its text half.
2026-08-11 20:58:29 +00:00
Steffen Becker 95d174501d fix: a Word hyperlink keeps the look its file gives it
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.
2026-08-11 20:52:12 +00:00
Steffen Becker 8db6214c11 fix: a CMYK image keeps the colours its file draws
Chromium colour-manages a CMYK JPEG through its embedded ICC profile
where LibreOffice and Word convert naively, so a fixture's
logo arrived dull teal on grey instead of vivid blue on black. The
importer drops the profile — measured back onto LibreOffice's own
values, and 655 KB smaller for that one logo.
2026-08-11 20:42:00 +00:00
Steffen Becker 1122f6d0a6 fix: an index entry's dots reach its page number
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.
2026-08-11 20:31:34 +00:00
Steffen Becker 2803e6196e fix: a table split across pages closes its box on both
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.
2026-08-11 20:24:56 +00:00
Steffen Becker 40d58463c7 docs: per-section margins ship, and what the corpus reports
Also records the harness change that made the longest fixture measurable at all.
2026-08-11 19:52:39 +00:00
Steffen Becker 3cf1d34035 fix: an index entry keeps the hard break its heading has
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).
2026-08-11 19:52:02 +00:00
Steffen Becker 860e25d8c8 fix: saving keeps a section's own page margins
Both exports wrote one geometry for the whole document, so a title page's
margins survived import and were lost on the next save. Each ODF section master
now names a page layout of its own — the document's, shifted by the section's
difference, so what the header/footer pass folded into it survives — and every
w:sectPr carries its section's w:pgMar. DOCX import takes the document pair from
the first section, the one .tiptap's padding draws.

The parity harness spread a 458-page document's word list into Math.max and blew
the stack; the longest fixture was never measured.
2026-08-11 19:35:55 +00:00
Steffen Becker baf0865256 fix: a section keeps its own side margins too
.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.
2026-08-11 19:03:58 +00:00
Steffen Becker 9bce48f6e0 fix: a section keeps the top and bottom margins its own page setup gives 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.
2026-08-11 18:51:12 +00:00
Steffen Becker 71cf17f47a docs: what the corpus still reports, and why it stays
Three probed findings written down rather than fixed: an outside ::marker has
no metric influence in CSS (a bulleted line is 0.12mm short of LibreOffice's),
an unbreakable run longer than the line moves whole instead of breaking at the
margin, and a typeset formula keeps its own baseline rather than its frame's —
honouring the frame measured worse. Plus the live-vs-cached index numbers.
2026-08-11 18:06:42 +00:00
Steffen Becker e2ee55cf35 fix: a header/footer band is as tall as its own font, not the body's
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.
2026-08-11 17:10:08 +00:00
Steffen Becker 853762a774 fix: a float keeps the gap its file declares, not one of ours
`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.
2026-08-11 16:40:34 +00:00
Steffen Becker a4bcaa9ded fix: a frame keeps the size its file states, to the fraction
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.
2026-08-11 08:03:02 +00:00
Steffen Becker 3f6b64cd95 fix: pagination measures the flow, not the paint correction
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.
2026-08-11 07:43:48 +00:00
Steffen Becker 12f2e7a623 fix: naming a page style is a page break, same master or not
Repeating the current master was read as "no switch, no break", so a book
whose chapter heading style names its own page style ran the chapters
together — a fixture lost the break before every chapter. An explicitly empty
name now ends the parent walk, so nothing inherits a break it never asked
for; the section itself still starts only where the master changes.
2026-08-11 07:30:45 +00:00
Steffen Becker 361ce14864 feat: pair kerning follows the file, not the browser
A browser always kerns; ODF kerns unless told not to and Word kerns only
above the size w:kern names — nothing, in most files. Measured 5% on a
kerning-heavy string, enough to break a line a word early throughout every
Word document in the corpus. Carried on the style, since that is where
w:docDefaults and the default paragraph style put it.
2026-08-11 07:23:20 +00:00
Steffen Becker b825e483ac fix: an ODF cell that declares no padding has none
fo:padding defaults to 0 in ODF, but an undeclared side was left open and
fell through to Word's implicit 0.19cm, indenting every cell of a table
whose file names no cell style — a fixture's figure captions by 1.9mm.
2026-08-11 07:09:17 +00:00
Steffen Becker 0830fff86f fix: a frame is alone on its line, not only on its block
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.
2026-08-11 06:58:40 +00:00
Steffen Becker db8f4439b1 feat: a table that may not break between rows moves whole
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.
2026-08-11 00:02:31 +00:00
Steffen Becker 0d3506a51c fix: alignment means nothing to a frame that is its whole line
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.
2026-08-10 23:22:15 +00:00
Steffen Becker 6c7f7834e9 fix: a frame alone on its line hangs no descent below it
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.
2026-08-10 22:36:01 +00:00
Steffen Becker c41dce79cb fix: a zone reserves the band its own type needs
hfReachPx floored the zone's line at the 12pt body default and only ever grew
it, so a footer set throughout in 10pt still reserved a 12pt line and pushed
the body ~0.8mm too far up. The default now stands in per run, for one that
declares no size of its own.
2026-08-10 22:20:27 +00:00
Steffen Becker 72b39ca6ad fix: a break ends its block even inside a mark span
A run's own formatting wraps its hard break in a span, so the block's last
element was the span, not the BR — the trailing-break BR was hidden and the
empty line the break opens went missing. LibreOffice renders it (probed: a
Word paragraph closing on <w:br/> keeps a blank line before the next block).
2026-08-10 22:20:19 +00:00
Steffen Becker 3781a79d4a fix: a line-end space hangs past the paragraph mark
The ¶ reserved after every block sat in the line's flow, so a trailing space
was no longer at the line end and could not hang into the margin: a last line
that exactly filled its width pushed the zero-width mark onto a line of its
own, and the block came out one line too tall. Out of flow at its static
position the mark is invisible to line breaking.
2026-08-10 22:20:06 +00:00
Steffen Becker 13e026fb8b fix: a zone's own margins survive the ODF round trip
The Header/Footer style carries them, and the band that grows to hold them
is rebuilt from there on the way back in.
2026-08-10 21:31:38 +00:00
Steffen Becker 0fe1a8395d fix: the header/footer band grows to hold the zone it carries
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.
2026-08-10 21:22:56 +00:00
Steffen Becker dc22bb7523 fix: a style keeps the line spacing its document default does not
Word writes 1.08 into w:docDefaults, so a style declaring single spacing was
dropped as "the default anyway" and inherited 1.08 instead — every Title and
every heading that asks for 1 rendered a line too tall. The parent style is
the yardstick, which ownProps already applies.
2026-08-10 21:22:34 +00:00
Steffen Becker af144362fe feat: an as-char frame sits where the file says it does
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.
2026-08-10 20:48:31 +00:00
Steffen Becker 970ad3776a fix: space below a block need not fit on its page
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.
2026-08-10 20:46:43 +00:00
Steffen Becker 80e935145b fix: keep-with-next reserves a successor that cannot split
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.
2026-08-10 20:46:35 +00:00
Steffen Becker 06bee1d3f1 fix: the same run-size yardstick on the DOCX side
Mirrors the ODF fix: a block that cannot name its style is measured against the
default paragraph style but renders at its own, so a run matching the default was
suppressed and came out at the wrong size.
2026-08-10 19:58:17 +00:00
Steffen Becker 138458b127 fix: a run keeps the size its block does not supply
A list item is measured against the default paragraph style, but it renders at
its own style's size — so a 12pt run under an 11pt ListParagraph was suppressed
as "the style supplies it" and came out 11pt. Runs are now measured against the
size the block actually gives them.

The guidance document: 15 of 16 pages start on the same line, from 1.
2026-08-10 19:54:48 +00:00
Steffen Becker 9720e1b0ec fix: a float reserves no space above or below itself
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.
2026-08-10 19:43:32 +00:00
Steffen Becker 333ca80b7c docs: what the parity corpus still reports after the spacing round 2026-08-10 19:35:38 +00:00
Steffen Becker 314f26a9d4 fix: a cell with no padding below still absorbs its border overshoot
A painted border is a whole pixel however thin the file declares it, and the
excess came off padding-bottom alone — a cell that declares none (a fixture's Mac
table) kept growing 0.25mm a row. What is left over now comes off padding-top.
2026-08-10 19:35:09 +00:00
Steffen Becker b134911e7b fix: pagination measures a text box's own spacing
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.
2026-08-10 19:35:09 +00:00
Steffen Becker 71ecc17a9e fix: a header/footer's gap to the body is inside its band, not above it
The body margin reserved the zone's height *plus* its fo:margin-top/-bottom, so
every page with a footer lost a line — a fixture ran a page over. Probed against
soffice: moving a footer's fo:margin-top leaves the last body line exactly where
it was and only shifts the footer text down its band; style:dynamic-spacing
changes neither side. The band is the declared height, the gap sits in it.

One fixture paginates to LibreOffice's 63 pages again.
2026-08-10 19:34:53 +00:00
Steffen Becker d6c3602429 fix: a lifted text box keeps its anchor paragraph's spacing
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.
2026-08-10 18:54:37 +00:00
Steffen Becker 96e69773c8 fix: keep-with-next reserves the lines widow control would keep
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.
2026-08-10 18:34:54 +00:00
Steffen Becker b7f6328005 fix: a table takes the vertical space LibreOffice gives it
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.
2026-08-10 18:34:32 +00:00
Steffen Becker 819f51587e fix: direct formatting on a run beats its named character style
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.
2026-08-10 18:08:13 +00:00
Steffen Becker d407febd4b feat: insert a chapter field, and set where a cell's content sits
Both round-tripped through save/load already but had no way in: the
header/footer bar gains a Chapter button next to page number/count, and the
table toolbar three buttons for the cell's vertical alignment.
2026-08-10 17:01:48 +00:00
Steffen Becker e2179b75fb feat: a table keeps the space its style puts above and below it
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.
2026-08-10 14:05:08 +00:00
Steffen Becker 0712d44ebf fix: every leaf's natural top is reconstructed the same way
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.
2026-08-10 14:00:12 +00:00
Steffen Becker 7a4174cd07 fix: a table cell's metrics follow LibreOffice, not the browser
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.
2026-08-10 13:49:12 +00:00
Steffen Becker 7b8c7e21f5 fix: an index's entries follow the styles its own file names
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.
2026-08-10 13:12:17 +00:00