883 Commits
Author SHA1 Message Date
Steffen Becker fb400798d1 fix: a ribbon button is no wider than its label's longest line
A wrapped label kept the full 78px it was allowed to wrap at, so the
two-word labels of Review and References made every button there 90px wide
against 40–70px elsewhere. One- and two-word labels now hug their longest
line — a longer one keeps the cap, or it would break into a third line and
run out of the band. The split button's arrow follows the narrower label
instead of overlapping it, so each half tints only its own surface.
2026-09-04 19:30:04 +02:00
Steffen Becker 71fc8ef942 fix: the Shape Format group lines up and its menu opens above the toolbar
The shape trigger stood 34px tall next to the 28px colour pickers, so its
caption sat a line below theirs; every captioned picker now shares one 32px
box and the border-width field centres on it. The shape menu was absolute
inside the band, which clips its overflow and stacks below the floating box
toolbar, so it is placed from the button's rect in the top layer, as the
colour picker already does. Its caret is the same chevron as theirs.
2026-09-04 19:30:04 +02:00
Steffen Becker cdecbee234 feat: the password fields have an eye that shows what was typed 2026-09-04 19:30:04 +02:00
Steffen Becker b3b8e58bd4 test: LibreOffice opens what we encrypt, and we open what it encrypts
Five legs in tests/lo-password.test.ts, driven through the repo's first UNO
helper because --convert-to cannot pass a password; self-skips without soffice
or python3-uno and runs with npm run test:lo. The probed format details live in
docs/architecture/encryption.md.
2026-09-04 19:30:04 +02:00
Steffen Becker 2a20f1d107 feat: a .docx can be saved encrypted too, and an encrypted one opened
Writes agile encryption with Word's own parameters (AES-256-CBC in 4096-byte
segments, 100000 SHA-512 rounds, HMAC integrity) into a compound file built
here — cfb.ts, MiniFAT and DIFAT included. Reads that and the older standard
encryption LibreOffice writes, whose package is AES-ECB. Verified both ways
against LibreOffice.
2026-09-04 19:30:04 +02:00
Steffen Becker 5856de6c2d feat: a password can be set for a document, and one is asked for on opening
The File menu of both chromes protects the document; the password reaches the
two write primitives in saveFile.ts, so .odt, .docx and both template kinds are
encrypted after every post-processing pass. Opening a protected file asks until
it fits or the user gives up, before any importer reads the archive. The
password is session state and never persisted; the autosaved copy stays plain.
2026-09-04 19:30:04 +02:00
Steffen Becker 4b163e6d10 feat: a document can be saved encrypted with a password (ODF)
Writes LibreOffice's whole-package form: AES-256-GCM keyed by Argon2id over
SHA-256 of the password, the deflated package as one encrypted-package entry.
Reads that plus the per-entry AES-CBC/GCM form with PBKDF2 that every earlier
version writes; the nonce sits ahead of the ciphertext, which no manifest says.
Blowfish and unknown algorithms report an unsupported-encryption error.
2026-09-04 19:30:03 +02:00
Steffen Becker d273c04f9c feat: a split button's arrow is bigger where its menu is the point
The caret half of the Show comments and Show revisions buttons was a 10px glyph
in the dim grey, hard to aim at and easy to miss, though the menu it opens is
the half of that control used most: there it is 16px scaled past its box, in
the text colour, and sits against the label rather than at the button's far
edge. Each half now answers its own hover a step past the wrapper's tint, so
the pointer says which of the two it would hit.
2026-09-04 19:30:03 +02:00
Steffen Becker df3690f634 fix: every big button's icon sits on the same line
A group aligns its controls to the top: centred, a two-line label made its
button taller and pushed every one-line neighbour's icon out of the row. A run
of small buttons keeps its centred place — it joins no icon row, and at the
top it would hang over the gap below it.
2026-09-04 19:30:03 +02:00
Steffen Becker a589c4ab78 style: the ribbon's group labels are a size smaller
10px instead of 11px, closer to Word's own footer. The 13px line height and
the foot's own height stay, so the band's 84px is untouched.
2026-09-04 19:30:03 +02:00
Steffen Becker 90a1a03fc6 fix: a big ribbon button's label wraps onto two lines
Capped at 78px, so "Display for review" costs the group a column of width
instead of one of the widest buttons in the band. The caret moves up beside
the icon to free that second line, which lets the split button drop its
stacked layout and read as a row again.
2026-09-04 19:30:03 +02:00
Steffen Becker de71830922 feat: a comment or a change gets a balloon beside its own line
ReviewMarginLayer draws the cards in a strip right of the page, at the height
of the line each belongs to, with a dashed leader running through the gap
under it. A balloon is pushed down by the one above (balloonStack, measured
per card, unit-tested) and the column belongs to its page, so it never runs
past the sheet. The strip is only reserved where the margin has something to
show; the smoke test guards a card's content against its box.
2026-09-04 19:30:03 +02:00
Steffen Becker 4b66631b56 feat: the Review tab picks how much markup is shown and where
One button per kind shows or hides it and its caret says where its cards go —
balloons or the pane, which is why the two pane toggles are gone. Display for
review carries Word's four modes beside them, and the accept / reject / walk
pairs sit under each kind. The PDF path takes the mode as page attributes, so
a printout of a page in no markup reads as the page does.
2026-09-04 19:30:03 +02:00
Steffen Becker 01b8a97404 refactor: one card builds a comment or a change in either review view
CommentCard and RevisionCard hold what the pane rows carried inline, so the
margin balloon can show the same thing. The comment card gains the reply box
its thread needs and moves edit / resolve / remove behind a ⋯ menu that stays
in the flow — a balloon clips a floating popover, and three buttons in a row
cost it two lines.
2026-09-04 19:30:03 +02:00
Steffen Becker 6d5db7135b feat: one reading of the review view for every layer that draws it
storage/markup holds the display mode (all, simple, none, original) plus, per
kind, whether it shows and where its cards go; markupView folds that into what
the page acts on. Bars, leader and both panes ask reviewItems instead of the
document, the page takes the hiding attributes, and trackChanges drops its
author decoration where the mode shows a finished text.
2026-09-04 19:30:02 +02:00
Steffen Becker f338aec89c feat: a comment carries the answers to it
The thread rides the comment mark's attrs, so two runs of one comment still
merge back together and no second store is needed. ODF writes each answer as
its own annotation over the same range, naming its comment in
loext:parent-name; Word writes its own w:comment tied by w15:paraIdParent in
commentsExtended.xml (both probed). Either importer folds the answers back
into the comment they belong to, and the printed list carries them indented.
2026-09-04 19:30:02 +02:00
Steffen Becker f51e3377e6 fix: a printed comment says which page it is anchored on
The list quoted its text but named no place, so on a five-page printout the
entry was a needle. It now leads with the page, the format LibreOffice prints
its own end-of-document comments in. Only the raster paths can say it — they
print the editor's own pagination, the vector one re-paginates in the browser.

commentsHeading gives way to commentLabels, since the page needs a word too.
2026-09-04 19:30:02 +02:00
Steffen Becker 29433e1b1d feat: a switch decides whether a printout carries the review markup
The Review tab's "Print markup", on by default and stored only when off
(edentext-print-markup). Off, the raster paths drop the bar layer from the
clone and skip the comment pages, the vector path emits neither the markup CSS
nor the list.

printPdf rebuilds its options field by field, so it never saw commentsHeading
either — the printed list was headed in English whatever the UI language.
2026-09-04 19:30:02 +02:00
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 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