Commit Graph
207 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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
Steffen Becker 3153c2f7cc feat: the running head names the chapter the page is in
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).
2026-08-10 13:04:25 +00:00
Steffen Becker 9d4820f0f4 fix: an index's page numbers end at its own tab stop
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.
2026-08-10 12:24:47 +00:00
Steffen Becker 1de879b490 fix: an index draws the leader its file declares, or none
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.
2026-08-10 11:47:34 +00:00
Steffen Becker 6b6ee22f5b fix: a hoisted box keeps its anchor paragraph's alignment
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.
2026-08-10 11:20:32 +00:00
Steffen Becker a021bc42aa feat: a cell's content sits where its style puts it
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.
2026-08-10 11:03:34 +00:00
Steffen Becker f81109a434 fix: a text box takes the padding its file declares
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.
2026-08-10 10:58:47 +00:00
Steffen Becker 615f2af52d fix: a figure frame stays in the middle of the column
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).
2026-08-10 10:51:14 +00:00
Steffen Becker c26aac1bee fix: the index's leader fills its own gap, at LibreOffice's indents
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.
2026-08-10 09:32:49 +00:00
Steffen Becker 8690129c83 fix: a header or footer tab lands on one line, once
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.
2026-08-10 09:20:37 +00:00
Steffen Becker 54a15c96b1 fix: pagination measures the document, not its own last answer
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.
2026-08-10 09:05:51 +00:00
Steffen Becker e3a5594f10 fix: page geometry from the body's own master, and a TOC that breaks
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.
2026-08-10 08:44:34 +00:00
Steffen Becker db62256a99 fix: a tab's advance is decided in one pass, not raced into place
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.
2026-08-10 08:11:13 +00:00
Steffen Becker ad446e9535 fix: a block at a page top loses its space above for good
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.
2026-08-10 08:01:34 +00:00
Steffen Becker a350b8f59e fix: a page-anchored frame sits on its page instead of in the text
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.
2026-08-10 07:38:54 +00:00
Steffen Becker 6a5bd631c8 fix: a block's space below decides whether it still fits the page
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.
2026-08-09 20:59:50 +00:00
Steffen Becker d6bc656c83 feat: the space between two blocks follows the document's own model
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.
2026-08-09 16:53:13 +00:00
Steffen Becker 30b5b7890e feat: a header's page background paints behind the page it belongs to
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.
2026-08-09 15:13:46 +00:00
Steffen Becker 84eb7d6bf3 fix: an index lists the levels it declares, not all five
The TOC node regenerated over every heading level and minted a title of its
own. A file asking for level 1 only (text:outline-level, Word's TOC \o range)
got a 43-entry index instead of 12 — a full page where LibreOffice needs a
quarter, plus a second "Table of Contents" under the file's own heading.

maxLevel and an empty title now round-trip through both formats.
2026-08-09 14:54:58 +00:00
Steffen Becker 848c18fbe3 fix: the last spacer goes when the last page does
Deleting a document down to one paragraph left its page-break spacer in the
DOM, so an empty second page kept showing. The ping-pong guard reset its
memory to '', which is also the key of a spacer-free layout, so the pass that
removed every spacer looked like an oscillation and never dispatched. The
reset now uses null.
2026-08-09 14:08:29 +00:00
Steffen Becker 78e35cef2c feat: a reference finds the text it points at, and follows it
Bookmarks are a mark, not a point: both formats store a range, and only a
range lets a REF recompute its text. A cross-reference caches its display
like a Word field and resolves live on each pagination settle — the
bookmark's text, or the page it renders on — keeping the cached value when
the target is gone. Ctrl+click jumps to it, internal #name links too.

ODF splices its sentinels into the run text so cells and custom paragraphs
need no separate emitter; DOCX needs no post-pass at all.
2026-08-09 13:56:22 +00:00
Steffen Becker 3be4333517 feat: two frames set against opposite ends share one band
A top-and-bottom frame carrying positionH align / style:horizontal-pos
keeps it only where a nearby frame is set against the other end
(pairAlignedFrames, both importers); such a pair floats to its own side
instead of filling the band, so the two sit side by side as they do in
LibreOffice. A pair slightly wider than the column is scaled to it —
Word lets the two overlap, two floats cannot.
2026-08-09 05:59:58 +00:00
Steffen Becker 3154a9b7a2 feat: a list label takes the alignment the file gives it
Word's w:lvlJc / ODF's fo:text-align on the level: set right, the label grows
into the margin instead of over the text it labels. That is what the built-in
Roman numberings use, and a fixture's numerals collided without it.
2026-08-08 23:09:58 +00:00
Steffen Becker ee1b0dd180 fix: a line's extra leading sits below it, not half above
A spacing above single puts all of its leading after the line (probed against
LibreOffice at 150%); CSS splits it. The block is shifted back up by that half,
which leaves the flow — and pagination — where it was.
2026-08-08 22:51:56 +00:00
Steffen Becker 2830e8a9d4 feat: a top-and-bottom frame is drawn where the file places it
Both anchor offsets now render: the x moves the rotor inside the full-width
float, the y the float's top. Chromium puts every line after such a float
below it, so a frame set below the paragraph top is sunk behind the text at
import and the node view measures what those lines already cover.
2026-08-08 22:51:39 +00:00
Steffen Becker ee56a348ee feat: formulas can be inserted and edited, and survive both formats
A formula node stores only its LaTeX source; the MathML on screen, the ODF
formula object and Word's OMML are derived from it, and both importers parse
foreign markup back into that source, so an imported formula stays editable.

Word documents were losing their formulas outright: m:oMath sits beside the w:r
runs in its own namespace, past convertInline's `namespaceURI !== W` guard.
2026-08-08 21:50:36 +00:00
Steffen Becker e9e543fe8c fix: an image stays on its side of the page break
An as-character image is now a line box in getLineRects, so a paragraph of
stacked images breaks between them instead of overflowing the page. Only a
floated image keeps its paragraph atomic, and the leaf's height reaches down
to the lowest float — a float hangs out of the paragraph's box, so
offsetHeight alone let the image walk off the page foot. prevPlacementsKey
damps the ping-pong that overhang causes between two layouts.
2026-08-08 20:57:41 +00:00
Steffen Becker ca3767f3d4 feat: a cell keeps its own margins where they differ from the table's
w:tcMar and an ODF cell style's own fo:padding were both dropped: the
table's value (in ODF the first cell's) stood for every cell.

