A formula node stores only its LaTeX source; the MathML on screen, the ODF formula object and Word's OMML are derived from it, and both importers parse foreign markup back into that source, so an imported formula stays editable. Word documents were losing their formulas outright: m:oMath sits beside the w:r runs in its own namespace, past convertInline's `namespaceURI !== W` guard.
6.5 KiB
CLAUDE.md
Serverless, fully client-side rich-text editor that saves .odt (and .docx). No backend; all
state lives in the browser (localStorage).
Stack: Svelte 5 (runes) + TypeScript + Vite. Editor engine TipTap 3 (ProseMirror). ODF export
via odf-kit + fflate for re-zipping during post-processing.
Commands
npm run dev # dev server (Vite, hot-reload, --host)
npm run build # production build → dist/
npm run preview # serve the dist/ build locally
npm run check # svelte-check type-check (svelte + ts)
npm test # Vitest suite once (tests/**/*.test.ts)
npm run test:watch # Vitest in watch mode
npm run test:lo # LibreOffice round-trip leg only (needs `soffice` on PATH)
npm run test:parity # render parity vs LibreOffice (tests/render-parity/README.md)
Tests live in tests/ (outside src/, so svelte-check ignores them), jsdom via Vitest.
roundtrip.test.ts covers the ODF export↔import round trip + a foreign-doc/style-resolver leg;
lo-roundtrip.test.ts re-saves through LibreOffice and self-skips without soffice, so
npm test/CI stay green; tests/unit/ holds fast helper tests. All test tooling stays a
devDependency. No linter/formatter. CI (.github/workflows/ci.yml) runs check + test.
Rules
Comments — keep them precise and short:
- Never longer than three lines. No exceptions — file-header comments, tests and config included. A comment that needs a fourth line is explaining what the code already says; cut it back to what the code can't say.
- Never describe how the current code differs from an older version (no "previously…", "this used to…", "changed from…"). Comment only what the current code does and why — git history covers the rest.
- Don't use Word as a placeholder for "a word processor". Where LibreOffice does the same thing, describe the behaviour itself ("the caret moves", "the zone auto-grows") instead of "Word-style" / "like Word" / "as in Word". Name a product only where the statement really is about that product: its file format (
w:tblLook, DOCX), or a quirk only it has — then name both if both apply.
IMPORTANT: never introduce a default only this editor has. Both importers suppress values equal
to the defaults, so an editor-only default is indistinguishable from a failed style resolution and
silently lands in every imported document as direct formatting. The defaults follow LibreOffice
(we save .odt, so LO wins the ties against Word's Calibri 11pt / 8pt after / 2.54cm):
- Paragraph spacing 0 — no
margin-top/-bottomonp,ul/olor the table wrapper. Blank lines come from the document's own empty paragraphs. - Body Liberation Serif 12pt, single line spacing; page margins 2cm; tab/indent step 1.25cm.
- Headings sans (
HEADING_FONT= Arial, bundled@font-facemaps it to metric-identical Liberation Sans), sizes/margins inHEADING_STYLE_OVERRIDES(export/odt.ts— seesrc/lib/export/CLAUDE.md). - Liberation Serif TTFs are bundled (
src/assets/fonts/) and metric-identical to Times New Roman, so editor, LibreOffice and Word share the same metrics.utils/fontDetect.tsfiltersCANDIDATE_FONTSto what's installed for the font picker.
IMPORTANT: keep the layout constants in sync between editor/extensions/pageBreaks.ts,
components/Editor.svelte and styles/editor.css (PAGE_HEIGHT 1123px, PAGE_GAP 20px, the
--user-page-*/--user-margin-* custom properties) — details in docs/architecture/pagination.md.
Headless browser testing — this container is linux arm64. Never puppeteer /
@puppeteer/browsers: they fetch an x86-64 Chrome that cannot run here. Use playwright-core +
its Chromium; full recipe in docs/headless-testing.md. Driving the live app is the only way to
verify rendering, layout or NodeView behaviour — there is no DOM test suite for it.
Naming — components PascalCase.svelte, every .ts module camelCase; extension files are
named by feature (image.ts, indent.ts), not XyzExtension.ts.
Documenting a change — a feature that changes how this codebase behaves gets documented where
it loads on demand, not here. This file only grows for a new command, a new hard rule, or a new
top-level directory; everything else is one line in the nearest directory CLAUDE.md, or a section
in its docs/architecture/ file. Write only what the code can't say — probed Word/LibreOffice
behaviour, sentinel order, constants that must agree across files. Keep this file under 120 lines:
a section growing past ~5 lines belongs somewhere else.
Source layout
src/
App.svelte – app shell + app-level state
lib/
components/ – all Svelte UI (toolbars, pickers, dialogs, Editor, HeaderFooterLayer)
editor/
extensions.ts – the TipTap extension registry
extensions/ – the custom TipTap/ProseMirror extensions, one file per feature
utils/ – framework-free helpers (fontDetect, specialChars, wordCount,
orderedListTypes, historyLog, colorDebug)
math/ – formula AST: LaTeX parser/serializer, MathML, OMML
export/ import/ spell/ storage/ styles/ – ODF+DOCX I/O, spell-check, persistence, named styles
styles/ – global.css + editor.css
Where the detail lives
Directory-level CLAUDE.md files load automatically when you touch that directory; the
docs/ files are read on demand.
| Topic | File |
|---|---|
| Components, data flow, zoom, header/footer layer, debug dump | src/lib/components/CLAUDE.md |
| Extension registry index, shortcuts, context menu | src/lib/editor/CLAUDE.md, src/lib/editor/extensions/CLAUDE.md |
| ODF/DOCX export, post-processing passes, sentinels | src/lib/export/CLAUDE.md |
| ODF/DOCX import, style resolver, image formats | src/lib/import/CLAUDE.md |
| Paragraph + character styles, style manager | src/lib/styles/CLAUDE.md |
| localStorage keys, page margins/orientation, themes | src/lib/storage/CLAUDE.md |
| Pagination, widow/orphan, columns flow | docs/architecture/pagination.md |
| Images, text boxes, wrap modes | docs/architecture/frames.md |
| Table extensions + table styles | docs/architecture/tables.md |
| Text effects, indents, tab stops, list markers, date fields | docs/architecture/formatting.md |
| Formulas (LaTeX model, MathML, OMML, the math font) | docs/architecture/formulas.md |
| Headless browser + PDF-export debugging | docs/headless-testing.md |