Commit Graph
96 Commits
Author SHA1 Message Date
Steffen Becker bbbe72bd22 feat: a note renders in the file's own style, marker included
The marker moves from a CSS ::before to a widget decoration: generated
content is no text node, so it reached neither the PDF nor a measurement.
The note also carries the paragraph style its file names, so font, size and
colour are the document's; only the hanging indent stays the editor's,
because the style model has no first-line half to pair with the margin.
Adds a unit test for the numbering and the settings, plus the architecture
doc and the two open parity findings.
2026-08-13 12:17:45 +00:00
Steffen Becker 1aa74c0a9f feat: a footnotes and endnotes options dialog
Numbering format, start value, restart, position, prefix/suffix and the two
styles per class, plus the separator's length, thickness, spacing, alignment
and colour. The separator rides :root custom properties, so editor.css draws
the line and pageBreaks.ts reserves its band from the same values. A settings
change is no document change, so it carries a resync meta — without it the
numbering kept the format it was typed under.
2026-08-13 11:50:53 +00:00
Steffen Becker f07e020f88 feat: a footnote is drawn at the foot of its anchor's page
The placement loop moves into placeLeaves(), which takes a per-page note
reservation off each contentEnd; a bounded fixed point re-runs it until the
reservation and the pages its own anchors land on agree. Each break now
carries the leaf offset it applies from, so an anchor below a line split
follows its half of the paragraph. Endnotes stay in flow and open their own
page, as LibreOffice collects them.
2026-08-13 11:10:00 +00:00
Steffen Becker 0c74fe94b1 feat: footnotes and endnotes can be inserted and edited
The anchor is an inline atom, the note text a block in the one noteSection
at the document end; a sync plugin keeps notes in anchor order, renumbers
them per class and drops the ones whose anchor is gone. Numbering, indent,
size and separator defaults are LibreOffice's, read out of a document it
saved. Endnotes still render in flow at the document end — lifting a
footnote to the foot of its page is the next step.
2026-08-13 09:22:17 +00:00
Steffen Becker b35551acd0 fix: a document opens on the page it settles on
A pagination pass measures the DOM the last one changed, so a fresh
document searched for its layout in public — 12 of them over 550ms on a
two-column file, a phantom third page among them. Two real causes: the
flow's overflow test had no headroom, so it read its own last answer
(880px fits undecorated, 913px overflows once the decoration pins the
column height), and a split dropped the height decorations without
clearing their cache key. The rest is inherent to measuring rendered
DOM, so the page stays hidden until its layout repeats: one visible
layout instead of 12, and 21-139ms where there are no columns.
2026-08-12 21:50:15 +00:00
Steffen Becker e58e8a180e fix: a page-anchored cover graphic sits in front, at the page's own corner
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.
2026-08-11 23:48:52 +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 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 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 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 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 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 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 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 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 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 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 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 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 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 8c15f37490 fix: nothing is drawn past the edge of the sheet
A formula wider than the text area painted over the app background
instead of being cut at the page edge, where LibreOffice cuts it
(thesis fixture, page 16). `overflow-x: clip` so .paper does not
become a scroll box.
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 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 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
Steffen Becker 63149d52d7 feat: a list marker sits at the level's hanging indent
CSS gives ::marker no position, so Chromium set it flush against the
text — about 4mm right of where Word and LibreOffice draw it. The marker
is now the item block's ::before, placed at the hanging indent both
exports already write (LIST_HANGING_CM 0.635cm): measured against LO's
own render, the three bullet levels land within 0.04mm.

Out of flow, because in flow it grew every list line by a pixel and cost
a fixture fixture a page. `list-style: none` drops only the marker box,
so the list-item counter and <ol start> keep numbering; a bullet's own
char rides an inherited property, since attr() in a pseudo would read the
block rather than the list.
2026-08-08 12:57:24 +00:00
Steffen Becker 09d8cf20b8 fix: break a word that is wider than its line
A long URL or a German compound ran straight past the right margin, where
Word and LibreOffice break it — measured against LO in a 2cm column, the
break now falls on the same character.

`break-word` rather than `anywhere`, so a table column's minimum width is
still its longest word.
2026-08-08 12:35:05 +00:00
Steffen Becker 77980e4b9c fix: a block ending in a hard break is not a line taller
The reserved paragraph mark is inline content past the block's last BR,
so it opened a line of its own: LibreOffice set the next paragraph 4.7mm
higher than we did, and a fixture paid for every one of them.

The empty-block rule already pulled the mark out of flow for exactly this
reason — it now covers any block that ends in a break, seating the glyph
on the last line rather than the first.
2026-08-08 12:25:22 +00:00
Steffen Becker db0fe71cdb feat: a table keeps the cell margins its file gave it
The cell padding was a constant in three places that disagreed with each
other, so a narrow column lost 0.38cm it did not have: a bibliography
marker column of 0.71cm wrapped "[2] " onto three lines and every entry
grew with it, costing a fixture a whole page.

