Commit Graph
576 Commits
Author SHA1 Message Date
Steffen Becker 6987eda4fc fix: a link keeps its own paint over a baked region color
The editor's a-rule and LibreOffice's Internet Link style both outrank the
region CSS, so neither export lets the baked region color override a
non-plain link's paint: ODT leaves the color off the link run, DOCX paints
the link blue unless the run carries a direct color — the DOCX import
otherwise read its own link back as plain.
2026-08-16 13:42:30 +02:00
Steffen Becker 4c55e9b9f5 fix: imports re-derive a registry table style's regions and unbake its text
Both importers resolve the table's style name + look + grid position through
resolveTableCell: the region attr comes back on each cell, a bold region's
runs convert like headings (boldByDefault) and a run matching the region's
color loses that mark — the style's own text no longer lands as direct
formatting on reopen. A DOCX file carrying its own w:tblStylePr keeps the
baked areas instead; a style name not in the registry stays untouched.
2026-08-16 13:42:24 +02:00
Steffen Becker c473fd5f0f test: fuzz round 7 — indexes, table styles, frame pairs, vertical boxes
The generator now emits tableOfContents nodes of all five families, styled
tables painted through the real resolveTableCell (name + look + materialized
fill/borders), side-by-side topBottom image pairs (wrapAlign), vertical-text
boxes and the Caption paragraph style. normalize learns the index attr
defaults the DOCX TOC field has no switch for.
2026-08-16 13:17:47 +02:00
Steffen Becker 6789377b48 fix: DOCX carries the citation style and an all-off table look
The b:Sources part is also the citation style's only carrier, so a document
whose bibliography index sets a non-default style writes it even with no
source cited. The importer keeps a declared all-off w:tblLook ('') instead
of dropping it back to the default look.
2026-08-16 13:17:47 +02:00
Steffen Becker 6a36b89c18 fix: ODT indexes and shape boxes read back clean
The TOC importer no longer fabricates settings our own export mints: a right
stop at the column's end is the attr's default (null), and an entry template
resolving to the default style names no levelStyles entry. A custom-shape box
keeps its vertical text (the writing-mode read only ran for plain frames),
and a declared all-off tableLook ('') survives instead of reverting to the
default look.
2026-08-16 13:17:38 +02:00
Steffen Becker 4c11a527c9 test: fuzz round 6 — columns, named styles, floating boxes, table extras
The generator now emits columns sections, Title/Subtitle/Quotations
paragraphs (heading-safe runs), floating text boxes with offsets, and
table margins, cell borders, cell padding and formula cells.
2026-08-16 12:50:15 +02:00
Steffen Becker db3fbe32c1 fix: DOCX sections, box offsets and cell margins round clean
- a continuous break that only encodes a columns region no longer
  fabricates a section break or its own header/footer set
- a topBottom text box keeps its x offset (posOffset now written)
- cell margins read back in hundredth-cm, so twips don't show through
- a table margin's zero side stays the default instead of -0
2026-08-16 12:50:10 +02:00
Steffen Becker d32d40be3c fix: named styles and floating boxes read clean through ODT
- what a block's named style supplies (Title's centring, keep-with-next,
  its size as a raised run's percent reference) is direct formatting no more
- a topBottom box keeps its left place instead of gaining a fabricated
  centring; horizontal-pos right reads back as the box's wrapAlign
