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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
The preview drew a fixed three lines, and the toggle's label ("Show all
levels") read as a preview control when it switches chained numbering.
Preview now renders one line per defined level and scrolls past six;
the label says "Multilevel numbering (1, 1.1, 1.2.1)", matching the
numbering dropdown's notation.
The cursor-follow effect read the sheet and the editor state, so every
edit (sheet replaced + FORCE_PAGE_RECALC) re-ran it and reset all four
tabs' selections to the cursor's style or the first entry. Untrack the
body; only the dialog opening triggers it now.
setListStyle assigns on the outermost list, clearing the subtree's direct
marker attrs (applying a Listenformatvorlage replaces direct numbering).
Both chromes' bullet/numbering dropdowns grow a "List styles" section split
by the style's level-1 kind, plus a manage entry; the style manager gets a
fourth, flat tab with a per-level editor and a three-level preview. The
decoration now also sets data-bullet — editor.css keys the var()-reading
marker rule on it, so a style's bullet paints (verified live, headless).
One shared abstract per style (each list its own w:num instance, so numbering
restarts); a post-pack pass injects w:styleLink and the numbering style's real
numId, which the docx library cannot express. Import follows w:styleLink and
w:numStyleLink — a linked abstract used to resolve to {} and silently import
as a bullet list — maps the style onto listStyleName + sheet.list, and derives
no per-level attrs. Probed: LO ignores DOCX numbering styles both ways (mints
WWNum# on import, flattens to the look on export); ODF carries the identity.
Fourth style family (LibreOffice's Listenformatvorlagen): per-level model in
styles/listStyles.ts, five probed LO built-ins, listStyleName on the outermost
list, one effective-level resolver shared by the decoration walk and the export.
ODF writes the full named text:list-style into styles.xml and repoints
unoverridden lists at it (overrides keep the resolved automatic clone);
import maps named list styles back to the attr + registry, attr-free.
Probed: LO keeps the named reference and display-name across a re-save.
Cover letter, CV, invoice and meeting minutes as builder modules on a shared
builders.ts (JSON helpers + the 45mm window-envelope arithmetic), localized
en/de. They sit in the registry's SHELVED list: compiled and schema-checked
per locale by tests/unit/templates.test.ts, but not offered in the gallery
until their content is settled. Gallery gains per-template thumbnails.
The selected-node highlight is color-mix(), whose computed value
serializes as a color() function html2canvas cannot parse — the whole
capture aborted. The export clone now strips selection highlights
(which should not print anyway). Reproduced and verified headlessly.
Two lines above the address (DIN's Zusatz- und Vermerkzone, filled
bottom-up) share their rows with the top of the info block, which now
starts at the norm's 50mm; the lower line carries a notation
placeholder (e.g. registered mail). Measured headlessly: info block
at 50.0mm, notation 54.8mm, address zone from 59.7mm ending 79mm —
inside the window.
Same window-envelope geometry as the Form B letter (45mm address
field, fold marks), but no info block: a three-line sender block on
top and a right-aligned place/date line instead. The gallery thumb
swaps the info column for a date line on this card. Positions
verified headlessly: return address 44.8mm, recipient 49.8mm, date
flush with the right margin.
Both edges help fold the sheet; the punch mark stays left, where the
pages are filed. The exports mirror them as EdenFoldMark<N>R lines
(page width from the ODF page layout / the DOCX page size), the
importers already match on the shared name prefix. Verified headlessly:
5mm from either edge at 87/192mm.
The return-address paragraph gets fontSize 8pt so its line box is 8pt
tall (a 12pt paragraph mark pushed the address zone 1.6mm low), and an
info line with no address half indents 10cm instead of leading with a
tab (a leading tab stays on the default grid). Measured in Chromium:
return address 44.97mm, address zone 49.98mm, info column 124.98mm,
fold marks 87/148.5/192mm.
lib/templates/: registry + one module per template, localized via t()
at build time. applyTemplate (App.svelte) reuses the extracted
resetDocumentState() from handleNew, then assigns the template's
margins, styles and fold marks; no file handle, so Save asks where.
Gallery dialog reachable from the ribbon File menu and the modern
chrome; first template is a DIN 5008 Form B business letter built from
placeholder fields, with the address field at 45mm and a 10cm info tab.
edentext-fold-marks flag: FoldMarkLayer draws the 87/192mm folds and the
148.5mm punch mark per page inside .paper, so print and PDF include them.
Toggled in the Layout tab. Exported as named EdenFoldMark* lines in the
master page's header (ODF page-relative draw:line, DOCX VML v:line);
both importers read the names back into the flag. Roundtrip Leg 32.
An inline atom drawn gray as ‹label›: a click selects the whole field,
typing replaces it, Tab/Shift-Tab jump between fields. Exports to ODF
<text:placeholder> (PLH sentinel U+E022) and a DOCX w:sdt content
control tagged edentext-placeholder; both importers read it back, the
export-minted gray stripped. Covered in roundtrip Leg 31 + fuzzDoc.
A file opened without a File System Access handle (no API — Firefox,
Safari, Brave — or a template) is no longer recorded: nothing could
ever reopen it, so every click failed and dropped the entry. A startup
prune clears such stale entries. Reopening now asks for 'read' instead
of 'readwrite' (Save re-prompts on its own), and a declined permission
prompt keeps the entry — only a moved or deleted file drops it.