Commit Graph
605 Commits
Author SHA1 Message Date
Steffen Becker 2aecb1a1b3 feat: a printed document carries its comments and changes
The bar in the margin beside every changed or commented block, and the comment
bodies as a list after the document — a comment's text is nowhere on the page,
so a bar alone could only say that one exists. Resolved comments print nowhere,
and a document with no markup prints exactly as before.

The print engine clips to the page area (measured), so the vector path buys the
bar's strip out of the left @page margin and gives it back as padding. The
raster paths clone the editor's own layer and append the list as whole pages.
2026-09-04 19:30:02 +02:00
Steffen Becker eb7f3c731d feat: a comment or change says which line it belongs to
Three pairings, the way both word processors draw them: a bar in the margin
beside every line a change or an open comment covers (ChangeBarLayer), the
reviewing pane's card marked and scrolled into view, and a dashed leader from
the text to that card (ConnectorLayer). The leader is drawn for the one at the
caret only, and dropped where a second pane sits between text and card.

commentIdAt/revisionIdAt compare ranges, not the marks at the caret: both marks
are non-inclusive, so the range a click on a card selects would match nothing.
2026-09-04 19:30:02 +02:00
Steffen Becker 8c080b1d16 fix: a Word index takes the file's own entry styles
A TOC field regenerates its rows from the styles named `toc 1`…`toc 9`, and its
cached rows are skipped on import — so those styles were never seen and the index
fell back to the editor's 0.5cm per level, dropping the file's space-after with it.
Measured on a 60-row index: a 5.1mm row pitch against LibreOffice's 6.9mm.

They now land as Contents 1…10, and the DOCX export writes the rows back under
them instead of the fallback indent.
2026-09-04 19:30:02 +02:00
Steffen Becker 198c16f7ab fix: a Word bibliography no longer imports twice
Word caches a BIBLIOGRAPHY field's rows as a w:tbl inside the field, and
only the field's paragraphs were skipped — so the list arrived once as the
regenerated index node and once as that table. Measured on a 49-page
document: two pages of duplicate. StyleName IEEE now reads as the numbered
style it is, so its citations show [1] instead of the source key.
2026-09-04 19:30:02 +02:00
Steffen Becker f1f9a4df15 fix: a text box survives a paste from another window
The clipboard carries HTML, and in an HTML string any block tag inside a
<p> closes it — so a copied box arrived as loose paragraphs. It is now
serialized as an inline span carrying its blocks as JSON, read back by a
matching parse rule; its plain text stays in the span for readers that
cannot take the attribute back.
2026-09-04 19:30:02 +02:00
Steffen Becker 572e3a760d fix: an AutoText entry keeps a text box
Entries were stored as an HTML string, where any block tag inside a <p>
closes it — so a copied text box, which holds blocks, lost its place in
the line. They now keep the slice's nodes; an entry written earlier keeps
its HTML string, which insertContent takes just as well.
2026-09-04 19:30:01 +02:00
Steffen Becker 8d3139439c fix: the caret reaches the position a text box occupies
A box that starts its paragraph was a trap: there is no text position beside
it, so the browser pulled the caret meant for the box's own place in the line
into the frame and everything typed there landed inside it.

A frame nobody is editing is now an atom to the browser — a contenteditable
node decoration, dropped while the caret is in its text, the frame placing the
caret itself on the click that enters it. ArrowLeft/ArrowRight at the box's
first and last text position step out to it. The decoration walk enters blocks,
without which it never reached an inline box at all.
2026-09-04 19:30:01 +02:00
Steffen Becker f0cf382fb0 fix: only a band-wrapped text box gets the alignment buttons
A box in the line is a character its paragraph places, as a picture is, so the
three frame-toolbar buttons were the paragraph's own alignment under a frame
icon — a second place to look for the same thing. They now show for the band
wrap alone, where nothing else can express the position.

The ordinary command needed one correction to carry that: a selected box is one
node whose range covers its own paragraphs, so aligning it moved the frame and
re-set its text in the same keystroke.
2026-09-04 19:30:01 +02:00
Steffen Becker 060dcd7380 feat: a text box sits in the line of text
textBox becomes an inline node carrying block content, like the frame both
formats write, so "in line with text" really is in the line and a box reaches
a table cell or a list item through their paragraphs. Both exports write it
where it sits (an as-char draw:frame / a wp:inline drawing) and both imports
leave it there, which drops the hoisting machinery on either side.

