Files
edentext/src/lib/storage/CLAUDE.md
T
Steffen Becker 137fce047a fix: the autosave writes what is pending when the page is hidden
The 1 s debounce lost a tab closed inside it. Writes now queue behind each
other, so an older write finishing late can neither store an older document
nor sweep the pictures a newer one added; a page revived from the
back/forward cache writes again.
2026-09-04 19:30:12 +02:00

16 KiB
Raw Blame History

src/lib/storage/

The autosaved document (autosave.ts) stays plain text even when the document is password-protected: the protection is on the saved file, not on the browser copy.

Page layout settings

  • pageMargins.ts — PageMargins in cm (default { top: 2, bottom: 2, left: 2, right: 2 } — LibreOffice's; fitMargins only caps an imported pair at what leaves the page a 1cm strip of text). applyMarginVars sets --user-margin-* (px) on :root; PX_PER_CM = 96/2.54. Optional mirrored (ODF style:page-usage="mirrored", Word w:mirrorMargins) makes left/right the inner/outer pair, which an even page swaps: Editor.svelte publishes the difference per section as --pb-section-mirror (a cover between mirrored pages mirrors nothing), pageBreaks.ts insets an even page's blocks by it and HeaderFooterLayer.svelte moves the band. The key is absent, never false, when off — margins are compared whole in the round-trip tests, which is what withMirrored is for; marginAxisLabel gives both margin UIs the Inner/Outer field names that go with it. Which side a page is on comes from its printed number (pageNumbering.ts's printedPageNumber + isLeftPage), not its sheet: probed, LibreOffice calls a page left when its number is even, so a section restarting on the other parity really does show two left pages in a row. The mirror inset, the page border and the odd/even zone all read it.
  • tabInterval.ts — the document's default tab interval in cm (DEFAULT_TAB_INTERVAL_CM = 1.25, LibreOffice's for a new document; clamped 0.05–10). applyTabIntervalVar sets --tab-interval, which is .tiptap's tab-size. A file that declares none falls back to its format's own value, neither of them ours: ODF_IMPLIED_TAB_CM 2cm (measured against LibreOffice), DOCX_IMPLIED_TAB_CM 1.27cm — so both exports always write the value out.
  • spacingModel.ts — how the gap between two blocks is measured: 'add' (LibreOffice's own — space-below plus space-above) or 'max' (the larger of the two, what it uses for a Word document). Probed against soffice; per document, read from ODF settings.xml (AddParaTableSpacing), always 'max' on DOCX import, and written back on ODF export. editor.css turns --space-before into padding or margin accordingly (CSS margins can only collapse, so 'add' cannot be a margin). The same module holds spacing at a page start (AddParaTableSpacingAtStart, Word's w:suppressSpBfAfterPgBrk read the other way): off, the block that opens a page loses its space above however the page broke — probed by flipping the flag in one document, whose heading then moved 25.4mm → 20.0mm. On is the default both products write, and pageBreaks.ts reads it as --pb-space-at-page-start.
  • writingMode.ts — the page's text direction (loadPageRtl/savePageRtl, edentext-page-rtl), from ODF style:writing-mode="rl-tb" or Word's section-level w:bidi. Editor.svelte puts it on the body host as dir, which is what makes a multi-column page fill from the right; the header/footer band is left alone (LibreOffice lays those out unchanged — measured). The vertical modes are not read.
  • pageNumbering.ts — the page-number field's format and start value (edentext-page-numbering, absent at the 1/1 default). HeaderFooterLayer.svelte formats each page through formatOrdinal; the page count stays decimal, as the field both word processors write does. ODF keeps the format on the page layout and the start on the first paragraph's own style (style:page-number — probed, ODF has no document-level start); Word keeps both in the first section's w:pgNumType. A section can also demand the side it opens on (HfSet.startsOn, ODF style:page-usage right/left — read only where the master is no half of a mirrored pair — Word's w:type oddPage/evenPage): pageBreaks.ts holds its first page back by one where the printed number would land it on the other side, which is the blank page both word processors insert. A later section can restart the count (HfSet.pageNumberStart): ODF writes style:page-number on the same paragraph that switches master page, Word a w:pgNumType w:start — and that section is then exported as a page-starting break, since both word processors ignore a start on a continuous one (probed: LibreOffice drops it).
  • hyphenation.ts — automatic hyphenation for the whole document (edentext-hyphenation, off by default as in both word processors). Editor.svelte puts hyphens: auto and the document lang on the body host — the browser needs the language to pick its patterns. ODF keeps it as fo:hyphenate in the text properties of the base paragraph style (probed: in paragraph-properties LibreOffice ignores it and drops it on the next save), Word as w:autoHyphenation in settings.xml. A paragraph can only opt out (noHyphenation, pageBreak.ts): Word has no per-paragraph "on", and both importers drop the flag under a document that does not hyphenate, where it says what is already true.
  • wordCompletion.ts / .svelte.ts — the word-completion settings and the collected words (edentext-word-completion), LibreOffice's defaults: on, no appended space, words from eight letters, 1000 of them. The list outlives the document that filled it, as LibreOffice's profile copy does; a reactive singleton, since the extension both reads it and adds to it on every keystroke while the AutoCorrect dialog edits it.
  • autoText.ts / .svelte.ts — the AutoText library (edentext-autotext, the key removed when empty): name, shortcut and the entry's nodes (an older entry's HTML string is read as the content it is). App-level, not document-level, so no exporter looks at it.
  • tableOptions.svelte.ts — table number recognition (edentext-number-recognition, the key removed when off, as it is by default in LibreOffice). A reactive singleton like trackChanges.svelte.ts: the extension reads it on every transaction, both table surfaces flip it. It also holds the number locale — the document's language, which is what LibreOffice reads and writes a cell's number in — pushed by App.svelte's language effect; the exporters take that locale from their own language argument instead, so they stay store-free. See docs/architecture/tables.md.
  • pageDecor.ts — the page's own decoration (edentext-page-decor, the key removed when all three are off): background colour, a border around the text area at paddingCm from it, and a text watermark (LibreOffice's Format ▸ Watermark, field for field). All three ride the ODF page layout — the watermark as a fontwork-plain-text <draw:custom-shape> named PowerPlusWaterMarkObject in the master page's header, the only place LibreOffice reads one from (probed: hung off the master page directly it is dropped, and an undeclared xmlns:draw in styles.xml drops it too). Word keeps the background on w:document (plus w:displayBackgroundShape), the border in w:sectPr after w:pgMar — the child order is fixed, and ahead of the header references Word's reader loses those — and the watermark as the VML _x0000_t136 shape, whose definition has to travel or LibreOffice cannot resolve it. PageDecorLayer.svelte draws border and watermark per page; the background is a --color-page-bg override on .paper. ODF lays border + fo:padding out inside the page margin, shrinking the text area — the editor and Word (offsetFrom="text") keep text at the margin — so the ODT export shrinks every fo:margin-* by border + padding and the import grows them back (decorInsetCm).
  • trackChanges.svelte.ts — whether the document records revisions (edentext-record-changes, the key removed when off, as it is by default in both word processors). It travels with the file — ODF text:track-changes on the change registry, Word w:trackRevisions in settings.xml (both probed against soffice) — so an import adopts the file's flag and a new document starts off; only the open document's state lives under the key. A reactive singleton because the extension reads it on every transaction while the Review tab flips it; the recorded changes themselves live in the document, on the marks.
  • lineNumbering.ts — numbers in the left margin (edentext-line-numbering, the key removed when off, as it is by default in both word processors). ODF keeps one <text:linenumbering-configuration> in office:styles, Word a <w:lnNumType> per w:sectPr (probed). LineNumberLayer.svelte measures the lines: CSS exposes no line boxes, so a Range per block is taken and its distinct client-rect tops are the lines.
  • printMarkup.svelte.ts — whether a printout carries the review markup (edentext-print-markup, the key removed at its on default, so it is the off state that is stored — the switch both word processors offer). A reactive singleton like trackChanges.svelte.ts: the Review tab flips it, all three export paths read it. What it governs lives in export/reviewPrint.ts.
  • markup.svelte.ts — the whole review view. The mode (edentext-markup-mode, absent at all) is Word's four steps — all (every change marked up), simple and none (the final text, the second without bars, cards or lists), original (the text before the changes). Per kind it holds whether it shows (edentext-markup-comments, edentext-markup-changes) and where its cards go (edentext-markup-comment-place, edentext-markup-change-place, absent at balloons) — beside the line or in the pane, which is why no separate pane flag exists any more. The two are tied: picking a mode carries both kinds with it, and switching a kind on lifts a mode that hides everything back to all, so no button is left claiming what the page does not show. markupView() folds it into what the page and the layers act on, read once instead of re-derived at every call site; markupAttrs() is the same view as .paper attributes, for the print path that builds its page from scratch. Both formats keep the choice out of the file — it is a view, like zoom.
  • foldMarks.ts — fold + punch marks in the left margin (edentext-fold-marks, the key removed when off; neither word processor prints any by default). The DIN 5008 positions (87/192mm folds, 148.5mm punch) are this module's constants; FoldMarkLayer.svelte draws them per page — the folds on both edges (both hands find the crease), the punch mark only left, where the pages are filed. They export as named lines (EdenFoldMark*) in the master page's header — the one place that repeats on every page, where LibreOffice's own DIN templates keep them — ODF as page-relative <draw:line>s, DOCX as VML <v:line>s; both importers read the names back into the flag and never into the zone's content.
  • pageOrientation.ts — 'portrait' | 'landscape'. applyOrientationVars sets --user-page-{width,height}; landscape swaps the A4 dimensions (matching odf-kit's automatic swap).

Both margins and orientation are passed into the ODT export so the exported document's geometry/line-wrapping matches the on-screen preview.

localStorage keys

All keys share the edentext- prefix — a namespace, not a description: on GitHub Pages every repo of an account shares one origin, so a generic prefix would collide.

  • Document: edentext-doc — TipTap JSON, debounced 1 s on every onUpdate (autosave.ts); hiding the page (visibilitychange/pagehide) writes what is pending at once, and writes queue behind each other so a slow picture round trip never stores an older document over a newer one. Loading it raises edentext-doc-loading, which only a completed startup clears (markDocumentLoaded, two frames after Editor.svelte's mount). Still set at the next load = that document hung or crashed the editor: it moves to edentext-doc-broken and the app starts empty, so a reload escapes the freeze instead of repeating it. Pictures do not live in it: imageStore.ts swaps every data-URI past 4 KB for an idb:<key> before the write and keeps the bytes in the edentext-images IndexedDB store (a handful of photos is the whole localStorage quota). The key is a content hash of the data-URI's head, tail and length — sampled, because this runs on every autosave — so a picture used twice is stored once, and each save sweeps the keys the document no longer names. Where IndexedDB is unavailable the data-URIs stay inline, exactly as before; a key the store has lost keeps its idb: src and is reported, rather than quietly dropping the reader's content. loadDocument is therefore async, and Editor.svelte awaits it before building the editor.
  • Recent files: edentext-recent-files — {id, name, at} per entry, newest first, eight kept (recentFiles.ts). Only the label lives here: the FileSystemFileHandle that reopens one is structured-cloneable but not JSON, so it sits in the edentext-recent IndexedDB store under the same id, and each write sweeps the handles no entry names. Two handles compare only through isSameEntry, which is async — hence the walk when recording a file already in the list. A file without a handle is never recorded (nothing could reopen it — no File System Access API, or a template), so the list simply stays empty in Firefox/Safari/Brave; a startup prune drops entries whose handle is gone. Reopening asks for read permission again (the API drops it on reload) — a declined prompt keeps the entry for the next try, only a moved or deleted file drops it (with a message). Save re-prompts for write on its own (Save-As fallback in App.svelte).
  • Theme: edentext-theme — 'light' | 'dark' | 'allBlack' | 'auto'.
  • Toolbar expanded: edentext-toolbar-expanded — boolean string.
  • Chrome: edentext-chrome — 'modern' | 'ribbon' ('classic' read as the modern island's legacy stored name); absent = ribbon.
  • Ribbon collapsed: edentext-ribbon-collapsed — boolean string; absent = expanded.
  • Formatting marks: edentext-formatting-marks — boolean string.
  • Ruler: edentext-ruler — boolean string; absent = on.
  • Split view: edentext-split — boolean string; absent = off. The divider's position is not kept, as neither word processor restores one.
  • Pages side by side: edentext-page-columns — how many, 1–MAX_PAGE_COLUMNS; absent = 1. Only one pane layout at a time, so turning this on ends the split.
  • Zoom: edentext-zoom — integer percent.
  • Page margins: edentext-page-margins — JSON cm values.
  • Page orientation: edentext-page-orientation — 'portrait' | 'landscape'.
  • Tab interval: edentext-tab-interval — cm as a decimal string.
  • Spacing model: edentext-spacing-model — 'add' | 'max'.
  • Recent fonts: edentext-recent-fonts — JSON string array (ToolbarExpanded).
  • Download hint shown: edentext-download-hint — set by the first fallback download (export/saveFile.ts); the one-time "always ask where to save" hint.
  • Header/footer: edentext-header / -footer (HfDoc), edentext-hf-distances.
  • Styles: edentext-styles — the style registry (styles/sheet.svelte.ts).
  • Document properties: edentext-doc-properties — title/subject/author/keywords/comments (docProperties.ts); the key is removed when every field is empty, so a fresh document writes nothing into meta.xml.
  • AutoCorrect: edentext-autocorrect — one flag per rule (autoCorrect.ts, reactive singleton in autoCorrect.svelte.ts), LibreOffice's defaults (all on).
  • Notes: edentext-notes — footnote/endnote numbering and the separator (noteSettings.ts, reactive singleton in notes.svelte.ts); applyNoteVars puts the separator on :root, where editor.css draws it and pageBreaks.ts reserves its band.

Themes (theme.ts, styles/global.css)

Four modes: light, dark, allBlack (forces font colors white), auto (follows prefers-color-scheme). Applied by setting data-theme on <html>; CSS variables for each theme live in global.css. The sheet keeps its own pair through light and dark — white paper, black text, the colour LibreOffice and Word draw an uncoloured run in (the app's slate would also differ from a run the file colours #000000); only allBlack inverts it.