The DOCX importer read w:tblBorders off the table but never off its table style, so a table whose style declares none (Word's Normal Table, and the thesis' Formel-tab around every formula) fell back to the editor's 0.5pt black default. Resolved per side along the w:basedOn chain, and an undeclared side is not drawn — LibreOffice writes fo:border="none" for exactly those tables when it re-saves the file. The ODF importer already treated an undeclared side that way.
204 lines
22 KiB
Markdown
204 lines
22 KiB
Markdown
# Changelog
|
||
|
||
## [0.1.0] · MVP
|
||
|
||
### Stack
|
||
- Vite + Svelte 5 (runes mode) + TypeScript
|
||
- TipTap 3 / ProseMirror (individual extensions, no starter-kit)
|
||
- odf-kit for ODT generation, `fflate` for (un)zipping `.odt`/`.docx`
|
||
- `docx` for Word (.docx) export/import
|
||
- `jspdf` + `html2canvas` for PDF export
|
||
- Hunspell (`hunspell-asm`, WASM) with `dictionary-en` / `dictionary-de` for spell check
|
||
- Vitest (+ jsdom) for testing; `svelte-check` for type-checking
|
||
- No backend — fully client-side
|
||
|
||
### Implemented
|
||
|
||
**Architecture**
|
||
- WYSIWYG editor built on TipTap 3 (individual extensions, no starter-kit); toolbar active-states stay reactive via a `tick` counter (Svelte 5 same-reference workaround)
|
||
- Fixed A4 paper canvas (794 × 1123 px @ 96 dpi) with CSS-simulated pagination and visual page breaks
|
||
- LocalStorage auto-save (debounced 1 s); restores document, theme, zoom, margins, orientation, etc. on reload
|
||
- Fully client-side / serverless; hand-coded SVG favicon
|
||
- Placeholder text on an empty document
|
||
|
||
**Text formatting**
|
||
- Bold, Italic, Underline, Strikethrough
|
||
- Subscript / Superscript
|
||
- Font family picker (lists only fonts installed on the machine; remembers recently used fonts)
|
||
- Font size
|
||
- Font color and highlight color (color picker with custom colors)
|
||
- Headings H1–H5, including un-bolding a heading
|
||
|
||
**Styles**
|
||
- Named paragraph styles with LibreOffice's inheritance model: a style has a parent, a follow-on style and its own properties; changing a style updates every paragraph using it. Built-ins Default text → Heading → Heading 1–5 / Title / Subtitle / Quote, with LibreOffice's values (18/16/14/13/12 pt headings)
|
||
- Named character styles (Emphasis / Strong Emphasis / Source Text + own ones) as a mark on a text run
|
||
- Style gallery in the toolbar lists both families; assigning a style keeps hard formatting (Word/LibreOffice behavior), Ctrl+M clears direct formatting but keeps the style
|
||
- Style manager ("Manage styles…"): inheritance tree, live property editing (empty field = inherit again), new / update from selection, rename, delete, reset a built-in — for paragraph and character styles alike
|
||
- Round-trips as real styles, not baked formatting: ODF `style:style` (`style:family="paragraph"`/`"text"`, `style:parent-style-name`, `style:next-style-name`) and DOCX `w:pStyle`/`w:rStyle` with `w:basedOn`/`w:next`; import adopts the file's used styles plus their parent chains, so only formatting beyond the style stays direct
|
||
- Named table styles as the third family (Word's table styles / LibreOffice's AutoFormats): 14 built-ins — Grid (+ rows / columns only), List shaded / columns, Plain, four colored box lists, Grid & List Accent, Academic, Financial. Picking one from the gallery in the table toolbar paints header row, banded rows/columns, first/last column and total row, and a styled table re-bands itself when rows or columns are added
|
||
- Word's Table Style Options as six checkboxes beside the gallery (header row, total row, first/last column, banded rows/columns): an area is painted only when the style defines it *and* the table opts in, and turning the header row off shifts the banding by one row. The toolbar's header row / first column buttons drive the same flags, so both surfaces always agree
|
||
- Table styles are edited in the same style manager (third tab: area picker for fill and font, outer / row line / column line controls, new / rename / remove), also reachable from the insert-table dropdown
|
||
- Round-trip: the style name travels as an ODF `style:family="table"` (`style:parent-style-name`) and DOCX `w:tblStyle`, the options as ODF's `table:use-*-styles` and Word's `w:tblLook`. Fills and borders are materialized into the cells and the region's font is baked onto the runs, so the table looks right in Word/LibreOffice even though ODF has no banding concept — the style definitions themselves live in the app, as LibreOffice's AutoFormats do
|
||
- A new document starts from the built-in styles, like Word's/LibreOffice's default template
|
||
|
||
**Paragraphs & lists**
|
||
- Text alignment: left, center, right, justify
|
||
- Line spacing and paragraph spacing (space before / after)
|
||
- Increase / decrease indent (paragraphs and lists)
|
||
- Bulleted and ordered lists — nested, with multiple numbering styles (decimal, alpha, roman; `.` or `)` suffix) and whole-list indent; Tab / Shift-Tab to nest / un-nest
|
||
- Word-style nesting defaults for ordered lists: an indented level numbers 1. → a. → i. (repeating) instead of restarting at "1." everywhere; explicit styles per level still override. Plus legal/outline numbering (1., 1.1., 1.2.1. …) as a list type — rendered via CSS counters, round-trips to ODF `text:display-levels` and DOCX `%1.%2.` lvlText chains
|
||
- Nested ordered levels advance the cycle relative to the level above and inherit its suffix (a level-1 `a)` gives `i)` then `1)`); the numbering style chosen for a level is reused when you nest into it again
|
||
- Customizable bullet symbols per list level (picker on the bullet-list split button: • ◦ ▪ ❖ ➢ ⇨ ✓ – >); round-trips to ODF `text:bullet-char` and DOCX `w:lvlText`, and the DOCX/ODT import maps Wingdings/Symbol bullets (arrows, diamonds, checkmarks …) to their Unicode equivalents instead of flattening them to plain dots. Symbols Liberation Serif lacks render from a bundled 2 KB DejaVu Sans subset (`EdenSymbols.woff2`), so markers look compact and identical on every platform instead of a stretched OS fallback
|
||
- Tab stops per paragraph: left, centre, right and decimal, honoured in the rendered text (CSS only has a fixed tab grid, so each tab is measured and placed); a hanging indent implies a stop at the text position. Set them on the ruler; round-trips to ODF `style:tab-stops` and DOCX `w:tabs`
|
||
- Paragraph borders and shading: per-side borders with presets (all / single edges / none), width and color, plus a background fill, on paragraphs and headings; typing `---`, `___` or `===` on a line of its own turns it into a rule line (Word/LibreOffice AutoCorrect). Round-trips to ODF `fo:border-*`/`fo:background-color` and DOCX `w:pBdr`/`w:shd`
|
||
- Manual line breaks (Shift+Enter)
|
||
|
||
**Insert**
|
||
- Tables: insert via size picker, Word-style row/column drag-resize, add / delete rows & columns, delete table, cell borders, merge cells and split cells (N×M, Word/LibreOffice-style), cell background shading, header row / first column toggles, named table styles (see Styles); a table splits cleanly across page boundaries
|
||
- Table border control (Word/LibreOffice-style): per-side cell borders with presets (all / outside / inside / single edges / none), line width and color; buttons show active states matching the current pen and toggle borders off; round-trips to ODF `fo:border-*` and DOCX `w:tcBorders`
|
||
- Images: inline or floating with text wrap (left / right / top-bottom), resize handles, rotation, live size badge; insert via toolbar, drag-and-drop, or paste. A floating frame is placed by the file's own offsets, and two top-and-bottom frames set against opposite ends of the text share one band, side by side
|
||
- EMF metafiles are drawn (`import/emf.ts`): the picture is rebuilt as SVG from the metafile's own records — paths, filled shapes, text with its font and colour, embedded bitmaps — so a plot pasted out of MATLAB or Excel arrives as a picture instead of a placeholder
|
||
- Anything wider than the sheet — an oversized formula, a frame reaching past the margin — is cut at the page edge, as it is in LibreOffice
|
||
- Charts are drawn from the file — DrawingML `chartN.xml` and ODF `chart:chart`, bar / line / area / scatter / pie, with their titles, axis titles, gridlines, axis bounds and series colours (read-only; see the limitations below)
|
||
- Special characters picker
|
||
- Date and time fields: picker with 7 date and 4 time formats (live samples) and an "update automatically" toggle — fixed fields keep the inserted moment, auto fields refresh on open. Round-trips to ODF `text:date`/`text:time` (minted `number:date/time-style`) and DOCX `DATE`/`TIME` fields; the field carries the surrounding font
|
||
- Table of contents: generated from headings (H1–H5) with live page numbers and dot leaders, click an entry to jump to its heading; round-trips to ODF `text:table-of-content` and a Word TOC field
|
||
- Formulas: dialog with a LaTeX field and live preview; only the LaTeX is stored, the MathML the browser typesets, the ODF formula object and the OMML are derived from it. Inline or as a centered display line, double-click to edit. Round-trips as a real embedded ODF formula object (`draw:object` + `Formula{n}/content.xml`, our LaTeX kept in the MathML `annotation`) and as Word's `m:oMath`. STIX Two Math is bundled, so stretched brackets and ∑/∫ look the same on every platform
|
||
- Hyperlinks: create / edit / remove (toolbar + Ctrl+K), Ctrl/Cmd+click to open, hover hint showing the URL; ODF `text:a` round-trip
|
||
- Manual page break (Ctrl+Enter); round-trips to ODF `fo:break-before`
|
||
- Text boxes and basic shapes (rectangle / rounded rectangle / ellipse): editable block content, fill and border colors, border width, resize/rotate handles, text wrap (inline / left / right / top-bottom) like images; floating toolbar for wrap, shape kind and colors. Round-trips to ODF `draw:frame`/`draw:text-box` + `draw:custom-shape` and DOCX DrawingML `wps:wsp`/`wps:txbx` (imports Word's `mc:AlternateContent` and legacy VML text boxes too)
|
||
|
||
**Page & layout**
|
||
- Page margins (cm) and page orientation (portrait / landscape)
|
||
- Page format picker (Layout panel): 15 Word/LibreOffice sizes — A3–A6, ISO B4–B6, JIS B4/B5, Letter, Legal, Tabloid, Executive, Folio, Statement — each showing its cm dimensions; drives the on-screen page, pagination, and ODT/DOCX/PDF export, and is detected & adopted on import
|
||
- Multi-column (newspaper) layout: 1–3 columns with adjustable gap, applied to the whole document (no selection) or to selected paragraphs, Word-style — text fills column 1 to the page bottom, then column 2, and flows across pages with mid-paragraph line breaks; a section followed by other content balances its columns. Round-trips to ODF `text:section`/`style:columns` and DOCX continuous sections with `w:cols`
|
||
- Headers & footers: page-number / page-count fields, configurable edge distances, and variants — different first page and different odd & even pages (edit a page's zone directly; blank first/even zones supported). Press Enter to add blank lines that grow a zone into the page while body text reflows so the page break sits above the footer / below the header, never overlapping. Inline images/logos can be inserted in a zone. Round-trips to ODF (`style:header-first`/`-left`, `draw:frame`) and DOCX (`w:titlePg`/`w:evenAndOddHeaders`, `ImageRun`)
|
||
- Zoom (20–300 %)
|
||
- Horizontal ruler above the page: click to place a tab stop of the selected type, drag to move it, drag it off to remove it; three markers set the paragraph's first-line, left and right indent. Toggle in the extended toolbar
|
||
- Show formatting marks (spaces, tabs, paragraph marks)
|
||
|
||
**Editing aids**
|
||
- Word/LibreOffice keyboard shortcuts throughout: Ctrl+L/E/R/J alignment, Ctrl+Alt+1–5 headings and Ctrl+Shift+N default style, Ctrl+1/2/5 line spacing, Ctrl+Shift+P / Ctrl+Shift+B super/subscript, Ctrl+Shift+. / Ctrl+Shift+, grow/shrink font, Ctrl+Space clear formatting, Ctrl+Shift+Space non-breaking space, Ctrl+Shift+- soft hyphen, Alt+Shift+D / Alt+Shift+T date & time field, Ctrl+O open, F3 / Shift+F3 find next/previous, Ctrl+F10 formatting marks — alongside the existing Ctrl+B/I/U, Ctrl+M, Ctrl+K, Ctrl+Enter, Ctrl+F/H, Ctrl+S/P and Tab/Shift+Tab. All bindings live in one table, so tooltips stay in sync and they can be remapped later
|
||
- Undo / Redo with a labelled history dropdown
|
||
- Search & Replace (Ctrl+F / Ctrl+H): live match highlighting, match count, next / previous, match-case and whole-word options, replace current / replace all
|
||
- Right-click context menu for text: cut / copy / paste / paste without formatting, link insert-edit-remove, clear formatting — with the spelling suggestions merged in on top. Shift+right-click keeps the browser's own menu; images, text boxes and header/footer stay with their existing UI
|
||
- Spell check in English and German with squiggles and suggestions in the context menu (add / ignore word); selectable document language. Powered by Hunspell (WASM), so German compound words (Fußgänger, Krankenversicherung …) are recognised; dictionaries are lazy-loaded per language on demand
|
||
- Word / character count statistics (whole document and selection)
|
||
|
||
**UI & theming**
|
||
- Light / Dark / AllBlack / Auto appearance modes (settings menu in toolbar)
|
||
- Primary toolbar plus an expandable extended toolbar (horizontal scroll on narrow windows)
|
||
- Status bar with page number, word count, language and zoom controls
|
||
- About dialog
|
||
- UI internationalization (English / German): language picker, auto-detects the browser
|
||
language on first load, persisted; covers toolbar, dialogs, menus, status bar and warnings
|
||
|
||
**File & export**
|
||
- ODT export via odf-kit with extensive content.xml / styles.xml post-processing (custom node types, tables with borders, images, fonts and colors); filename derived from the first heading (fallback `document.odt`)
|
||
- ODT import / open existing file: parses content.xml / styles.xml directly, adopts the file's margins and orientation, and reports graceful-degradation warnings
|
||
- Opens `.ott` templates (OpenDocument Text Template): read like an `.odt`, but as a new untitled document — the first Save writes a fresh `.odt` and never overwrites the template (Word/LibreOffice behavior)
|
||
- Embedded font loading: fonts embedded in an opened `.odt`/`.docx` (Word `.odttf` de-obfuscated) are registered via the FontFace API so text renders in its real face even when the font isn't installed; persisted per-document in IndexedDB so it survives a reload. Fonts the document only names but neither embeds nor installs are still flagged as substituted
|
||
- Word (.docx) export and import — round-trips the editor's formatting (text, fonts, lists, tables, images, headers/footers, page geometry) and opens real Word documents
|
||
- PDF export — Raster (pixel-exact copy of the editor with a selectable text layer) and Vector (crisp, fonts embedded, via the browser print dialog)
|
||
- Print (printer button / Ctrl+P) — opens the browser print dialog with a pixel-exact raster of the document (tables, headers/footers and page breaks intact)
|
||
|
||
### Not yet implemented
|
||
|
||
The gap against Word/LibreOffice, most valuable first. Reviewed 2026-08-08.
|
||
|
||
**Content an imported document loses**
|
||
- Footnotes / endnotes: dropped on import with a warning (`text:note`, `w:footnotes`), and there is no way to add one
|
||
- Comments / annotations: dropped on import (`office:annotation`, `w:comment`)
|
||
- Track changes / revisions: no recording, no accept/reject, no author colors (`text:tracked-changes`, `w:ins`/`w:del`); imported revisions are flattened to their current state
|
||
- Bookmarks and cross-references: no anchor, no reference field ("see chapter 3 on page 7"), none survive an import — this also blocks internal hyperlinks
|
||
- Charts are **drawn** from the file (`import/chart.ts`: DrawingML `chartN.xml` and ODF `chart:chart`), but as a picture, not a chart object — a re-export carries the drawing and the numbers behind it are no longer editable. The same holds for an **EMF** metafile (`import/emf.ts`): it is drawn, but as the SVG picture it was rebuilt into, and only from the record set a plot consists of — a hatched brush, a clipping region or a rotated bitmap is skipped. **WMF/SVM** metafiles and OLE objects still keep their box and a placeholder label, and export writes that back out: WMF is a different (16-bit) record format, SVM is StarOffice-proprietary, and an OLE object cannot be rendered without its application
|
||
- Shapes beyond rect / round-rect / ellipse: lines, arrows, connectors, polygons, freeform, and rotated shape text (dropped on import with a warning)
|
||
- Text boxes in the DOCX export: lists inside a box are flattened to literal-marker paragraphs (`•` / `1.`) and images inside a box are dropped — the box XML is injected by a post-pack string pass (`export/docx.ts` `applyTextBoxesDocx`) that mints no numbering.xml or media/rels entries. The ODT export has neither limitation
|
||
|
||
**Missing while writing**
|
||
- AutoCorrect while typing: only the `---` rule line exists. Missing are typographic quotes (`"…"` / `„…“` per document language), en/em dash, `--> → →`, capitalize the first letter, auto-URL, auto-list
|
||
- Automatic hyphenation: `fo:hyphenate` / `w:hyphenationZone` are ignored and the editor sets no `hyphens`, so a justified paragraph breaks its lines differently than LibreOffice — a systematic source of page-count drift in render parity
|
||
- Captions with numbered figures/tables ("Abbildung 1: …") and the lists built from them (list of figures / tables). The TOC machinery exists, the other index families do not
|
||
- Page numbering options: the page field is decimal-only — no start value, no roman/alpha format, no restart per section
|
||
- Document properties: title, author, subject, keywords. No `meta.xml` is written and the DOCX creator is hard-coded, so an exported file carries no authorship
|
||
- Heading level H6 (clamped to H5; Word and LibreOffice go to 10)
|
||
- Alphabetical index and bibliography
|
||
- Line numbering (LibreOffice Tools ▸ Line numbering / Word Layout ▸ Line numbers)
|
||
- Section-level page setup: a section with its own page size, orientation or margins. Page geometry is one document-wide setting, so a file whose sections disagree keeps the last one's (per-section headers/footers already work)
|
||
- Table extras: repeat the header row on every page, keep a row from splitting across pages, sort, sum/formula in a cell, number recognition
|
||
- A list level's own hanging indent: the marker sits at the 0.635 cm both exports write (`LIST_HANGING_CM`), so a wider *left*-set marker overflows where Word moves the text to the next list tab (a **right**-set one — `w:lvlJc`, which is what the built-in Roman numberings use — grows into the margin and is fine). Reading the value back naively also moves the markers of our own ODT exports — LibreOffice draws its own flat hanging at exactly the value odf-kit writes for level 2; see `tests/render-parity/README.md` before building on it
|
||
- Images live as data-URIs in the autosaved JSON, so an image-heavy document can exceed the ~5 MB localStorage quota; `storage/autosave.ts` warns once and a reload restores the last version that still fit. IndexedDB (as `embeddedFontStore.ts` already uses) would lift the ceiling
|
||
- Linked / chained text frames
|
||
- Find & Replace by regular expression or by formatting/style
|
||
- Save as template (`.ott` / `.dotx`), multi-document management, recent-files list
|
||
- Watermark; page background and page border
|
||
- Thesaurus, grammar check, word completion, AutoText / building blocks
|
||
- Right-to-left and vertical text, Asian typography (ruby)
|
||
- Password-protected ODT/DOCX; digital signatures
|
||
- Navigator / outline view, split view
|
||
- PWA / offline support
|
||
|
||
**Out of scope for now**
|
||
- Mail merge, data sources, form fields
|
||
- Master documents
|
||
- Real-time collaboration (there is no backend by design)
|
||
- Macros / scripting
|
||
|
||
### Known limitations
|
||
|
||
Deviations from Word/LibreOffice the browser does not let us remove. Anything
|
||
merely unimplemented belongs in the list above, not here.
|
||
|
||
- Zoom 100% does not visually match Word/LibreOffice at 100% on the same
|
||
screen. Root cause: the editor uses the browser's fixed 96 CSS DPI
|
||
(794×1123 px for A4 — see `editor.css`, `pageBreaks.ts`), while Word and
|
||
LibreOffice render against the OS-reported physical screen DPI so that
|
||
1 cm on paper ≈ 1 cm on screen. Possible future fix: add a user-side
|
||
calibration (DPI value or visual ruler) that scales the `zoom` factor.
|
||
Decided 2026-05-26 not worth the effort for now.
|
||
- An image or text box cannot be dropped at a free point on the page. It is
|
||
anchored to a text position: inline, or floating left / right / top-bottom,
|
||
and dragging it re-anchors it to the paragraph under the cursor rather than
|
||
placing it. Root cause: `float` is the only way to make browser text wrap
|
||
around a box. CSS Exclusions, which would wrap around a freely placed one,
|
||
are unimplemented in every engine, and `position: absolute` takes the frame
|
||
out of flow so text runs underneath it.
|
||
A file's own offsets are drawn: both round-trip (`wrapOffset`/`wrapOffsetY` =
|
||
`svg:x`/`svg:y`, `positionH`/`positionV`) and both place the frame, the vertical
|
||
one for top-and-bottom wrap, where no text sits beside it. What stays out of
|
||
reach is a frame a word processor puts *inside* running text: Chromium moves
|
||
every line after a full-width float below it, so such a frame lands after the
|
||
paragraph's text instead. Noted 2026-08-08, revised 2026-08-09.
|
||
- A line takes one word more or fewer than LibreOffice, for two reasons neither
|
||
of which CSS exposes. LibreOffice **compresses** inter-word spaces to fit a
|
||
line (measured on the thesis: 91 of 349 full-width justified lines, up to
|
||
0.83 px per space at 12 pt) where CSS justification only expands; and it
|
||
quantizes every glyph advance where Chromium keeps it fractional, so the two
|
||
drift apart along the line (identical word widths, 0.45 mm apart by the
|
||
150th mm). A matching negative `word-spacing` was tried and reverted — it
|
||
fixes the break but shortens every last line; see
|
||
`tests/render-parity/README.md`. Noted 2026-08-09.
|
||
- A text box anchored inside a paragraph loses the vertical offset it was
|
||
anchored by: it is a block node here, so the importer lifts it out and it
|
||
simply follows that paragraph (measured 4.7 mm on the thesis' figure page).
|
||
It also costs flow height a word processor does not spend: Word's picture
|
||
caption is a text box declared 0.05 pt tall that overflows its own box, so
|
||
LibreOffice reserves nothing for it and the empty paragraph after it holds the
|
||
caption. Here it is a block, ~55 px — enough to push two lines off the thesis'
|
||
page 15 and keep it one page behind through page 45. Noted 2026-08-09.
|
||
- Line height follows the paragraph, not the line: the block's CSS strut applies
|
||
to every line, where a word processor takes each line's own runs. A paragraph
|
||
whose runs all agree takes theirs, so only a paragraph of *mixed* sizes struts
|
||
too low. A fix (`line-height: 0` + inline decoration) measured right on a
|
||
purpose-built fixture but cost the contract fixture 22 issues; reverted. Detail
|
||
in `tests/render-parity/README.md`. Noted 2026-08-08.
|
||
- A plain text box's height is a *minimum* (content grows the box, like the
|
||
editor). LibreOffice recomputes auto-grow heights on open, so a re-saved box
|
||
keeps its content but sheds excess empty height. Shapes (rect/ellipse) export
|
||
with fixed geometry instead. Noted 2026-07-03.
|
||
|
||
---
|
||
|
||
<!-- Add new entries above this line in the format: -->
|
||
<!-- ## [x.y.z] — YYYY-MM-DD -->
|