4.3 KiB
4.3 KiB
CHUNK #7 — Spell & Grammar
Duration: ~2 days · Prereqs: CHUNK #3 (editor, decoration pipeline) · Follows: CHUNK #6 · Unblocks: CHUNK #9 (AI suggestions share the decoration pipeline)
Purpose: Add offline spellcheck (hunspell compiled to WASM) and optional grammar/style checking (self-hostable LanguageTool server), rendered as inline squiggles with a side panel to jump/apply/ignore. Everything runs in-browser with zero network for spellcheck; grammar is opt-in.
Steps
1. Spellcheck engine (packages/editor/src/spell/)
- hunspell-WASM (
packages/editor/src/spell/hunspell.ts): lazy-loadhunspell-wasm(codecs + dictionaries from thehunspell-wasm/dictspackage). Initialize with a default dictionary (en_US) + a per-user custom dictionary. - Dictionary management (
packages/editor/src/spell/dicts.ts):- Load dictionaries on demand (lazy, per active language).
- Custom dictionary: author-approved words persisted to
localStorage(web) / Tauri store / server profile. Words added here are never re-underlined. - Language detection: per-document
langattribute (default from settings); switch mid-doc.
- Checker (
packages/editor/src/spell/checker.ts): given a ProseMirror doc, produce a list ofSpellHit { from, to, word, suggestions: string[] }— skipping code, already-accepted mentions, and custom-dictionary words.
2. Grammar engine (packages/editor/src/grammar/)
- LanguageTool adapter (
packages/editor/src/grammar/languagetool.ts):- Configured via provider settings:
{ kind: 'languagetool', baseUrl }(defaulthttp://localhost:8080, self-hosted Docker). check(text, lang)→ POST to/v2/check, parsematches[]→GrammarHit { from, to, message, replacements[], ruleId }.- Debounced (800ms) + cached per text hash to avoid spamming the server.
- Configured via provider settings:
- Self-hostable server (
docker/languagetool/): adocker-composesnippet that runs the official LanguageTool image; documented indocker/README.md. API is OpenAI-compatible-ish (documented endpoints).
3. Decoration integration (packages/editor/src/decorations/)
- Plug both engines into the decoration pipeline built in Chunk 3 (
useDecorations):- Spellcheck → red squiggle (
Decoration.inlinewithclass="ps-spell"). - Grammar → blue squiggle (
class="ps-grammar").
- Spellcheck → red squiggle (
- Memoize: decorations recomputed only on relevant transactions (doc change, accept/reject).
4. Side panel (apps/web/src/features/check/CheckPanel.tsx)
- Lists all hits grouped by chapter:
word/message, suggested replacements (click to apply), "Add to dictionary", "Ignore this time", "Ignore all". - Jump: clicking a hit scrolls the editor to that range and highlights it.
- Counts: X spelling / Y grammar issues remaining; per-chapter breakdown.
5. Settings
- Toggle spellcheck/grammar on/off, per-language dictionaries, grammar server URL, check-on-type vs. check-on-save.
6. Tests
- Unit:
checkeron a known doc → correctSpellHits; custom dictionary word ignored. - Unit: grammar adapter parses a mock LanguageTool response correctly.
- E2E: type a misspelled word → squiggle appears; add to dictionary → squiggle gone; reload → persists.
- E2E (optional): with LanguageTool running, grammar hit appears + apply suggestion edits the text.
Acceptance criteria (all must pass)
- Misspelled word → red squiggle; suggestions shown in panel.
- "Add to dictionary" → word never underlined again; persists across reload.
- Grammar (if LanguageTool configured) → blue squiggle; apply suggestion edits text.
- Panel jumps to a hit and highlights it in the editor.
- Code blocks and mentions are never spellchecked.
- Toggle spellcheck off → no squiggles; setting persists.
- Contract suite still green on both adapters.
Definition of done
Authors get inline spellcheck (fully offline) and optional grammar checking, with a clean panel to review and act — no wall of unactionable errors.
Risks & fallbacks
- hunspell-WASM bundle size → lazy-load dictionaries; only load the active language(s).
- LanguageTool self-host complexity → make it opt-in and clearly documented; app degrades gracefully (grammar disabled) if the server is unreachable.
- Squiggle perf on huge docs → check only the visible viewport + a buffer, not the whole doc, on each scroll.