@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,279 @@
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
+
20
+ /** A single suggested correction the SymSpell lookup returned. */
21
+ interface SuggestedItem {
22
+ readonly term: string;
23
+ readonly distance: number;
24
+ readonly count: number;
25
+ }
26
+
27
+ /** The lookup options the wasm class accepts. SymSpell's Verbosity: 0 Top, 1 Closest, 2 All. */
28
+ interface CheckSpellingOptions {
29
+ readonly verbosity: number;
30
+ readonly maxEditDistance: number;
31
+ readonly includeUnknown: boolean;
32
+ readonly includeSelf: boolean;
33
+ }
34
+
35
+ /** The structural shape of the spellchecker-wasm browser class this worker drives. */
36
+ interface SpellcheckerWasm {
37
+ prepareSpellchecker(
38
+ wasmResponse: Response,
39
+ dictionaryResponse: Response,
40
+ bigramResponse?: Response | null,
41
+ ): Promise<void>;
42
+ checkSpelling(word: string, options?: CheckSpellingOptions): void;
43
+ }
44
+
45
+ /**
46
+ * The thin engine seam. The handler talks only to this interface, so the merged-set logic is a pure
47
+ * unit and any dialect-dictionary backend (spellchecker-wasm today, nspell tomorrow) can drop in.
48
+ */
49
+ export interface SpellEngine {
50
+ /** True when the word is correct per the loaded dialect dictionary (case-insensitive). */
51
+ check(word: string): boolean;
52
+ /** Ranked replacement terms for the word, the word itself dropped. */
53
+ suggest(word: string): string[];
54
+ }
55
+
56
+ // Drives the check path. `includeSelf: true` makes a known word return itself at edit distance 0, so
57
+ // correctness is a distance-0 self match rather than the absence of suggestions (a rare misspelling
58
+ // can also return no suggestions). Verbosity Closest keeps the suggestion set tight.
59
+ const CHECK_OPTIONS: CheckSpellingOptions = {
60
+ verbosity: 1,
61
+ maxEditDistance: 2,
62
+ includeUnknown: false,
63
+ includeSelf: true,
64
+ };
65
+
66
+ // Drives the suggest path. `includeSelf: false` drops the word itself, so the list is replacements.
67
+ const SUGGEST_OPTIONS: CheckSpellingOptions = {
68
+ verbosity: 1,
69
+ maxEditDistance: 2,
70
+ includeUnknown: false,
71
+ includeSelf: false,
72
+ };
73
+
74
+ /**
75
+ * The init message: the URLs of the two fetched assets, resolved by the consumer build. The
76
+ * dictionaryUrl is the dialect dictionary the main thread resolved from `spellcheck.dialect` (Task 7,
77
+ * defaulting to US English); the worker receives the resolved URL and does not read config itself.
78
+ */
79
+ interface InitMessage {
80
+ readonly type: 'init';
81
+ readonly wasmUrl: string;
82
+ readonly dictionaryUrl: string;
83
+ }
84
+
85
+ /** A batch spell check: each word carries a caller-side id so answers map back in any order. */
86
+ interface CheckMessage {
87
+ readonly type: 'check';
88
+ readonly seq: number;
89
+ readonly words: ReadonlyArray<{ readonly id: number; readonly word: string }>;
90
+ }
91
+
92
+ /** A suggestion request for a single word. */
93
+ interface SuggestMessage {
94
+ readonly type: 'suggest';
95
+ readonly seq: number;
96
+ readonly word: string;
97
+ }
98
+
99
+ /** Add a word (or a batch) to the in-memory personal dictionary, so a later check answers correct. */
100
+ interface AddWordMessage {
101
+ readonly type: 'addWord';
102
+ readonly word?: string;
103
+ readonly words?: ReadonlyArray<string>;
104
+ }
105
+
106
+ /** Add a word to the in-memory session ignore list, so a later check answers correct for it. */
107
+ interface IgnoreWordMessage {
108
+ readonly type: 'ignoreWord';
109
+ readonly word: string;
110
+ }
111
+
112
+ type InboundMessage = InitMessage | CheckMessage | SuggestMessage | AddWordMessage | IgnoreWordMessage;
113
+
114
+ /** A message the handler can act on without the engine (every kind except init). */
115
+ type HandlerMessage = CheckMessage | SuggestMessage | AddWordMessage | IgnoreWordMessage;
116
+
117
+ /** The init ack, posted once the dictionary has streamed in and lookups are live. */
118
+ interface ReadyMessage {
119
+ readonly type: 'ready';
120
+ }
121
+
122
+ /** The check answer: one verdict per requested word, correctness keyed by the caller's id. */
123
+ interface CheckResult {
124
+ readonly type: 'checked';
125
+ readonly seq: number;
126
+ readonly results: ReadonlyArray<{ readonly id: number; readonly correct: boolean }>;
127
+ }
128
+
129
+ /** The suggest answer: a ranked list of replacement terms for the word. */
130
+ interface SuggestResult {
131
+ readonly type: 'suggested';
132
+ readonly seq: number;
133
+ readonly word: string;
134
+ readonly suggestions: ReadonlyArray<string>;
135
+ }
136
+
137
+ /** An init or lookup failure, surfaced to the main thread rather than thrown into the void. */
138
+ interface ErrorResult {
139
+ readonly type: 'error';
140
+ readonly detail: string;
141
+ }
142
+
143
+ export type OutboundMessage = ReadyMessage | CheckResult | SuggestResult | ErrorResult;
144
+
145
+ /** A capturing sink for outbound messages, so the handler never references `self` or `Worker`. */
146
+ type Post = (message: OutboundMessage) => void;
147
+
148
+ /**
149
+ * The pure message handler. It owns the personal dictionary and the session ignore set and answers
150
+ * `check`/`suggest`/`addWord`/`ignoreWord` against the injected engine. It holds NO reference to a
151
+ * Worker context: it posts through the sink it is handed, so a test drives it with a fake engine and
152
+ * a capturing post.
153
+ *
154
+ * The merged set is layered: dialect (the engine), then site/personal, then session ignore. For a
155
+ * boolean correctness verdict the order does not change the answer, but the layers are kept distinct
156
+ * so the source of a verdict stays clear (and a future per-layer behavior has somewhere to live).
157
+ */
158
+ export function createSpellcheckHandler(engine: SpellEngine): {
159
+ handle(message: HandlerMessage, post: Post): void;
160
+ } {
161
+ const personal = new Set<string>();
162
+ const ignored = new Set<string>();
163
+
164
+ function isCorrect(word: string): boolean {
165
+ const lower = word.toLowerCase();
166
+ // Dialect first, then personal, then ignore. Sets are matched lowercased to mirror the engine's
167
+ // case-insensitive lookup, so "Cairn" added once answers for "cairn".
168
+ return engine.check(word) || personal.has(lower) || ignored.has(lower);
169
+ }
170
+
171
+ return {
172
+ handle(message, post) {
173
+ switch (message.type) {
174
+ case 'check': {
175
+ const results = message.words.map(({ id, word }) => ({ id, correct: isCorrect(word) }));
176
+ post({ type: 'checked', seq: message.seq, results });
177
+ break;
178
+ }
179
+ case 'suggest': {
180
+ post({
181
+ type: 'suggested',
182
+ seq: message.seq,
183
+ word: message.word,
184
+ suggestions: engine.suggest(message.word),
185
+ });
186
+ break;
187
+ }
188
+ case 'addWord': {
189
+ if (message.word) personal.add(message.word.toLowerCase());
190
+ if (message.words) for (const w of message.words) personal.add(w.toLowerCase());
191
+ break;
192
+ }
193
+ case 'ignoreWord': {
194
+ ignored.add(message.word.toLowerCase());
195
+ break;
196
+ }
197
+ }
198
+ },
199
+ };
200
+ }
201
+
202
+ /**
203
+ * The real engine: the spellchecker-wasm wrapper. SymSpell calls a result handler synchronously after
204
+ * each lookup, so the wrapper captures the most recent items in a closure variable and reads them
205
+ * right after `checkSpelling()` returns. This is the production engine handed to the handler on init.
206
+ */
207
+ function createWasmEngine(instance: SpellcheckerWasm, latest: { items: SuggestedItem[] }): SpellEngine {
208
+ return {
209
+ check(word) {
210
+ latest.items = [];
211
+ instance.checkSpelling(word, CHECK_OPTIONS);
212
+ // A word is correct when the lookup returns it unchanged at edit distance 0 (case-insensitive).
213
+ const lower = word.toLowerCase();
214
+ return latest.items.some((item) => item.distance === 0 && item.term.toLowerCase() === lower);
215
+ },
216
+ suggest(word) {
217
+ latest.items = [];
218
+ instance.checkSpelling(word, SUGGEST_OPTIONS);
219
+ // SymSpell returns the items ranked by edit distance then frequency. Drop the word itself (a
220
+ // distance-0 self match) so the list is replacements only.
221
+ const lower = word.toLowerCase();
222
+ return latest.items.filter((item) => item.term.toLowerCase() !== lower).map((item) => item.term);
223
+ },
224
+ };
225
+ }
226
+
227
+ async function createEngine(message: InitMessage): Promise<SpellEngine> {
228
+ // The browser class accepts fetch Responses for both assets and streams the dictionary into wasm
229
+ // memory in chunks, so the 1.5MB corpus is never held as one JS string. Import the class module
230
+ // directly, not the package's `browser/index.js` UMD bundle: that bundle's UMD prelude references
231
+ // `window` as the global root, which is undefined in a Worker. The class module is plain CommonJS
232
+ // that Vite interops, so it loads off the main thread cleanly.
233
+ const mod = (await import('spellchecker-wasm/lib/browser/SpellcheckerWasm.js')) as unknown as {
234
+ SpellcheckerWasm: new (handler: (items: SuggestedItem[]) => void) => SpellcheckerWasm;
235
+ };
236
+ const latest = { items: [] as SuggestedItem[] };
237
+ const instance = new mod.SpellcheckerWasm((items) => {
238
+ latest.items = items;
239
+ });
240
+ const [wasmResponse, dictionaryResponse] = await Promise.all([
241
+ fetch(message.wasmUrl),
242
+ fetch(message.dictionaryUrl),
243
+ ]);
244
+ await instance.prepareSpellchecker(wasmResponse, dictionaryResponse);
245
+ return createWasmEngine(instance, latest);
246
+ }
247
+
248
+ // The module-scope wiring runs only inside a Worker, where `self` and `addEventListener` exist. It is
249
+ // guarded so this module imports cleanly in node (the unit test imports the handler factory and the
250
+ // engine seam without a Worker context). On `init` it constructs the real wasm engine and a handler
251
+ // bound to it; every other message is delegated to the handler, which posts back through `self`.
252
+ if (typeof self !== 'undefined' && typeof (self as unknown as Worker).addEventListener === 'function') {
253
+ const worker = self as unknown as Worker;
254
+ const post: Post = (message) => worker.postMessage(message);
255
+
256
+ let handler: ReturnType<typeof createSpellcheckHandler> | null = null;
257
+
258
+ worker.addEventListener('message', (event: MessageEvent<InboundMessage>) => {
259
+ const message = event.data;
260
+ try {
261
+ if (message.type === 'init') {
262
+ void createEngine(message)
263
+ .then((engine) => {
264
+ handler = createSpellcheckHandler(engine);
265
+ post({ type: 'ready' });
266
+ })
267
+ .catch((err) => {
268
+ post({ type: 'error', detail: err instanceof Error ? err.message : String(err) });
269
+ });
270
+ } else if (!handler) {
271
+ post({ type: 'error', detail: 'spellchecker not ready' });
272
+ } else {
273
+ handler.handle(message, post);
274
+ }
275
+ } catch (err) {
276
+ post({ type: 'error', detail: err instanceof Error ? err.message : String(err) });
277
+ }
278
+ });
279
+ }