Two guards the schema cannot express: no box inside a box, and a selection
reaching into a box grows to the box's bounds rather than dissolving it.
2026-09-04 19:30:01 +02:00
Steffen Becker 21999afbc6 fix: a DOCX text box keeps the height it declares
The shape body wrote spAutoFit, which LibreOffice reads as min-height 0
and then draws the box's text detached from the shape, over whatever
follows it — measured on a 90px box: the frame collapsed to one line and
its text landed 22pt below it, on the next paragraph. noAutofit is what
LibreOffice writes for the same frame; with it the text sits inside the
box at its declared height. Content taller than that is clipped, DOCX
having no fo:min-height for the ODF leg's auto-grow.
2026-09-04 19:30:01 +02:00
Steffen Becker 6081a85626 feat: set where an in-flow text box sits across the column
wrapAlign only ever came from a file, so a box inserted here stayed at
the left in both modes that leave it in flow. The floating toolbar now
offers left/centre/right for `inline` and `topBottom` (a side wrap names
the side itself) and drops a coordinate placement when one is picked.

DOCX carried neither: `topBottom` now writes wp:align and an as-char box
the w:jc of its rebuilt marker paragraph, both read back on import. ODF
already wrote both. Measured centred at 297px of a 76…718px column, and
rendered through LibreOffice from .odt and .docx alike.
2026-09-04 19:30:00 +02:00
Steffen Becker fcdd490668 feat: add the DIN 5008 Form A letter template
The norm defines two business-letter forms and only Form B existed. Both
now come out of one builder parameterized by where the address field opens,
27mm for Form A against 45mm for Form B. Measured headlessly: 26.98 and
44.97mm, their info blocks at 31.99 and 49.98mm, the info column at
124.99mm. The gallery draws Form A's address field higher, or the two
cards would be indistinguishable.
2026-08-30 11:51:56 +00:00
Steffen Becker b892accdd5 fix: redraw tab stops when the document is replaced
The measuring pass keeps the layout it last drew as a string key and skips
the redraw when a pass measures the same again. Replacing the document maps
every decoration away, so an identical layout has to be drawn again — the
guard now compares the live decoration count as well. Two documents with
the same tab layout otherwise left every tab on the default grid.
2026-08-30 11:51:45 +00:00
Steffen Becker c115b11c8d fix: draw sub/superscript at 58% and out of the line box
The browser's `smaller` + `vertical-align: super` grows every line holding a
raised run by ~4px. LibreOffice keeps its raised run within 0.25pt of the plain
line's box (measured off its PDF), and both exporters already write
`style:text-position="super 58%"` — only the screen diverged. The same three
declarations already sit on the footnote anchor. Scoped to the paper, since a
header/footer zone carries the marks and its height is reserved off the page.

