@glw907/cairn-cms 0.60.1 → 0.62.2
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.
- package/CHANGELOG.md +78 -0
- package/dist/components/AdminLayout.svelte +22 -0
- package/dist/components/CairnAdmin.svelte +3 -0
- package/dist/components/CairnTidySettings.svelte +2 -2
- package/dist/components/CairnTidySettings.svelte.d.ts +1 -1
- package/dist/components/EditPage.svelte +116 -39
- package/dist/components/HelpHome.svelte +824 -0
- package/dist/components/HelpHome.svelte.d.ts +22 -0
- package/dist/components/MarkdownHelpDialog.svelte +4 -15
- package/dist/components/client-ingest.d.ts +16 -8
- package/dist/components/client-ingest.js +12 -6
- package/dist/components/editor-media.js +16 -8
- package/dist/components/editor-placeholder.d.ts +4 -2
- package/dist/components/editor-tidy.d.ts +24 -12
- package/dist/components/editor-tidy.js +8 -4
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/link-completion.d.ts +12 -6
- package/dist/components/link-completion.js +12 -6
- package/dist/components/markdown-directives.d.ts +9 -6
- package/dist/components/markdown-directives.js +9 -6
- package/dist/components/markdown-format.d.ts +7 -2
- package/dist/components/markdown-format.js +59 -28
- package/dist/components/markdown-reference.d.ts +8 -0
- package/dist/components/markdown-reference.js +22 -0
- package/dist/components/media-upload-outcome.d.ts +12 -6
- package/dist/components/objective-errors.d.ts +8 -4
- package/dist/components/objective-errors.js +8 -4
- package/dist/components/preview-doc.d.ts +4 -2
- package/dist/components/preview-doc.js +4 -2
- package/dist/components/spellcheck.d.ts +55 -29
- package/dist/components/spellcheck.js +39 -21
- package/dist/components/tidy-categorize.d.ts +20 -10
- package/dist/components/tidy-categorize.js +16 -8
- package/dist/components/tidy-validate.d.ts +12 -6
- package/dist/components/tidy-validate.js +20 -10
- package/dist/components/topbar-context.d.ts +4 -2
- package/dist/content/advisories.d.ts +56 -0
- package/dist/content/advisories.js +87 -0
- package/dist/content/compose.d.ts +4 -2
- package/dist/content/compose.js +1 -0
- package/dist/content/excerpt.js +4 -2
- package/dist/content/getting-started.d.ts +18 -0
- package/dist/content/getting-started.js +12 -0
- package/dist/content/links.d.ts +16 -8
- package/dist/content/links.js +12 -6
- package/dist/content/manifest.d.ts +36 -18
- package/dist/content/manifest.js +32 -16
- package/dist/content/media-refs.d.ts +4 -2
- package/dist/content/media-refs.js +4 -2
- package/dist/content/media-rewrite.d.ts +8 -4
- package/dist/content/media-rewrite.js +76 -38
- package/dist/content/schema.d.ts +20 -10
- package/dist/content/site-dictionary.d.ts +4 -2
- package/dist/content/site-dictionary.js +8 -4
- package/dist/content/types.d.ts +97 -42
- package/dist/delivery/content-index.d.ts +16 -8
- package/dist/delivery/feeds.js +4 -2
- package/dist/delivery/json-ld.d.ts +3 -0
- package/dist/delivery/json-ld.js +3 -0
- package/dist/delivery/manifest.d.ts +4 -2
- package/dist/delivery/manifest.js +4 -2
- package/dist/delivery/public-routes.d.ts +12 -6
- package/dist/delivery/public-routes.js +4 -2
- package/dist/delivery/seo-fields.d.ts +12 -6
- package/dist/delivery/seo-fields.js +8 -4
- package/dist/delivery/site-indexes.d.ts +4 -2
- package/dist/delivery/site-resolver.d.ts +4 -2
- package/dist/delivery/site-resolver.js +4 -2
- package/dist/doctor/cloudflare-api.d.ts +6 -0
- package/dist/doctor/cloudflare-api.js +6 -0
- package/dist/doctor/index.d.ts +12 -6
- package/dist/doctor/report.d.ts +3 -0
- package/dist/doctor/report.js +3 -0
- package/dist/doctor/run.d.ts +3 -0
- package/dist/doctor/run.js +3 -0
- package/dist/doctor/types.d.ts +10 -2
- package/dist/doctor/types.js +6 -0
- package/dist/doctor/wrangler-config.d.ts +7 -2
- package/dist/doctor/wrangler-config.js +3 -0
- package/dist/email.d.ts +4 -2
- package/dist/env.d.ts +0 -3
- package/dist/env.js +0 -3
- package/dist/github/branches.d.ts +4 -2
- package/dist/github/branches.js +4 -2
- package/dist/github/signing.d.ts +1 -1
- package/dist/github/signing.js +2 -2
- package/dist/log/events.d.ts +1 -1
- package/dist/media/bulk-delete-plan.d.ts +8 -4
- package/dist/media/config.d.ts +12 -6
- package/dist/media/config.js +16 -8
- package/dist/media/delivery-bucket.d.ts +4 -2
- package/dist/media/library-entry.d.ts +4 -2
- package/dist/media/library-entry.js +4 -2
- package/dist/media/manifest.d.ts +29 -15
- package/dist/media/manifest.js +29 -16
- package/dist/media/naming.d.ts +12 -6
- package/dist/media/naming.js +24 -12
- package/dist/media/orphan-scan.d.ts +4 -2
- package/dist/media/reconcile.d.ts +21 -11
- package/dist/media/reconcile.js +12 -6
- package/dist/media/reference.d.ts +8 -4
- package/dist/media/reference.js +12 -6
- package/dist/media/rewrite-plan.d.ts +12 -6
- package/dist/media/sniff.d.ts +4 -2
- package/dist/media/sniff.js +28 -14
- package/dist/media/store.d.ts +16 -8
- package/dist/media/store.js +4 -2
- package/dist/media/transform-url.d.ts +12 -6
- package/dist/media/transform-url.js +8 -4
- package/dist/media/usage.d.ts +8 -4
- package/dist/nav/site-config.d.ts +16 -8
- package/dist/render/component-grammar.d.ts +23 -10
- package/dist/render/component-grammar.js +19 -8
- package/dist/render/component-insert.d.ts +8 -4
- package/dist/render/component-insert.js +4 -2
- package/dist/render/component-reference.d.ts +4 -2
- package/dist/render/component-reference.js +4 -2
- package/dist/render/component-validate.d.ts +3 -0
- package/dist/render/component-validate.js +3 -0
- package/dist/render/glyph.d.ts +4 -2
- package/dist/render/glyph.js +4 -2
- package/dist/render/pipeline.d.ts +20 -10
- package/dist/render/pipeline.js +4 -2
- package/dist/render/registry.d.ts +40 -20
- package/dist/render/registry.js +16 -8
- package/dist/render/rehype-dispatch.d.ts +22 -8
- package/dist/render/rehype-dispatch.js +22 -8
- package/dist/render/remark-directives.d.ts +3 -0
- package/dist/render/remark-directives.js +3 -0
- package/dist/render/remark-figure.d.ts +4 -2
- package/dist/render/remark-figure.js +4 -2
- package/dist/render/resolve-links.d.ts +4 -2
- package/dist/render/resolve-links.js +4 -2
- package/dist/render/resolve-media.d.ts +16 -8
- package/dist/render/resolve-media.js +12 -6
- package/dist/sveltekit/admin-dispatch.d.ts +2 -0
- package/dist/sveltekit/admin-dispatch.js +9 -3
- package/dist/sveltekit/auth-routes.d.ts +3 -0
- package/dist/sveltekit/auth-routes.js +3 -0
- package/dist/sveltekit/cairn-admin.d.ts +16 -5
- package/dist/sveltekit/cairn-admin.js +26 -10
- package/dist/sveltekit/content-routes.d.ts +191 -86
- package/dist/sveltekit/content-routes.js +297 -107
- package/dist/sveltekit/editors-routes.d.ts +3 -0
- package/dist/sveltekit/editors-routes.js +3 -0
- package/dist/sveltekit/guard.d.ts +4 -2
- package/dist/sveltekit/guard.js +4 -2
- package/dist/sveltekit/https-required-page.d.ts +1 -1
- package/dist/sveltekit/https-required-page.js +1 -1
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/media-route.d.ts +1 -2
- package/dist/sveltekit/media-route.js +13 -8
- package/dist/sveltekit/nav-routes.d.ts +7 -2
- package/dist/sveltekit/nav-routes.js +3 -0
- package/dist/sveltekit/types.d.ts +4 -2
- package/dist/vite/index.d.ts +32 -16
- package/dist/vite/index.js +52 -26
- package/dist/vite/resolve-root.d.ts +8 -4
- package/dist/vite/resolve-root.js +4 -2
- package/package.json +7 -1
- package/src/lib/components/AdminLayout.svelte +22 -0
- package/src/lib/components/CairnAdmin.svelte +3 -0
- package/src/lib/components/CairnTidySettings.svelte +2 -2
- package/src/lib/components/ComponentForm.svelte +0 -1
- package/src/lib/components/EditPage.svelte +133 -41
- package/src/lib/components/HelpHome.svelte +850 -0
- package/src/lib/components/MarkdownHelpDialog.svelte +4 -15
- package/src/lib/components/client-ingest.ts +20 -10
- package/src/lib/components/editor-media.ts +20 -10
- package/src/lib/components/editor-placeholder.ts +12 -6
- package/src/lib/components/editor-tidy.ts +28 -14
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/link-completion.ts +12 -6
- package/src/lib/components/markdown-directives.ts +13 -8
- package/src/lib/components/markdown-format.ts +63 -30
- package/src/lib/components/markdown-reference.ts +30 -0
- package/src/lib/components/media-upload-outcome.ts +12 -6
- package/src/lib/components/objective-errors.ts +16 -8
- package/src/lib/components/preview-doc.ts +4 -2
- package/src/lib/components/spellcheck.ts +79 -41
- package/src/lib/components/tidy-categorize.ts +28 -14
- package/src/lib/components/tidy-validate.ts +28 -14
- package/src/lib/components/topbar-context.ts +4 -2
- package/src/lib/content/advisories.ts +150 -0
- package/src/lib/content/compose.ts +5 -2
- package/src/lib/content/excerpt.ts +4 -2
- package/src/lib/content/getting-started.ts +31 -0
- package/src/lib/content/links.ts +16 -8
- package/src/lib/content/manifest.ts +36 -18
- package/src/lib/content/media-refs.ts +4 -2
- package/src/lib/content/media-rewrite.ts +100 -50
- package/src/lib/content/schema.ts +20 -10
- package/src/lib/content/site-dictionary.ts +8 -4
- package/src/lib/content/types.ts +97 -42
- package/src/lib/delivery/content-index.ts +16 -8
- package/src/lib/delivery/feeds.ts +4 -2
- package/src/lib/delivery/json-ld.ts +3 -0
- package/src/lib/delivery/manifest.ts +4 -2
- package/src/lib/delivery/public-routes.ts +16 -8
- package/src/lib/delivery/seo-fields.ts +12 -6
- package/src/lib/delivery/site-indexes.ts +4 -2
- package/src/lib/delivery/site-resolver.ts +4 -2
- package/src/lib/doctor/cloudflare-api.ts +6 -0
- package/src/lib/doctor/index.ts +12 -6
- package/src/lib/doctor/report.ts +3 -0
- package/src/lib/doctor/run.ts +3 -0
- package/src/lib/doctor/types.ts +10 -2
- package/src/lib/doctor/wrangler-config.ts +7 -2
- package/src/lib/email.ts +4 -2
- package/src/lib/env.ts +0 -3
- package/src/lib/github/branches.ts +4 -2
- package/src/lib/github/signing.ts +2 -2
- package/src/lib/log/events.ts +1 -0
- package/src/lib/media/bulk-delete-plan.ts +8 -4
- package/src/lib/media/config.ts +24 -12
- package/src/lib/media/delivery-bucket.ts +4 -2
- package/src/lib/media/library-entry.ts +4 -2
- package/src/lib/media/manifest.ts +33 -18
- package/src/lib/media/naming.ts +24 -12
- package/src/lib/media/orphan-scan.ts +4 -2
- package/src/lib/media/reconcile.ts +21 -11
- package/src/lib/media/reference.ts +12 -6
- package/src/lib/media/rewrite-plan.ts +12 -6
- package/src/lib/media/sniff.ts +28 -14
- package/src/lib/media/store.ts +16 -8
- package/src/lib/media/transform-url.ts +12 -6
- package/src/lib/media/usage.ts +8 -4
- package/src/lib/nav/site-config.ts +16 -8
- package/src/lib/render/component-grammar.ts +23 -10
- package/src/lib/render/component-insert.ts +8 -4
- package/src/lib/render/component-reference.ts +4 -2
- package/src/lib/render/component-validate.ts +3 -0
- package/src/lib/render/glyph.ts +4 -2
- package/src/lib/render/pipeline.ts +20 -10
- package/src/lib/render/registry.ts +44 -22
- package/src/lib/render/rehype-dispatch.ts +22 -8
- package/src/lib/render/remark-directives.ts +3 -0
- package/src/lib/render/remark-figure.ts +4 -2
- package/src/lib/render/resolve-links.ts +4 -2
- package/src/lib/render/resolve-media.ts +16 -8
- package/src/lib/sveltekit/admin-dispatch.ts +10 -4
- package/src/lib/sveltekit/auth-routes.ts +3 -0
- package/src/lib/sveltekit/cairn-admin.ts +37 -15
- package/src/lib/sveltekit/content-routes.ts +494 -197
- package/src/lib/sveltekit/editors-routes.ts +3 -0
- package/src/lib/sveltekit/guard.ts +4 -2
- package/src/lib/sveltekit/https-required-page.ts +1 -1
- package/src/lib/sveltekit/index.ts +3 -0
- package/src/lib/sveltekit/media-route.ts +13 -8
- package/src/lib/sveltekit/nav-routes.ts +7 -2
- package/src/lib/sveltekit/types.ts +4 -2
- package/src/lib/vite/index.ts +60 -30
- package/src/lib/vite/resolve-root.ts +8 -4
|
@@ -40,8 +40,10 @@ const SKIP_NODES = new Set([
|
|
|
40
40
|
// surrounding whitespace). A token inside an image is already caught by the URL skip; this catches
|
|
41
41
|
// the form authors type directly in prose, so it is never split into "media" plus a flagged hash.
|
|
42
42
|
const MEDIA_TOKEN = /media:[\w.-]+/g;
|
|
43
|
-
/**
|
|
44
|
-
*
|
|
43
|
+
/**
|
|
44
|
+
* Merge overlapping or touching ranges into a sorted, disjoint set, so the keep-span computation
|
|
45
|
+
* subtracts one clean list of skip regions.
|
|
46
|
+
*/
|
|
45
47
|
function mergeRanges(ranges) {
|
|
46
48
|
if (ranges.length === 0)
|
|
47
49
|
return [];
|
|
@@ -57,9 +59,11 @@ function mergeRanges(ranges) {
|
|
|
57
59
|
}
|
|
58
60
|
return out;
|
|
59
61
|
}
|
|
60
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Every absolute skip range in the document, from all three mechanisms, merged. This is the single
|
|
61
64
|
* skip authority the spec calls for: the tree decides node kind, frontmatterSpan covers the `---`
|
|
62
|
-
* region, and fenceTokens covers the directive machinery the tree parses as plain text.
|
|
65
|
+
* region, and fenceTokens covers the directive machinery the tree parses as plain text.
|
|
66
|
+
*/
|
|
63
67
|
function skipRanges(text, tree) {
|
|
64
68
|
const skips = [];
|
|
65
69
|
// 1. The Lezer tree: the single authority for node-kind skips.
|
|
@@ -131,8 +135,10 @@ export function classifyProse(text, tree, from, to) {
|
|
|
131
135
|
out.push({ from: cursor, to });
|
|
132
136
|
return out;
|
|
133
137
|
}
|
|
134
|
-
/**
|
|
135
|
-
*
|
|
138
|
+
/**
|
|
139
|
+
* The prose ranges worth checking across the whole document. The lint source narrows this to the
|
|
140
|
+
* visible window; the unit test reads the whole-document set.
|
|
141
|
+
*/
|
|
136
142
|
export function spellcheckRanges(text, tree) {
|
|
137
143
|
return classifyProse(text, tree, 0, text.length);
|
|
138
144
|
}
|
|
@@ -141,8 +147,10 @@ export function spellcheckRanges(text, tree) {
|
|
|
141
147
|
// with the inner apostrophe (straight or curly) and hyphen allowed only between word characters.
|
|
142
148
|
const WORD = /[\p{L}\p{N}]+(?:[-'’][\p{L}\p{N}]+)*/gu;
|
|
143
149
|
const ALL_DIGITS = /^\p{N}+$/u;
|
|
144
|
-
/**
|
|
145
|
-
*
|
|
150
|
+
/**
|
|
151
|
+
* Whether a word is worth a lookup. Words under three characters, pure numbers, and all-caps tokens
|
|
152
|
+
* are skipped to cut false positives (the conservative posture VSCode's spell checker takes).
|
|
153
|
+
*/
|
|
146
154
|
function isCheckable(word) {
|
|
147
155
|
if (word.length < 3)
|
|
148
156
|
return false;
|
|
@@ -175,7 +183,7 @@ export function extractWords(text, from, to) {
|
|
|
175
183
|
// wall of near-ties.
|
|
176
184
|
const MAX_SUGGESTIONS = 5;
|
|
177
185
|
/**
|
|
178
|
-
* Build the correction popover for one misspelled word, as a
|
|
186
|
+
* Build the correction popover for one misspelled word, as a `@codemirror/lint` Diagnostic whose
|
|
179
187
|
* `actions` CodeMirror renders as tooltip buttons (no custom popover code). The actions, in order:
|
|
180
188
|
* up to five ranked suggestions (each replaces the word's range with one transaction), then "Add to
|
|
181
189
|
* dictionary", then "Ignore". The severity is `info` so the underline is quiet, and the message names
|
|
@@ -227,7 +235,7 @@ export function arbitrateChecked() {
|
|
|
227
235
|
};
|
|
228
236
|
}
|
|
229
237
|
/**
|
|
230
|
-
* Build the quick-fix popover for one objective-error finding, as a
|
|
238
|
+
* Build the quick-fix popover for one objective-error finding, as a `@codemirror/lint` Diagnostic whose
|
|
231
239
|
* one `actions` entry applies the finding's deterministic fix. The severity is `info` so the underline
|
|
232
240
|
* shares the spellcheck surface and the locked amber color (an editor reads spelling and these
|
|
233
241
|
* mechanical errors as one "spellcheck" layer). The fix range is recomputed from the live diagnostic
|
|
@@ -271,8 +279,10 @@ let lintMod = null;
|
|
|
271
279
|
let langMod = null;
|
|
272
280
|
let viewMod = null;
|
|
273
281
|
let stateMod = null;
|
|
274
|
-
/**
|
|
275
|
-
*
|
|
282
|
+
/**
|
|
283
|
+
* Construct the real spellcheck Worker, the spike's delivery shape. Kept behind the seam so the
|
|
284
|
+
* lint source never references `Worker` at module scope and a test can swap it.
|
|
285
|
+
*/
|
|
276
286
|
export function createSpellWorker() {
|
|
277
287
|
return new Worker(new URL('./spellcheck-worker.js', import.meta.url), {
|
|
278
288
|
type: 'module',
|
|
@@ -290,19 +300,23 @@ export function createSpellWorker() {
|
|
|
290
300
|
export function resolveWasmUrl() {
|
|
291
301
|
return new URL('./spellcheck-assets/spellchecker-wasm.wasm', import.meta.url).href;
|
|
292
302
|
}
|
|
293
|
-
/**
|
|
303
|
+
/**
|
|
304
|
+
* Each shipped dictionary, mapped to a resolver that builds its asset URL with a LITERAL
|
|
294
305
|
* `new URL(..., import.meta.url)`. The literal path is load-bearing. A templated `new URL` makes Vite
|
|
295
306
|
* and rolldown treat the directory as a glob and parse every sibling module to build it, including the
|
|
296
307
|
* `.svelte` components that still carry `lang="ts"` in `dist`, and the glob parser chokes on the TS
|
|
297
308
|
* syntax and breaks the consumer build. This set mirrors the dialect map in `nav/site-config.ts`; add
|
|
298
|
-
* one line per new shipped dialect dictionary.
|
|
309
|
+
* one line per new shipped dialect dictionary.
|
|
310
|
+
*/
|
|
299
311
|
const DICTIONARY_URLS = {
|
|
300
312
|
'dictionary-en-us.txt': () => new URL('./spellcheck-assets/dictionary-en-us.txt', import.meta.url).href,
|
|
301
313
|
};
|
|
302
|
-
/**
|
|
314
|
+
/**
|
|
315
|
+
* The real dictionary asset URL for a dictionary filename, resolved module-relative. The caller
|
|
303
316
|
* passes the dialect-resolved filename (default `dictionary-en-us.txt`). `dictionaryFileForDialect`
|
|
304
317
|
* already collapses an unknown dialect to the default, so an unmapped name falls back the same way
|
|
305
|
-
* rather than pointing at an asset that does not ship.
|
|
318
|
+
* rather than pointing at an asset that does not ship.
|
|
319
|
+
*/
|
|
306
320
|
export function resolveDictionaryUrl(dictionaryFile) {
|
|
307
321
|
const resolve = DICTIONARY_URLS[dictionaryFile] ?? DICTIONARY_URLS['dictionary-en-us.txt'];
|
|
308
322
|
return resolve();
|
|
@@ -345,7 +359,7 @@ function lockedUnderlineTheme(EditorViewMod) {
|
|
|
345
359
|
});
|
|
346
360
|
}
|
|
347
361
|
/**
|
|
348
|
-
* The
|
|
362
|
+
* The `@codemirror/lint` linter() source, made markdown-aware by the Lezer tree. It runs over the
|
|
349
363
|
* visible viewport plus a margin (not the whole document), extracts the checkable words via the pure
|
|
350
364
|
* classifier, posts them to the Worker keyed by a monotonic latest-wins seq, and maps the
|
|
351
365
|
* `correct: false` answers back to ranges. Each wrong word becomes a correction popover: the source
|
|
@@ -457,10 +471,12 @@ export async function cairnSpellcheck(options = {}) {
|
|
|
457
471
|
worker.postMessage({ type: 'addWord', words: siteWords });
|
|
458
472
|
return worker;
|
|
459
473
|
}
|
|
460
|
-
/**
|
|
474
|
+
/**
|
|
475
|
+
* Fetch a single word's ranked suggestions over the Worker, a one-shot listener removed on the
|
|
461
476
|
* answer. The suggest path is independent of the check seq, so a slow suggest never blocks a fresh
|
|
462
477
|
* check; an empty list (the engine returned nothing) still yields a popover with the two
|
|
463
|
-
* management actions.
|
|
478
|
+
* management actions.
|
|
479
|
+
*/
|
|
464
480
|
function fetchSuggestions(w, word) {
|
|
465
481
|
suggestSeq += 1;
|
|
466
482
|
const seq = suggestSeq;
|
|
@@ -476,8 +492,10 @@ export async function cairnSpellcheck(options = {}) {
|
|
|
476
492
|
w.postMessage({ type: 'suggest', seq, word });
|
|
477
493
|
});
|
|
478
494
|
}
|
|
479
|
-
/**
|
|
480
|
-
*
|
|
495
|
+
/**
|
|
496
|
+
* Turn the wrong words into correction popovers, each carrying its ranked suggestions and the two
|
|
497
|
+
* management actions.
|
|
498
|
+
*/
|
|
481
499
|
async function buildDiagnostics(wrong) {
|
|
482
500
|
const w = ensureWorker();
|
|
483
501
|
const callbacks = {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Change } from './tidy-diff.js';
|
|
2
2
|
import type { TidyConventions } from '../nav/site-config.js';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* A change's locally-inferred category. The first four are objective (safe to sweep); `normalization`
|
|
4
5
|
* and `grammar` are judgment (held undecided, never swept by Accept-fixes). `normalization` carries
|
|
5
|
-
* the convention key that authorized it, so the surface can name the setting and label the badge.
|
|
6
|
+
* the convention key that authorized it, so the surface can name the setting and label the badge.
|
|
7
|
+
*/
|
|
6
8
|
export type TidyCategory = {
|
|
7
9
|
kind: 'spelling';
|
|
8
10
|
} | {
|
|
@@ -17,13 +19,17 @@ export type TidyCategory = {
|
|
|
17
19
|
} | {
|
|
18
20
|
kind: 'grammar';
|
|
19
21
|
};
|
|
20
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* True for the objective categories: the safe, pre-kept, Accept-fixes-swept rank. A judgment
|
|
21
24
|
* category (`normalization` or `grammar`) returns false. The bulk action and the surface both read
|
|
22
|
-
* this, so the safety rank is one source of truth.
|
|
25
|
+
* this, so the safety rank is one source of truth.
|
|
26
|
+
*/
|
|
23
27
|
export declare function isObjective(category: TidyCategory): boolean;
|
|
24
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* The enabled-convention keys a normalization can be attributed to. Each maps to one config field on
|
|
25
30
|
* TidyConventions and to a because-line. A change is only ever labelled a normalization when it matches
|
|
26
|
-
* one of these AND the config has the matching variant enabled; otherwise it is never a normalization.
|
|
31
|
+
* one of these AND the config has the matching variant enabled; otherwise it is never a normalization.
|
|
32
|
+
*/
|
|
27
33
|
export type NormalizationKey = 'oxfordComma' | 'numberStyle' | 'measurements' | 'percent' | 'emDash' | 'enDashRanges' | 'ellipsis' | 'timeFormat' | 'smartQuotes';
|
|
28
34
|
/**
|
|
29
35
|
* Categorize one change against the captured original and the resolved conventions. The rules are
|
|
@@ -39,11 +45,13 @@ export type NormalizationKey = 'oxfordComma' | 'numberStyle' | 'measurements' |
|
|
|
39
45
|
* offers a normalization that cannot cite an enabled setting.
|
|
40
46
|
*/
|
|
41
47
|
export declare function categorize(change: Change, original: string, conventions: TidyConventions): TidyCategory;
|
|
42
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* The because-line data for a hunk: the convention's display name and the variant phrasing, both pure
|
|
43
50
|
* strings derived from the config. The surface renders "Your <label> setting is <variant>, ..." from
|
|
44
51
|
* these. Only a normalization carries a because-line; an objective or grammar hunk returns null (a
|
|
45
52
|
* grammar hunk's rationale, when shown, is the local subject-verb note the surface composes, not a
|
|
46
|
-
* config citation).
|
|
53
|
+
* config citation).
|
|
54
|
+
*/
|
|
47
55
|
export interface BecauseLine {
|
|
48
56
|
/** The convention's display label, e.g. "Oxford-comma". */
|
|
49
57
|
label: string;
|
|
@@ -61,7 +69,9 @@ export interface BecauseLine {
|
|
|
61
69
|
* null when the convention is somehow not enabled (defensive: categorize never produces such a hunk).
|
|
62
70
|
*/
|
|
63
71
|
export declare function buildBecause(key: NormalizationKey, conventions: TidyConventions): BecauseLine | null;
|
|
64
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* The human badge label for a category, the word shown in the hunk's category pill. A normalization's
|
|
65
74
|
* label is the convention's display name (its comma style, its time format), never "consistency" and
|
|
66
|
-
* never a count.
|
|
75
|
+
* never a count.
|
|
76
|
+
*/
|
|
67
77
|
export declare function categoryLabel(category: TidyCategory): string;
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
// quiet and are swept by Accept-fixes. Judgment categories (a declared normalization, or a grammar fix
|
|
10
10
|
// that reworded more than one token) carry the review-this treatment and are never swept until the
|
|
11
11
|
// author confirms each. The category alone decides the rank, so the surface and the bulk action agree.
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* True for the objective categories: the safe, pre-kept, Accept-fixes-swept rank. A judgment
|
|
13
14
|
* category (`normalization` or `grammar`) returns false. The bulk action and the surface both read
|
|
14
|
-
* this, so the safety rank is one source of truth.
|
|
15
|
+
* this, so the safety rank is one source of truth.
|
|
16
|
+
*/
|
|
15
17
|
export function isObjective(category) {
|
|
16
18
|
return (category.kind === 'spelling' ||
|
|
17
19
|
category.kind === 'typo' ||
|
|
@@ -94,9 +96,11 @@ function isWhitespaceOnly(text) {
|
|
|
94
96
|
function isPunctuationOnly(text) {
|
|
95
97
|
return text.length > 0 && /^[^A-Za-z0-9_\s]+$/.test(text);
|
|
96
98
|
}
|
|
97
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* The word ending immediately before `offset` in `text`, skipping any whitespace just before the
|
|
98
101
|
* offset, or null when none. The doubled-word rule reads it to confirm the deleted word repeats the
|
|
99
|
-
* one before it. Pure text inspection, never a count.
|
|
102
|
+
* one before it. Pure text inspection, never a count.
|
|
103
|
+
*/
|
|
100
104
|
function precedingWord(text, offset) {
|
|
101
105
|
let i = offset;
|
|
102
106
|
while (i > 0 && /\s/.test(text[i - 1]))
|
|
@@ -106,8 +110,10 @@ function precedingWord(text, offset) {
|
|
|
106
110
|
j--;
|
|
107
111
|
return j < i ? text.slice(j, i) : null;
|
|
108
112
|
}
|
|
109
|
-
/**
|
|
110
|
-
*
|
|
113
|
+
/**
|
|
114
|
+
* The word starting immediately after `offset` in `text`, skipping any whitespace just after the
|
|
115
|
+
* offset, or null when none. The doubled-word rule reads it as the other half of the look-around.
|
|
116
|
+
*/
|
|
111
117
|
function followingWord(text, offset) {
|
|
112
118
|
let i = offset;
|
|
113
119
|
while (i < text.length && /\s/.test(text[i]))
|
|
@@ -349,9 +355,11 @@ export function buildBecause(key, conventions) {
|
|
|
349
355
|
}
|
|
350
356
|
}
|
|
351
357
|
}
|
|
352
|
-
/**
|
|
358
|
+
/**
|
|
359
|
+
* The human badge label for a category, the word shown in the hunk's category pill. A normalization's
|
|
353
360
|
* label is the convention's display name (its comma style, its time format), never "consistency" and
|
|
354
|
-
* never a count.
|
|
361
|
+
* never a count.
|
|
362
|
+
*/
|
|
355
363
|
export function categoryLabel(category) {
|
|
356
364
|
switch (category.kind) {
|
|
357
365
|
case 'spelling':
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import type { Change } from './tidy-diff.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The reason a tidy result was rejected. Task 14 branches on this; every value maps to the one
|
|
3
4
|
* honest author-facing message, so the reason is for logging and tests, not the user surface.
|
|
4
5
|
* - `structure`: a directive opener/closer sequence, a heading count or level, or a fenced-code
|
|
5
6
|
* count diverged (the result restructured the document).
|
|
6
7
|
* - `frontmatter`: the frontmatter block is not byte-for-byte equal.
|
|
7
8
|
* - `media`: the multiset of `media:` hashes differs (a hash was altered, dropped, or invented).
|
|
8
9
|
* - `code`: a code span or fenced code block was edited.
|
|
9
|
-
* - `divergence`: the changed-token amount exceeds the length-aware bound (a wholesale rewrite).
|
|
10
|
+
* - `divergence`: the changed-token amount exceeds the length-aware bound (a wholesale rewrite).
|
|
11
|
+
*/
|
|
10
12
|
export type TidyRejectionReason = 'structure' | 'frontmatter' | 'media' | 'code' | 'divergence';
|
|
11
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The honest author-facing message a rejection maps to. The same message for every reason, by
|
|
12
15
|
* design: an author does not need the validator's internal taxonomy, only that the result was
|
|
13
|
-
* discarded and their text is safe.
|
|
16
|
+
* discarded and their text is safe.
|
|
17
|
+
*/
|
|
14
18
|
export declare const TIDY_REJECTION_MESSAGE = "Tidy returned a result that changed more than the wording, so it was discarded. Your text is unchanged.";
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
19
|
+
/**
|
|
20
|
+
* The outcome of validating a tidy result. On success it carries the Task 12 change set the review
|
|
21
|
+
* surface accepts and rejects against; on failure it carries the typed reason and the message.
|
|
22
|
+
*/
|
|
17
23
|
export type TidyValidation = {
|
|
18
24
|
ok: true;
|
|
19
25
|
changes: Change[];
|
|
@@ -16,9 +16,11 @@ import { visit } from 'unist-util-visit';
|
|
|
16
16
|
import { fenceScan, frontmatterSpan } from './markdown-directives.js';
|
|
17
17
|
import { parseMediaToken } from '../media/reference.js';
|
|
18
18
|
import { diffTokens, diffChanges } from './tidy-diff.js';
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The honest author-facing message a rejection maps to. The same message for every reason, by
|
|
20
21
|
* design: an author does not need the validator's internal taxonomy, only that the result was
|
|
21
|
-
* discarded and their text is safe.
|
|
22
|
+
* discarded and their text is safe.
|
|
23
|
+
*/
|
|
22
24
|
export const TIDY_REJECTION_MESSAGE = 'Tidy returned a result that changed more than the wording, so it was discarded. Your text is unchanged.';
|
|
23
25
|
// The divergence bound. The floor allows a fixed number of changed tokens regardless of fraction so
|
|
24
26
|
// a legitimate heavy proofread of a SHORT input is not penalized: a short paragraph with a typo in
|
|
@@ -38,10 +40,12 @@ const DIVERGENCE_FRACTION = 0.5;
|
|
|
38
40
|
// site, which the validator otherwise has no reason to know. A token mangled inside a code fence is
|
|
39
41
|
// caught here too, redundantly with the code check, which is the right posture for a backstop.
|
|
40
42
|
const MEDIA_TOKEN = /media:[A-Za-z0-9.-]+/g;
|
|
41
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* The sorted multiset of valid media hashes in the text. Each `media:` occurrence is parsed; a
|
|
42
45
|
* malformed token (a broken hash, an illegal slug) parses to null and is dropped, so a tidy that
|
|
43
46
|
* CORRUPTED a hash drops it from the multiset and the comparison fails. Sorted so two multisets
|
|
44
|
-
* compare by value, order-independent.
|
|
47
|
+
* compare by value, order-independent.
|
|
48
|
+
*/
|
|
45
49
|
function mediaHashes(text) {
|
|
46
50
|
const hashes = [];
|
|
47
51
|
for (const m of text.matchAll(MEDIA_TOKEN)) {
|
|
@@ -51,11 +55,13 @@ function mediaHashes(text) {
|
|
|
51
55
|
}
|
|
52
56
|
return hashes.sort();
|
|
53
57
|
}
|
|
54
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* The directive structure signature: each opener or closer in document order, paired with the depth
|
|
55
60
|
* the fence scan assigned it. Two texts share a directive structure when these signatures are equal,
|
|
56
61
|
* so an added, removed, or relevelled container fails the comparison. A fence-shaped line inside a
|
|
57
62
|
* code block is already disowned by the scan (its role is null), so a documented `:::` example does
|
|
58
|
-
* not enter the signature.
|
|
63
|
+
* not enter the signature.
|
|
64
|
+
*/
|
|
59
65
|
function directiveSignature(text) {
|
|
60
66
|
const { depths, roles } = fenceScan(text.split('\n'));
|
|
61
67
|
const parts = [];
|
|
@@ -65,10 +71,12 @@ function directiveSignature(text) {
|
|
|
65
71
|
}
|
|
66
72
|
return parts.join(',');
|
|
67
73
|
}
|
|
68
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* The heading signature: every ATX heading's level in document order. Parsed as mdast so a `#`
|
|
69
76
|
* inside a code block or an escaped one is never counted, and the level is the parser's own depth.
|
|
70
77
|
* Two texts share a heading structure when these are equal, so an added, removed, or relevelled
|
|
71
|
-
* heading fails the comparison.
|
|
78
|
+
* heading fails the comparison.
|
|
79
|
+
*/
|
|
72
80
|
function headingSignature(text) {
|
|
73
81
|
const tree = unified().use(remarkParse).use(remarkGfm).parse(text);
|
|
74
82
|
const levels = [];
|
|
@@ -78,11 +86,13 @@ function headingSignature(text) {
|
|
|
78
86
|
});
|
|
79
87
|
return levels.join(',');
|
|
80
88
|
}
|
|
81
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Every code span and fenced or indented code block in the text, as a sorted multiset of values.
|
|
82
91
|
* Parsed as mdast so the comparison sees exactly what the parser treats as code, the same authority
|
|
83
92
|
* the media body scan uses. Sorted so the comparison is order-independent: the divergence and
|
|
84
93
|
* structure checks own ordering, this check owns the contents. A `code` node is a block, an
|
|
85
|
-
* `inlineCode` node is a span.
|
|
94
|
+
* `inlineCode` node is a span.
|
|
95
|
+
*/
|
|
86
96
|
function codeContents(text) {
|
|
87
97
|
const tree = unified().use(remarkParse).use(remarkGfm).parse(text);
|
|
88
98
|
const values = [];
|
|
@@ -2,9 +2,11 @@ import { type Snippet } from 'svelte';
|
|
|
2
2
|
/** The shared holder: the desk snippet a document registers, or null on the office routes. */
|
|
3
3
|
export interface TopbarHolder {
|
|
4
4
|
desk: Snippet | null;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* True while the document is in zen: AdminLayout drops the whole topbar element so the band
|
|
6
7
|
* slides away (the desk's three clusters include AdminLayout-owned chrome, the drawer toggle and
|
|
7
|
-
* breadcrumb, that must vanish with it). EditPage sets this; the office routes leave it false.
|
|
8
|
+
* breadcrumb, that must vanish with it). EditPage sets this; the office routes leave it false.
|
|
9
|
+
*/
|
|
8
10
|
zen: boolean;
|
|
9
11
|
}
|
|
10
12
|
/** Called by AdminLayout once: creates the holder, provides it on context, returns it to render. */
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ConceptDescriptor } from './types.js';
|
|
2
|
+
import type { RepoRef } from '../github/types.js';
|
|
3
|
+
import type { Manifest } from './manifest.js';
|
|
4
|
+
/** One action an advisory offers, as a label and an optional link target. */
|
|
5
|
+
export interface AdvisoryAction {
|
|
6
|
+
/** The action's button or link label. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** The link target, when the action navigates. */
|
|
9
|
+
href?: string;
|
|
10
|
+
}
|
|
11
|
+
/** A non-blocking editor notice, serializable so it can ride EditData across the SSR boundary. */
|
|
12
|
+
export interface AdvisoryNotice {
|
|
13
|
+
/** The notice kind, e.g. "address-collision". */
|
|
14
|
+
kind: string;
|
|
15
|
+
/** The advisory severity; warn-and-allow, never a gate. */
|
|
16
|
+
severity: 'warn';
|
|
17
|
+
/** The notice text shown to the editor. */
|
|
18
|
+
message: string;
|
|
19
|
+
/** The notice's offered actions, when any. */
|
|
20
|
+
actions?: AdvisoryAction[];
|
|
21
|
+
}
|
|
22
|
+
/** One entry that resolves to an address, in a shape the collision check and the message read. */
|
|
23
|
+
export interface AddressEntry {
|
|
24
|
+
/** The concept id, e.g. "pages". */
|
|
25
|
+
concept: string;
|
|
26
|
+
/** The entry id (its filename stem). */
|
|
27
|
+
id: string;
|
|
28
|
+
/** The entry title for display, from the manifest (main) or frontmatter (branch). */
|
|
29
|
+
title: string;
|
|
30
|
+
/** The published corpus on main, or an open cairn/* edit branch. */
|
|
31
|
+
source: 'main' | 'branch';
|
|
32
|
+
}
|
|
33
|
+
/** Permalink to the distinct entries that resolve to it, across main and every open branch. */
|
|
34
|
+
export type AddressIndex = Map<string, AddressEntry[]>;
|
|
35
|
+
/**
|
|
36
|
+
* The address index over main only: a synchronous reverse map of each manifest entry's resolved
|
|
37
|
+
* permalink. No backend read, so an edit-load can build it for free from the manifest it already holds.
|
|
38
|
+
*/
|
|
39
|
+
export declare function mainAddressIndex(manifest: Manifest): AddressIndex;
|
|
40
|
+
/**
|
|
41
|
+
* Build the permalink-keyed address index over main (from each manifest entry's resolved permalink)
|
|
42
|
+
* plus every open cairn/* branch (resolved from its edited markdown).
|
|
43
|
+
*
|
|
44
|
+
* The build fails open: a branch read that throws and a permalink that cannot resolve are both caught
|
|
45
|
+
* and skipped, so a transient failure degrades to a thinner index, never a thrown editor or a blocked
|
|
46
|
+
* publish. The branches are read in one Promise.all, the way buildUsageIndex reads them.
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildAddressIndex(repo: RepoRef, token: string, concepts: ConceptDescriptor[], manifest: Manifest): Promise<AddressIndex>;
|
|
49
|
+
/**
|
|
50
|
+
* Find the first other entry that already resolves to an address, or null when the address is free
|
|
51
|
+
* or holds only the entry itself. The self entry is identified by its concept and id together.
|
|
52
|
+
*/
|
|
53
|
+
export declare function addressCollision(index: AddressIndex, self: {
|
|
54
|
+
concept: string;
|
|
55
|
+
id: string;
|
|
56
|
+
}, address: string): AddressEntry | null;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { listBranches } from '../github/branches.js';
|
|
2
|
+
import { readRaw } from '../github/repo.js';
|
|
3
|
+
import { PENDING_PREFIX, parsePendingBranch } from './pending.js';
|
|
4
|
+
import { findConcept } from './concepts.js';
|
|
5
|
+
import { isValidId, filenameFromId } from './ids.js';
|
|
6
|
+
import { parseMarkdown } from './frontmatter.js';
|
|
7
|
+
import { entryIdentity } from './identity.js';
|
|
8
|
+
/** Append a row under its permalink, creating the bucket on first use. */
|
|
9
|
+
function push(index, permalink, entry) {
|
|
10
|
+
const rows = index.get(permalink);
|
|
11
|
+
if (rows)
|
|
12
|
+
rows.push(entry);
|
|
13
|
+
else
|
|
14
|
+
index.set(permalink, [entry]);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The address index over main only: a synchronous reverse map of each manifest entry's resolved
|
|
18
|
+
* permalink. No backend read, so an edit-load can build it for free from the manifest it already holds.
|
|
19
|
+
*/
|
|
20
|
+
export function mainAddressIndex(manifest) {
|
|
21
|
+
const index = new Map();
|
|
22
|
+
for (const entry of manifest.entries) {
|
|
23
|
+
push(index, entry.permalink, {
|
|
24
|
+
concept: entry.concept,
|
|
25
|
+
id: entry.id,
|
|
26
|
+
title: entry.title,
|
|
27
|
+
source: 'main',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return index;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the permalink-keyed address index over main (from each manifest entry's resolved permalink)
|
|
34
|
+
* plus every open cairn/* branch (resolved from its edited markdown).
|
|
35
|
+
*
|
|
36
|
+
* The build fails open: a branch read that throws and a permalink that cannot resolve are both caught
|
|
37
|
+
* and skipped, so a transient failure degrades to a thinner index, never a thrown editor or a blocked
|
|
38
|
+
* publish. The branches are read in one Promise.all, the way buildUsageIndex reads them.
|
|
39
|
+
*/
|
|
40
|
+
export async function buildAddressIndex(repo, token, concepts, manifest) {
|
|
41
|
+
// The main arm: the manifest already carries each entry's resolved permalink, so seed from the
|
|
42
|
+
// synchronous main-only index and union the branch arm on top.
|
|
43
|
+
const index = mainAddressIndex(manifest);
|
|
44
|
+
// The branch arm: read each open cairn/* branch's one edited file and resolve its permalink. The
|
|
45
|
+
// path is derivable from the branch name, so no tree-listing is needed.
|
|
46
|
+
const names = await listBranches(repo, PENDING_PREFIX, token);
|
|
47
|
+
const perBranch = await Promise.all(names.map(async (name) => {
|
|
48
|
+
// Resolve the branch name with the branch tooling's guard: a malformed name, an id that fails
|
|
49
|
+
// the slug rule, or an unconfigured concept is skipped with no read attempted.
|
|
50
|
+
const ref = parsePendingBranch(name);
|
|
51
|
+
if (!ref || !isValidId(ref.id))
|
|
52
|
+
return null;
|
|
53
|
+
const concept = findConcept(concepts, ref.concept);
|
|
54
|
+
if (!concept)
|
|
55
|
+
return null;
|
|
56
|
+
const path = `${concept.dir}/${filenameFromId(ref.id)}`;
|
|
57
|
+
try {
|
|
58
|
+
const raw = await readRaw({ ...repo, branch: name }, path, token);
|
|
59
|
+
if (raw === null)
|
|
60
|
+
return null; // The file is absent on the branch: nothing to resolve.
|
|
61
|
+
const { frontmatter } = parseMarkdown(raw);
|
|
62
|
+
const fmTitle = frontmatter.title;
|
|
63
|
+
const title = typeof fmTitle === 'string' && fmTitle.trim() ? fmTitle : ref.id;
|
|
64
|
+
// entryIdentity throws for a dated entry with no date; that branch is caught and skipped.
|
|
65
|
+
const { permalink } = entryIdentity(concept, path, frontmatter);
|
|
66
|
+
return { permalink, entry: { concept: concept.id, id: ref.id, title, source: 'branch' } };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// A failed branch read or an unresolvable permalink degrades this one branch, fail open.
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
// Fold the per-branch rows back in, preserving the branch order so the index reads stably.
|
|
74
|
+
for (const row of perBranch) {
|
|
75
|
+
if (row)
|
|
76
|
+
push(index, row.permalink, row.entry);
|
|
77
|
+
}
|
|
78
|
+
return index;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Find the first other entry that already resolves to an address, or null when the address is free
|
|
82
|
+
* or holds only the entry itself. The self entry is identified by its concept and id together.
|
|
83
|
+
*/
|
|
84
|
+
export function addressCollision(index, self, address) {
|
|
85
|
+
const rows = index.get(address) ?? [];
|
|
86
|
+
return rows.find((row) => row.concept !== self.concept || row.id !== self.id) ?? null;
|
|
87
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { CairnAdapter, CairnExtension, CairnRuntime } from './types.js';
|
|
2
2
|
import { type SiteConfig } from '../nav/site-config.js';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
|
|
4
5
|
* always derived from one source and can never be silently dropped. `extensions` fold in after the
|
|
5
|
-
* adapter's concepts.
|
|
6
|
+
* adapter's concepts.
|
|
7
|
+
*/
|
|
6
8
|
export interface ComposeInput {
|
|
7
9
|
adapter: CairnAdapter;
|
|
8
10
|
siteConfig: SiteConfig;
|
package/dist/content/compose.js
CHANGED
|
@@ -28,6 +28,7 @@ export function composeRuntime({ adapter, siteConfig, extensions = [] }) {
|
|
|
28
28
|
concepts: resolveConcepts(content, siteConfig),
|
|
29
29
|
backend: adapter.backend,
|
|
30
30
|
sender: adapter.sender,
|
|
31
|
+
supportContact: adapter.supportContact,
|
|
31
32
|
render: adapter.render,
|
|
32
33
|
manifestPath: adapter.manifestPath ?? 'src/content/.cairn/index.json',
|
|
33
34
|
registry: adapter.registry,
|
package/dist/content/excerpt.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// cairn-cms: excerpt and word count for content summaries (public-delivery design, decision
|
|
2
2
|
// 5). A light markdown strip keeps summaries cheap, so a list card, an og:description, and a
|
|
3
3
|
// summary-mode feed read one derived excerpt without a full render.
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
4
|
+
/**
|
|
5
|
+
* Reduce markdown to readable plain text: drop fenced code, images, and markup; unwrap inline
|
|
6
|
+
* code and links to their text; collapse whitespace.
|
|
7
|
+
*/
|
|
6
8
|
function toPlainText(md) {
|
|
7
9
|
return md
|
|
8
10
|
.replace(/```[\s\S]*?```/g, ' ')
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Manifest } from './manifest.js';
|
|
2
|
+
/** The three getting-started steps, their completion count, and the fixed step total. */
|
|
3
|
+
export interface GettingStarted {
|
|
4
|
+
wrotePost: boolean;
|
|
5
|
+
publishedPost: boolean;
|
|
6
|
+
createdPage: boolean;
|
|
7
|
+
doneCount: number;
|
|
8
|
+
total: 3;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Map the manifest and the pending-branch list to the three getting-started step states. Writing a
|
|
12
|
+
* post (published or pending) completes the first step; publishing one completes the second; a page
|
|
13
|
+
* (published or pending) completes the third.
|
|
14
|
+
*/
|
|
15
|
+
export declare function deriveGettingStarted(manifest: Manifest, pending: {
|
|
16
|
+
concept: string;
|
|
17
|
+
id: string;
|
|
18
|
+
}[]): GettingStarted;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map the manifest and the pending-branch list to the three getting-started step states. Writing a
|
|
3
|
+
* post (published or pending) completes the first step; publishing one completes the second; a page
|
|
4
|
+
* (published or pending) completes the third.
|
|
5
|
+
*/
|
|
6
|
+
export function deriveGettingStarted(manifest, pending) {
|
|
7
|
+
const publishedPost = manifest.entries.some((e) => e.concept === 'posts');
|
|
8
|
+
const wrotePost = publishedPost || pending.some((p) => p.concept === 'posts');
|
|
9
|
+
const createdPage = manifest.entries.some((e) => e.concept === 'pages') || pending.some((p) => p.concept === 'pages');
|
|
10
|
+
const doneCount = Number(wrotePost) + Number(publishedPost) + Number(createdPage);
|
|
11
|
+
return { wrotePost, publishedPost, createdPage, doneCount, total: 3 };
|
|
12
|
+
}
|
package/dist/content/links.d.ts
CHANGED
|
@@ -3,20 +3,28 @@ export interface CairnRef {
|
|
|
3
3
|
concept: string;
|
|
4
4
|
id: string;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a reference to its live permalink. Returns undefined when the target is missing (the
|
|
8
|
+
* preview marks it); the build resolver throws instead, so a dangling token fails the build.
|
|
9
|
+
*/
|
|
8
10
|
export type LinkResolve = (ref: CairnRef) => string | undefined;
|
|
9
11
|
/** Parse a `cairn:<concept>/<id>` href, or null for any other href or a malformed token. */
|
|
10
12
|
export declare function parseCairnToken(href: string): CairnRef | null;
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
13
|
+
/**
|
|
14
|
+
* Write the `cairn:<concept>/<id>` token for a ref. The inverse of parseCairnToken, so the editor
|
|
15
|
+
* link picker and the autocomplete write exactly the form the resolver reads back.
|
|
16
|
+
*/
|
|
13
17
|
export declare function formatCairnToken(ref: CairnRef): string;
|
|
14
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Escape the characters that would break a markdown link's display text: a backslash and the
|
|
15
20
|
* square brackets that delimit the text. Used where a content title becomes link display text,
|
|
16
|
-
* so an unbalanced bracket in a title cannot truncate the generated link.
|
|
21
|
+
* so an unbalanced bracket in a title cannot truncate the generated link.
|
|
22
|
+
*/
|
|
17
23
|
export declare function escapeLinkText(text: string): string;
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
24
|
+
/**
|
|
25
|
+
* The cairn links a markdown body points at, in first-occurrence order, deduped by concept/id.
|
|
26
|
+
* Parses the body as mdast, so a token inside a code span or fence is never matched.
|
|
27
|
+
*/
|
|
20
28
|
export declare function extractCairnLinks(body: string): CairnRef[];
|
|
21
29
|
/**
|
|
22
30
|
* Rewrite every cairn: link whose href is exactly `oldHref` so its href becomes `newHref`, keeping
|