@glw907/cairn-cms 0.60.1 → 0.62.1
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 +69 -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 +51 -0
- package/dist/content/advisories.js +79 -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 +295 -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 +141 -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 +492 -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
package/src/lib/media/store.ts
CHANGED
|
@@ -12,12 +12,16 @@ import type {
|
|
|
12
12
|
R2Range,
|
|
13
13
|
} from '@cloudflare/workers-types';
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
15
|
+
/**
|
|
16
|
+
* The narrow R2 surface the media pipeline uses. The engine depends on this, not on R2Bucket, so the
|
|
17
|
+
* multipart, list, and conditional-read surface R2 also carries never leaks into the media code.
|
|
18
|
+
*/
|
|
17
19
|
export interface MediaStore {
|
|
18
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Store bytes under a content-addressed key, with the response HTTP metadata (the content type)
|
|
19
22
|
* and optional custom metadata (the upload stores the full sha256 here, so a short-hash collision
|
|
20
|
-
* is detectable on a later dedup probe).
|
|
23
|
+
* is detectable on a later dedup probe).
|
|
24
|
+
*/
|
|
21
25
|
put(
|
|
22
26
|
key: string,
|
|
23
27
|
bytes: ArrayBuffer | Uint8Array,
|
|
@@ -26,10 +30,12 @@ export interface MediaStore {
|
|
|
26
30
|
): Promise<void>;
|
|
27
31
|
/** The object's metadata, or null when no object lives at the key (the dedup probe). */
|
|
28
32
|
head(key: string): Promise<R2Object | null>;
|
|
29
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The object body for streaming to a delivery response, or null when the key is absent. The
|
|
30
35
|
* delivery route passes `onlyIf` and `range` through for conditional and partial reads: an
|
|
31
36
|
* `onlyIf` etag match returns a body-less R2Object (the 304 shape), so the return widens to
|
|
32
|
-
* `R2Object` alongside `R2ObjectBody`.
|
|
37
|
+
* `R2Object` alongside `R2ObjectBody`.
|
|
38
|
+
*/
|
|
33
39
|
get(
|
|
34
40
|
key: string,
|
|
35
41
|
opts?: { range?: R2Range; onlyIf?: R2Conditional },
|
|
@@ -38,9 +44,11 @@ export interface MediaStore {
|
|
|
38
44
|
delete(key: string): Promise<void>;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Wrap an R2 bucket binding as a MediaStore. Each method delegates to the binding; put folds the
|
|
42
49
|
* HTTP and custom metadata into R2's options shape and drops the returned R2Object the pipeline does
|
|
43
|
-
* not read.
|
|
50
|
+
* not read.
|
|
51
|
+
*/
|
|
44
52
|
export function r2Store(bucket: R2Bucket): MediaStore {
|
|
45
53
|
return {
|
|
46
54
|
async put(key, bytes, httpMetadata, customMetadata) {
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
// a CDN cache keys on it cleanly. The delivery path is appended unaltered, since it already carries
|
|
6
6
|
// its own leading slash.
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* A single image variant: the resize and format directives Cloudflare Images applies to the
|
|
9
10
|
* original bytes. Every field is optional. width, height, quality, and fit are emitted only when
|
|
10
|
-
* set; format and gravity always appear, defaulting to auto.
|
|
11
|
+
* set; format and gravity always appear, defaulting to auto.
|
|
12
|
+
*/
|
|
11
13
|
export interface VariantSpec {
|
|
12
14
|
/** Target width in pixels. */
|
|
13
15
|
width?: number;
|
|
@@ -23,10 +25,12 @@ export interface VariantSpec {
|
|
|
23
25
|
format?: 'auto' | 'webp' | 'avif' | string;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Build the on-demand Cloudflare Images transform URL for a delivery path. The options are
|
|
27
30
|
* comma-joined in the stable order width, height, quality, fit, format, gravity, with width through
|
|
28
31
|
* fit emitted only when the spec sets them and format and gravity always present (defaulting to
|
|
29
|
-
* auto). The publicPath is appended unaltered, so the result is `/cdn-cgi/image/<options><publicPath>`.
|
|
32
|
+
* auto). The publicPath is appended unaltered, so the result is `/cdn-cgi/image/<options><publicPath>`.
|
|
33
|
+
*/
|
|
30
34
|
export function variantUrl(publicPath: string, spec: VariantSpec): string {
|
|
31
35
|
const options: string[] = [];
|
|
32
36
|
if (spec.width !== undefined) options.push(`width=${spec.width}`);
|
|
@@ -42,9 +46,11 @@ export function variantUrl(publicPath: string, spec: VariantSpec): string {
|
|
|
42
46
|
return `/cdn-cgi/image/${options.join(',')}${source}`;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Build a variant URL from a named preset. Looks up presetName in variants and builds its spec with
|
|
46
51
|
* variantUrl. Throws a cairn:-prefixed error naming the unknown preset when the name is absent, so a
|
|
47
|
-
* typo in a preset name fails loudly rather than silently rendering an unsized image.
|
|
52
|
+
* typo in a preset name fails loudly rather than silently rendering an unsized image.
|
|
53
|
+
*/
|
|
48
54
|
export function presetUrl(
|
|
49
55
|
publicPath: string,
|
|
50
56
|
presetName: string,
|
package/src/lib/media/usage.ts
CHANGED
|
@@ -46,14 +46,18 @@ export interface UsageEntry {
|
|
|
46
46
|
origin: UsageOrigin;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
49
|
+
/**
|
|
50
|
+
* Content hash to the distinct entries that reference it. A hash with no row is "no references
|
|
51
|
+
* found" (see the raw-HTML caveat above), never a proven orphan.
|
|
52
|
+
*/
|
|
51
53
|
export type UsageIndex = Map<string, UsageEntry[]>;
|
|
52
54
|
|
|
53
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Build options. `branches` lets a caller that already listed the open cairn/* branches pass them
|
|
54
57
|
* in so the index does not list them a second time (the load path lists once for the media-union).
|
|
55
58
|
* `strict` flips the per-branch read from degrade-and-skip to fail-closed: a delete gate must not
|
|
56
|
-
* treat a transient branch-read failure as an absent reference, so it rethrows instead.
|
|
59
|
+
* treat a transient branch-read failure as an absent reference, so it rethrows instead.
|
|
60
|
+
*/
|
|
57
61
|
export interface BuildUsageOptions {
|
|
58
62
|
/** The open cairn/* branch names, already listed. When present the index skips its own listing. */
|
|
59
63
|
branches?: string[];
|
|
@@ -81,14 +81,18 @@ export interface SiteConfig {
|
|
|
81
81
|
menus?: Record<string, unknown>;
|
|
82
82
|
/** Per-concept URL policy: the permalink pattern and date-prefix granularity, keyed by concept id. */
|
|
83
83
|
content?: Record<string, ConceptUrlPolicy>;
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* The editor spellcheck settings. The dialect is declared once per site (spec 1.2), so a British
|
|
85
86
|
* site loads the British word list and "colour" reads as correct. Today only US English ships, so an
|
|
86
|
-
* unset or unknown dialect resolves to it.
|
|
87
|
+
* unset or unknown dialect resolves to it.
|
|
88
|
+
*/
|
|
87
89
|
spellcheck?: { dialect?: string };
|
|
88
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* The editor tidy (LLM copy-edit) settings. Opt-in at the site level (spec 2.8): tidy is a remote,
|
|
89
92
|
* costly model call, so the whole block is optional and `enabled` defaults false. The model is a
|
|
90
93
|
* developer-tier fact; the `conventions` block is the editor-tier per-convention config that builds
|
|
91
|
-
* the prompt's CONVENTIONS section. The Anthropic API key is a Worker secret, never config.
|
|
94
|
+
* the prompt's CONVENTIONS section. The Anthropic API key is a Worker secret, never config.
|
|
95
|
+
*/
|
|
92
96
|
tidy?: TidyConfig;
|
|
93
97
|
[key: string]: unknown;
|
|
94
98
|
}
|
|
@@ -117,14 +121,18 @@ export const DEFAULT_TIDY_MODEL = 'claude-sonnet-4-6';
|
|
|
117
121
|
* Sentence spacing is dropped on purpose and regional spelling is `spellcheck.dialect`, not a toggle.
|
|
118
122
|
*/
|
|
119
123
|
export interface TidyConventions {
|
|
120
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* The objective Fixes group (spelling, grammar, doubled words, whitespace, capitals, terminal
|
|
121
126
|
* punctuation). Default on. The always-on core governs it; this toggle lets the screen turn the
|
|
122
|
-
* group off.
|
|
127
|
+
* group off.
|
|
128
|
+
*/
|
|
123
129
|
fixes: boolean;
|
|
124
130
|
/** Oxford comma position. Off when undefined; `always` | `complex-only` (AP) | `never`. */
|
|
125
131
|
oxfordComma?: 'always' | 'complex-only' | 'never';
|
|
126
|
-
/**
|
|
127
|
-
*
|
|
132
|
+
/**
|
|
133
|
+
* Number style threshold. Off when undefined; the always-numeral exception sets (ages, dates,
|
|
134
|
+
* measurements, percentages) apply at any threshold.
|
|
135
|
+
*/
|
|
128
136
|
numberStyle?: 'under-ten' | 'under-hundred' | 'always-numerals';
|
|
129
137
|
/** Measurement notation only (never the system, never the number). Off when undefined. */
|
|
130
138
|
measurements?: 'abbreviate' | 'spell-out';
|
|
@@ -33,6 +33,9 @@ function nestedSlots(def: ComponentDef): SlotDef[] {
|
|
|
33
33
|
return (def.slots ?? []).filter((s) => s.name !== 'title' && s.name !== 'body');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
*
|
|
38
|
+
*/
|
|
36
39
|
export function serializeComponent(def: ComponentDef, values: ComponentValues): string {
|
|
37
40
|
const fence = COLON.repeat(nestedSlots(def).length > 0 ? 4 : 3);
|
|
38
41
|
|
|
@@ -136,26 +139,33 @@ function rawKeysFromRoot(root: (RootContent & DirectiveNode) | undefined): strin
|
|
|
136
139
|
return Object.keys(root?.attributes ?? {});
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* Parse a serialized component directive back into guided-form values, the inverse of
|
|
140
144
|
* {@link serializeComponent}. The grammar is reversible, so the editor can round-trip a
|
|
141
|
-
* saved directive through the form.
|
|
145
|
+
* saved directive through the form.
|
|
146
|
+
*/
|
|
142
147
|
export async function parseComponent(markdown: string, def: ComponentDef): Promise<ComponentValues> {
|
|
143
148
|
return valuesFromRoot(findComponentRoot(markdown, def), def);
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
/**
|
|
147
|
-
*
|
|
151
|
+
/**
|
|
152
|
+
* The raw attribute keys present on the component's opening directive, read from the parsed tree
|
|
153
|
+
* (quote-aware, unlike a regex over the source). Used by validation to flag unknown keys.
|
|
154
|
+
*/
|
|
148
155
|
export function parseRawAttributeKeys(markdown: string, def: ComponentDef): string[] {
|
|
149
156
|
return rawKeysFromRoot(findComponentRoot(markdown, def));
|
|
150
157
|
}
|
|
151
158
|
|
|
152
|
-
/**
|
|
153
|
-
*
|
|
159
|
+
/**
|
|
160
|
+
* The result of {@link componentRoundTripSafety}: whether re-opening a placed block into the
|
|
161
|
+
* guided form and re-serializing it is provably lossless.
|
|
162
|
+
*/
|
|
154
163
|
export type RoundTripSafety =
|
|
155
164
|
| { safe: true }
|
|
156
165
|
| { safe: false; reason: 'unknown-attribute' | 'undeclared-child' | 'not-idempotent' | 'not-a-component' };
|
|
157
166
|
|
|
158
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* Decide whether guided edit of this placed block is provably lossless. A block a person typed by
|
|
159
169
|
* hand can carry more than the schema models (an attribute the def does not list, a child container
|
|
160
170
|
* the def does not declare, slot content the form cannot represent stably), and parsing such a block
|
|
161
171
|
* into the form then re-serializing would silently drop it. The edit affordance is offered only when
|
|
@@ -165,7 +175,8 @@ export type RoundTripSafety =
|
|
|
165
175
|
* 2. `unknown-attribute`: the block carries an attribute key the def does not declare.
|
|
166
176
|
* 3. `undeclared-child`: the root has a direct child container directive that is not a declared
|
|
167
177
|
* nested slot. Such a child would otherwise fold into the body slot and move on re-serialize.
|
|
168
|
-
* 4. `not-idempotent`: `parse -> serialize -> parse` does not recover the same values.
|
|
178
|
+
* 4. `not-idempotent`: `parse -> serialize -> parse` does not recover the same values.
|
|
179
|
+
*/
|
|
169
180
|
export async function componentRoundTripSafety(markdown: string, def: ComponentDef): Promise<RoundTripSafety> {
|
|
170
181
|
const root = findComponentRoot(markdown, def);
|
|
171
182
|
if (!root) return { safe: false, reason: 'not-a-component' };
|
|
@@ -191,9 +202,11 @@ export async function componentRoundTripSafety(markdown: string, def: ComponentD
|
|
|
191
202
|
return { safe: true };
|
|
192
203
|
}
|
|
193
204
|
|
|
194
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* Parse the component once and derive both the guided-form values and the raw attribute keys.
|
|
195
207
|
* Validation needs both, so this seam spares it the double parse that calling
|
|
196
|
-
* {@link parseComponent} and {@link parseRawAttributeKeys} separately would cost.
|
|
208
|
+
* {@link parseComponent} and {@link parseRawAttributeKeys} separately would cost.
|
|
209
|
+
*/
|
|
197
210
|
export async function parseComponentWithRawKeys(
|
|
198
211
|
markdown: string,
|
|
199
212
|
def: ComponentDef,
|
|
@@ -2,12 +2,16 @@ import { serializeComponent } from './component-grammar.js';
|
|
|
2
2
|
import { validateComponent } from './component-validate.js';
|
|
3
3
|
import type { ComponentDef, ComponentValues } from './registry.js';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
5
|
+
/**
|
|
6
|
+
* The outcome of preparing a guided-form component for insertion: the markdown to insert, or the
|
|
7
|
+
* field-keyed errors to show on the form.
|
|
8
|
+
*/
|
|
7
9
|
export type ComponentInsert = { ok: true; markdown: string } | { ok: false; errors: Record<string, string> };
|
|
8
10
|
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
11
|
+
/**
|
|
12
|
+
* Serialize a component's form values, then validate the result against its schema. Returns the
|
|
13
|
+
* markdown to insert at the cursor, or the field errors keyed by attribute key or slot name.
|
|
14
|
+
*/
|
|
11
15
|
export async function buildComponentInsert(def: ComponentDef, values: ComponentValues): Promise<ComponentInsert> {
|
|
12
16
|
const markdown = serializeComponent(def, values);
|
|
13
17
|
const verdict = await validateComponent(markdown, def);
|
|
@@ -8,8 +8,10 @@ export interface ReferenceOptions {
|
|
|
8
8
|
summary: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
11
|
+
/**
|
|
12
|
+
* Build a self-contained markdown reference (the llms-full.txt shape) for a component registry, for
|
|
13
|
+
* authors and for pointing an LLM at one curated file.
|
|
14
|
+
*/
|
|
13
15
|
export function generateComponentReference(registry: ComponentRegistry, opts: ReferenceOptions): string {
|
|
14
16
|
const sections = registry.defs.map((def) => componentSection(def));
|
|
15
17
|
return `# ${opts.title}\n\n> ${opts.summary}\n\n${sections.join('\n\n')}\n`;
|
|
@@ -4,6 +4,9 @@ import type { ComponentDef, ComponentValues } from './registry.js';
|
|
|
4
4
|
/** A validation verdict: ok, or field-keyed error messages. */
|
|
5
5
|
export type ComponentValidation = { ok: true } | { ok: false; errors: Record<string, string> };
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
7
10
|
export async function validateComponent(markdown: string, def: ComponentDef): Promise<ComponentValidation> {
|
|
8
11
|
const { values, rawKeys } = await parseComponentWithRawKeys(markdown, def);
|
|
9
12
|
const errors: Record<string, string> = {};
|
package/src/lib/render/glyph.ts
CHANGED
|
@@ -4,10 +4,12 @@ import type { Element } from 'hast';
|
|
|
4
4
|
/** A glyph name to SVG path-data map (the site owns the icon set). */
|
|
5
5
|
export type IconSet = Record<string, string>;
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Inline SVG glyph as a real hast node: class ec-glyph, 256 viewBox, currentColor fill.
|
|
8
9
|
* An unknown icon name yields the bare svg shell with no path child, so it never serializes
|
|
9
10
|
* a stray empty (or undefined) path. Callers always wrap the returned element, so the shell
|
|
10
|
-
* keeps them safe.
|
|
11
|
+
* keeps them safe.
|
|
12
|
+
*/
|
|
11
13
|
export function glyph(name: string, icons: IconSet): Element {
|
|
12
14
|
const d = icons[name];
|
|
13
15
|
return s(
|
|
@@ -19,28 +19,38 @@ import { defineRegistry, type ComponentRegistry } from './registry.js';
|
|
|
19
19
|
import type { LinkResolve } from '../content/links.js';
|
|
20
20
|
|
|
21
21
|
export interface RendererOptions {
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Stamp a `data-rise` ordinal (0, 1, 2, …) on each top-level component so a site's
|
|
23
24
|
* CSS can drive an entrance-cascade delay off it. Omit for no stagger. The ordinal
|
|
24
|
-
* is inert, so a consumer's sanitize floor can keep `data-rise` and drop `style`.
|
|
25
|
+
* is inert, so a consumer's sanitize floor can keep `data-rise` and drop `style`.
|
|
26
|
+
*/
|
|
25
27
|
stagger?: boolean;
|
|
26
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Extend the sanitize allowlist. Receives cairn's default schema (defaultSchema plus the
|
|
27
30
|
* directive markers and the common benign tags) and returns the schema to use. Add to the
|
|
28
31
|
* allowlist for the benign HTML a site's content needs; start from the argument so the
|
|
29
|
-
* dangerous strip is preserved.
|
|
32
|
+
* dangerous strip is preserved.
|
|
33
|
+
*/
|
|
30
34
|
sanitizeSchema?: (defaults: Schema) => Schema;
|
|
31
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Developer-only escape hatch: disable the sanitize floor entirely. This reintroduces the XSS
|
|
32
37
|
* vector the floor closes, so it is only for a site whose content is fully developer-controlled.
|
|
33
|
-
* It is a code-level adapter decision, never an editor-facing setting.
|
|
38
|
+
* It is a code-level adapter decision, never an editor-facing setting.
|
|
39
|
+
*/
|
|
34
40
|
unsafeDisableSanitize?: boolean;
|
|
35
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* The `rel` value forced on every `target="_blank"` anchor, applied last so it also covers
|
|
36
43
|
* component-built anchors. Defaults to `'noopener noreferrer'`. Set a different string to change
|
|
37
|
-
* it, or `false` to disable the injection (a site that owns its own anchor hardening).
|
|
44
|
+
* it, or `false` to disable the injection (a site that owns its own anchor hardening).
|
|
45
|
+
*/
|
|
38
46
|
anchorRel?: string | false;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Compose a site's render pipeline from its component registry: directive syntax to
|
|
42
51
|
* stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
|
|
43
|
-
* rehype plugin arrays (so the admin editor preview can reuse the exact same set).
|
|
52
|
+
* rehype plugin arrays (so the admin editor preview can reuse the exact same set).
|
|
53
|
+
*/
|
|
44
54
|
export function createRenderer(
|
|
45
55
|
registry: ComponentRegistry = defineRegistry({ components: [] }),
|
|
46
56
|
options: RendererOptions = {},
|
|
@@ -24,16 +24,20 @@ export interface AttributeField {
|
|
|
24
24
|
help?: string;
|
|
25
25
|
/** A RegExp `source` to validate the value against, plus the message to show on a mismatch. */
|
|
26
26
|
pattern?: { source: string; message: string };
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* A pure, browser-safe cross-field validator. Returns an error string, or null when valid.
|
|
28
29
|
* Receives the field's value and the full {@link ComponentValues} so a rule can read sibling
|
|
29
|
-
* fields. The picker wraps the call in try/catch so an author's throw never crashes the form.
|
|
30
|
+
* fields. The picker wraps the call in try/catch so an author's throw never crashes the form.
|
|
31
|
+
*/
|
|
30
32
|
validate?: (value: string | boolean, all: ComponentValues) => string | null;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export type SlotKind = 'markdown' | 'inline' | 'repeatable';
|
|
34
36
|
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
+
/**
|
|
38
|
+
* One named content region of a component. The slots named `title` and `body` are special: `title`
|
|
39
|
+
* serializes to the directive `[label]` and `body` to the unmarked content (see the canonical grammar).
|
|
40
|
+
*/
|
|
37
41
|
export interface SlotDef {
|
|
38
42
|
name: string;
|
|
39
43
|
label: string;
|
|
@@ -42,15 +46,19 @@ export interface SlotDef {
|
|
|
42
46
|
help?: string;
|
|
43
47
|
/** For `kind: 'repeatable'`: the fields composing each list item (v1 uses the first field). */
|
|
44
48
|
itemFields?: AttributeField[];
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
49
|
+
/**
|
|
50
|
+
* For `kind: 'repeatable'`: derives a row's label from its item values and zero-based index.
|
|
51
|
+
* When it returns nothing, the picker falls back to `${label} ${index + 1}`.
|
|
52
|
+
*/
|
|
47
53
|
itemLabel?: (item: Record<string, string | boolean>, index: number) => string;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* The structured input a component's `build` receives. The engine stamps the component's
|
|
51
58
|
* attributes and partitions its slots from the rendered hast, so `build` arranges hast and
|
|
52
59
|
* never walks the tree. `slot(name)` returns a slot's rendered children (title, body, or any
|
|
53
|
-
* named slot); `items(name)` returns a repeatable slot's items, one child list per item.
|
|
60
|
+
* named slot); `items(name)` returns a repeatable slot's items, one child list per item.
|
|
61
|
+
*/
|
|
54
62
|
export interface ComponentContext {
|
|
55
63
|
/** Declared attribute values, keyed by attribute key. Booleans are real booleans. */
|
|
56
64
|
attributes: Record<string, string | boolean>;
|
|
@@ -72,9 +80,11 @@ export interface ComponentDef {
|
|
|
72
80
|
description: string;
|
|
73
81
|
/** Markdown scaffold inserted at the cursor by the editor palette. */
|
|
74
82
|
insertTemplate?: string;
|
|
75
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Build the final hast element from the component context (attributes plus partitioned
|
|
76
85
|
* slots). The engine stamps the entrance-stagger ordinal (`data-rise`) on the top-level
|
|
77
|
-
* result, so a build fn stays free of any motion concern.
|
|
86
|
+
* result, so a build fn stays free of any motion concern.
|
|
87
|
+
*/
|
|
78
88
|
build: (ctx: ComponentContext) => Element;
|
|
79
89
|
/** Optional role-to-default-icon, e.g. `{ caution: 'warning' }`. */
|
|
80
90
|
defaultIconByRole?: Record<string, string>;
|
|
@@ -90,8 +100,10 @@ export interface ComponentDef {
|
|
|
90
100
|
group?: string;
|
|
91
101
|
/** Omit from the top-level picker (for a nested or round-trip-only component). */
|
|
92
102
|
hidden?: boolean;
|
|
93
|
-
/**
|
|
94
|
-
*
|
|
103
|
+
/**
|
|
104
|
+
* A structured sample the picker seeds the form with and renders through the same path a real
|
|
105
|
+
* insert takes. Declaring `preview` is what opts the component into the two-pane configure layout.
|
|
106
|
+
*/
|
|
95
107
|
preview?: {
|
|
96
108
|
attributes?: Record<string, string | boolean>;
|
|
97
109
|
slots?: Record<string, string | string[]>;
|
|
@@ -107,16 +119,20 @@ export interface ComponentRegistry {
|
|
|
107
119
|
iconField(name: string): AttributeField | undefined;
|
|
108
120
|
}
|
|
109
121
|
|
|
110
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* The hast property name carrying one declared attribute from stamp to dispatch, e.g. `tone`
|
|
111
124
|
* becomes `dataAttrTone`. The directive stamp writes it and the rehype dispatch reads it, so both
|
|
112
|
-
* sides derive the name from this one helper rather than spelling the capitalize twice.
|
|
125
|
+
* sides derive the name from this one helper rather than spelling the capitalize twice.
|
|
126
|
+
*/
|
|
113
127
|
export function dataAttrProp(key: string): string {
|
|
114
128
|
return `dataAttr${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
115
129
|
}
|
|
116
130
|
|
|
117
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* A component's first `type:'icon'` attribute, or undefined when it declares none. Both the
|
|
118
133
|
* construction-time guard and the registry's `iconField` derive the icon field from this one
|
|
119
|
-
* predicate rather than spelling the `type === 'icon'` find twice.
|
|
134
|
+
* predicate rather than spelling the `type === 'icon'` find twice.
|
|
135
|
+
*/
|
|
120
136
|
function findIconField(def: ComponentDef): AttributeField | undefined {
|
|
121
137
|
return def.attributes?.find((field) => field.type === 'icon');
|
|
122
138
|
}
|
|
@@ -151,15 +167,19 @@ export function defineRegistry({ components }: { components: ComponentDef[] }):
|
|
|
151
167
|
};
|
|
152
168
|
}
|
|
153
169
|
|
|
154
|
-
/**
|
|
155
|
-
*
|
|
170
|
+
/**
|
|
171
|
+
* Guided-form values for one component: attribute values keyed by attribute key, slot values keyed
|
|
172
|
+
* by slot name (a string, or a string list for a repeatable slot).
|
|
173
|
+
*/
|
|
156
174
|
export interface ComponentValues {
|
|
157
175
|
attributes: Record<string, string | boolean>;
|
|
158
176
|
slots: Record<string, string | string[]>;
|
|
159
177
|
}
|
|
160
178
|
|
|
161
|
-
/**
|
|
162
|
-
*
|
|
179
|
+
/**
|
|
180
|
+
* Seed an empty {@link ComponentValues} from a component's schema: attribute defaults (or '' / false)
|
|
181
|
+
* and empty slot values ([] for repeatable, '' otherwise).
|
|
182
|
+
*/
|
|
163
183
|
export function emptyValues(def: ComponentDef): ComponentValues {
|
|
164
184
|
const attributes: Record<string, string | boolean> = {};
|
|
165
185
|
for (const field of def.attributes ?? []) {
|
|
@@ -172,9 +192,11 @@ export function emptyValues(def: ComponentDef): ComponentValues {
|
|
|
172
192
|
return { attributes, slots };
|
|
173
193
|
}
|
|
174
194
|
|
|
175
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* Seed {@link ComponentValues} from a component's `preview` sample: the {@link emptyValues} base
|
|
176
197
|
* with `def.preview.attributes` and `def.preview.slots` overlaid (a shallow merge per side). When
|
|
177
|
-
* the def declares no `preview`, returns exactly the {@link emptyValues} output.
|
|
198
|
+
* the def declares no `preview`, returns exactly the {@link emptyValues} output.
|
|
199
|
+
*/
|
|
178
200
|
export function previewValues(def: ComponentDef): ComponentValues {
|
|
179
201
|
const base = emptyValues(def);
|
|
180
202
|
if (!def.preview) return base;
|
|
@@ -2,12 +2,17 @@ import type { Root, Element, ElementContent } from 'hast';
|
|
|
2
2
|
import { h } from 'hastscript';
|
|
3
3
|
import { dataAttrProp, type ComponentContext, type ComponentDef, type ComponentRegistry } from './registry.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
5
8
|
export function isElement(node: ElementContent | undefined): node is Element {
|
|
6
9
|
return !!node && node.type === 'element';
|
|
7
10
|
}
|
|
8
11
|
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
12
|
+
/**
|
|
13
|
+
* Read a declared string attribute off the component context, returning undefined for a boolean or
|
|
14
|
+
* absent value. Replaces the `typeof ctx.attributes[key] === 'string'` narrowing a build repeats.
|
|
15
|
+
*/
|
|
11
16
|
export function strAttr(ctx: ComponentContext, key: string): string | undefined {
|
|
12
17
|
const value = ctx.attributes[key];
|
|
13
18
|
return typeof value === 'string' ? value : undefined;
|
|
@@ -16,6 +21,9 @@ export function strAttr(ctx: ComponentContext, key: string): string | undefined
|
|
|
16
21
|
// hast Properties values are PropertyValue (string | number | boolean | array | null).
|
|
17
22
|
// Directive markers (dataPrimitive/dataRole/dataAttr<Key>) are always stamped as strings;
|
|
18
23
|
// this reads them back with that guarantee instead of casting at each call site.
|
|
24
|
+
/**
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
19
27
|
export function strProp(node: Element, name: string): string | undefined {
|
|
20
28
|
const value = node.properties?.[name];
|
|
21
29
|
return typeof value === 'string' ? value : undefined;
|
|
@@ -35,10 +43,12 @@ export function cardShell(classes: string[], body: ElementContent[]): Element {
|
|
|
35
43
|
return h('section', { className: classes }, [h('div', { className: ['card-body'] }, body)]);
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Card head row: `<div class="ec-head">[icon]<hN class="card-title">{title}</hN></div>`.
|
|
39
48
|
* Pass the title's inline children, an optional pre-built icon element, and an optional heading
|
|
40
49
|
* level (default 2). This factors the icon-plus-heading head that a titled component build would
|
|
41
|
-
* otherwise rebuild by hand (the shape the removed `splitHead` produced).
|
|
50
|
+
* otherwise rebuild by hand (the shape the removed `splitHead` produced).
|
|
51
|
+
*/
|
|
42
52
|
export function headRow(title: ElementContent[], icon?: Element, level: number = 2): Element {
|
|
43
53
|
const children: ElementContent[] = [];
|
|
44
54
|
if (icon) children.push(icon);
|
|
@@ -46,8 +56,10 @@ export function headRow(title: ElementContent[], icon?: Element, level: number =
|
|
|
46
56
|
return h('div', { className: ['ec-head'] }, children);
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
59
|
+
/**
|
|
60
|
+
* Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
|
|
61
|
+
* text nodes so the bare list serializes without newlines. Returns that <ul>.
|
|
62
|
+
*/
|
|
51
63
|
export function markFirstList(children: ElementContent[]): Element | undefined {
|
|
52
64
|
const ul = children.find((c) => isElement(c) && c.tagName === 'ul') as Element | undefined;
|
|
53
65
|
if (ul) {
|
|
@@ -142,12 +154,14 @@ function transformNode(node: Element, registry: ComponentRegistry): Element {
|
|
|
142
154
|
return def.build(ctx);
|
|
143
155
|
}
|
|
144
156
|
|
|
145
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* Rehype transformer: dispatch each stamped element through its registry `build`
|
|
146
159
|
* fn. When `stagger` is on, each top-level primitive gets a `data-rise` attribute
|
|
147
160
|
* carrying its document-order index (0, 1, 2, …); the site's CSS maps that ordinal
|
|
148
161
|
* to an entrance delay. The index is inert, so a consumer's sanitize floor can keep
|
|
149
162
|
* `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
|
|
150
|
-
* content (lede, intro paragraphs, the page-toc nav) passes through untouched.
|
|
163
|
+
* content (lede, intro paragraphs, the page-toc nav) passes through untouched.
|
|
164
|
+
*/
|
|
151
165
|
export function rehypeDispatch(registry: ComponentRegistry, stagger?: boolean) {
|
|
152
166
|
return (tree: Root) => {
|
|
153
167
|
let idx = 0;
|
|
@@ -51,6 +51,9 @@ function restoreLiteral(node: TextDirective | LeafDirective): PhrasingContent[]
|
|
|
51
51
|
// component name, icon, and role. No structure is built here; the rehype
|
|
52
52
|
// dispatcher rewrites the marked elements once their children are hast.
|
|
53
53
|
// Text and leaf directives are restored to literal text (accidental prose colons).
|
|
54
|
+
/**
|
|
55
|
+
*
|
|
56
|
+
*/
|
|
54
57
|
export function remarkDirectiveStamp(registry: ComponentRegistry) {
|
|
55
58
|
const known = new Set(registry.names);
|
|
56
59
|
return (tree: Root) => {
|
|
@@ -69,8 +69,10 @@ function trimLeadingNewline(children: PhrasingContent[]): PhrasingContent[] {
|
|
|
69
69
|
return children;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/**
|
|
73
|
-
*
|
|
72
|
+
/**
|
|
73
|
+
* Rewrite the reserved `figure` container directive into a placed <figure>. Every other directive
|
|
74
|
+
* is left to remarkDirectiveStamp, which already skips unregistered names.
|
|
75
|
+
*/
|
|
74
76
|
export function remarkFigure() {
|
|
75
77
|
return (tree: Root): void => {
|
|
76
78
|
visit(tree, 'containerDirective', (node: ContainerDirective) => {
|
|
@@ -15,9 +15,11 @@ interface LinkNode {
|
|
|
15
15
|
data?: { hProperties?: Record<string, unknown> };
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Resolve cairn: link nodes against the VFile's resolver. A non-cairn href and a malformed token
|
|
19
20
|
* pass through. A missing target is marked with the cairn-broken-link class (the resolver returns
|
|
20
|
-
* undefined) or, when the resolver throws, the error propagates and fails the build.
|
|
21
|
+
* undefined) or, when the resolver throws, the error propagates and fails the build.
|
|
22
|
+
*/
|
|
21
23
|
export function remarkResolveCairnLinks() {
|
|
22
24
|
return (tree: unknown, file: VFile): void => {
|
|
23
25
|
const resolve = file.data[CAIRN_RESOLVE] as LinkResolve | undefined;
|