The table now carries its own margins (w:tblCellMar, direct or from the
table style; fo:padding on the ODF cell style) and both exports write
them back. A side within a twentieth of a millimetre of the default is
the default, or Word's 108 twips would tag every table.
2026-08-08 12:18:16 +00:00
Steffen Becker 5549d625f7 feat: the index reads like the one a word processor generates
Its leader is real dots now, not a border, so it scales with the font and
reaches the PDF as text. Title and entries follow the document's Heading 1
and default style, which is what the index styles of Word and LO are based on.
2026-08-08 09:33:25 +00:00
Steffen Becker c82a78cc6a feat: character spacing, and a line box that multiplies its two halves
Word's Title style tightens by -0.5pt, so a title came out ~3mm wide of
LibreOffice's. letterSpacingPt joins TextProps and rides both formats
(w:spacing, fo:letter-spacing).

Line spacing needed the same care: it is proportional to the font's natural
line height, so the factor and that height are now separate CSS variables and
editor.css multiplies them in one place. Before, whichever of the two was
declared nearer simply overwrote the other — a block carrying its own font
silently dropped the style's spacing.
2026-08-08 08:30:05 +00:00
Steffen Becker c34fb4312a fix: drop the trailing-break line after an inline atom
A paragraph holding an image rendered a line taller than it does anywhere
else: under white-space: pre-wrap the browser already shows the last line, so
ProseMirror's hack BR adds an empty one. The hack is kept where it earns its
keep — after a real break it carries the caret onto the empty line, and in an
empty block it is the only line box there is.
2026-08-08 08:30:04 +00:00
Steffen Becker 943a885754 feat: carry the item's formatting into its list marker
The marker took only the weight of the item's first text portion. It now takes
the family, slant, size and color too — as custom properties on every item,
since they inherit and a nested item must not take its parent's.

A named paragraph style reaches it a second way: the marker inherits the list
item's own font, and the document stylesheet only styled the item's paragraph,
so an imported document whose Standard style is Arial drew its numbers in the
editor's serif default. styleCss now emits the style's text half for the item
too (li:has(> …)).

Both file formats carry marker formatting per level, so a level definition is
written only for a list whose items agree: ODF mints a character style the level
points at, DOCX the level's w:rPr. A mixed list gets none on purpose, so
LibreOffice keeps applying its own first-portion rule.
2026-08-07 20:26:44 +00:00
Steffen Becker 0b54f6cc44 feat: render a list marker bold when the item starts bold
A bold mark sits inside the item's paragraph and never reaches ::marker, so the
roman numerals of a numbered heading stayed light while their text was bold.
A plugin tags such items and editor.css weights the marker. The trigger is the
first text portion, which is the rule LibreOffice numbering itself uses.
2026-08-07 17:41:09 +00:00
Steffen Becker 7c09727cda docs: remove word as placeholder for general text editor 2026-08-07 18:08:20 +02:00
Steffen Becker 7d4a0b256e fix: indent list levels by the step both exporters write
Lists were padded 1.5rem per level on screen while the DOCX and ODT
exports place item text at LIST_LEFT_STEP_CM (1.27cm) per level, so the
editor disagreed with its own output and with Word and LibreOffice by
6.4mm on the first level, doubling with each nesting level.
2026-08-06 20:01:23 +00:00
Steffen Becker 275a77564e fix: use Word's asymmetric cell padding for tables
Cells padded 0.1cm on all sides; Word and LibreOffice use 0.19cm left and
right and none top or bottom. Text sat 0.7mm too far left and every row
was 2mm too tall, so the offset accumulated down a table.

odf-kit only writes the fo:padding shorthand, which ODF defines as a
single length — LibreOffice drops a two-value form — so the export
expands it per side.
2026-08-06 19:26:56 +00:00
Steffen Becker 6c256c6533 fix: add pre-wrap for line-end space 2026-08-06 20:46:17 +02:00
Steffen Becker ca4fbd422b feat: table margin edge resize 2026-07-31 17:46:26 +02:00
Steffen Becker ac9d6cdcff feat: named paragraph styles with inheritance 2026-07-28 23:53:00 +02:00
Steffen Becker d461ce09a6 fix: align styles and headings to libreoffice 2026-07-28 22:45:24 +02:00
Steffen Becker 56f884a412 feat: dropdown for text styles 2026-07-27 20:35:55 +02:00
Steffen Becker f1ba14c948 feat: add paragraph frames and background 2026-07-19 16:34:23 +02:00
Steffen Becker cf665e25a0 fix: stop formatting marks from moving text 2026-07-17 07:51:58 +02:00