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.
16 KiB
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—PageMarginsin cm (default{ top: 2, bottom: 2, left: 2, right: 2 }— LibreOffice's;fitMarginsonly caps an imported pair at what leaves the page a 1cm strip of text).applyMarginVarssets--user-margin-*(px) on:root;PX_PER_CM = 96/2.54. Optionalmirrored(ODFstyle:page-usage="mirrored", Wordw:mirrorMargins) makes left/right the inner/outer pair, which an even page swaps:Editor.sveltepublishes the difference per section as--pb-section-mirror(a cover between mirrored pages mirrors nothing),pageBreaks.tsinsets an even page's blocks by it andHeaderFooterLayer.sveltemoves the band. The key is absent, neverfalse, when off — margins are compared whole in the round-trip tests, which is whatwithMirroredis for;marginAxisLabelgives 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'sprintedPageNumber+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).applyTabIntervalVarsets--tab-interval, which is.tiptap'stab-size. A file that declares none falls back to its format's own value, neither of them ours:ODF_IMPLIED_TAB_CM2cm (measured against LibreOffice),DOCX_IMPLIED_TAB_CM1.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 ODFsettings.xml(AddParaTableSpacing), always'max'on DOCX import, and written back on ODF export.editor.cssturns--space-beforeinto 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'sw:suppressSpBfAfterPgBrkread 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, andpageBreaks.tsreads it as--pb-space-at-page-start.writingMode.ts— the page's text direction (loadPageRtl/savePageRtl,edentext-page-rtl), from ODFstyle:writing-mode="rl-tb"or Word's section-levelw:bidi.Editor.svelteputs it on the body host asdir, 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.svelteformats each page throughformatOrdinal; 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'sw:pgNumType. A section can also demand the side it opens on (HfSet.startsOn, ODFstyle:page-usageright/left — read only where the master is no half of a mirrored pair — Word'sw:typeoddPage/evenPage):pageBreaks.tsholds 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 writesstyle:page-numberon the same paragraph that switches master page, Word aw: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.svelteputshyphens: autoand the documentlangon the body host — the browser needs the language to pick its patterns. ODF keeps it asfo:hyphenatein the text properties of the base paragraph style (probed: in paragraph-properties LibreOffice ignores it and drops it on the next save), Word asw:autoHyphenationin 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 liketrackChanges.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 byApp.svelte's language effect; the exporters take that locale from their ownlanguageargument instead, so they stay store-free. Seedocs/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 atpaddingCmfrom it, and a text watermark (LibreOffice's Format ▸ Watermark, field for field). All three ride the ODF page layout — the watermark as afontwork-plain-text<draw:custom-shape>namedPowerPlusWaterMarkObjectin the master page's header, the only place LibreOffice reads one from (probed: hung off the master page directly it is dropped, and an undeclaredxmlns:drawin styles.xml drops it too). Word keeps the background onw:document(plusw:displayBackgroundShape), the border inw:sectPrafterw:pgMar— the child order is fixed, and ahead of the header references Word's reader loses those — and the watermark as the VML_x0000_t136shape, whose definition has to travel or LibreOffice cannot resolve it.PageDecorLayer.sveltedraws border and watermark per page; the background is a--color-page-bgoverride on.paper. ODF lays border +fo:paddingout inside the page margin, shrinking the text area — the editor and Word (offsetFrom="text") keep text at the margin — so the ODT export shrinks everyfo: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 — ODFtext:track-changeson the change registry, Wordw:trackRevisionsin 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>inoffice:styles, Word a<w:lnNumType>perw:sectPr(probed).LineNumberLayer.sveltemeasures 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 liketrackChanges.svelte.ts: the Review tab flips it, all three export paths read it. What it governs lives inexport/reviewPrint.ts.markup.svelte.ts— the whole review view. The mode (edentext-markup-mode, absent atall) 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 atballoons) — 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.paperattributes, 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.sveltedraws 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'.applyOrientationVarssets--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 everyonUpdate(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 raisesedentext-doc-loading, which only a completed startup clears (markDocumentLoaded, two frames afterEditor.svelte's mount). Still set at the next load = that document hung or crashed the editor: it moves toedentext-doc-brokenand the app starts empty, so a reload escapes the freeze instead of repeating it. Pictures do not live in it:imageStore.tsswaps every data-URI past 4 KB for anidb:<key>before the write and keeps the bytes in theedentext-imagesIndexedDB 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 itsidb:src and is reported, rather than quietly dropping the reader's content.loadDocumentis therefore async, andEditor.svelteawaits 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: theFileSystemFileHandlethat reopens one is structured-cloneable but not JSON, so it sits in theedentext-recentIndexedDB store under the same id, and each write sweeps the handles no entry names. Two handles compare only throughisSameEntry, 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 forreadpermission 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 inApp.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 intometa.xml. - AutoCorrect:
edentext-autocorrect— one flag per rule (autoCorrect.ts, reactive singleton inautoCorrect.svelte.ts), LibreOffice's defaults (all on). - Notes:
edentext-notes— footnote/endnote numbering and the separator (noteSettings.ts, reactive singleton innotes.svelte.ts);applyNoteVarsputs the separator on:root, whereeditor.cssdraws it andpageBreaks.tsreserves 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.