@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
|
@@ -49,10 +49,12 @@ interface Edit {
|
|
|
49
49
|
kind: RepointPlacement['kind'];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Drop any span that overlaps a span already kept, in source order. A final safety net so two
|
|
53
54
|
* splices can never target the same or overlapping bytes and clobber each other into a corrupt
|
|
54
55
|
* result, no matter how the locating arms behaved. A pure-insert span (`start === end`) overlaps
|
|
55
|
-
* another span only when it sits strictly inside it, so adjacent inserts and edits are kept.
|
|
56
|
+
* another span only when it sits strictly inside it, so adjacent inserts and edits are kept.
|
|
57
|
+
*/
|
|
56
58
|
function dropOverlappingEdits<T extends { start: number; end: number }>(edits: T[]): T[] {
|
|
57
59
|
const kept: T[] = [];
|
|
58
60
|
for (const e of edits) {
|
|
@@ -62,32 +64,40 @@ function dropOverlappingEdits<T extends { start: number; end: number }>(edits: T
|
|
|
62
64
|
return kept;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* A locating scan for candidate `media:` token substrings. Deliberately broad (it accepts
|
|
66
69
|
* uppercase and other out-of-grammar characters) so a malformed token is still found and then
|
|
67
70
|
* rejected by parseMediaToken, never silently skipped by the locator. The character class stops at
|
|
68
71
|
* whitespace, a quote, or any YAML or markdown delimiter, so a frontmatter value or an image
|
|
69
|
-
* destination ends the candidate.
|
|
72
|
+
* destination ends the candidate.
|
|
73
|
+
*/
|
|
70
74
|
const MEDIA_TOKEN_SCAN = /media:[A-Za-z0-9._-]+/g;
|
|
71
75
|
|
|
72
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Split a leading frontmatter block off the markdown. `fmBlock` is the `---` fenced block including
|
|
73
78
|
* both fences and the trailing newline (empty when there is none); `body` is everything after it.
|
|
74
79
|
* The block leads the document, so a frontmatter offset is already absolute and a body offset needs
|
|
75
|
-
* `fmBlock.length` added. Shared by every arm so they agree on the boundary.
|
|
80
|
+
* `fmBlock.length` added. Shared by every arm so they agree on the boundary.
|
|
81
|
+
*/
|
|
76
82
|
function splitFrontmatter(markdown: string): { fmBlock: string; body: string } {
|
|
77
83
|
const m = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
78
84
|
const fmBlock = m ? m[0] : '';
|
|
79
85
|
return { fmBlock, body: markdown.slice(fmBlock.length) };
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
/**
|
|
83
|
-
*
|
|
88
|
+
/**
|
|
89
|
+
* Parse a doc with the figure-aware pipeline, so the body arm agrees with what remarkFigure renders
|
|
90
|
+
* and can see the enclosing `:::figure` container. Mirrors parseFigureDoc in markdown-format.ts.
|
|
91
|
+
*/
|
|
84
92
|
function parseFigureDoc(doc: string): Root {
|
|
85
93
|
return unified().use(remarkParse).use(remarkGfm).use(remarkDirective).parse(doc) as Root;
|
|
86
94
|
}
|
|
87
95
|
|
|
88
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Whether `target` sits inside a `figure`-named container directive. Walks the tree to find the
|
|
89
98
|
* ancestor, since unist-util-visit's per-call ancestors are not retained across the traversal.
|
|
90
|
-
* Mirrors enclosingFigure in markdown-format.ts, reduced to a boolean.
|
|
99
|
+
* Mirrors enclosingFigure in markdown-format.ts, reduced to a boolean.
|
|
100
|
+
*/
|
|
91
101
|
function inFigure(tree: Root, target: Image): boolean {
|
|
92
102
|
let found = false;
|
|
93
103
|
visit(tree, 'containerDirective', (dir: ContainerDirective) => {
|
|
@@ -99,16 +109,20 @@ function inFigure(tree: Root, target: Image): boolean {
|
|
|
99
109
|
return found;
|
|
100
110
|
}
|
|
101
111
|
|
|
102
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* The split of fmBlock into its lines, each with its block-relative start and end offsets (the end
|
|
103
114
|
* is the index of the trailing newline, or the block length for the last line). Block offsets are
|
|
104
|
-
* already absolute since the frontmatter leads the document.
|
|
115
|
+
* already absolute since the frontmatter leads the document.
|
|
116
|
+
*/
|
|
105
117
|
interface FmLine {
|
|
106
118
|
start: number;
|
|
107
119
|
end: number;
|
|
108
120
|
}
|
|
109
121
|
|
|
110
|
-
/**
|
|
111
|
-
*
|
|
122
|
+
/**
|
|
123
|
+
* Split fmBlock into lines once, so the locator helpers walk a shared structure instead of
|
|
124
|
+
* re-scanning the block per call.
|
|
125
|
+
*/
|
|
112
126
|
function fmLines(fmBlock: string): FmLine[] {
|
|
113
127
|
const lines: FmLine[] = [];
|
|
114
128
|
let pos = 0;
|
|
@@ -122,12 +136,14 @@ function fmLines(fmBlock: string): FmLine[] {
|
|
|
122
136
|
return lines;
|
|
123
137
|
}
|
|
124
138
|
|
|
125
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* The inclusive line-index range `[lo, hi]` of the block-style mapping a top-level key opens: the
|
|
126
141
|
* line `^<key>:` at indent 0 through the last line before the next top-level key (or the document
|
|
127
142
|
* end). A flow-style value (`key: { ... }` all on one line) yields a single-line range. Returns null
|
|
128
143
|
* when the key has no top-level line, which a malformed or non-canonical block can cause. Scoping the
|
|
129
144
|
* per-key search to this range is what lets two image fields that share one hash, or an image field
|
|
130
|
-
* whose hash also appears in a sibling text value, resolve to distinct, correct spans.
|
|
145
|
+
* whose hash also appears in a sibling text value, resolve to distinct, correct spans.
|
|
146
|
+
*/
|
|
131
147
|
function frontmatterKeyRange(lines: FmLine[], fmBlock: string, key: string): [number, number] | null {
|
|
132
148
|
const opener = new RegExp(`^${escapeForRegExp(key)}:`);
|
|
133
149
|
const topLevelKey = /^[^\s#][^:]*:/;
|
|
@@ -153,8 +169,10 @@ function frontmatterKeyRange(lines: FmLine[], fmBlock: string, key: string): [nu
|
|
|
153
169
|
return [lo, hi];
|
|
154
170
|
}
|
|
155
171
|
|
|
156
|
-
/**
|
|
157
|
-
*
|
|
172
|
+
/**
|
|
173
|
+
* A located `src:` line inside a block-style mapping: the line's start and end, its leading indent,
|
|
174
|
+
* and the exact `media:` token's block-relative offsets and text.
|
|
175
|
+
*/
|
|
158
176
|
interface SrcLineHit {
|
|
159
177
|
lineStart: number;
|
|
160
178
|
lineEnd: number;
|
|
@@ -164,10 +182,12 @@ interface SrcLineHit {
|
|
|
164
182
|
token: string;
|
|
165
183
|
}
|
|
166
184
|
|
|
167
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Find the block-style `src:` line within `[lo, hi]` whose value token parses to `hash`. The token
|
|
168
187
|
* is located by the broad scan and validated through parseMediaToken (matching on hash), so a
|
|
169
188
|
* malformed token is found then rejected. Returns null for a flow-style value (no own `src:` line),
|
|
170
|
-
* which leaves that shape unanchorable rather than splicing a guessed span.
|
|
189
|
+
* which leaves that shape unanchorable rather than splicing a guessed span.
|
|
190
|
+
*/
|
|
171
191
|
function findSrcLineInRange(
|
|
172
192
|
lines: FmLine[],
|
|
173
193
|
fmBlock: string,
|
|
@@ -199,11 +219,13 @@ function findSrcLineInRange(
|
|
|
199
219
|
return null;
|
|
200
220
|
}
|
|
201
221
|
|
|
202
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* The image-like top-level frontmatter keys whose `src` parses to `hash`, in source order. A key is
|
|
203
224
|
* image-like when its value is an object carrying a string `src`; this is the same shape
|
|
204
225
|
* extractMediaRefs reads, so a token in a plain-text value (a `title:`/`note:`) is never treated as a
|
|
205
226
|
* reference. The bucket-classifying data comes from gray-matter (which handles every quoting form);
|
|
206
|
-
* the byte edit is located structurally by the caller, keyed back to this key name.
|
|
227
|
+
* the byte edit is located structurally by the caller, keyed back to this key name.
|
|
228
|
+
*/
|
|
207
229
|
function imageFieldKeys(data: Record<string, unknown>, hash: string): { key: string; obj: Record<string, unknown> }[] {
|
|
208
230
|
const out: { key: string; obj: Record<string, unknown> }[] = [];
|
|
209
231
|
for (const [key, value] of Object.entries(data)) {
|
|
@@ -217,11 +239,13 @@ function imageFieldKeys(data: Record<string, unknown>, hash: string): { key: str
|
|
|
217
239
|
return out;
|
|
218
240
|
}
|
|
219
241
|
|
|
220
|
-
/**
|
|
242
|
+
/**
|
|
243
|
+
* Collect hero src-token edits inside the frontmatter block. Only an image-field `src:` line is
|
|
221
244
|
* rewritten: the structure is read via gray-matter (image-like keys), and each key's `src:` line is
|
|
222
245
|
* located structurally within that key's block. A `media:` token sitting in a plain-text value (a
|
|
223
246
|
* `title:` or `description:`) is on no `src:` line, so it is left untouched, keeping the byte-exact
|
|
224
|
-
* contract and agreeing with extractMediaRefs. A flow-style hero has no `src:` line and is skipped.
|
|
247
|
+
* contract and agreeing with extractMediaRefs. A flow-style hero has no `src:` line and is skipped.
|
|
248
|
+
*/
|
|
225
249
|
function frontmatterEdits(markdown: string, fmBlock: string, oldHash: string): Edit[] {
|
|
226
250
|
if (fmBlock === '') return [];
|
|
227
251
|
const data = matter(markdown).data as Record<string, unknown>;
|
|
@@ -237,10 +261,12 @@ function frontmatterEdits(markdown: string, fmBlock: string, oldHash: string): E
|
|
|
237
261
|
return edits;
|
|
238
262
|
}
|
|
239
263
|
|
|
240
|
-
/**
|
|
264
|
+
/**
|
|
265
|
+
* Locate the exact `media:` token substring inside one image node's source span. The destination
|
|
241
266
|
* begins at the `](` that follows the alt text, so the search starts there to avoid a false match on
|
|
242
267
|
* a `media:`-like string inside the alt. Returns null when the token cannot be located, which leaves
|
|
243
|
-
* the image untouched rather than splicing a guessed range.
|
|
268
|
+
* the image untouched rather than splicing a guessed range.
|
|
269
|
+
*/
|
|
244
270
|
function locateImageToken(span: string, url: string): { start: number; end: number } | null {
|
|
245
271
|
const destStart = span.indexOf('](');
|
|
246
272
|
const from = destStart === -1 ? 0 : destStart + 2;
|
|
@@ -249,9 +275,11 @@ function locateImageToken(span: string, url: string): { start: number; end: numb
|
|
|
249
275
|
return { start: at, end: at + url.length };
|
|
250
276
|
}
|
|
251
277
|
|
|
252
|
-
/**
|
|
278
|
+
/**
|
|
279
|
+
* One body image whose url parses to the target hash, with its absolute node-span offsets (block
|
|
253
280
|
* length added) and whether it sits inside a `:::figure`. The shared body-image find that both the
|
|
254
|
-
* token-rewrite and alt-fill arms walk, so they agree on what an image is and how a figure is named.
|
|
281
|
+
* token-rewrite and alt-fill arms walk, so they agree on what an image is and how a figure is named.
|
|
282
|
+
*/
|
|
255
283
|
interface MatchedBodyImage {
|
|
256
284
|
node: Image;
|
|
257
285
|
/** Absolute start offset of the `` node in the whole markdown. */
|
|
@@ -261,9 +289,11 @@ interface MatchedBodyImage {
|
|
|
261
289
|
kind: 'body' | 'figure';
|
|
262
290
|
}
|
|
263
291
|
|
|
264
|
-
/**
|
|
292
|
+
/**
|
|
293
|
+
* Find every body image whose url parses to `hash`, in source order, with absolute offsets. Parses
|
|
265
294
|
* with the figure-aware pipeline, so a `media:` token inside a code span or fence is not an image
|
|
266
|
-
* node and is correctly skipped, matching extractMediaRefs.
|
|
295
|
+
* node and is correctly skipped, matching extractMediaRefs.
|
|
296
|
+
*/
|
|
267
297
|
function matchedBodyImages(body: string, blockLength: number, hash: string): MatchedBodyImage[] {
|
|
268
298
|
const tree = parseFigureDoc(body);
|
|
269
299
|
const hits: MatchedBodyImage[] = [];
|
|
@@ -283,9 +313,11 @@ function matchedBodyImages(body: string, blockLength: number, hash: string): Mat
|
|
|
283
313
|
return hits;
|
|
284
314
|
}
|
|
285
315
|
|
|
286
|
-
/**
|
|
316
|
+
/**
|
|
317
|
+
* Collect body edits over the body slice. Each matching image is located within its own source span
|
|
287
318
|
* and recorded with an absolute offset. The kind is 'figure' when the image is inside a `:::figure`,
|
|
288
|
-
* else 'body'.
|
|
319
|
+
* else 'body'.
|
|
320
|
+
*/
|
|
289
321
|
function bodyEdits(body: string, blockLength: number, oldHash: string): Edit[] {
|
|
290
322
|
const edits: Edit[] = [];
|
|
291
323
|
for (const hit of matchedBodyImages(body, blockLength, oldHash)) {
|
|
@@ -337,13 +369,17 @@ export function repointMediaRef(markdown: string, oldHash: string, newToken: str
|
|
|
337
369
|
return { markdown: out, placements };
|
|
338
370
|
}
|
|
339
371
|
|
|
340
|
-
/**
|
|
341
|
-
*
|
|
372
|
+
/**
|
|
373
|
+
* Which alt bucket a placement falls in: an empty alt always gets filled, a non-empty (custom) alt is
|
|
374
|
+
* reported and only overwritten on opt-in, and a decorative hero is never touched.
|
|
375
|
+
*/
|
|
342
376
|
export type AltBucket = 'will-fill' | 'customized' | 'decorative-skipped';
|
|
343
377
|
|
|
344
|
-
/**
|
|
378
|
+
/**
|
|
379
|
+
* One placement of the target hash and what the alt-fill does to it: which surface it lives on, its
|
|
345
380
|
* bucket, the existing alt, and the alt after the transform (unchanged for a customized alt left as
|
|
346
|
-
* is and for a decorative hero).
|
|
381
|
+
* is and for a decorative hero).
|
|
382
|
+
*/
|
|
347
383
|
export interface AltPlacement {
|
|
348
384
|
kind: 'body' | 'figure' | 'hero';
|
|
349
385
|
bucket: AltBucket;
|
|
@@ -359,10 +395,12 @@ export interface AltFillResult {
|
|
|
359
395
|
placements: AltPlacement[];
|
|
360
396
|
}
|
|
361
397
|
|
|
362
|
-
/**
|
|
398
|
+
/**
|
|
399
|
+
* A placement plus its optional byte edit. `apply` is false for a reported-but-unchanged placement
|
|
363
400
|
* (a kept custom alt, a decorative hero), which carries a diff entry but no splice. When `apply` is
|
|
364
401
|
* true, `[start, end)` is the absolute source span to replace with `text` (a pure insert is
|
|
365
|
-
* `start === end`). Keeping the placement here keeps the diff and the edits in step.
|
|
402
|
+
* `start === end`). Keeping the placement here keeps the diff and the edits in step.
|
|
403
|
+
*/
|
|
366
404
|
interface AltEdit {
|
|
367
405
|
apply: boolean;
|
|
368
406
|
start: number;
|
|
@@ -371,25 +409,31 @@ interface AltEdit {
|
|
|
371
409
|
placement: AltPlacement;
|
|
372
410
|
}
|
|
373
411
|
|
|
374
|
-
/**
|
|
412
|
+
/**
|
|
413
|
+
* Classify an existing alt into its non-decorative bucket: an empty (or whitespace-only) alt is
|
|
375
414
|
* filled, a non-empty alt is a custom alt the caller may opt in to overwrite. Mirrors the empty-alt
|
|
376
|
-
* test findMediaImagesNeedingAlt uses.
|
|
415
|
+
* test findMediaImagesNeedingAlt uses.
|
|
416
|
+
*/
|
|
377
417
|
function classifyAlt(existing: string): 'will-fill' | 'customized' {
|
|
378
418
|
return existing.trim() === '' ? 'will-fill' : 'customized';
|
|
379
419
|
}
|
|
380
420
|
|
|
381
|
-
/**
|
|
382
|
-
*
|
|
421
|
+
/**
|
|
422
|
+
* Whether a bucket plus the overwrite choice means the alt text is actually rewritten. A will-fill
|
|
423
|
+
* always writes; a customized alt writes only on opt-in; a decorative hero never writes.
|
|
424
|
+
*/
|
|
383
425
|
function altIsEdited(bucket: AltBucket, overwrite: boolean): boolean {
|
|
384
426
|
if (bucket === 'will-fill') return true;
|
|
385
427
|
if (bucket === 'customized') return overwrite;
|
|
386
428
|
return false;
|
|
387
429
|
}
|
|
388
430
|
|
|
389
|
-
/**
|
|
431
|
+
/**
|
|
432
|
+
* Collect the body and figure alt edits over the body slice. The alt source span sits between ` is spliced there. The existing alt is the parser's already
|
|
392
|
-
* unescaped `node.alt`. A body image has no decorative slot, so an empty alt is always will-fill.
|
|
435
|
+
* unescaped `node.alt`. A body image has no decorative slot, so an empty alt is always will-fill.
|
|
436
|
+
*/
|
|
393
437
|
function bodyAltEdits(body: string, blockLength: number, hash: string, defaultAlt: string, overwrite: boolean): AltEdit[] {
|
|
394
438
|
const edits: AltEdit[] = [];
|
|
395
439
|
for (const hit of matchedBodyImages(body, blockLength, hash)) {
|
|
@@ -426,18 +470,22 @@ function bodyAltEdits(body: string, blockLength: number, hash: string, defaultAl
|
|
|
426
470
|
return edits;
|
|
427
471
|
}
|
|
428
472
|
|
|
429
|
-
/**
|
|
430
|
-
*
|
|
473
|
+
/**
|
|
474
|
+
* Escape a literal string for safe interpolation into a RegExp source. A key name or an indent is
|
|
475
|
+
* matched literally, so its characters must not act as metacharacters.
|
|
476
|
+
*/
|
|
431
477
|
function escapeForRegExp(literal: string): string {
|
|
432
478
|
return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
433
479
|
}
|
|
434
480
|
|
|
435
|
-
/**
|
|
481
|
+
/**
|
|
482
|
+
* Find a sibling key line (`alt:` or `decorative:`) at exactly `indent` within the inclusive
|
|
436
483
|
* line-index range `[lo, hi]` of one mapping. The range is the mapping's own block, so the search
|
|
437
484
|
* spans the whole mapping rather than a same-indent contiguous run: a blank line or a deeper-nested
|
|
438
485
|
* child between `src:` and `alt:` no longer hides the existing key (which would otherwise insert a
|
|
439
486
|
* duplicate key and break the YAML). Returns the key line's value span (after the key and its space,
|
|
440
|
-
* to end of line) or null when the mapping has no such key at that indent.
|
|
487
|
+
* to end of line) or null when the mapping has no such key at that indent.
|
|
488
|
+
*/
|
|
441
489
|
function findSiblingKeyValue(
|
|
442
490
|
lines: FmLine[],
|
|
443
491
|
fmBlock: string,
|
|
@@ -454,7 +502,8 @@ function findSiblingKeyValue(
|
|
|
454
502
|
return null;
|
|
455
503
|
}
|
|
456
504
|
|
|
457
|
-
/**
|
|
505
|
+
/**
|
|
506
|
+
* Collect the hero alt edits inside the frontmatter block. The image-field objects (and their
|
|
458
507
|
* decorative and alt values) are read via gray-matter to classify the bucket robustly across quoting
|
|
459
508
|
* forms; the byte edit is then located structurally, scoped to each field's own mapping block, keyed
|
|
460
509
|
* back by the top-level field name. Iterating the fields in source order keeps the hero placements in
|
|
@@ -463,7 +512,8 @@ function findSiblingKeyValue(
|
|
|
463
512
|
* blank line or a nested child) has its value replaced; an absent one is inserted right after the
|
|
464
513
|
* `src:` line at the same indent. The new value is a JSON-quoted scalar, valid YAML that handles a
|
|
465
514
|
* colon, a quote, or an empty string. A flow-style hero (`image: { ... }`, no own `src:` line) is
|
|
466
|
-
* unanchorable, so it is reported from the gray-matter read but never spliced.
|
|
515
|
+
* unanchorable, so it is reported from the gray-matter read but never spliced.
|
|
516
|
+
*/
|
|
467
517
|
function heroAltEdits(
|
|
468
518
|
markdown: string,
|
|
469
519
|
fmBlock: string,
|
|
@@ -11,8 +11,10 @@ export interface StandardInput {
|
|
|
11
11
|
body: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
14
|
+
/**
|
|
15
|
+
* A minimal local copy of the Standard Schema v1 interface (https://standardschema.dev), so the
|
|
16
|
+
* schema is a drop-in where the ecosystem accepts a validator, with no runtime dependency.
|
|
17
|
+
*/
|
|
16
18
|
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
17
19
|
readonly '~standard': {
|
|
18
20
|
readonly version: 1;
|
|
@@ -25,9 +27,11 @@ type StandardResult<Output> =
|
|
|
25
27
|
| { readonly value: Output; readonly issues?: undefined }
|
|
26
28
|
| { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray<PropertyKey> }> };
|
|
27
29
|
|
|
28
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Map one field descriptor to the TS type of its normalized value. text, textarea, and date
|
|
29
32
|
* normalize to a string; a closed-vocabulary `tags` field to the option-union array; an `image`
|
|
30
|
-
* field to its nested object.
|
|
33
|
+
* field to its nested object.
|
|
34
|
+
*/
|
|
31
35
|
type FieldValue<K extends FrontmatterField> = K extends { type: 'boolean' }
|
|
32
36
|
? boolean
|
|
33
37
|
: K extends { type: 'image' }
|
|
@@ -41,16 +45,20 @@ type FieldValue<K extends FrontmatterField> = K extends { type: 'boolean' }
|
|
|
41
45
|
/** Flatten an intersection into a single readable object type. */
|
|
42
46
|
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
43
47
|
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
48
|
+
/**
|
|
49
|
+
* The normalized frontmatter type inferred from a field tuple. A field declared
|
|
50
|
+
* `required: true` is a required key; every other field is optional.
|
|
51
|
+
*/
|
|
46
52
|
export type InferFields<F extends readonly FrontmatterField[]> = Prettify<
|
|
47
53
|
{ [K in F[number] as K extends { required: true } ? K['name'] : never]: FieldValue<K> } & {
|
|
48
54
|
[K in F[number] as K extends { required: true } ? never : K['name']]?: FieldValue<K>;
|
|
49
55
|
}
|
|
50
56
|
>;
|
|
51
57
|
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
58
|
+
/**
|
|
59
|
+
* A concept's schema: the plain-data field projection, the generated validator, and the
|
|
60
|
+
* Standard Schema conformance property.
|
|
61
|
+
*/
|
|
54
62
|
export interface ConceptSchema<F extends readonly FrontmatterField[] = readonly FrontmatterField[]> {
|
|
55
63
|
/** The declared fields as plain serializable data, for the editor form. */
|
|
56
64
|
readonly fields: FrontmatterField[];
|
|
@@ -82,9 +90,11 @@ function applyRules(field: FrontmatterField, value: unknown, errors: Record<stri
|
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Options for `defineFields`. `refine` runs after the per-field rules pass, for cross-field and
|
|
86
95
|
* body-dependent checks. It is validation-only: it returns field-keyed errors to merge, or
|
|
87
|
-
* nothing, and never transforms the data.
|
|
96
|
+
* nothing, and never transforms the data.
|
|
97
|
+
*/
|
|
88
98
|
export interface DefineFieldsOptions<F extends readonly FrontmatterField[]> {
|
|
89
99
|
refine?: (data: InferFields<F>, body: string) => Record<string, string> | undefined;
|
|
90
100
|
}
|
|
@@ -19,11 +19,13 @@ const HEADER = '# cairn personal dictionary: one word per line, sorted, kept in
|
|
|
19
19
|
// inbound words through this before a merge.
|
|
20
20
|
const WORD_RE = /^[^\s\p{Cc}]+$/u;
|
|
21
21
|
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* True when a word is a single valid dictionary line (no whitespace, no control characters, non-empty
|
|
23
24
|
* and within the length bound). A leading "#" is rejected: parseDictionary re-reads such a line as a
|
|
24
25
|
* comment, so committing it would silently drop the word on the next read. The action uses this to
|
|
25
26
|
* reject untrusted input before the merge, so a newline or a control byte can never inject an extra
|
|
26
|
-
* line into the committed file.
|
|
27
|
+
* line into the committed file.
|
|
28
|
+
*/
|
|
27
29
|
export function isValidDictionaryWord(word: string, maxLength = 64): boolean {
|
|
28
30
|
if (word.startsWith('#')) return false;
|
|
29
31
|
return word.length > 0 && word.length <= maxLength && WORD_RE.test(word);
|
|
@@ -47,8 +49,10 @@ export function parseDictionary(text: string | null): string[] {
|
|
|
47
49
|
return words;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
/**
|
|
51
|
-
*
|
|
52
|
+
/**
|
|
53
|
+
* Case-insensitive, locale-stable comparator for the canonical sort. Words are compared lowercased
|
|
54
|
+
* so "Cairn" and "cairn" collapse to one entry, the same case-folding the Worker's merged set uses.
|
|
55
|
+
*/
|
|
52
56
|
function byWord(a: string, b: string): number {
|
|
53
57
|
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
54
58
|
}
|