- a table margin's zero side stays the default instead of an explicit 0
2026-08-16 12:50:04 +02:00
Steffen Becker 5bf470f4b7 test: fuzz round 5 — revisions, fields, citations, char styles, floats
Generator gains insertion/deletion marks, sequence/index/date-time fields,
citations (one record per identifier), builtin character styles (redundant
direct marks dropped — import suppresses them like heading defaults) and
floating images (wrapDist only beside a side wrap). normalize canonicalizes
revision ids/dates like comments, recounts sequence numbers per category,
and drops an auto date field's value and a citation's derived label.
2026-08-16 12:07:22 +02:00
Steffen Becker 205ae72ecf fix: DOCX closes round-5 gaps — XE quotes, linked revisions, box features
An XE field's literal quote is backslash-escaped (Word's own escaping;
import already unescaped it) instead of flattened to an apostrophe. A
w:ins/w:del inside w:hyperlink imports (shared handleRevision closure).
Text boxes carry charStyle (w:rStyle), index entries, citations,
date/time fields and revision marks (w:delText for deletions);
dateTimeFieldParts/xeInstr are shared with the body path.
2026-08-16 12:07:14 +02:00
Steffen Becker afec07579a fix: a deletion's text keeps its formatting through ODT
The deleted runs are hoisted through every serialization pass and spliced
into the tracked-changes registry as styled runs (as LibreOffice stores
them); import converts them back as real inline content. One point marker
per change instead of one per fragment. A deletion's marker also no longer
makes a formula beside it display (aloneInParagraph counts text:change).
applyComments now declares dc:/loext: only when absent — beside a revision
the duplicate xmlns:dc made content.xml invalid.
2026-08-16 12:07:07 +02:00
Steffen Becker 1c5b6d73d5 test: fuzz round 4 — boxes, formulas, comments, bookmarks, ruby, row merges
Generator: text boxes (shape, fill, stroke), inline and display
formulas, comments, bookmarks with cross-references, ruby, rowspan
merges. Display rides a formula owning its line; a stroke width needs
a stroke color; a vertical merge keeps a second column so no row is
all covered cells. Normalize canonicalizes comment ids and dates
(naive local vs UTC are one instant) and trims a formula's latex
(the OMML side keeps the serializer's trailing space on purpose).
2026-08-16 11:22:30 +02:00
Steffen Becker 8e6616c562 feat: resolved comments travel through DOCX (commentsExtended.xml)
Word keeps a comment's resolved state as w15:done in
word/commentsExtended.xml, keyed by the w14:paraId of the comment
body's last paragraph. The docx package knows neither part, so a
post-pack pass stamps the paraId, mints the part with its content
type and relationship, and the importer reads it back.
2026-08-16 11:22:21 +02:00
Steffen Becker 1de86377e5 fix: DOCX text boxes carry paragraph formatting, inline features and marks
The hand-serialized txbxContent emitted only heading style, indent and
alignment: spacing, line height, indents, direction, shading, borders,
links, comment ranges, bookmarks, cross-references, formulas, rubies
and image rotation were all dropped. A bookmark still open at a
paragraph's end lost its runs (missing flush), and the formula/ruby
run regexes could start at a box's drawing run and swallow the box
preamble — they now stay inside one run.
2026-08-16 11:21:59 +02:00
Steffen Becker fdb456ffe6 fix: ODT sentinel passes anchor at the block tag, effects ride past annotations
A text box's anchor paragraph nests its children, so the STY/PBX passes'
scan to the closing tag started at the anchor and stole the first box
child's spec (its indent, direction, shading and borders). Both passes
now match the sentinel right after the opening tag, where it is emitted.
The TEF fallback also accepts bookmark markers and comment annotations
inside a run (tempered to the annotation's first close tag, -end kept
distinct), instead of cutting the effect short at them.
2026-08-16 11:21:20 +02:00
Steffen Becker 1264f9742c fix: dropping a fully covered table row shrinks the spans across it
A row holding only covered cells cannot exist in the editor and was
dropped on ODT import, but every rowspan reaching over it kept its
full count, leaving an inconsistent grid. Spans now shrink by the
dropped rows they covered.
2026-08-16 11:21:13 +02:00
Steffen Becker 6c79ec1197 test: fuzz round 3 — paragraph boxes, flow flags, tab stops, note labels
New in the pool: paragraph shading/rule lines (also in cells and lists), rtl
paragraphs, keepNext/keepLines/widowControl, tab stops, custom bullet chars,
heading levels to 8, image alt/rotation, row heights and custom note labels.
noHyphenation stays out: without the document-wide hyphenate flag the
importers suppress it as meaningless. 1000 sweep seeds clean on both legs.
2026-08-16 10:30:00 +02:00
Steffen Becker 044c59d895 feat: custom note marks travel through DOCX (w:customMarkFollows)
The docx package cannot express a custom footnote/endnote mark, so the anchor
always went out as an automatic reference and the label was lost — and the
importer never read one either, so a Word file's custom mark leaked into the
note's body text. A post-pack pass now rewrites the anchor (flag + literal
run) and the note part's <w:footnoteRef/>; the importer reads the flag, takes
the run's text as the label and strips its repeat off the note body.
2026-08-16 10:30:00 +02:00
Steffen Becker 5dab73ac3a test: fuzz round 2 — cell merges, list types, inline images
colspan merges (grid-consistent), orderedList listStyleType (upper-* only on
nested lists: lower/decimal forms are depth defaults the importers suppress),
and inline PNG images. 300 sweep seeds clean on both legs.
2026-08-16 07:58:24 +02:00
Steffen Becker 5258feb146 fix: sibling nested lists of different formats stop sharing one w:lvl
A DOCX numbering level keeps the first format it saw, so a second nested list
under the same parent with another number format or bullet char silently
rendered as the first one's (an attr-less nested list under an upper-roman
parent came back lower-alpha). The kind-mismatch fork now covers format and
bullet char too, and copies the shallower levels so the importer's cycle walk
still sees the ancestry.
2026-08-16 07:58:24 +02:00
Steffen Becker a7fe334900 test: widen the fuzz generator to effects, links, notes and cell props
New in the pool: underline/strike line styles, super/subscript, caps,
textPosition, links, page breaks, footnotes/endnotes, cell shading and
vertical alignment. normalize() canonicalizes importer-generated note ids and
drops the link mark's default plain:false; FUZZ_SEEDS widens the sweep beyond
the default 50. This is the suite that surfaced the last four export/import
fixes; all 300 sweep seeds now round-trip clean on both legs.
2026-08-16 07:50:22 +02:00
Steffen Becker 1f1176283a fix: a text effect covers its whole run and raises against the block's size
Two applyTextEffects bugs: the fallback span for an effect-only run stopped at
the first element, so everything behind a tab/line-break fell out of the span
and lost the effect — the run is now terminated by an empty sentinel pair and
inline empties ride along. And a raised run (textPosition) was written as a
percentage of the default 12pt even inside a heading, so the importer, which
resolves against the block's real size, read a different height back.
2026-08-16 07:46:43 +02:00
Steffen Becker ed7a40967a fix: an empty block no longer steals the next block's minted style
The whole-block regexes in the sentinel passes (page breaks, paragraph
styles/boxes, text effects, whitespace collapse) matched a self-closing
<text:p/> too — [^>]* happily ends in a slash — and then swallowed everything
up to the next real closing tag, so an empty cell paragraph took the restyle
meant for a later paragraph's sentinel and that paragraph lost it. The
opening-tag pattern now rejects self-closing tags.
2026-08-16 07:46:34 +02:00
Steffen Becker 97f9016369 fix: a note's stock body style no longer imports as direct formatting
Both importers stamped the exporter's own note style back onto the note
(styleName "Footnote" from ODT, "footnote text" from DOCX), so an
editor-authored document accreted a foreign attr on every round trip and the
two formats disagreed. A style that only re-states the stock note look is now
suppressed like any other default-equal value; a customized one still lands.
2026-08-16 07:35:42 +02:00
Steffen Becker 7a8c552342 fix: a page break survives beside other paragraph formatting
applyPageBreaks required its sentinel directly after the opening tag, but a
custom paragraph's serialized text starts with the FSZ/STY/PBX payloads, so
the break was silently dropped. It also minted PB# names, colliding with
applyParagraphBoxes' PB# — the importer then resolved both paragraphs to one
definition, painting the break paragraph with a stranger's box props. Now the
sentinel matches anywhere in the block and the pass mints PBK# instead.
2026-08-16 07:29:06 +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 c500762275 fix: Escape closes the character and date/time pickers
Both handled the key on the dropdown, but opening one leaves the focus on
its trigger button, which sits outside it — so Escape never arrived and the
popover stayed open. Both elements listen now.
2026-08-16 03:04:43 +02:00
Steffen Becker 273c533fce fix: open a file whose extension names the wrong format
Which importer ran was decided by the extension alone, so a .docx saved or
renamed as .odt (and the reverse) was reported as a broken file. A failed
read is retried as the other format; a file that is neither still reports
the error for the extension it carries.
2026-08-16 03:00:27 +02:00
Steffen Becker 96863395f3 fix: recording a replacement keeps the text it replaced
Only a pure delete was turned into a deletion mark; typing or pasting over
a selection ran as it was, so the replaced text left the document unmarked
and rejecting the change lost the original for good. A one-step replacement
now marks its range deleted and puts the new content after it, as both word
processors do.
2026-08-16 02:36:31 +02:00
Steffen Becker 8b1bef9706 fix: paragraph styles apply to every block, not just the first
The document's content ended in an optional noteSection, so the content
match after the first child offered noteSection as its defaultType — and
that is what TipTap's clearNodes converts a block into. Every style command
on a top-level block past the first threw "Invalid content for node type
noteSection" and applied nothing. Spelling the same language as an
alternation puts a paragraph first again.
2026-08-16 02:18:55 +02:00
Steffen Becker dc1f5bb7b1 fix: leaving a header/footer no longer throws state_unsafe_mutation
Clearing hfActive removes the live zone's DOM during Svelte's flush;
ProseMirror dispatches a blur transaction as it goes, and the hfTick
mutation in onTransaction then landed mid-render and threw, aborting the
rest of the batch. The tick is queued to the next microtask instead.
2026-08-16 01:47:05 +02:00
Steffen Becker 6eb9032c46 fix: paste no longer stamps a font the document never had
transformPasted fell back to a DEFAULT_EDITOR_FONT of 'Georgia' — the
comment said it had to match ToolbarExpanded's, which is Liberation Serif.
Text pasted where the caret carries no font mark got Georgia as direct
formatting, and pasting into a heading overrode its sans font. Only an
explicit font at the caret is carried over now.
2026-08-16 01:28:28 +02:00
Steffen Becker 9ce01fa32f ci: CLA signing check on pull requests
CLA Assistant Lite workflow: a PR stays red until its author signs
CLA.md by comment; signatures land on the cla-signatures branch.
2026-08-16 00:32:49 +02:00
Steffen Becker 0705385c4a docs: date the 0.1.0 changelog entry for the first release 2026-08-15 23:47:16 +02:00
Steffen Becker 3cec6990ef feat: label feature nodes and marks in the history dropdowns
Replace the hardcoded node if/else chain in describeTransaction with a
NODE_LABELS map covering image, text box, formula, page break, TOC,
fields, cross-reference, index/bibliography entries, ruby and columns;
foot-/endnotes labelled via their kind attr. MARK_LABELS gains link,
comment, bookmark, character style and tracked change.
2026-08-15 23:31:22 +02:00
Steffen Becker bb15572b57 test: smoke-check the raster-PDF export
exportPdf gains a test seam: a window.__edentextPdfSink takes the bytes
instead of doc.save(), whose download hangs in headless Chromium. The
smoke test clicks the real export menu over the imported corpus document
and asserts %PDF magic and a plausible size. The test browser is pinned
to en-US so the labels it clicks are deterministic across machines.
2026-08-15 23:18:55 +02:00
Steffen Becker 32cf72a1d0 test: cover recentFiles and the history log
The two framework-free 0%-coverage modules: the recent-files list
(dedupe by name, eight-entry cap, corrupt-localStorage tolerance) and
the labelled undo/redo mirror against a live jsdom editor (labels per
edit kind, undo/redo moves, redo branch discarded by a fresh edit).
2026-08-15 23:16:08 +02:00
Steffen Becker 82993aeda3 chore: coverage report via npm run test:coverage
@vitest/coverage-v8 with include over src/ so untested files show up
at 0%; text summary plus the browsable coverage/ HTML (gitignored).
2026-08-15 23:01:50 +02:00
Steffen Becker 47bbbfc7dd test: cross-format fuzz leg, ODT vs DOCX round-trip
The same seeded documents go through buildOdt->importOdt and
buildDocx->importDocx; the two imports must agree, so one exporter
silently losing what the other keeps now fails with the seed named.
Column widths are ignored here (ODT leaves defaults implicit, DOCX
always writes w:tcW); each format's own leg still covers them.
2026-08-15 22:58:29 +02:00
Steffen Becker 76856f4aff fix: DOCX cell spacing accretion and nested-list kind flips
Found by the cross-format fuzz leg. importDocx baked the resolved cell
chain in as direct formatting even where it equals what an unset attr
already renders (0/0 spacing; single line height over a single default) —
every re-imported own file grew spacing attrs on every cell paragraph.
And a numbering level kept the first list kind it saw, so a nested bullet
list beside a nested ordered one came back ordered; a kind conflict now
forks its own numbering reference, as the ODT side mints a list style.
2026-08-15 22:58:29 +02:00
Steffen Becker d78181fdd9 fix: Save round-trips a .docx-opened document as .docx
Ctrl+S always built .odt bytes and wrote them into whatever handle was
open — including a handle obtained by opening a .docx, silently
overwriting it with ODF content under its old name. Track the loaded
format and route Save through the matching exporter and handle; the
title's extension label now reflects it instead of a hardcoded .odt.
Save As stays .odt-only, so it switches the format back.
2026-08-15 22:40:32 +02:00
Steffen Becker 873af0794d test: fuzz round-trip leg over seeded random documents
tests/fuzzDoc.ts generates documents from a seeded PRNG (marks, lists,
tables, indents, XML-hostile text); fuzz-roundtrip.test.ts runs 50 seeds
through buildOdt -> importOdt and diffs the normalized JSON, naming the
failing seed. normalize/firstDiff moved out of roundtrip.test.ts into
tests/normalize.ts for reuse. The generator documents what is lossy by
design: default-equal marks in headings, indents and start in nested lists.
2026-08-15 22:17:35 +02:00
Steffen Becker 3c7b598169 fix: cell paragraph indents and minted-style collisions on ODT export
Three defects in the cell/list paragraph style pipe, found by fuzzing:
ParaStyle had no indent/indentFirst/indentRight, so a cell paragraph's
indents never reached content.xml (list items keep skipping them — their
indent lives in the list style). The minted-style dedupe keyed on only
four of the style's fields, so two different styles could share one name;
both keys now serialize via paraStyleProps. applyListItemStyles' regex
missed items carrying text:start-value, shifting every later item's style.
2026-08-15 22:17:18 +02:00
Steffen Becker 0e04a41290 perf: stop deflating the ODT archive once per export pass
Each of the ~30 post-processing passes re-zipped every entry (images
included) at level 6 just for the next pass's unzipSync. Intermediate
archives are now stored; zipFinal compresses once at the end, keeping
mimetype first/stored and storing already-compressed picture formats.
Measured ~10x on an image-heavy document (853ms -> 87ms).
2026-08-15 22:03:12 +02:00
Steffen Becker 6b64b42c42 ci: run the LibreOffice round-trip leg in its own job
The leg self-skips wherever soffice is absent, so until now it only ran
when someone remembered to locally. A parallel job installs
libreoffice-writer + fonts-liberation and runs npm run test:lo.
2026-08-15 21:59:31 +02:00
Steffen Becker 026f6b6d83 test: headless-Chromium smoke test for the UI layer
tests/smoke/run.mjs boots the production build in Playwright Chromium and
checks what jsdom cannot: the bundle boots, typing and the bold shortcut
reach ProseMirror, autosave survives a reload, a corpus ODT imports and
paginates; any uncaught page error fails the run. Wired as npm run
test:smoke and a CI job (build + chromium install), playwright-core is
now a devDependency.
2026-08-15 21:58:47 +02:00
Steffen Becker 0a91e573b4 refactor: the thesaurus is called Synonyms in the UI
Both products name the tool after what it is, which says nothing to a
reader who does not know the word. Every label is "Synonyms" now — ribbon,
context menu, tools menu, dialog — and "thesaurus" stays in the tooltip so
it is still findable by name. Keys and code keep the technical name.
2026-08-15 21:12:46 +02:00
Steffen Becker ef1d9abc14 fix: the thesaurus kept throwing its own results away
The pass that reads the caret's word on open also writes it, so it was a
dependency of itself: it re-ran on every flush and cleared term and groups
just as a lookup landed. It runs untracked now, on `open` alone. The
dialog also shows the language it is asking and lets it be switched, as
both products do — a German word in an English document found nothing,
with nothing on screen saying why.
2026-08-15 20:41:51 +02:00
Steffen Becker 44b351e865 feat: a thesaurus, LibreOffice's own synonyms
Ctrl+F7, Review ▸ Thesaurus and the context menu open one dialog: the
word at the caret, the sense groups it is in, click one to replace it,
and a box to look up any other word. The data is MyThes re-packed to one
group per line (de 3.0MB/37k groups, en 6.7MB/143k) — the format repeats
every group once per member, ten times the bytes. Loaded on first use for
the document's language and scanned, not indexed: ~4ms a lookup, where a
word→group index measured ~100MB of memory against the text's ~8MB.
2026-08-15 20:23:32 +02:00
Steffen Becker 5b88de8c19 test: reword comment tripping the gitignored-corpus CI guard
The CI grep for "render-parity/fixtures" outside tests/render-parity
flags any occurrence of that path, including comments. corpus.test.ts
only mentioned the path in prose; reworded it so the guard passes.
2026-08-15 19:52:56 +02:00