Files
edentext/src/lib/spell/controller.ts
T
Steffen Becker 0da75ed456 refactor: the project is called edentext
Renames the product in the docs, the page title and the DOCX creator, and moves
the whole storage namespace from odf-editor- to edentext-. The prefix is a
per-origin namespace, so it stays the product name rather than a generic one.
2026-08-13 21:43:12 +02:00

87 lines
2.4 KiB
TypeScript

import { loadChecker, type Checker } from './dictionary';
import { loadDocumentLanguage, type DocumentLanguage } from '../storage/documentLanguage';
const PERSONAL_KEY = 'edentext-user-dictionary';
function loadPersonal(): string[] {
try {
const raw = localStorage.getItem(PERSONAL_KEY);
const arr = raw ? JSON.parse(raw) : [];
return Array.isArray(arr) ? arr.filter((w): w is string => typeof w === 'string') : [];
} catch {
return [];
}
}
// Singleton bridging the static TipTap extension to the live spell state. The
// extension subscribes and re-decorates whenever the active checker, personal
// dictionary, or ignore set changes.
class SpellController {
private code: DocumentLanguage = '';
private checker: Checker | null = null;
private token = 0;
private readonly personal = new Set<string>(loadPersonal());
private readonly ignored = new Set<string>(); // session-only "Ignore all"
private readonly subs = new Set<() => void>();
constructor() {
if (typeof localStorage !== 'undefined') {
void this.setLanguage(loadDocumentLanguage());
}
}
getLanguage(): DocumentLanguage {
return this.code;
}
isEnabled(): boolean {
return this.checker !== null;
}
async setLanguage(code: DocumentLanguage): Promise<void> {
if (code === this.code) return;
this.code = code;
const token = ++this.token;
const checker = await loadChecker(code);
if (token !== this.token) return; // a newer setLanguage superseded this one
if (checker) for (const w of this.personal) checker.add(w);
this.checker = checker;
this.notify();
}
check(word: string): boolean {
if (!this.checker) return true;
if (this.ignored.has(word)) return true;
return this.checker.correct(word);
}
suggest(word: string): string[] {
return this.checker ? this.checker.suggest(word) : [];
}
addWord(word: string): void {
if (!word || this.personal.has(word)) return;
this.personal.add(word);
localStorage.setItem(PERSONAL_KEY, JSON.stringify([...this.personal]));
this.checker?.add(word);
this.notify();
}
ignoreWord(word: string): void {
if (!word || this.ignored.has(word)) return;
this.ignored.add(word);
this.notify();
}
subscribe(cb: () => void): () => void {
this.subs.add(cb);
return () => this.subs.delete(cb);
}
private notify(): void {
for (const cb of this.subs) cb();
}
}
export const spellController = new SpellController();