Commit Graph
703 Commits
Author SHA1 Message Date
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 1424d7c23f docs: note that Word puts the endnote list on the last page
LibreOffice opens a fresh page for it and we follow, so a Word-made PDF
of a document with endnotes is one page shorter — measured by rendering
one .docx in both.
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 7733b53068 test: add the Word re-save of the formula fixture
Completes corpus/word/: all fourteen documents now carry a Word-written
twin.
2026-08-30 10:06:44 +00:00
Steffen Becker bc3c25ece1 test: take Word re-saves of the corpus as a third author
corpus/word/ holds each document saved again by Word — rsids,
proofErr, separator notes, theme docDefaults — asserted to read like
the docx-lib original and survive our round trip. The formula fixture
is reauthored: a hand-wrapped MathNumerator doubled m:num/m:den, which
Word refuses to open, and the radical gets a visible degree.
2026-08-30 10:06:44 +00:00
Steffen Becker c19d082099 test: grow the corpus by marks, footnotes, images and formulas
Four new themed documents (11–14), authored by the docx lib with
LibreOffice ODT twins like the rest; make-fixtures takes a regex to
regenerate a subset without rewriting the committed files. The twins
need libreoffice-math — without it soffice drops every formula
silently.
2026-08-30 10:06:43 +00:00
Steffen Becker 6fa05fa097 test: cover the status bar's text statistics
countText: word/char/paragraph counts, block boundaries as word
breaks that count as no character, atoms as no text, ranges.
2026-08-30 10:06:43 +00:00
Steffen Becker 2ac8d58603 test: a page border must not move the rendered text
Renders the same document with and without the border through
LibreOffice and compares every word box — pins the fo:margin carve-out
at render level, where the round trip is blind (both sides symmetric).
2026-08-30 10:06:43 +00:00
Steffen Becker b1b33daca5 test: fuzz named list styles
The generator assigns a builtin (node species following the style's
level kinds, override-, start- and marker-format-free — each of those
drops the name by design) and no longer pairs textAlign right with an
rtl paragraph, where it is the suppressed default. Found the w:jc
suppression fix the commit before this one.
2026-08-30 10:06:43 +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 78e8b81bf7 test: pin the fixes the round trip alone could not see
The margin carve-out under a page border, the bare note anchor and a
Writer frame keeping a list and a nested image through an soffice
re-save each get an asymmetric assertion — a symmetric export/import
pair had hidden all three.
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 43828d0d70 chore: drop a leftover scratch test 2026-08-30 10:06:42 +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 dc06d88299 test: own soffice profile for the render-consistency leg
Run in parallel with lo-roundtrip, both soffice instances raced for
the shared user profile lock and the conversion died intermittently.
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 845ad4a881 docs: changelog — unreleased list styles, letter templates, export fixes
Adds the Unreleased section: named list styles, the template gallery
with the two letters and fold marks, the schema-validity and duplicate
heading-style fixes, and a design note on the Liberation/Microsoft
metric font pairs the exports translate between.
2026-08-27 20:55:45 +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 742b1cc2b0 test: kitchen-sink fixture + schema validation of both exports
tests/kitchenSink.ts authors one deterministic document using every
feature; schema-validation.test.ts validates its .odt against the ODF 1.3
RelaxNG and its .docx parts (after MCE resolution, as Word reads them)
against the ECMA-376 transitional XSDs — the structural half of Word's
strict reader, which LibreOffice silently forgives. Schemas vendored under
tests/schemas/ (sources and offline patches in its README); self-skips
without xmllint, which CI now installs.
2026-08-26 15:42:31 +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