Commit Graph
101 Commits
Author SHA1 Message Date
Steffen Becker c34299ca73 test: a browser run over the pagination and the editing invariants
npm run test:dom opens a corpus document in headless Chromium and holds
its page count against a settle, a reload, a zoom, a page break and its
undo, plus the unsaved dot — the components the vitest suite cannot see.
The smoke run now shares its boot with it (tests/browser.mjs).
2026-09-04 19:30:13 +02:00
Steffen Becker ad7b840662 fix: a picture pasted from a web page is fetched into the document
It came in by its URL, showed from the web and was dropped without a word
by every export. It is now fetched into a data-URI right after the paste,
sized like a picked file; one the site withholds is removed with a message.
2026-09-04 19:30:12 +02:00
Steffen Becker cb0db7dd96 fix: a reload keeps the document protected, and the first save asks for the password
The password itself is never stored, so a reload left the document open and
the next save wrote the protected file unencrypted without a word. The fact
of the protection now survives; the save dialog asks for the password again
or lets the protection go, and backing out cancels the save.
2026-09-04 19:30:12 +02:00
Steffen Becker 759759a997 test: the cross-format leg compares the look, not only the words
The corpus leg out of one format and into the other compared block types and
text, so anything the trip dropped below that passed. It now compares every
attribute each node carries, with the values a format states in its own units
excluded: proportional vs absolute column widths, and a formula's inline flag,
which ODF has no place for.

Two the stricter leg caught: a note lost the paragraph style it names and fell
to the stock 10pt, and a link stored an attribute equal to its default.
2026-09-04 19:30:11 +02:00
Steffen Becker bc87912e5e docs: the page-start spacing switch reaches the space-above rule 2026-09-04 19:30:10 +02:00
Steffen Becker 43d9a864d4 docs: the band is measured, a manual break keeps its space, headings number
Corrects two probed claims that measurement contradicted: a hard page break
does *not* swallow the block's space above (only an automatic one does), and
the header band is max(declared height, content + gap) rather than a line
count. Adds the chapter-numbering model, the mirrored master pair, and the
harness ceiling a CSS-drawn label is.
2026-09-04 19:30:06 +02:00
Steffen Becker 583bc0ddd8 fix: a list number wider than the hanging indent pushes the text
The marker pseudo is floated into the indent instead of absolutely
positioned, so a label the indent cannot hold — a "1.1.1." outline number,
a Roman numeral — moves the first line right the way both word processors
do, rather than printing over the words it labels. Out of flow either way,
so no list line grows.

The PDF clone's injected marker follows suit, and its chain now reads the
effective numbering and walks to the outermost multilevel list: a list
numbered by a named style had none of its own and printed a bare separator.
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 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 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 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 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 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 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 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 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 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 5034ac7d6f docs: add an editor screenshot to the README
docs/screenshot.png sits right before the Features section, linked to
the app. Plain Markdown image syntax, so the VS Code preview renders
it too; the title bar's file name is painted out.
2026-08-20 18:59:53 +02:00
Steffen Becker 6bbf4286c3 style: the floating-island chrome is dev-only in the menu
The appearance menu's "Modern (beta)" entry sits behind
import.meta.env.DEV, so a production build offers no way into the
island chrome until it catches up with the ribbon. The chrome itself
and a stored 'modern' pref keep working.
2026-08-20 14:56:48 +02:00
Steffen Becker 10b650c64c refactor: the floating island's mode id is 'modern', matching its label
The id said 'classic' while the UI called the ribbon Classic and the
island Modern (beta). Type, i18n keys, comments and docs now agree;
a stored legacy 'classic' still selects the island.
2026-08-17 20:05:15 +02:00
Steffen Becker 501bfe501f fix: a first visit opens the ribbon, not the floating island
The tabbed chrome is the layout users know; the stored choice still wins.
2026-08-17 20:03:06 +02:00
Steffen Becker 847fe59d3d feat: a frame the text runs through reserves no space at all
Word's in-front-of / behind-text (wp:wrapNone + behindDoc) and ODF's
style:wrap="run-through" both read as a side float, so a near-column-wide
picture reserved its whole height and pushed the text 23-52mm down. They
are a wrap mode of their own now: out of flow at the static position it
was anchored at, z-order from the file (inFront). Both exports carry it
back; tests/unit/wrap-through.test.ts covers the round trip.
The image fixture 72 -> 38 over both formats and 7 pages -> 6, matching
the reference; corpus 1108 -> 1074, no other fixture moved.
2026-08-17 13:45:39 +02:00
Steffen Becker a62fa11cc1 feat: the index options menu switches its page numbers
References ▸ Options, where the depth already lives: Word's "Show page numbers"
as a checkbox, over the pageNumbers attr both formats now carry. The menu is no
longer only levels, so its button says Options.
Its node view repainted on the entries alone, so nothing an attr changes ever
reached the rows — leader, title and level styles included. Keyed on the look
as well now, minus the cached entries the pass writes back itself.
2026-08-17 00:09:48 +02:00
Steffen Becker 6c731b9233 feat: insert caption is in the picture and table tabs too
References keeps it, as in both products, but that is a tab away from the
object being captioned. The contextual tabs mount their own CaptionDialog —
it reads the caret for category and position, so the copies take no props.
2026-08-16 22:22:29 +02:00
Steffen Becker 2d3395137f style: the status bar follows the chrome above it
Under the ribbon it takes the same --w-* remap .ribbon gives its pickers: the
app's dark neutrals are bluer than Word's, so the two ends of the window did
not match. Classic keeps the themed set. --color-primary is the brand slate
now, so both chromes share one accent.
2026-08-16 14:19:17 +02:00
Steffen Becker b3e5a1c8a9 style: the ribbon takes the logo's colours
--w-accent is the logo's slate instead of Word's blue (5.1:1 on the chrome,
carries white), and the clay marks the contextual tabs — the pair meets where
those sit beside the normal ones. Both defined once from favicon.svg; the
label tone is darkened, the clay itself only 3.3:1 on white.
2026-08-16 14:12:30 +02:00
Steffen Becker bc98446198 docs: table-style import re-derives regions and unbakes the region text 2026-08-16 13:43:08 +02:00
Steffen Becker 6397594d35 docs: how to hunt the bugs the suite cannot see
Coverage names the untested surface, a pageerror collector reports what
nobody predicted, and semantics beat crash-detection — plus the false
alarms the probes themselves produce.
2026-08-16 07:12:59 +02:00
Steffen Becker 41ab0ef06c feat: two panes on one document, scrolled apart
View ▸ Split (Ctrl+Alt+S): a second live view of the editor's state, stacked
below the first and editable the same way. The panes share a width, so the
second renders the pagination the first measures — pageBreaks, columnsFlow
and tabStops skip the pass on a split pane rather than run it twice.