Measured: a table LibreOffice fits above the page edge now fits here too,
instead of moving to the next page.
2026-08-30 10:06:44 +00:00
Steffen Becker 4aef4d7641 fix: keep rows a cell spans on one page, and clear floats before an in-flow box
A break between spanned rows stretched the cell across the gap: the
merged table drew over the page gap and the next page's header. A
group too tall for a page still breaks per row. An in-flow text box
took the same treatment for floats — CSS lays a block box over one
instead of beside it, which had the vertical frame covering its
neighbour. Both match LibreOffice's render of the same document.
2026-08-30 10:06:44 +00:00
Steffen Becker 104ec2366c fix: suppress the base direction's own edge when reading w:jc
The DOCX import dropped left as the default and kept right regardless
of w:bidi; an rtl paragraph's default edge is right. start/end read as
physical left/right — LibreOffice spells an rtl paragraph's physical
left as w:jc="start" (probed via its own DOCX export).
2026-08-30 10:06:43 +00:00
Steffen Becker 27a8f4a11e refactor: extract the chapter-field resolution into a pure helper
utils/chapterField owns the per-zone reading (header = the chapter at
the page top, footer = the last begun) so the probed LibreOffice
semantics is unit-tested; the DOM measurement feeding atTop stays in
Editor.svelte.
2026-08-30 10:06:43 +00:00
Steffen Becker 9bfee4e0bd fix: keep the note prefix/suffix out of the anchor
The affixes are note-area text: LibreOffice draws its Before/After
around the number in the footnote area only, while the anchor stayed
the bare number. The editor showed them at both.
2026-08-30 10:06:43 +00:00
Steffen Becker f72be2f0e3 fix: resolve the chapter field per zone the way LibreOffice does
The header shows the chapter in force at the page's top (a heading
counts once it opens a page), the footer the last one begun on the
page. Both zones showed the footer's reading before.
2026-08-30 10:06:43 +00:00
Steffen Becker 35059d8126 fix: export plain text boxes as Writer text frames
Without a parent chain to the Frame style LibreOffice read the box as a
drawing object, whose text loses lists, nested images and borders. The
frame takes fo:background-color/fo:border and its writing mode in the
graphic properties; an inline box's centring rides the anchor paragraph.
The import reads the frame spellings back and drops LO's from-top/0
respelling of the default alignment.
2026-08-30 10:06:43 +00:00
Steffen Becker 6d85a88907 fix: keep the text area at the page margins under a page border
ODF lays a page border and its padding out inside the margin, so the
bordered ODT rendered every line narrower than the editor and Word
(offsetFrom="text") and paginated a page long. The export shrinks the
fo:margin-* by border + padding, the import grows them back.
2026-08-30 10:06:43 +00:00
Steffen Becker d9496abe4f fix: write the cell-format picture with the document language's separators
Word reads a \# picture with the reader's regional separators, so a
German Word took "0.00"'s dot as grouping and printed 84 as "084".
The importer now reads a picture back by its shape, whichever locale
wrote it.
2026-08-30 10:06:43 +00:00
Steffen Becker 15049377a0 fix: cache the index rows in the DOCX TOC/INDEX/BIBLIOGRAPHY fields
The three index kinds exported as fields with an empty result; Word for
Mac never updates fields on open, so every index rendered as nothing.
They are now complex fields whose result is the editor's cached entries
(indent per level, leader tab, pages), the form Word itself writes; the
importer recognizes the complex INDEX/BIBLIOGRAPHY form and keeps the
attrs the fldSimple path set.
2026-08-30 10:06:42 +00:00
Steffen Becker c4f32bb380 fix: clear the block after a topBottom frame below its band
A float only pushes lines that intersect it, so a short block (a heading)
slotted into the clearance gap a prior side float opens above the band.
Word and LibreOffice both start the next paragraph below the band; a
page-break spacer and the partner block of a wrapAlign pair are exempt.
2026-08-30 10:06:42 +00:00
Steffen Becker 6144b9fde0 fix: unscale the line-number measurements by the zoom factor
Client rects come back in zoom-scaled screen space while the layer,
the page boxes and the per-page restart all work in document px. At
any zoom but 100% every number drifted by the factor: printed beside
the wrong line, restarts fired on the wrong page, and a mark could
land in the page gap.
2026-08-30 10:06:42 +00:00
Steffen Becker 1c50d2484b fix: line numbering counts as Word does around frames and formulas
A floating frame's band no longer counts as an extra line of its
anchor paragraph, a display formula's stacked boxes merge into one
line (overlap against the smaller height), and a top-level text box
counts as the one empty anchor-paragraph line the file gives it
(probed in Word). The kitchen sink's numbers now match Word's PDF
line for line on equal page content.
2026-08-30 10:06:42 +00:00
Steffen Becker ee984ae8b0 fix: count visual lines, list and column lines in the line numbering
Rects merged by vertical overlap are the lines: a super/subscript,
formula or rotated run no longer opens a phantom line (5 was followed
by 10 one line later). List items and column paragraphs now count, as
both word processors number them; a line pushed past the page surface
is skipped so no number prints into the page gap.
2026-08-30 10:06:42 +00:00
Steffen Becker 9578dea57e fix: watermark and fold marks on first/even pages, border wraps the hf band
The decor shapes now ride every header variant: DOCX gets an empty
w:type=first/even header part for the post-passes to fill, ODT the
same splice into style:header-first/-left. The editor's page border
now starts at the zone's edge distance where a header/footer exists —
both word processors draw the band inside the border (probed).
2026-08-30 10:06:42 +00:00
Steffen Becker 3d305a9920 fix: write STYLEREF by outline level, not style name
Word resolves the quoted style name against the localized built-in
name, so 'STYLEREF "Heading 1"' shows a field error in any
non-English Word. The numeric argument means outline level in Word
and LibreOffice alike (probed), and the importer already reads it.
2026-08-30 10:06:42 +00:00
Steffen Becker 43903319d9 fix: only one w:displayBackgroundShape in settings.xml
The docx package always writes the element; the page-background pass
prepended a second one, breaking the CT_Settings sequence so Word
repairs the file on open — and the repair drops every header and
footer reference. The schema test's DOCX leg now builds with the
page decor, line numbering and fold marks, which covers the pass.
2026-08-30 10:06:42 +00:00
Steffen Becker 92a2421a83 fix: keep the paragraph mark in flow inside a columns section
The always-on absolutely-positioned ¶ pseudo made Chromium give up
balancing the multicol: every block stacked into column 1, while Word
and LibreOffice balance the section end. Static inside .columns-node
(zero width, line breaking unchanged); past a trailing BR the mark is
dropped there so an empty paragraph stays one line.
2026-08-30 10:06:42 +00:00
Steffen Becker 9d6f0bebb9 fix: titlePg only on the section group that begins its header set
A continuous section pushed onto a fresh page by a page break repeated
titlePg, and Word blanks a titlePg section's first page — the running
header, footer and watermark vanished from that page.
2026-08-30 10:06:42 +00:00
Steffen Becker 1660e07873 fix: no letter headings in the exported INDEX field
The \h "A" switch made Word insert an alphabet heading between the
index entries on field update — a row the editor's index never draws.
2026-08-30 10:06:42 +00:00
Steffen Becker e752b4ca10 fix: write an index-entry XE as a complex field, as Word does
Word's field updater splices the document apart around a result-less
fldSimple XE when it rebuilds the INDEX: content after the marker lands
before the heading. Both the body and the text-box path now emit
begin/instrText/end; the importer already reads that form.
2026-08-30 10:06:42 +00:00
Steffen Becker 41a64ad412 fix: no titlePg on a clamped section, no cached result in marker fields
A section reusing the last header set through the clamp repeated
w:titlePg, and with no w:type="first" reference Word blanks that
section's first page — page 2 lost header, footer and watermark.
XE/INDEX/BIBLIOGRAPHY fields cached an empty run the serializer
rendered as a newline inside w:t; they now carry no result at all,
matching how LibreOffice writes an XE field.
2026-08-30 10:06:42 +00:00
Steffen Becker 9a6282fce9 fix: floor a DOCX anchor's paragraph posOffset at one twip
Anchored exactly on the paragraph top (posOffset 0), LibreOffice's wrap
layout derails and paints a neighbouring inline text box's content
outside its shape; LO itself never writes less than a twip there.
Probed by bisecting a broken box against LO's own DOCX export of the
same document. The importer rounds the sub-visible offset back to none,
so nothing accretes on the round trip.
2026-08-30 10:06:42 +00:00
Steffen Becker aac93e22e8 fix: a note opens with a tab into LibreOffice's hanging pair in DOCX
FootnoteText/EndnoteText ride the factory slots with the probed 0.6cm
start/hanging pair, and the note body puts a tab run after the marker,
matching LibreOffice's own DOCX export; Word no longer glues the number
to the text. The importer strips the tab (also behind a custom mark on
a run of its own) and counts an equal start/hanging pair as the stock
note look, so neither a styleName nor an indent accretes on re-import.
2026-08-30 10:06:41 +00:00
Steffen Becker 7ce97b1d50 fix: an RTL block also carries fo:text-align="end" in ODT
style:writing-mode alone leaves the paragraph left-aligned in
LibreOffice; the mode+end pair is what it writes itself (probed), so
both export paths add it where the block sets no alignment of its own,
and the importer suppresses end/right against an RTL block's default —
the pair is its normal look, not direct formatting. The render-
consistency leg now compares under spacing model 'max', the one both
formats express; thresholds re-measured.
2026-08-30 10:06:41 +00:00
Steffen Becker 26593d0a7a fix: suppressed cell borders export as w:val="nil", not "none"
nil is the explicit absence that wins every border-conflict rule in
Word — a cell's none can still lose against table-level borders there,
which kept the black grid over styled tables. LibreOffice reads both
alike, and the importer already maps nil to none.
2026-08-30 10:06:41 +00:00
Steffen Becker 2642fc61c6 fix: a floating text box lands inside the column at first paint
applyWrap computed the right float's margin from the wrapper width,
which fitWrapper only sets once the ResizeObserver has measured the
mounted rotor — at construction it read 0, shoving the box its whole
width off the column until the next node update (a click) re-ran it.
The margin now falls back to the attr size through the rotation math,
and the observer re-applies the wrap after each refit.
2026-08-30 10:05:43 +00:00
Steffen Becker 3f047efca8 fix: a styled table writes no table-level borders
Every cell already writes all four sides itself, and on a styled table
the table-level black default showed in Word, which lets table borders
beat a cell's "none" (only "nil" loses) — LibreOffice lets the cell
win, so only Word drew the grid over the style. Plain tables keep the
default grid via the per-cell fallback.
2026-08-30 10:05:43 +00:00
Steffen Becker 1e88427154 test: semantic package lint, LO style resolution, cross-format rendering
Three legs against the closed-loop blind spot the duplicate-style bug
exposed: package-lint.test.ts checks invariants the schemas cannot
express (unique style ids/names, dangling style/num/rel references,
balanced ranges, manifest completeness); lo-roundtrip gains a
soffice→fodt leg asserting the styles LibreOffice resolves (Heading
chain Arial/bold/sizes, Word defaults); lo-render-consistency.test.ts
renders the kitchen sink from both formats through LibreOffice and
compares pagination and pixels (ratchet thresholds). CI's LO job
gains poppler-utils; test:lo runs both LO files.
2026-08-30 10:05:43 +00:00
Steffen Becker 4295baa284 fix: ODT — define the Comment pool style, no master ref for set-less sections
A comment body references LibreOffice's pool style Comment; the
definition is now written the way LibreOffice re-saves it, so the
package resolves without that pool. A section break with no page setup
of its own no longer writes a master-page reference: the name was
never defined (LibreOffice drops the dangling reference, probed), and
a defined one would force a page break the editor does not make.
2026-08-30 10:05:43 +00:00
Steffen Becker 29a72e143e fix: splice table and numbering styles post-pack instead of importedStyles
The docx package's Styles merge spreads our options over its factory
set, so a passed importedStyles array replaced the whole set —
w:docDefaults (default font, size, language) and the Title/Heading1-6
factory slots included. Every document using a table or list style
exported without document defaults; Word fell back to its own. The
style XML is now spliced into styles.xml by a post-pack pass
(applyRawStylesDocx), and the regression test builds with a table
style and asserts w:docDefaults survives.
2026-08-30 10:05:43 +00:00
Steffen Becker 51f1ab372c fix: route Title/Heading1-6 through the docx factory's override slots
The docx package always emits its own Title and Heading1-6 styles; our
same-id copies in paragraphStyles made styles.xml define them twice, and
both Word and LibreOffice then dropped the basedOn chain — headings
rendered serif, regular, at the raw size. buildStyles now feeds those
seven through styles.default.title/headingN, leaving a single
definition with the chain intact. Regression test asserts unique
styleIds and Heading1's basedOn.
2026-08-27 20:15:44 +00:00
Steffen Becker 3a6bc336f7 fix: schema-valid ODF and DOCX output
ODF: every part declares 1.3 (the version LibreOffice writes; header-first
is a 1.3 element), draw:image gains the mandatory xlink:type trio,
text:time-value becomes an xsd dateTime (import still reads the legacy
duration), index-entry-link-start/-end stay in TOC templates only, and
dont-balance-text-columns moves onto style:section-properties (probed: LO
reads and re-writes it there). DOCX: a final pass re-sorts w:settings into
the CT_Settings sequence, and comments.xml declares w14 mc:Ignorable.
2026-08-26 15:41:12 +00:00
Steffen Becker c566950258 fix: unwrap the imported style XML so w:style lands directly in w:styles
ImportedXmlComponent.fromXmlString returns a nameless wrapper that
serialized as a literal <undefined> element around every table and
numbering style. LibreOffice skips the unknown element; Word declares
styles.xml unreadable and rebuilds it with its own defaults, so the
recovered document also lost the 0-spacing Normal style. Regression
assertions on both DOCX style paths.
2026-08-26 14:28:53 +00:00
Steffen Becker 525c95f3a8 feat: add Numbering 1.a.i, the default cycle as a preset
The gallery avoids presets the dropdown plus default nesting already
covers, but the default cycle itself earns its slot: once a list wears
another style, switching back needs a style to switch to. No sheet
version bump — an added built-in survives the merge on its own.
2026-08-26 14:03:55 +00:00
Steffen Becker 6f57a8e7ed feat: level-varied preset gallery for the list-style built-ins
The uniform LibreOffice definitions duplicated the dropdown formats plus
default nesting. The gallery now ships patterns the depth cycle cannot
produce: the outline chains Outline I.A.1 / A.I.1, the kind-mixing
Numbering with Bullets and Checklist, and the Diamond Bullets ladder,
all on the plain 1.27cm step. STYLE_SHEET_VERSION 12 swaps stored
built-ins while user styles survive.
2026-08-26 09:58:33 +00:00
Steffen Becker 697eea4d86 fix: a list style's level decides the depth's kind
effectiveListLevel resolved a level only for its own node species, so a
Tab-nested <ul> under a number level kept cycle bullets while the named
ODF definition (and LibreOffice) numbered it. The level's kind now wins:
the walk numbers a <ul>/bullets an <ol> (CSS keys generalized), both
exporters resolve clones and forks through eff.kind (new ODT pass
applyListLevelKinds switches L# level species), and setListStyle retypes
the subtree. Node marker attrs still override with their own species.
2026-08-26 09:21:45 +00:00