cellPadding is now an attr on tableCell/tableHeader too, measured against
the table's rather than the editor default, so only a real disagreement
carries one. --cell-pad inherits, so the cell's own declaration wins for
its own td and the CSS is unchanged.

Checked against LibreOffice: a 1.2cm cell in a 0.3cm table puts its text
at 332.2pt where the grid column starts at 297.6.
2026-08-08 17:47:06 +00:00
Steffen Becker 7d5bc5e0c9 feat: a run of tabs too wide for its line carries on the next
LibreOffice restarts the tab grid on the continuation line; Chromium
hangs the leftover tabs and starts the next line at the margin, 160mm
away in the repro. The run is walked from the pen before it — the one
thing the fix can't move, so the answer holds whether or not the break
is already in place — and a break widget goes where the line runs out.

Measuring this turned up why the two disagreed in the first place: a
file that declares no tab interval falls back to ODF's own 2cm, not to
LibreOffice's UI default of 1.25, so leaving ours implied on export
rendered every tab elsewhere. Both exports declare it now, and both
imports fall back to their format's value rather than to ours.

Only runs of two or more are walked: reading a block's geometry forces
a reflow, and doing it per tab across 48 pages slowed pagination enough
that the parity harness compared a layout still settling.
2026-08-08 15:58:40 +00:00
Steffen Becker ae2290e22c feat: a header or footer keeps its own tab stops
The left⇥centre⇥right layout every letterhead uses rides on them, and a
zone had none: Word keeps them on the Header style, ODF on the zone
paragraph's, and neither was read or written.

An inactive zone is static generateHTML output no plugin reaches, so it
is measured straight on the DOM. Two bugs surfaced doing it: a segment
its own advance had pushed onto the next line measured as wrapped, and
the alignment then had no way back — it takes the sum of the segment's
client rects now. And the scale came from offsetHeight, which rounds
half a percent away on a zone one line tall.
2026-08-08 15:13:58 +00:00
Steffen Becker ea39dde58b feat: a tab lands on the file's own grid, and can draw its leader
The default tab interval was fixed at 1.25cm, so a file's own
w:defaultTabStop / style:tab-stop-distance was ignored and the DOCX
export wrote 1.25 back over Word's 1.27. It is a document setting now.

A stop can also carry the character it repeats across its gap. odf-kit
has no leader option and mints one automatic style per option set, so
the leader rides style:type behind a sentinel — matching on position
and alignment alone gave every stop at 6cm the same leader, which
LibreOffice drew back plain as day.
2026-08-08 14:40:26 +00:00