Every widget decoration now builds its DOM in a function: two views render
one decoration set, and a shared node is taken back and forth between the
two documents until the tab locks up (measured on a two-page document).
2026-08-15 13:15:37 +02:00
Steffen Becker 503c9821d6 feat: a freeform drawing keeps its own outline
A polygon, a polyline, a curve or a connector's elbow used to be dropped
with a warning; it now rides the box as an SVG path in the 0…100 box a
preset's points use, and the editor draws it. Read from all four dialects
the two products write (ODF points and svg:d, a non-primitive
enhanced-path, DrawingML custGeom, VML's reversed-case path), written
back as enhanced-path and custGeom — LibreOffice renders both exactly as
the editor draws them. An open outline is stroked, never filled.
2026-08-15 12:09:12 +02:00
Steffen Becker 9be1578b97 feat: a formula cell prints a currency and a date too
Both come from the document's language, read off Intl: the symbol's side
and spacing, the short date's order and padding. Each minted data style
names that language — without it LibreOffice prints its own locale's
separators (probed: a German document's 1.234,00 € came out $1,234.00).
A date is a serial day from LibreOffice's day 0, 1899-12-30. Word's
formula dialog offers no date, so the DOCX leg uses the field's \@
picture. Also drops a document's name from three older comments.
2026-08-15 11:44:43 +02:00
Steffen Becker c800618299 feat: a text box can run its text top-to-bottom
Word's Text Direction and LibreOffice's, on the frame's own toolbar: the
browser lays the vertical flow out, so the box needs nothing but the mode.

ODF keeps it in the frame style's *paragraph* properties — probed: in the
graphic properties LibreOffice drops it, there it keeps it and lays the box
out — and Word in the shape body's `vert`, the VML fallback's
`layout-flow:vertical` read too. Both survive a LibreOffice re-save.
2026-08-15 11:13:46 +02:00
Steffen Becker eda134f162 feat: a formula cell prints its result in its own number format
The Formula dialog gains the number format Word's has: whole numbers, two
decimals, grouped thousands and the two percentages beside the general
one.

ODF keeps it on the cell *style* as a reference to a data style — odf-kit
shares one cell style across every cell, so a formatted cell gets a copy
of it — and LibreOffice writes both back unchanged. Word keeps it on the
field as its \# switch; LibreOffice's DOCX filter neither reads nor writes
that switch, so that leg holds through our own round trip.
2026-08-15 10:53:30 +02:00
Steffen Becker dab1d662c9 feat: the table sort gets its three keys and a sort type
Both dialogs offer three sort keys and let you pick the type; only the
first key was offered, and the type was always the detected one. A later
key decides only where the ones before tie, and the sort stays stable.

Alphanumeric is the plain collation ("10" before "2") — that is what
choosing it over the detected type is for, so the two collators differ in
`numeric`. Sorted numerically, a cell that is no number sorts last either
way, as an empty cell does in a spreadsheet.
2026-08-15 10:12:24 +02:00
Steffen Becker 0ec35b0168 feat: each page is centred, and a section can pick its own paper
A section narrower than the reserved sheet now splits the difference
between its insets instead of hanging it all on the right, and every
per-page layer draws from the box's own left — sheet, header/footer, page
border and line numbers. The ribbon's Size menu gains the same per-section
block Orientation already had.
2026-08-15 09:52:43 +02:00
Steffen Becker bf564fbab4 fix: footnote counting per page/chapter and collecting at the end really work
Both were selectable in the dialog and changed nothing — they only rode the
file formats, so an imported document numbered and placed its notes wrong.
noteLabels now counts within a bucket (class, chapter, or the page pageBreaks
publishes through setNoteAnchorPages); a collected footnote leaves position:
absolute and opens its own page, as LibreOffice does. Counting and position
move to the footnote tab, the only place LibreOffice offers them.

The separator's own alignment moves the line alone — probed, notes stay left.
2026-08-14 23:11:10 +02:00
Steffen Becker ba827b4ce9 fix: a footnote keeps its place at the page foot after an edit above it
The placements key watched each note's top but not its doc position, so an
edit that shifted the note section without moving a note left the decoration
set naming stale positions — ProseMirror dropped it and every footnote fell
back to its static spot, stacked on top of the next one. The position now
rides the key, resolved once beside the top.
2026-08-14 22:40:42 +02:00
Steffen Becker 46165b82f7 fix: a caption is boxed to its frame, and the frame stays clickable
Two faults on one picture. A block after a float keeps its box over
the float — only its lines move — so the caption on top swallowed
every click meant for the picture; the frame is lifted above the
in-flow boxes. And a caption can only sit under a side-wrapped frame
if it is boxed to it, so its span in the column is measured and
becomes the caption's indent, which also drops it clear of the float.
Measured at 100% and 150%: caption under the picture, its width, in
all four wrap modes.
2026-08-14 22:40:42 +02:00
Steffen Becker 8d866e4f1a fix: a table popover opens where it fits, in both chromes
The three dialogs placed themselves, so the ribbon's viewport
coordinates met CSS that hangs the box above its anchor: the
formula popover landed under the band, clipped. Each caller now
owns the placement — `anchored` in the ribbon, like every other
menu there, and a wrapper in the editor that clamps a tall one
into the scroll area instead of letting the overflow cut it.
2026-08-14 22:11:49 +02:00
Steffen Becker 3b24b14470 feat: lines and arrows, which both importers used to drop
Two endpoints rather than a box: no text, no fill, and the diagonal of
the frame with `flipV` picking which one. The three kinds share their
preset names, since the heads a file declares are what tell them apart.

ODF gives a line its own element and its heads a named marker, which
needs the draw: prefix declared in styles.xml — undeclared, the file
does not parse at all. Word keeps the frame and flips it.
2026-08-14 22:11:49 +02:00
Steffen Becker 147342655d feat: sort a table, sum a column, recognise a number
The formula rides the cell, as ODF writes it; Word's field syntax is what
is stored, since it is the wider language. `ooow:` in the ODF attribute
value is a namespace prefix — undeclared, LibreOffice reads it as part of
the formula and prints "Expression is faulty".

Sort takes one key and detects the type per cell where both dialogs make
you pick. Number recognition is off by default, as it is in LibreOffice,
and a formula parses a cell's text either way — exactly as there.
2026-08-14 22:11:49 +02:00
Steffen Becker 639fac90fd feat: a section can have its own paper, landscape amid portrait
The page grid was one repeating cycle, so every page had to be the same
size. It is now a table of height runs the placement pass grows as each
section's first page becomes known, and the sheets are one box per page
instead of a repeating gradient — .paper reserves the widest section's
paper, a narrower one takes the difference as a right inset.

Layout ▸ Orientation offers "this section"; the size stays document-wide
in the ribbon but round-trips per section from a file.
2026-08-14 10:36:52 +02:00
Steffen Becker 4cda289272 feat: a shape can be a triangle, a star or a block arrow
Twelve presets beside the three rectangular ones. Each outline is one
polygon in utils/shapes.ts, given once: the node view draws it as an SVG
path, the ODF export scales the same points into draw:enhanced-path, and
Word gets the preset's name and draws its own geometry.

A gallery in the box toolbar and the ribbon renders its tiles from that
same table, so a new shape needs no icon. Verified through LibreOffice
from both formats; a preset we can't draw still imports as a warning.
2026-08-14 08:02:08 +02:00