@glw907/cairn-cms 0.58.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/components/CairnAdmin.svelte +3 -0
  3. package/dist/components/CairnMediaLibrary.svelte +1101 -27
  4. package/dist/components/CairnMediaLibrary.svelte.d.ts +10 -2
  5. package/dist/components/CairnTidySettings.svelte +553 -0
  6. package/dist/components/CairnTidySettings.svelte.d.ts +32 -0
  7. package/dist/components/EditPage.svelte +371 -2
  8. package/dist/components/MarkdownEditor.svelte +168 -1
  9. package/dist/components/MarkdownEditor.svelte.d.ts +44 -0
  10. package/dist/components/TidyReview.svelte +463 -0
  11. package/dist/components/TidyReview.svelte.d.ts +47 -0
  12. package/dist/components/admin-icons.d.ts +1 -0
  13. package/dist/components/admin-icons.js +1 -0
  14. package/dist/components/cairn-admin.css +913 -2
  15. package/dist/components/editor-tidy.d.ts +31 -0
  16. package/dist/components/editor-tidy.js +199 -0
  17. package/dist/components/index.d.ts +1 -0
  18. package/dist/components/index.js +1 -0
  19. package/dist/components/markdown-directives.d.ts +16 -0
  20. package/dist/components/markdown-directives.js +34 -0
  21. package/dist/components/objective-errors.d.ts +30 -0
  22. package/dist/components/objective-errors.js +113 -0
  23. package/dist/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
  24. package/dist/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
  25. package/dist/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
  26. package/dist/components/spellcheck-worker.d.ts +80 -0
  27. package/dist/components/spellcheck-worker.js +161 -0
  28. package/dist/components/spellcheck.d.ts +146 -0
  29. package/dist/components/spellcheck.js +541 -0
  30. package/dist/components/tidy-categorize.d.ts +67 -0
  31. package/dist/components/tidy-categorize.js +392 -0
  32. package/dist/components/tidy-diff.d.ts +60 -0
  33. package/dist/components/tidy-diff.js +147 -0
  34. package/dist/components/tidy-validate.d.ts +37 -0
  35. package/dist/components/tidy-validate.js +174 -0
  36. package/dist/content/compose.d.ts +1 -1
  37. package/dist/content/compose.js +11 -0
  38. package/dist/content/site-dictionary.d.ts +31 -0
  39. package/dist/content/site-dictionary.js +82 -0
  40. package/dist/content/types.d.ts +25 -0
  41. package/dist/doctor/checks-local.d.ts +1 -0
  42. package/dist/doctor/checks-local.js +55 -6
  43. package/dist/doctor/index.js +2 -1
  44. package/dist/log/events.d.ts +1 -1
  45. package/dist/media/bulk-delete-plan.d.ts +24 -0
  46. package/dist/media/bulk-delete-plan.js +25 -0
  47. package/dist/media/orphan-scan.d.ts +37 -0
  48. package/dist/media/orphan-scan.js +42 -0
  49. package/dist/media/reconcile.d.ts +3 -0
  50. package/dist/media/reconcile.js +3 -2
  51. package/dist/nav/site-config.d.ts +98 -0
  52. package/dist/nav/site-config.js +132 -0
  53. package/dist/sveltekit/admin-dispatch.d.ts +2 -0
  54. package/dist/sveltekit/admin-dispatch.js +6 -2
  55. package/dist/sveltekit/cairn-admin.d.ts +16 -1
  56. package/dist/sveltekit/cairn-admin.js +28 -3
  57. package/dist/sveltekit/content-routes.d.ts +171 -4
  58. package/dist/sveltekit/content-routes.js +597 -3
  59. package/dist/sveltekit/index.d.ts +1 -1
  60. package/dist/sveltekit/tidy-prompt.d.ts +11 -0
  61. package/dist/sveltekit/tidy-prompt.js +118 -0
  62. package/package.json +10 -1
  63. package/src/lib/components/CairnAdmin.svelte +3 -0
  64. package/src/lib/components/CairnMediaLibrary.svelte +1101 -27
  65. package/src/lib/components/CairnTidySettings.svelte +553 -0
  66. package/src/lib/components/EditPage.svelte +371 -2
  67. package/src/lib/components/MarkdownEditor.svelte +168 -1
  68. package/src/lib/components/TidyReview.svelte +463 -0
  69. package/src/lib/components/admin-icons.ts +1 -0
  70. package/src/lib/components/cairn-admin.css +25 -0
  71. package/src/lib/components/editor-tidy.ts +241 -0
  72. package/src/lib/components/index.ts +1 -0
  73. package/src/lib/components/markdown-directives.ts +35 -0
  74. package/src/lib/components/objective-errors.ts +155 -0
  75. package/src/lib/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
  76. package/src/lib/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
  77. package/src/lib/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
  78. package/src/lib/components/spellcheck-worker.ts +279 -0
  79. package/src/lib/components/spellcheck.ts +679 -0
  80. package/src/lib/components/tidy-categorize.ts +460 -0
  81. package/src/lib/components/tidy-diff.ts +196 -0
  82. package/src/lib/components/tidy-validate.ts +202 -0
  83. package/src/lib/content/compose.ts +11 -1
  84. package/src/lib/content/site-dictionary.ts +84 -0
  85. package/src/lib/content/types.ts +25 -0
  86. package/src/lib/doctor/checks-local.ts +59 -5
  87. package/src/lib/doctor/index.ts +2 -0
  88. package/src/lib/log/events.ts +9 -1
  89. package/src/lib/media/bulk-delete-plan.ts +54 -0
  90. package/src/lib/media/orphan-scan.ts +74 -0
  91. package/src/lib/media/reconcile.ts +3 -2
  92. package/src/lib/nav/site-config.ts +197 -0
  93. package/src/lib/sveltekit/admin-dispatch.ts +7 -3
  94. package/src/lib/sveltekit/cairn-admin.ts +38 -4
  95. package/src/lib/sveltekit/content-routes.ts +795 -7
  96. package/src/lib/sveltekit/index.ts +1 -0
  97. package/src/lib/sveltekit/tidy-prompt.ts +153 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Justin Wilaby
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The thin engine seam. The handler talks only to this interface, so the merged-set logic is a pure
3
+ * unit and any dialect-dictionary backend (spellchecker-wasm today, nspell tomorrow) can drop in.
4
+ */
5
+ export interface SpellEngine {
6
+ /** True when the word is correct per the loaded dialect dictionary (case-insensitive). */
7
+ check(word: string): boolean;
8
+ /** Ranked replacement terms for the word, the word itself dropped. */
9
+ suggest(word: string): string[];
10
+ }
11
+ /** A batch spell check: each word carries a caller-side id so answers map back in any order. */
12
+ interface CheckMessage {
13
+ readonly type: 'check';
14
+ readonly seq: number;
15
+ readonly words: ReadonlyArray<{
16
+ readonly id: number;
17
+ readonly word: string;
18
+ }>;
19
+ }
20
+ /** A suggestion request for a single word. */
21
+ interface SuggestMessage {
22
+ readonly type: 'suggest';
23
+ readonly seq: number;
24
+ readonly word: string;
25
+ }
26
+ /** Add a word (or a batch) to the in-memory personal dictionary, so a later check answers correct. */
27
+ interface AddWordMessage {
28
+ readonly type: 'addWord';
29
+ readonly word?: string;
30
+ readonly words?: ReadonlyArray<string>;
31
+ }
32
+ /** Add a word to the in-memory session ignore list, so a later check answers correct for it. */
33
+ interface IgnoreWordMessage {
34
+ readonly type: 'ignoreWord';
35
+ readonly word: string;
36
+ }
37
+ /** A message the handler can act on without the engine (every kind except init). */
38
+ type HandlerMessage = CheckMessage | SuggestMessage | AddWordMessage | IgnoreWordMessage;
39
+ /** The init ack, posted once the dictionary has streamed in and lookups are live. */
40
+ interface ReadyMessage {
41
+ readonly type: 'ready';
42
+ }
43
+ /** The check answer: one verdict per requested word, correctness keyed by the caller's id. */
44
+ interface CheckResult {
45
+ readonly type: 'checked';
46
+ readonly seq: number;
47
+ readonly results: ReadonlyArray<{
48
+ readonly id: number;
49
+ readonly correct: boolean;
50
+ }>;
51
+ }
52
+ /** The suggest answer: a ranked list of replacement terms for the word. */
53
+ interface SuggestResult {
54
+ readonly type: 'suggested';
55
+ readonly seq: number;
56
+ readonly word: string;
57
+ readonly suggestions: ReadonlyArray<string>;
58
+ }
59
+ /** An init or lookup failure, surfaced to the main thread rather than thrown into the void. */
60
+ interface ErrorResult {
61
+ readonly type: 'error';
62
+ readonly detail: string;
63
+ }
64
+ export type OutboundMessage = ReadyMessage | CheckResult | SuggestResult | ErrorResult;
65
+ /** A capturing sink for outbound messages, so the handler never references `self` or `Worker`. */
66
+ type Post = (message: OutboundMessage) => void;
67
+ /**
68
+ * The pure message handler. It owns the personal dictionary and the session ignore set and answers
69
+ * `check`/`suggest`/`addWord`/`ignoreWord` against the injected engine. It holds NO reference to a
70
+ * Worker context: it posts through the sink it is handed, so a test drives it with a fake engine and
71
+ * a capturing post.
72
+ *
73
+ * The merged set is layered: dialect (the engine), then site/personal, then session ignore. For a
74
+ * boolean correctness verdict the order does not change the answer, but the layers are kept distinct
75
+ * so the source of a verdict stays clear (and a future per-layer behavior has somewhere to live).
76
+ */
77
+ export declare function createSpellcheckHandler(engine: SpellEngine): {
78
+ handle(message: HandlerMessage, post: Post): void;
79
+ };
80
+ export {};
@@ -0,0 +1,161 @@
1
+ // The spellcheck Web Worker. It owns the spellchecker-wasm (SymSpell) instance and the dictionary,
2
+ // so the main thread never holds the 1.5MB corpus and never runs a lookup on the typing thread.
3
+ //
4
+ // This module is loaded the same dynamic-import way CodeMirror is: the editor constructs it with
5
+ // `new Worker(new URL('./spellcheck-worker.js', import.meta.url), { type: 'module' })`, and the
6
+ // consumer build (Vite) resolves and emits this file plus the wasm and dictionary it fetches. The
7
+ // wasm and dictionary are NEVER bundled into this JS; they arrive as two `fetch` Responses whose URLs
8
+ // the main thread passes in the `init` message, so they stay out-of-bundle fetched assets.
9
+ //
10
+ // spellchecker-wasm's browser entry is CommonJS and its class is not reachable as a typed named
11
+ // export under NodeNext, so the surface is typed structurally and the dynamic import is cast, the
12
+ // same pattern the carta editor uses.
13
+ //
14
+ // The Worker owns the MERGED SET, not just the dialect dictionary: a word is correct if the engine
15
+ // (the loaded dialect dictionary) says so, OR it is in the personal dictionary, OR it is in the
16
+ // session ignore list. The layering and message handling live in a pure handler factory below so
17
+ // they unit-test without a Worker or wasm; the engine sits behind a thin interface so the real wasm
18
+ // wrapper is injected in production and a fake is injected in tests (and nspell could drop in too).
19
+ // Drives the check path. `includeSelf: true` makes a known word return itself at edit distance 0, so
20
+ // correctness is a distance-0 self match rather than the absence of suggestions (a rare misspelling
21
+ // can also return no suggestions). Verbosity Closest keeps the suggestion set tight.
22
+ const CHECK_OPTIONS = {
23
+ verbosity: 1,
24
+ maxEditDistance: 2,
25
+ includeUnknown: false,
26
+ includeSelf: true,
27
+ };
28
+ // Drives the suggest path. `includeSelf: false` drops the word itself, so the list is replacements.
29
+ const SUGGEST_OPTIONS = {
30
+ verbosity: 1,
31
+ maxEditDistance: 2,
32
+ includeUnknown: false,
33
+ includeSelf: false,
34
+ };
35
+ /**
36
+ * The pure message handler. It owns the personal dictionary and the session ignore set and answers
37
+ * `check`/`suggest`/`addWord`/`ignoreWord` against the injected engine. It holds NO reference to a
38
+ * Worker context: it posts through the sink it is handed, so a test drives it with a fake engine and
39
+ * a capturing post.
40
+ *
41
+ * The merged set is layered: dialect (the engine), then site/personal, then session ignore. For a
42
+ * boolean correctness verdict the order does not change the answer, but the layers are kept distinct
43
+ * so the source of a verdict stays clear (and a future per-layer behavior has somewhere to live).
44
+ */
45
+ export function createSpellcheckHandler(engine) {
46
+ const personal = new Set();
47
+ const ignored = new Set();
48
+ function isCorrect(word) {
49
+ const lower = word.toLowerCase();
50
+ // Dialect first, then personal, then ignore. Sets are matched lowercased to mirror the engine's
51
+ // case-insensitive lookup, so "Cairn" added once answers for "cairn".
52
+ return engine.check(word) || personal.has(lower) || ignored.has(lower);
53
+ }
54
+ return {
55
+ handle(message, post) {
56
+ switch (message.type) {
57
+ case 'check': {
58
+ const results = message.words.map(({ id, word }) => ({ id, correct: isCorrect(word) }));
59
+ post({ type: 'checked', seq: message.seq, results });
60
+ break;
61
+ }
62
+ case 'suggest': {
63
+ post({
64
+ type: 'suggested',
65
+ seq: message.seq,
66
+ word: message.word,
67
+ suggestions: engine.suggest(message.word),
68
+ });
69
+ break;
70
+ }
71
+ case 'addWord': {
72
+ if (message.word)
73
+ personal.add(message.word.toLowerCase());
74
+ if (message.words)
75
+ for (const w of message.words)
76
+ personal.add(w.toLowerCase());
77
+ break;
78
+ }
79
+ case 'ignoreWord': {
80
+ ignored.add(message.word.toLowerCase());
81
+ break;
82
+ }
83
+ }
84
+ },
85
+ };
86
+ }
87
+ /**
88
+ * The real engine: the spellchecker-wasm wrapper. SymSpell calls a result handler synchronously after
89
+ * each lookup, so the wrapper captures the most recent items in a closure variable and reads them
90
+ * right after `checkSpelling()` returns. This is the production engine handed to the handler on init.
91
+ */
92
+ function createWasmEngine(instance, latest) {
93
+ return {
94
+ check(word) {
95
+ latest.items = [];
96
+ instance.checkSpelling(word, CHECK_OPTIONS);
97
+ // A word is correct when the lookup returns it unchanged at edit distance 0 (case-insensitive).
98
+ const lower = word.toLowerCase();
99
+ return latest.items.some((item) => item.distance === 0 && item.term.toLowerCase() === lower);
100
+ },
101
+ suggest(word) {
102
+ latest.items = [];
103
+ instance.checkSpelling(word, SUGGEST_OPTIONS);
104
+ // SymSpell returns the items ranked by edit distance then frequency. Drop the word itself (a
105
+ // distance-0 self match) so the list is replacements only.
106
+ const lower = word.toLowerCase();
107
+ return latest.items.filter((item) => item.term.toLowerCase() !== lower).map((item) => item.term);
108
+ },
109
+ };
110
+ }
111
+ async function createEngine(message) {
112
+ // The browser class accepts fetch Responses for both assets and streams the dictionary into wasm
113
+ // memory in chunks, so the 1.5MB corpus is never held as one JS string. Import the class module
114
+ // directly, not the package's `browser/index.js` UMD bundle: that bundle's UMD prelude references
115
+ // `window` as the global root, which is undefined in a Worker. The class module is plain CommonJS
116
+ // that Vite interops, so it loads off the main thread cleanly.
117
+ const mod = (await import('spellchecker-wasm/lib/browser/SpellcheckerWasm.js'));
118
+ const latest = { items: [] };
119
+ const instance = new mod.SpellcheckerWasm((items) => {
120
+ latest.items = items;
121
+ });
122
+ const [wasmResponse, dictionaryResponse] = await Promise.all([
123
+ fetch(message.wasmUrl),
124
+ fetch(message.dictionaryUrl),
125
+ ]);
126
+ await instance.prepareSpellchecker(wasmResponse, dictionaryResponse);
127
+ return createWasmEngine(instance, latest);
128
+ }
129
+ // The module-scope wiring runs only inside a Worker, where `self` and `addEventListener` exist. It is
130
+ // guarded so this module imports cleanly in node (the unit test imports the handler factory and the
131
+ // engine seam without a Worker context). On `init` it constructs the real wasm engine and a handler
132
+ // bound to it; every other message is delegated to the handler, which posts back through `self`.
133
+ if (typeof self !== 'undefined' && typeof self.addEventListener === 'function') {
134
+ const worker = self;
135
+ const post = (message) => worker.postMessage(message);
136
+ let handler = null;
137
+ worker.addEventListener('message', (event) => {
138
+ const message = event.data;
139
+ try {
140
+ if (message.type === 'init') {
141
+ void createEngine(message)
142
+ .then((engine) => {
143
+ handler = createSpellcheckHandler(engine);
144
+ post({ type: 'ready' });
145
+ })
146
+ .catch((err) => {
147
+ post({ type: 'error', detail: err instanceof Error ? err.message : String(err) });
148
+ });
149
+ }
150
+ else if (!handler) {
151
+ post({ type: 'error', detail: 'spellchecker not ready' });
152
+ }
153
+ else {
154
+ handler.handle(message, post);
155
+ }
156
+ }
157
+ catch (err) {
158
+ post({ type: 'error', detail: err instanceof Error ? err.message : String(err) });
159
+ }
160
+ });
161
+ }
@@ -0,0 +1,146 @@
1
+ import type { Tree } from '@lezer/common';
2
+ import type { Diagnostic } from '@codemirror/lint';
3
+ import type { Extension } from '@codemirror/state';
4
+ import { type ObjectiveError } from './objective-errors.js';
5
+ /** An absolute character range in the document. */
6
+ export interface Range {
7
+ from: number;
8
+ to: number;
9
+ }
10
+ /** A word extracted for lookup: the lowercased form the Worker checks, and its absolute range so a
11
+ * verdict maps straight back to an underline. */
12
+ export interface ExtractedWord {
13
+ /** The lowercased word, as the engine's case-insensitive lookup expects. */
14
+ text: string;
15
+ from: number;
16
+ to: number;
17
+ }
18
+ /**
19
+ * The keep spans inside one text window [from, to): the window with every skip range subtracted.
20
+ * This is the lower-level primitive {@link spellcheckRanges} composes over the whole document, and
21
+ * the one the lint source runs over `view.visibleRanges` plus a margin. The skip authority and its
22
+ * precedence live in {@link skipRanges}.
23
+ */
24
+ export declare function classifyProse(text: string, tree: Tree, from: number, to: number): Range[];
25
+ /** The prose ranges worth checking across the whole document. The lint source narrows this to the
26
+ * visible window; the unit test reads the whole-document set. */
27
+ export declare function spellcheckRanges(text: string, tree: Tree): Range[];
28
+ /**
29
+ * The checkable words inside [from, to), each lowercased for lookup with its absolute range recorded
30
+ * so a verdict maps straight back to an underline. Sub-three-character words, pure numbers, and
31
+ * all-caps tokens are dropped.
32
+ */
33
+ export declare function extractWords(text: string, from: number, to: number): ExtractedWord[];
34
+ /** The callbacks the management actions invoke. The lint source supplies these so the pure builder
35
+ * never touches the Worker or the re-lint mechanism: it only wires the buttons to these handlers. */
36
+ export interface SpellDiagnosticActions {
37
+ /** Add the word to the personal dictionary (posts addWord, records the pending addition, re-lints). */
38
+ onAddWord(word: string): void;
39
+ /** Ignore the word for this session only (posts ignoreWord, re-lints). */
40
+ onIgnoreWord(word: string): void;
41
+ }
42
+ /**
43
+ * Build the correction popover for one misspelled word, as a @codemirror/lint Diagnostic whose
44
+ * `actions` CodeMirror renders as tooltip buttons (no custom popover code). The actions, in order:
45
+ * up to five ranked suggestions (each replaces the word's range with one transaction), then "Add to
46
+ * dictionary", then "Ignore". The severity is `info` so the underline is quiet, and the message names
47
+ * the word so the underline is never the only signal. Pure: it takes canned suggestions and callbacks,
48
+ * so the unit test asserts the actions array without a browser or a real Worker.
49
+ */
50
+ export declare function buildSpellDiagnostic(word: string, range: Range, suggestions: readonly string[], callbacks: SpellDiagnosticActions): Diagnostic;
51
+ /**
52
+ * The latest-wins arbiter (the media-preview settling pattern). The lint source hands out a
53
+ * monotonic seq with {@link next} on each run and posts it on the check message; when a `checked`
54
+ * answer lands, {@link accept} returns true only for the highest seq seen, so a stale answer from an
55
+ * older document state is dropped and the underlines never lag the text. Pure, so the seq logic
56
+ * unit-tests without a Worker.
57
+ */
58
+ export interface SeqArbiter {
59
+ /** The next monotonic seq, recorded as the current run. */
60
+ next(): number;
61
+ /** True when this seq is still the latest one issued or accepted, false for a stale answer. */
62
+ accept(seq: number): boolean;
63
+ }
64
+ /** Build a fresh {@link SeqArbiter}. */
65
+ export declare function arbitrateChecked(): SeqArbiter;
66
+ /**
67
+ * Build the quick-fix popover for one objective-error finding, as a @codemirror/lint Diagnostic whose
68
+ * one `actions` entry applies the finding's deterministic fix. The severity is `info` so the underline
69
+ * shares the spellcheck surface and the locked amber color (an editor reads spelling and these
70
+ * mechanical errors as one "spellcheck" layer). The fix range is recomputed from the live diagnostic
71
+ * position CodeMirror passes, offset by the same delta as the original finding, so an edit elsewhere
72
+ * never makes the fix overwrite the wrong span. Pure: it takes a finding, so the unit test asserts the
73
+ * diagnostic without a browser.
74
+ */
75
+ export declare function buildObjectiveDiagnostic(error: ObjectiveError): Diagnostic;
76
+ /** The narrow Worker surface the lint source drives: it posts check, suggest, addWord, and ignoreWord
77
+ * messages and listens for the answers. A `suggest` answer is a one-shot, so the source removes its
78
+ * own listener once it lands. A test injects a fake; production injects a real Worker. */
79
+ export interface SpellWorker {
80
+ postMessage(message: unknown): void;
81
+ addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
82
+ removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
83
+ }
84
+ /** Construct the real spellcheck Worker, the spike's delivery shape. Kept behind the seam so the
85
+ * lint source never references `Worker` at module scope and a test can swap it. */
86
+ export declare function createSpellWorker(): SpellWorker;
87
+ /** The real wasm asset URL, resolved module-relative the same way the worker is. */
88
+ export declare function resolveWasmUrl(): string;
89
+ /** The real dictionary asset URL for a dictionary filename, resolved module-relative. The caller
90
+ * passes the dialect-resolved filename (default `dictionary-en-us.txt`). */
91
+ export declare function resolveDictionaryUrl(dictionaryFile: string): string;
92
+ /** Options for {@link cairnSpellcheck}, so the unit and component layers can inject a fake Worker
93
+ * factory in place of the real `new Worker(...)`. */
94
+ export interface SpellcheckOptions {
95
+ /** The Worker factory; defaults to {@link createSpellWorker}. Created lazily on the first lint. */
96
+ createWorker?: () => SpellWorker;
97
+ /** The pending personal-dictionary additions, owned by the caller. When an author chooses "Add to
98
+ * dictionary" the source posts addWord to the Worker (the underline clears at once) and records the
99
+ * word here. The set is the seam Task 9 commits to the git-backed dictionary file; this source only
100
+ * fills it and never persists. A caller that does not pass one gets a fresh internal set. */
101
+ pendingAdditions?: Set<string>;
102
+ /** The committed personal-dictionary words (spec 1.6) the source seeds the Worker's personal layer
103
+ * with, posted as one batch `addWord` right after `init`. The git-backed site dictionary is the
104
+ * durable layer; the editor reads it at load (EditData.siteDictionary) and hands it here, so a word
105
+ * another editor committed answers correct from the first lint. Empty by default (dialect-only). */
106
+ siteWords?: ReadonlyArray<string>;
107
+ /** The dialect-resolved dictionary filename, e.g. "dictionary-en-us.txt". The source resolves it to
108
+ * a real asset URL and posts it in the Worker's `init`. Defaults to US English. */
109
+ dictionaryFile?: string;
110
+ /** Override the resolved wasm and dictionary URLs the source posts in `init`. The real resolution
111
+ * uses {@link resolveWasmUrl}/{@link resolveDictionaryUrl} (module-relative `import.meta.url`); a
112
+ * component test that injects a fake Worker can pass canned URLs so it never touches the asset
113
+ * resolver. */
114
+ assetUrls?: {
115
+ wasmUrl: string;
116
+ dictionaryUrl: string;
117
+ };
118
+ /** Treat the Worker as ready without waiting for a `ready` message. The production path is strict
119
+ * (it posts `init` and waits for `ready` before painting); a fake Worker in a test that does not
120
+ * answer `ready` can set this so a lint run is not held back. Defaults to false. */
121
+ assumeReady?: boolean;
122
+ /** The already-loaded CodeMirror modules to reuse instead of importing them again. The editor
123
+ * component loads `@codemirror/view`/`@codemirror/language` for its own extensions, so passing them
124
+ * here keeps the lint source on the SAME module instances; a second dynamic import can resolve to a
125
+ * separate copy (the test bundler's dedup quirk), and CodeMirror's instanceof checks then reject the
126
+ * extension. When omitted, the source imports them itself (the standalone path). `@codemirror/lint`
127
+ * is loaded here when not supplied, since the editor does not otherwise need it. */
128
+ modules?: {
129
+ lint?: typeof import('@codemirror/lint');
130
+ language?: typeof import('@codemirror/language');
131
+ view?: typeof import('@codemirror/view');
132
+ state?: typeof import('@codemirror/state');
133
+ };
134
+ }
135
+ /**
136
+ * The @codemirror/lint linter() source, made markdown-aware by the Lezer tree. It runs over the
137
+ * visible viewport plus a margin (not the whole document), extracts the checkable words via the pure
138
+ * classifier, posts them to the Worker keyed by a monotonic latest-wins seq, and maps the
139
+ * `correct: false` answers back to ranges. Each wrong word becomes a correction popover: the source
140
+ * fetches the ranked suggestions in the same batch, then {@link buildSpellDiagnostic} wires the
141
+ * quick-fix actions (the suggestions, then add-to-dictionary, then ignore). The returned extension
142
+ * bundles the spellcheck linter, a second deterministic objective-error linter over the same prose
143
+ * spans (doubled words, double spaces, repeated punctuation), and the locked amber underline theme,
144
+ * so Task 7's single on/off toggle gates both surfaces by reconfiguring this one extension.
145
+ */
146
+ export declare function cairnSpellcheck(options?: SpellcheckOptions): Promise<Extension>;