5c28a94813
Offline spellcheck via hunspell compiled to WASM (runs fully in-browser), plus optional self-hostable LanguageTool grammar/style checking. - hunspell-wasm spell engine with bundled en_US dictionary (base64-decoded browser-safe via atob + TextDecoder) - LanguageTool engine (self-hostable, docker compose snippet under docker/languagetool/) - single decoration pipeline: spell + grammar hits merge into one squiggle layer (red=spelling, blue=grammar) with a side panel to jump/apply/ignore and add-to-dictionary - useCheckService hook wired into WritingView; check.css loaded in main.tsx - spell + grammar unit tests (30 editor tests green) PLAN.md and CHUNK#7.md updated to reflect completion.
6.3 KiB
6.3 KiB
CHUNK #7 — Spell & Grammar
Status: ✅ Complete — all steps implemented, tests passing, build green.
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.
Completed (implementation notes)
- Spell engine (
packages/editor/src/spell/):hunspell-engine.tswrapshunspell-wasmwith a lazyload()+check()/suggest()API;spell.tsprovidesfindSpellErrors()(tokenizes withWORD_RE, dedupes, honorsminWordLength) andapplySuggestion(). Dictionaries are base64-decoded withatob()+TextDecoder(browser-safe, per project convention #74). - Grammar engine (
packages/editor/src/grammar/):grammar.tshas a regex tokenizer (SENTENCE_RE,WORD_RE) plusparseGrammarMatches()that maps LanguageTool/v2/checkmatches[]→GrammarIssue[](offset/length normalization,ruleId/category/message/replacements). - CheckService (
packages/editor/src/check/check-service.ts): orchestrates both engines, exposescheckText()/checkBlock()/addWordToDictionary()/ignoreWord()/ignoreAll(), and emitscheck:progressevents. - Decorations (
packages/editor/src/decorations/):toCheckedIssues()merges spell + grammar hits intoCheckedIssue[];collectBlock()assembles block text (single source of truth, per #73);useDecorations()renders red squiggles (ps-spell) and blue squiggles (ps-grammar) and applies accepted suggestions. - Web integration (
apps/web/src/):useCheckService.tshook wires the service intoWritingView.tsx;CheckPanel.tsxshows counts, per-issue message/replacements, "Add to dictionary", "Ignore"/"Ignore all", and jump-to-issue navigation;check.cssstyles squiggles + panel;main.tsximportscheck.css. - Settings: spellcheck toggle + grammar toggle in
Settings.tsx(persisted viauseSettings). - Docker:
docker/languagetool/docker-compose.yml+README.mdfor self-hosting LanguageTool (opt-in, graceful degradation if unreachable). - Tests:
spell.test.ts(9 tests) andgrammar.test.ts(12 tests) — all 30 editor tests green.
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.