@glw907/cairn-cms 0.62.2 → 0.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +216 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/auth/types.d.ts +7 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +22 -11
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- package/dist/components/ConceptList.svelte +25 -4
- package/dist/components/EditPage.svelte +29 -107
- package/dist/components/EditPage.svelte.d.ts +2 -7
- package/dist/components/EntryPicker.svelte +117 -0
- package/dist/components/EntryPicker.svelte.d.ts +35 -0
- package/dist/components/FieldInput.svelte +218 -0
- package/dist/components/FieldInput.svelte.d.ts +51 -0
- package/dist/components/IconPicker.svelte +2 -2
- package/dist/components/IconPicker.svelte.d.ts +2 -0
- package/dist/components/LinkPicker.svelte +8 -75
- package/dist/components/LinkPicker.svelte.d.ts +4 -5
- package/dist/components/MediaHeroField.svelte +8 -5
- package/dist/components/MediaHeroField.svelte.d.ts +4 -0
- package/dist/components/ObjectGroupField.svelte +54 -0
- package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
- package/dist/components/ReferenceField.svelte +94 -0
- package/dist/components/ReferenceField.svelte.d.ts +27 -0
- package/dist/components/RepeatableField.svelte +221 -0
- package/dist/components/RepeatableField.svelte.d.ts +53 -0
- package/dist/components/cairn-admin.css +179 -2
- package/dist/components/preview-doc.js +5 -1
- package/dist/components/tidy-validate.js +1 -1
- package/dist/content/adapter.js +18 -0
- package/dist/content/advisories.d.ts +2 -2
- package/dist/content/advisories.js +3 -5
- package/dist/content/compose.d.ts +7 -6
- package/dist/content/compose.js +26 -20
- package/dist/content/concepts.d.ts +21 -15
- package/dist/content/concepts.js +55 -32
- package/dist/content/field-rules.d.ts +15 -0
- package/dist/content/field-rules.js +38 -0
- package/dist/content/fields.d.ts +169 -0
- package/dist/content/fields.js +41 -0
- package/dist/content/fieldset.d.ts +107 -0
- package/dist/content/fieldset.js +386 -0
- package/dist/content/frontmatter-region.d.ts +38 -0
- package/dist/content/frontmatter-region.js +75 -0
- package/dist/content/frontmatter.d.ts +35 -2
- package/dist/content/frontmatter.js +232 -11
- package/dist/content/manifest.d.ts +34 -0
- package/dist/content/manifest.js +80 -4
- package/dist/content/media-refs.d.ts +2 -2
- package/dist/content/media-rewrite.js +1 -69
- package/dist/content/reference-index.d.ts +56 -0
- package/dist/content/reference-index.js +95 -0
- package/dist/content/references.d.ts +40 -0
- package/dist/content/references.js +0 -0
- package/dist/content/standard-schema.d.ts +30 -0
- package/dist/content/standard-schema.js +4 -0
- package/dist/content/types.d.ts +127 -178
- package/dist/delivery/data.d.ts +2 -2
- package/dist/delivery/data.js +1 -1
- package/dist/delivery/public-routes.d.ts +10 -5
- package/dist/delivery/public-routes.js +25 -2
- package/dist/delivery/site-descriptors.d.ts +5 -1
- package/dist/delivery/site-descriptors.js +8 -3
- package/dist/delivery/site-indexes.d.ts +2 -2
- package/dist/delivery/site-resolver.d.ts +25 -0
- package/dist/delivery/site-resolver.js +49 -0
- package/dist/doctor/checks-local.js +6 -11
- package/dist/github/backend.d.ts +83 -0
- package/dist/github/backend.js +76 -0
- package/dist/github/credentials.d.ts +11 -5
- package/dist/github/credentials.js +3 -3
- package/dist/github/repo.d.ts +8 -19
- package/dist/github/repo.js +69 -80
- package/dist/github/types.d.ts +1 -1
- package/dist/github/types.js +4 -4
- package/dist/index.d.ts +18 -10
- package/dist/index.js +9 -5
- package/dist/islands/index.d.ts +12 -0
- package/dist/islands/index.js +83 -0
- package/dist/islands/types.d.ts +7 -0
- package/dist/islands/types.js +1 -0
- package/dist/log/events.d.ts +1 -1
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/manifest.d.ts +11 -0
- package/dist/media/manifest.js +13 -0
- package/dist/media/rewrite-plan.d.ts +2 -3
- package/dist/media/rewrite-plan.js +2 -3
- package/dist/media/usage.d.ts +2 -2
- package/dist/media/usage.js +3 -5
- package/dist/nav/site-config.d.ts +0 -6
- package/dist/nav/site-config.js +6 -4
- package/dist/render/component-grammar.js +11 -11
- package/dist/render/component-reference.js +5 -3
- package/dist/render/component-validate.d.ts +4 -1
- package/dist/render/component-validate.js +10 -35
- package/dist/render/highlight.d.ts +9 -0
- package/dist/render/highlight.js +206 -0
- package/dist/render/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +13 -2
- package/dist/render/registry.d.ts +44 -36
- package/dist/render/registry.js +47 -6
- package/dist/render/rehype-dispatch.d.ts +6 -10
- package/dist/render/rehype-dispatch.js +38 -17
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.d.ts +10 -0
- package/dist/render/sanitize-schema.js +30 -1
- package/dist/sveltekit/cairn-admin.d.ts +5 -5
- package/dist/sveltekit/cairn-admin.js +3 -4
- package/dist/sveltekit/content-routes.d.ts +10 -8
- package/dist/sveltekit/content-routes.js +269 -181
- package/dist/sveltekit/guard.js +10 -0
- package/dist/sveltekit/health.d.ts +7 -3
- package/dist/sveltekit/health.js +9 -3
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/nav-routes.d.ts +6 -5
- package/dist/sveltekit/nav-routes.js +22 -20
- package/dist/sveltekit/types.d.ts +2 -0
- package/dist/vite/index.d.ts +3 -3
- package/dist/vite/index.js +17 -8
- package/package.json +17 -2
- package/src/lib/ambient.ts +7 -0
- package/src/lib/auth/types.ts +7 -0
- package/src/lib/components/CairnAdmin.svelte +2 -6
- package/src/lib/components/ComponentForm.svelte +48 -27
- package/src/lib/components/ComponentInsertDialog.svelte +26 -14
- package/src/lib/components/ConceptList.svelte +41 -4
- package/src/lib/components/EditPage.svelte +43 -119
- package/src/lib/components/EntryPicker.svelte +154 -0
- package/src/lib/components/FieldInput.svelte +262 -0
- package/src/lib/components/IconPicker.svelte +4 -2
- package/src/lib/components/LinkPicker.svelte +10 -81
- package/src/lib/components/MediaHeroField.svelte +12 -5
- package/src/lib/components/ObjectGroupField.svelte +97 -0
- package/src/lib/components/ReferenceField.svelte +126 -0
- package/src/lib/components/RepeatableField.svelte +310 -0
- package/src/lib/components/preview-doc.ts +5 -1
- package/src/lib/components/tidy-validate.ts +1 -1
- package/src/lib/content/adapter.ts +21 -0
- package/src/lib/content/advisories.ts +4 -7
- package/src/lib/content/compose.ts +30 -23
- package/src/lib/content/concepts.ts +68 -40
- package/src/lib/content/field-rules.ts +39 -0
- package/src/lib/content/fields.ts +178 -0
- package/src/lib/content/fieldset.ts +470 -0
- package/src/lib/content/frontmatter-region.ts +90 -0
- package/src/lib/content/frontmatter.ts +231 -15
- package/src/lib/content/manifest.ts +101 -4
- package/src/lib/content/media-refs.ts +2 -2
- package/src/lib/content/media-rewrite.ts +7 -80
- package/src/lib/content/reference-index.ts +159 -0
- package/src/lib/content/references.ts +0 -0
- package/src/lib/content/standard-schema.ts +25 -0
- package/src/lib/content/types.ts +128 -195
- package/src/lib/delivery/data.ts +2 -2
- package/src/lib/delivery/public-routes.ts +36 -4
- package/src/lib/delivery/site-descriptors.ts +8 -3
- package/src/lib/delivery/site-indexes.ts +2 -2
- package/src/lib/delivery/site-resolver.ts +64 -0
- package/src/lib/doctor/checks-local.ts +6 -14
- package/src/lib/github/backend.ts +161 -0
- package/src/lib/github/credentials.ts +10 -7
- package/src/lib/github/repo.ts +79 -83
- package/src/lib/github/types.ts +5 -5
- package/src/lib/index.ts +40 -18
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -0
- package/src/lib/log/events.ts +1 -0
- package/src/lib/media/index.ts +1 -0
- package/src/lib/media/manifest.ts +14 -0
- package/src/lib/media/rewrite-plan.ts +4 -6
- package/src/lib/media/usage.ts +4 -7
- package/src/lib/nav/site-config.ts +8 -9
- package/src/lib/render/component-grammar.ts +10 -10
- package/src/lib/render/component-reference.ts +4 -3
- package/src/lib/render/component-validate.ts +10 -35
- package/src/lib/render/highlight.ts +259 -0
- package/src/lib/render/pipeline.ts +13 -8
- package/src/lib/render/registry.ts +88 -42
- package/src/lib/render/rehype-dispatch.ts +47 -16
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +32 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- package/src/lib/sveltekit/guard.ts +15 -0
- package/src/lib/sveltekit/health.ts +13 -6
- package/src/lib/sveltekit/index.ts +2 -2
- package/src/lib/sveltekit/nav-routes.ts +33 -29
- package/src/lib/sveltekit/types.ts +5 -1
- package/src/lib/vite/index.ts +20 -11
- package/dist/content/schema.d.ts +0 -87
- package/dist/content/schema.js +0 -89
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -167
- package/src/lib/content/validate.ts +0 -90
package/dist/index.d.ts
CHANGED
|
@@ -2,22 +2,27 @@ export { requireOrigin } from './env.js';
|
|
|
2
2
|
export type { Role, Editor, AuthEnv } from './auth/types.js';
|
|
3
3
|
export type { AuthBranding, MagicLinkMessage, SendMagicLink } from './email.js';
|
|
4
4
|
export { buildMagicLinkMessage, cloudflareSend } from './email.js';
|
|
5
|
-
export type { CairnAdapter, ConceptConfig,
|
|
6
|
-
export {
|
|
5
|
+
export type { CairnAdapter, ConceptConfig, NamedField, ImageValue, ValidationResult, ValidationIssue, SenderConfig, NavMenuConfig, PreviewConfig, ResolvedPreview, AssetConfig, RoutingRule, ConceptDescriptor, ConceptUrlPolicy, CairnExtension, CairnRuntime, SiteRender, AdminPanel, FieldTypeDef, } from './content/types.js';
|
|
6
|
+
export { normalizeConcepts, findConcept, defineConcept } from './content/concepts.js';
|
|
7
7
|
export { composeRuntime } from './content/compose.js';
|
|
8
8
|
export type { ComposeInput } from './content/compose.js';
|
|
9
9
|
export { frontmatterFromForm, dateInputValue, serializeMarkdown, parseMarkdown, } from './content/frontmatter.js';
|
|
10
|
-
export { defineFields } from './content/schema.js';
|
|
11
10
|
export { defineAdapter } from './content/adapter.js';
|
|
12
|
-
export type {
|
|
11
|
+
export type { StandardInput, StandardSchemaV1 } from './content/standard-schema.js';
|
|
12
|
+
export { fields } from './content/fields.js';
|
|
13
|
+
export type { FieldDescriptor, TextField, TextareaField, NumberField, SelectField, MultiselectField, UrlField, EmailField, DateField, DatetimeField, BooleanField, IconField, ImageField, ObjectField, ReferenceField, ArrayField, } from './content/fields.js';
|
|
14
|
+
export { fieldset, initialValues } from './content/fieldset.js';
|
|
15
|
+
export type { Fieldset, InferFieldset, FieldsetOptions, BehaviorTable, FieldBehavior } from './content/fieldset.js';
|
|
13
16
|
export { isValidId, idFromFilename, filenameFromId, slugify, slugFromId, composeDatedId, } from './content/ids.js';
|
|
14
17
|
export type { DatePrefix } from './content/ids.js';
|
|
15
18
|
export { parseCairnToken, extractCairnLinks, formatCairnToken, escapeLinkText } from './content/links.js';
|
|
16
19
|
export type { CairnRef, LinkResolve } from './content/links.js';
|
|
17
|
-
export { serializeManifest, parseManifest, emptyManifest, verifyManifest, diffManifests, upsertEntry, removeEntry, manifestEntryFromFile, manifestLinkResolver, inboundLinks, } from './content/manifest.js';
|
|
18
|
-
export type { Manifest, ManifestEntry, ManifestDiff, ManifestEntryDiff, LinkTarget, InboundLink } from './content/manifest.js';
|
|
19
|
-
export {
|
|
20
|
-
export type {
|
|
20
|
+
export { serializeManifest, parseManifest, emptyManifest, verifyManifest, verifyReferences, diffManifests, upsertEntry, removeEntry, manifestEntryFromFile, manifestLinkResolver, inboundLinks, } from './content/manifest.js';
|
|
21
|
+
export type { Manifest, ManifestEntry, ManifestDiff, ManifestEntryDiff, LinkTarget, InboundLink, InboundReference } from './content/manifest.js';
|
|
22
|
+
export type { ReferenceEdge } from './content/references.js';
|
|
23
|
+
export type { ResolvedReference } from './delivery/site-resolver.js';
|
|
24
|
+
export { defineRegistry, defineComponent, emptyValues } from './render/registry.js';
|
|
25
|
+
export type { ComponentDef, ComponentRegistry, SlotKind, SlotDef, ComponentValues, } from './render/registry.js';
|
|
21
26
|
export { serializeComponent, parseComponent } from './render/component-grammar.js';
|
|
22
27
|
export { validateComponent } from './render/component-validate.js';
|
|
23
28
|
export type { ComponentValidation } from './render/component-validate.js';
|
|
@@ -29,7 +34,10 @@ export type { IconSet } from './render/glyph.js';
|
|
|
29
34
|
export { remarkDirectiveStamp } from './render/remark-directives.js';
|
|
30
35
|
export { createRenderer } from './render/pipeline.js';
|
|
31
36
|
export type { RendererOptions } from './render/pipeline.js';
|
|
32
|
-
export type {
|
|
37
|
+
export type { RepoFile, CommitAuthor } from './github/types.js';
|
|
33
38
|
export { CommitConflictError } from './github/types.js';
|
|
34
|
-
export {
|
|
39
|
+
export { githubApp } from './github/backend.js';
|
|
40
|
+
export type { Backend, BackendProvider, GithubAppProvider, BackendEnv } from './github/backend.js';
|
|
41
|
+
export type { FileChange } from './github/repo.js';
|
|
42
|
+
export { parseSiteConfig, extractMenu, setMenu, validateNavTree, MAX_NAV_NODES, NavValidationError, SiteConfigError, } from './nav/site-config.js';
|
|
35
43
|
export type { NavNode, SiteConfig } from './nav/site-config.js';
|
package/dist/index.js
CHANGED
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
// GitHub read-and-commit backend in Plan 03; render and nav follow.
|
|
3
3
|
export { requireOrigin } from './env.js';
|
|
4
4
|
export { buildMagicLinkMessage, cloudflareSend } from './email.js';
|
|
5
|
-
export {
|
|
5
|
+
export { normalizeConcepts, findConcept, defineConcept } from './content/concepts.js';
|
|
6
6
|
export { composeRuntime } from './content/compose.js';
|
|
7
7
|
export { frontmatterFromForm, dateInputValue, serializeMarkdown, parseMarkdown, } from './content/frontmatter.js';
|
|
8
|
-
export { defineFields } from './content/schema.js';
|
|
9
8
|
export { defineAdapter } from './content/adapter.js';
|
|
9
|
+
// The Contract v2 field vocabulary: the one live field system.
|
|
10
|
+
export { fields } from './content/fields.js';
|
|
11
|
+
export { fieldset, initialValues } from './content/fieldset.js';
|
|
10
12
|
export { isValidId, idFromFilename, filenameFromId, slugify, slugFromId, composeDatedId, } from './content/ids.js';
|
|
11
13
|
// Internal-link token and the committed content manifest (content-graph design). The corpus
|
|
12
14
|
// builder and the request-time resolver ship from the delivery entry; this surface is the
|
|
13
15
|
// grammar, the manifest operations, and their types a migrating site adopts.
|
|
14
16
|
export { parseCairnToken, extractCairnLinks, formatCairnToken, escapeLinkText } from './content/links.js';
|
|
15
|
-
export { serializeManifest, parseManifest, emptyManifest, verifyManifest, diffManifests, upsertEntry, removeEntry, manifestEntryFromFile, manifestLinkResolver, inboundLinks, } from './content/manifest.js';
|
|
17
|
+
export { serializeManifest, parseManifest, emptyManifest, verifyManifest, verifyReferences, diffManifests, upsertEntry, removeEntry, manifestEntryFromFile, manifestLinkResolver, inboundLinks, } from './content/manifest.js';
|
|
16
18
|
// Render engine (Plan 04): generic directive pipeline; sites own the component registry.
|
|
17
|
-
export { defineRegistry, emptyValues } from './render/registry.js';
|
|
19
|
+
export { defineRegistry, defineComponent, emptyValues } from './render/registry.js';
|
|
18
20
|
export { serializeComponent, parseComponent } from './render/component-grammar.js';
|
|
19
21
|
export { validateComponent } from './render/component-validate.js';
|
|
20
22
|
export { buildComponentInsert } from './render/component-insert.js';
|
|
@@ -27,5 +29,7 @@ export { remarkDirectiveStamp } from './render/remark-directives.js';
|
|
|
27
29
|
// path. See docs/superpowers/specs/2026-06-05-cairn-render-authoring-surface-design.md.
|
|
28
30
|
export { createRenderer } from './render/pipeline.js';
|
|
29
31
|
export { CommitConflictError } from './github/types.js';
|
|
32
|
+
// The Backend seam (Contract v2 backend phase): the store interface and its default GitHub provider.
|
|
33
|
+
export { githubApp } from './github/backend.js';
|
|
30
34
|
// Nav tree and site-config helpers (Plan 06).
|
|
31
|
-
export { parseSiteConfig,
|
|
35
|
+
export { parseSiteConfig, extractMenu, setMenu, validateNavTree, MAX_NAV_NODES, NavValidationError, SiteConfigError, } from './nav/site-config.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IslandRegistry } from './types.js';
|
|
2
|
+
export type { IslandRegistry } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Mount each island in `root` (default `document`) over its server-rendered fallback. Call it after each
|
|
5
|
+
* client-side navigation, once the new DOM is in place (an `afterNavigate` callback): it tears down the
|
|
6
|
+
* previous pass first, so it is idempotent and leak-free. An eager island (`hydrate: true`) mounts at once;
|
|
7
|
+
* a `'visible'` island mounts on first intersection. An unknown directive name, a malformed prop payload,
|
|
8
|
+
* or a component that throws leaves the static fallback in place, so one bad island never breaks the page.
|
|
9
|
+
* Mount-and-replace clears the fallback, so an island whose fallback holds a focusable control should
|
|
10
|
+
* restore focus itself; the shipped fallbacks are non-interactive.
|
|
11
|
+
*/
|
|
12
|
+
export declare function hydrateIslands(islands: IslandRegistry, root?: ParentNode): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// cairn-cms islands (@glw907/cairn-cms/islands): the client runtime that mounts a site's live Svelte
|
|
2
|
+
// components over the static fallbacks the render pipeline emits. cairn is Svelte-only by design, so this
|
|
3
|
+
// mounts with Svelte's own mount()/unmount() directly, with no framework abstraction. A site imports this
|
|
4
|
+
// dynamically, gated on a non-empty registry, so a static site never ships it (zero cost when unused).
|
|
5
|
+
import { mount, unmount } from 'svelte';
|
|
6
|
+
// The live Svelte instances of the current pass and the observers still waiting to fire, kept module-level
|
|
7
|
+
// so the next pass can tear the previous one down. A layout calls hydrateIslands once per navigation, and
|
|
8
|
+
// the previous mounts must unmount before the next mount over the same DOM.
|
|
9
|
+
let mounted = [];
|
|
10
|
+
let observers = [];
|
|
11
|
+
// Tear down the previous pass: unmount live instances and disconnect observers that never fired. unmount
|
|
12
|
+
// runs with outro: false so teardown is synchronous and deterministic on navigation; an island declaring an
|
|
13
|
+
// out: transition would otherwise linger and briefly double-render against the next pass's fresh mount.
|
|
14
|
+
function teardown() {
|
|
15
|
+
for (const o of observers)
|
|
16
|
+
o.disconnect();
|
|
17
|
+
observers = [];
|
|
18
|
+
for (const instance of mounted) {
|
|
19
|
+
try {
|
|
20
|
+
void unmount(instance, { outro: false });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// a component that throws on teardown must not block the rest
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
mounted = [];
|
|
27
|
+
}
|
|
28
|
+
// Mount one island over its boundary: parse props (try/catch, a malformed payload leaves the fallback),
|
|
29
|
+
// clear the fallback, mount, and on a mount failure restore the fallback so the reader still sees content.
|
|
30
|
+
// WATCH: props are trusted to equal the directive's declared scalar attributes (serializeIslandProps emits
|
|
31
|
+
// only those). If a directive ever carries an attribute its island does not declare, this forwards it as-is.
|
|
32
|
+
function mountIsland(node, Comp) {
|
|
33
|
+
let props;
|
|
34
|
+
try {
|
|
35
|
+
props = JSON.parse(node.getAttribute('data-cairn-props') ?? '{}');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const fallback = [...node.childNodes];
|
|
41
|
+
node.replaceChildren();
|
|
42
|
+
try {
|
|
43
|
+
mounted.push(mount(Comp, { target: node, props }));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
node.replaceChildren(...fallback);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Defer a 'visible' island to first intersection, then mount once and stop observing.
|
|
50
|
+
function observeIsland(node, Comp) {
|
|
51
|
+
const observer = new IntersectionObserver((entries, self) => {
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (entry.isIntersecting) {
|
|
54
|
+
self.disconnect();
|
|
55
|
+
mountIsland(node, Comp);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
observer.observe(node);
|
|
60
|
+
observers.push(observer);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Mount each island in `root` (default `document`) over its server-rendered fallback. Call it after each
|
|
64
|
+
* client-side navigation, once the new DOM is in place (an `afterNavigate` callback): it tears down the
|
|
65
|
+
* previous pass first, so it is idempotent and leak-free. An eager island (`hydrate: true`) mounts at once;
|
|
66
|
+
* a `'visible'` island mounts on first intersection. An unknown directive name, a malformed prop payload,
|
|
67
|
+
* or a component that throws leaves the static fallback in place, so one bad island never breaks the page.
|
|
68
|
+
* Mount-and-replace clears the fallback, so an island whose fallback holds a focusable control should
|
|
69
|
+
* restore focus itself; the shipped fallbacks are non-interactive.
|
|
70
|
+
*/
|
|
71
|
+
export function hydrateIslands(islands, root = document) {
|
|
72
|
+
teardown();
|
|
73
|
+
for (const node of root.querySelectorAll('[data-cairn-island]')) {
|
|
74
|
+
const name = node.getAttribute('data-cairn-island');
|
|
75
|
+
const Comp = name ? islands[name] : undefined;
|
|
76
|
+
if (!Comp)
|
|
77
|
+
continue;
|
|
78
|
+
if (node.getAttribute('data-cairn-hydrate') === 'visible')
|
|
79
|
+
observeIsland(node, Comp);
|
|
80
|
+
else
|
|
81
|
+
mountIsland(node, Comp);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* A site's island components, keyed by directive name. Each value is the live Svelte component
|
|
4
|
+
* {@link hydrateIslands} mounts over the matching `hydrate` directive's static fallback. The props a
|
|
5
|
+
* component receives are the directive's declared scalar attributes (see the island boundary contract).
|
|
6
|
+
*/
|
|
7
|
+
export type IslandRegistry = Record<string, Component<Record<string, unknown>>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/log/events.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type CairnLogEvent = 'auth.link.requested' | 'auth.link.send_failed' | 'auth.token.minted' | 'auth.token.confirmed' | 'auth.session.created' | 'auth.session.destroyed' | 'commit.succeeded' | 'commit.failed' | 'config.invalid' | 'entry.published' | 'entry.discarded' | 'publish.failed' | 'publish.address_collision' | 'github.unreachable' | 'guard.rejected' | 'media.uploaded' | 'media.upload_failed' | 'media.delivery_failed' | 'media.orphan_reconcile' | 'media.resolve_missing' | 'media.deleted' | 'media.delete_blocked' | 'media.bulk_deleted' | 'media.orphans_purged' | 'media.replaced' | 'media.replace_blocked' | 'media.alt_propagated' | 'dictionary.added' | 'dictionary.add_conflict' | 'tidy.done' | 'tidy.error' | 'tidy.refused' | 'tidy.empty';
|
|
1
|
+
export type CairnLogEvent = 'auth.link.requested' | 'auth.link.send_failed' | 'auth.token.minted' | 'auth.token.confirmed' | 'auth.session.created' | 'auth.session.destroyed' | 'commit.succeeded' | 'commit.failed' | 'config.invalid' | 'entry.published' | 'entry.discarded' | 'publish.failed' | 'publish.address_collision' | 'github.unreachable' | 'guard.rejected' | 'media.uploaded' | 'media.upload_failed' | 'media.delivery_failed' | 'media.orphan_reconcile' | 'media.resolve_missing' | 'media.resolver_absent' | 'media.deleted' | 'media.delete_blocked' | 'media.bulk_deleted' | 'media.orphans_purged' | 'media.replaced' | 'media.replace_blocked' | 'media.alt_propagated' | 'dictionary.added' | 'dictionary.add_conflict' | 'tidy.done' | 'tidy.error' | 'tidy.refused' | 'tidy.empty';
|
package/dist/media/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { normalizeAssets, type ResolvedAssetConfig } from './config.js';
|
|
2
|
-
export { parseMediaManifest, findByHash, upsertMediaEntry, removeMediaEntry, serializeMediaManifest, parseMediaEntries, type MediaEntry, type MediaManifest, } from './manifest.js';
|
|
2
|
+
export { parseMediaManifest, readCommittedManifest, findByHash, upsertMediaEntry, removeMediaEntry, serializeMediaManifest, parseMediaEntries, type MediaEntry, type MediaManifest, } from './manifest.js';
|
|
3
3
|
export { hashBytes, shortHash, slugifyFilename, r2Key, publicPath } from './naming.js';
|
|
4
4
|
export { presetUrl, variantUrl, type VariantSpec } from './transform-url.js';
|
|
5
5
|
export { parseMediaToken, mediaToken, type MediaRef } from './reference.js';
|
package/dist/media/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// delivery-route factory and `requireBucket` stay on `/sveltekit`, off this surface, so the public
|
|
7
7
|
// `.d.ts` for `/media` names no kit or workers-types type.
|
|
8
8
|
export { normalizeAssets } from './config.js';
|
|
9
|
-
export { parseMediaManifest, findByHash, upsertMediaEntry, removeMediaEntry, serializeMediaManifest, parseMediaEntries, } from './manifest.js';
|
|
9
|
+
export { parseMediaManifest, readCommittedManifest, findByHash, upsertMediaEntry, removeMediaEntry, serializeMediaManifest, parseMediaEntries, } from './manifest.js';
|
|
10
10
|
export { hashBytes, shortHash, slugifyFilename, r2Key, publicPath } from './naming.js';
|
|
11
11
|
export { presetUrl, variantUrl } from './transform-url.js';
|
|
12
12
|
export { parseMediaToken, mediaToken } from './reference.js';
|
package/dist/media/manifest.d.ts
CHANGED
|
@@ -26,6 +26,17 @@ export type MediaManifest = Record<string, MediaEntry>;
|
|
|
26
26
|
* object is returned as the manifest.
|
|
27
27
|
*/
|
|
28
28
|
export declare function parseMediaManifest(json: unknown): MediaManifest;
|
|
29
|
+
/**
|
|
30
|
+
* Read the committed media manifest from an `import.meta.glob` eager result, degrading a missing
|
|
31
|
+
* file to an empty manifest. A static import of an absent `media.json` fails the Vite build before
|
|
32
|
+
* any runtime degrade can run, so a fresh site with no manifest cannot build. A glob result is the
|
|
33
|
+
* build-safe read: `import.meta.glob` returns `{}` when nothing matches rather than throwing, and
|
|
34
|
+
* this helper extracts the single matched value and parses it, so a missing file reads a clean `{}`.
|
|
35
|
+
* @param globResult - The eager glob result for the committed manifest, an empty object when the
|
|
36
|
+
* file is absent. The consumer passes
|
|
37
|
+
* `import.meta.glob('<path-to-media.json>', { eager: true, import: 'default' })`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readCommittedManifest(globResult: Record<string, unknown>): MediaManifest;
|
|
29
40
|
/**
|
|
30
41
|
* Parse the posted `media` field into a validated list of MediaEntry rows. The field arrives as a
|
|
31
42
|
* JSON string (the usual form-post shape), an already-parsed array, or junk. A string is JSON-parsed
|
package/dist/media/manifest.js
CHANGED
|
@@ -13,6 +13,19 @@ export function parseMediaManifest(json) {
|
|
|
13
13
|
return {};
|
|
14
14
|
return json;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Read the committed media manifest from an `import.meta.glob` eager result, degrading a missing
|
|
18
|
+
* file to an empty manifest. A static import of an absent `media.json` fails the Vite build before
|
|
19
|
+
* any runtime degrade can run, so a fresh site with no manifest cannot build. A glob result is the
|
|
20
|
+
* build-safe read: `import.meta.glob` returns `{}` when nothing matches rather than throwing, and
|
|
21
|
+
* this helper extracts the single matched value and parses it, so a missing file reads a clean `{}`.
|
|
22
|
+
* @param globResult - The eager glob result for the committed manifest, an empty object when the
|
|
23
|
+
* file is absent. The consumer passes
|
|
24
|
+
* `import.meta.glob('<path-to-media.json>', { eager: true, import: 'default' })`.
|
|
25
|
+
*/
|
|
26
|
+
export function readCommittedManifest(globResult) {
|
|
27
|
+
return parseMediaManifest(Object.values(globResult)[0]);
|
|
28
|
+
}
|
|
16
29
|
/**
|
|
17
30
|
* Validate one posted value as a MediaEntry, returning it narrowed or undefined. The trust boundary
|
|
18
31
|
* for an optimistic record the client re-posts: the upload action server-owned each field at
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConceptDescriptor } from '../content/types.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Backend } from '../github/backend.js';
|
|
3
3
|
import type { Manifest } from '../content/manifest.js';
|
|
4
4
|
/**
|
|
5
5
|
* One main entry the rewrite will touch: its identity, its file path, the transform's per-placement
|
|
@@ -59,8 +59,7 @@ export interface RewritePlan<P = unknown> {
|
|
|
59
59
|
* editor surface and node-safe; the only IO is the usage index build and the per-entry reads.
|
|
60
60
|
*/
|
|
61
61
|
export declare function planMediaRewrite<P = unknown>(args: {
|
|
62
|
-
backend:
|
|
63
|
-
token: string;
|
|
62
|
+
backend: Backend;
|
|
64
63
|
concepts: ConceptDescriptor[];
|
|
65
64
|
contentManifest: Manifest;
|
|
66
65
|
hash: string;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { findConcept } from '../content/concepts.js';
|
|
2
2
|
import { filenameFromId } from '../content/ids.js';
|
|
3
|
-
import { readRaw } from '../github/repo.js';
|
|
4
3
|
import { buildUsageIndex } from './usage.js';
|
|
5
4
|
/**
|
|
6
5
|
* Plan a media rewrite for one asset hash. Builds the cross-branch usage index in strict mode (so an
|
|
@@ -22,7 +21,7 @@ import { buildUsageIndex } from './usage.js';
|
|
|
22
21
|
export async function planMediaRewrite(args) {
|
|
23
22
|
// Strict so an unverifiable branch read rejects here rather than degrading to an absent reference.
|
|
24
23
|
// Do NOT wrap this: the throw is the fail-closed contract the apply relies on.
|
|
25
|
-
const index = await buildUsageIndex(args.backend, args.
|
|
24
|
+
const index = await buildUsageIndex(args.backend, args.concepts, args.contentManifest, {
|
|
26
25
|
strict: true,
|
|
27
26
|
});
|
|
28
27
|
const rows = index.get(args.hash) ?? [];
|
|
@@ -35,7 +34,7 @@ export async function planMediaRewrite(args) {
|
|
|
35
34
|
if (!concept)
|
|
36
35
|
return null;
|
|
37
36
|
const path = `${concept.dir}/${filenameFromId(row.id)}`;
|
|
38
|
-
const markdown = await
|
|
37
|
+
const markdown = await args.backend.readFile(path, args.backend.defaultBranch);
|
|
39
38
|
if (markdown === null)
|
|
40
39
|
return null;
|
|
41
40
|
const result = args.transform(markdown);
|
package/dist/media/usage.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConceptDescriptor } from '../content/types.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Backend } from '../github/backend.js';
|
|
3
3
|
import type { Manifest } from '../content/manifest.js';
|
|
4
4
|
/** Where a reference lives: the published corpus on main, or a named open edit branch. */
|
|
5
5
|
export type UsageOrigin = {
|
|
@@ -49,4 +49,4 @@ export interface BuildUsageOptions {
|
|
|
49
49
|
* failure so the caller fails closed. Pass `branches` to reuse a branch list the caller already has
|
|
50
50
|
* (the load path lists once for the media-union) rather than listing them a second time.
|
|
51
51
|
*/
|
|
52
|
-
export declare function buildUsageIndex(
|
|
52
|
+
export declare function buildUsageIndex(backend: Backend, concepts: ConceptDescriptor[], manifest: Manifest, opts?: BuildUsageOptions): Promise<UsageIndex>;
|
package/dist/media/usage.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { listBranches } from '../github/branches.js';
|
|
2
|
-
import { readRaw } from '../github/repo.js';
|
|
3
1
|
import { PENDING_PREFIX, parsePendingBranch } from '../content/pending.js';
|
|
4
2
|
import { findConcept } from '../content/concepts.js';
|
|
5
3
|
import { isValidId, filenameFromId } from '../content/ids.js';
|
|
@@ -24,7 +22,7 @@ function push(index, hash, entry) {
|
|
|
24
22
|
* failure so the caller fails closed. Pass `branches` to reuse a branch list the caller already has
|
|
25
23
|
* (the load path lists once for the media-union) rather than listing them a second time.
|
|
26
24
|
*/
|
|
27
|
-
export async function buildUsageIndex(
|
|
25
|
+
export async function buildUsageIndex(backend, concepts, manifest, opts = {}) {
|
|
28
26
|
const index = new Map();
|
|
29
27
|
// The main arm: the manifest already carries each entry's mediaRefs, so this is a pure reverse
|
|
30
28
|
// map with no per-file read.
|
|
@@ -41,7 +39,7 @@ export async function buildUsageIndex(repo, token, concepts, manifest, opts = {}
|
|
|
41
39
|
}
|
|
42
40
|
// The branch arm: read each open cairn/* branch's one edited file. The path is derivable from the
|
|
43
41
|
// branch name, so no tree-listing is needed. The branch list is reused when the caller passes it.
|
|
44
|
-
const names = opts.branches ?? (await listBranches(
|
|
42
|
+
const names = opts.branches ?? (await backend.listBranches(PENDING_PREFIX));
|
|
45
43
|
// Read the branches in parallel rather than one at a time, so the latency floor is one round trip
|
|
46
44
|
// instead of N. workerd self-throttles to 6 simultaneous outbound connections, so this batch and
|
|
47
45
|
// the load path's media-union batch each stay under the limit; do NOT merge the two into one
|
|
@@ -58,7 +56,7 @@ export async function buildUsageIndex(repo, token, concepts, manifest, opts = {}
|
|
|
58
56
|
return [];
|
|
59
57
|
const path = `${concept.dir}/${filenameFromId(ref.id)}`;
|
|
60
58
|
try {
|
|
61
|
-
const raw = await
|
|
59
|
+
const raw = await backend.readFile(path, name);
|
|
62
60
|
if (raw === null)
|
|
63
61
|
return []; // The file is absent on the branch: nothing to extract.
|
|
64
62
|
const { frontmatter, body } = parseMarkdown(raw);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ConceptUrlPolicy } from '../content/types.js';
|
|
2
1
|
/** One navigation node. An omitted or empty `url` is a label-only grouping header; no `children` is a leaf. */
|
|
3
2
|
export interface NavNode {
|
|
4
3
|
label: string;
|
|
@@ -28,12 +27,9 @@ export interface SiteConfig {
|
|
|
28
27
|
siteName: string;
|
|
29
28
|
description?: string;
|
|
30
29
|
author?: string;
|
|
31
|
-
url?: string;
|
|
32
30
|
locale?: string;
|
|
33
31
|
/** Named navigation menus, each a NavNode[] (normalized by extractMenu). */
|
|
34
32
|
menus?: Record<string, unknown>;
|
|
35
|
-
/** Per-concept URL policy: the permalink pattern and date-prefix granularity, keyed by concept id. */
|
|
36
|
-
content?: Record<string, ConceptUrlPolicy>;
|
|
37
33
|
/**
|
|
38
34
|
* The editor spellcheck settings. The dialect is declared once per site (spec 1.2), so a British
|
|
39
35
|
* site loads the British word list and "colour" reads as correct. Today only US English ships, so an
|
|
@@ -141,8 +137,6 @@ export declare class SiteConfigError extends Error {
|
|
|
141
137
|
export declare function parseSiteConfig(raw: string): SiteConfig;
|
|
142
138
|
/** Extract one named menu from a parsed config and validate it. Returns [] when the menu is absent. */
|
|
143
139
|
export declare function extractMenu(config: SiteConfig, name: string, maxDepth: number): NavNode[];
|
|
144
|
-
/** The per-concept URL policy from a parsed config, or an empty policy when the `content` key is absent. */
|
|
145
|
-
export declare function urlPolicyFrom(config: SiteConfig): Record<string, ConceptUrlPolicy>;
|
|
146
140
|
/**
|
|
147
141
|
* Replace one named menu in the YAML site-config text and reserialize, preserving every other
|
|
148
142
|
* top-level key (siteName, other menus, settings). Parses into a Document so the rest of the file
|
package/dist/nav/site-config.js
CHANGED
|
@@ -185,6 +185,12 @@ export function parseSiteConfig(raw) {
|
|
|
185
185
|
if (typeof siteName !== 'string' || !siteName.trim()) {
|
|
186
186
|
throw new SiteConfigError('Site config needs a siteName');
|
|
187
187
|
}
|
|
188
|
+
// Contract v2 moved per-concept URL policy out of the YAML and onto defineConcept. A leftover
|
|
189
|
+
// `content:` block here would silently do nothing while the concept defaulted its permalink, so a
|
|
190
|
+
// half-migrated site (one carrying a non-default datePrefix) would rewrite every post URL. Fail loud.
|
|
191
|
+
if (parsed.content !== undefined) {
|
|
192
|
+
throw new SiteConfigError('cairn: site config no longer carries per-concept URL policy; move permalink/datePrefix into defineConcept (Contract v2)');
|
|
193
|
+
}
|
|
188
194
|
return parsed;
|
|
189
195
|
}
|
|
190
196
|
/** Extract one named menu from a parsed config and validate it. Returns [] when the menu is absent. */
|
|
@@ -194,10 +200,6 @@ export function extractMenu(config, name, maxDepth) {
|
|
|
194
200
|
return [];
|
|
195
201
|
return validateNavTree(menu, maxDepth);
|
|
196
202
|
}
|
|
197
|
-
/** The per-concept URL policy from a parsed config, or an empty policy when the `content` key is absent. */
|
|
198
|
-
export function urlPolicyFrom(config) {
|
|
199
|
-
return config.content ?? {};
|
|
200
|
-
}
|
|
201
203
|
/**
|
|
202
204
|
* Replace one named menu in the YAML site-config text and reserialize, preserving every other
|
|
203
205
|
* top-level key (siteName, other menus, settings). Parses into a Document so the rest of the file
|
|
@@ -5,11 +5,11 @@ import remarkStringify from 'remark-stringify';
|
|
|
5
5
|
const COLON = ':';
|
|
6
6
|
function attrBlock(def, values) {
|
|
7
7
|
const parts = [];
|
|
8
|
-
for (const field of def.attributes ??
|
|
9
|
-
const v = values.attributes[
|
|
8
|
+
for (const [name, field] of Object.entries(def.attributes ?? {})) {
|
|
9
|
+
const v = values.attributes[name];
|
|
10
10
|
if (field.type === 'boolean') {
|
|
11
11
|
if (v === true)
|
|
12
|
-
parts.push(`${
|
|
12
|
+
parts.push(`${name}="true"`);
|
|
13
13
|
}
|
|
14
14
|
else if (typeof v === 'string' && v !== '') {
|
|
15
15
|
// The directive attribute grammar (mdast-util-directive) treats a literal `"` as the value
|
|
@@ -17,7 +17,7 @@ function attrBlock(def, values) {
|
|
|
17
17
|
// Encode `&` first (so existing entities are not double-decoded) then `"`; the parser decodes
|
|
18
18
|
// both back. A backslash is literal in this grammar and needs no escaping.
|
|
19
19
|
const escaped = v.replace(/&/g, '&').replace(/"/g, '"');
|
|
20
|
-
parts.push(`${
|
|
20
|
+
parts.push(`${name}="${escaped}"`);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
return parts.length ? `{${parts.join(' ')}}` : '';
|
|
@@ -80,12 +80,12 @@ function valuesFromRoot(root, def) {
|
|
|
80
80
|
const values = emptyComponentValues(def);
|
|
81
81
|
if (!root)
|
|
82
82
|
return values;
|
|
83
|
-
for (const field of def.attributes ??
|
|
84
|
-
const raw = root.attributes?.[
|
|
83
|
+
for (const [name, field] of Object.entries(def.attributes ?? {})) {
|
|
84
|
+
const raw = root.attributes?.[name];
|
|
85
85
|
if (field.type === 'boolean')
|
|
86
|
-
values.attributes[
|
|
86
|
+
values.attributes[name] = raw === 'true';
|
|
87
87
|
else if (typeof raw === 'string')
|
|
88
|
-
values.attributes[
|
|
88
|
+
values.attributes[name] = raw;
|
|
89
89
|
}
|
|
90
90
|
const titleSlot = slotByName(def, 'title');
|
|
91
91
|
const bodySlot = slotByName(def, 'body');
|
|
@@ -144,7 +144,7 @@ export async function componentRoundTripSafety(markdown, def) {
|
|
|
144
144
|
const root = findComponentRoot(markdown, def);
|
|
145
145
|
if (!root)
|
|
146
146
|
return { safe: false, reason: 'not-a-component' };
|
|
147
|
-
const declaredKeys = new Set((def.attributes ??
|
|
147
|
+
const declaredKeys = new Set(Object.keys(def.attributes ?? {}));
|
|
148
148
|
for (const key of parseRawAttributeKeys(markdown, def)) {
|
|
149
149
|
if (!declaredKeys.has(key))
|
|
150
150
|
return { safe: false, reason: 'unknown-attribute' };
|
|
@@ -177,8 +177,8 @@ export async function parseComponentWithRawKeys(markdown, def) {
|
|
|
177
177
|
// here; the parse must overwrite only the fields actually present in the markdown.
|
|
178
178
|
function emptyComponentValues(def) {
|
|
179
179
|
const attributes = {};
|
|
180
|
-
for (const
|
|
181
|
-
attributes[
|
|
180
|
+
for (const [name, field] of Object.entries(def.attributes ?? {}))
|
|
181
|
+
attributes[name] = field.type === 'boolean' ? false : '';
|
|
182
182
|
const slots = {};
|
|
183
183
|
for (const s of def.slots ?? [])
|
|
184
184
|
slots[s.name] = s.kind === 'repeatable' ? [] : '';
|
|
@@ -18,11 +18,13 @@ function componentSection(def) {
|
|
|
18
18
|
/** Seed example values that show every declared field: an ellipsis for strings, one sample list item. */
|
|
19
19
|
function exampleValues(def) {
|
|
20
20
|
const values = emptyValues(def);
|
|
21
|
-
for (const field of def.attributes ??
|
|
21
|
+
for (const [name, field] of Object.entries(def.attributes ?? {})) {
|
|
22
22
|
if (field.type === 'boolean')
|
|
23
|
-
values.attributes[
|
|
23
|
+
values.attributes[name] = false;
|
|
24
|
+
else if (field.type === 'select')
|
|
25
|
+
values.attributes[name] = field.options[0] ?? '…';
|
|
24
26
|
else
|
|
25
|
-
values.attributes[
|
|
27
|
+
values.attributes[name] = '…';
|
|
26
28
|
}
|
|
27
29
|
for (const slot of def.slots ?? []) {
|
|
28
30
|
if (slot.kind === 'repeatable')
|
|
@@ -7,6 +7,9 @@ export type ComponentValidation = {
|
|
|
7
7
|
errors: Record<string, string>;
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Validate a serialized component directive against its definition: the attributes through the same
|
|
11
|
+
* `fieldset` validator a concept field uses (coercion, constraints, required, select domain, pattern,
|
|
12
|
+
* and any per-attribute `behavior.validate`), then the two component-only checks, an unknown attribute
|
|
13
|
+
* key and an unfilled required slot.
|
|
11
14
|
*/
|
|
12
15
|
export declare function validateComponent(markdown: string, def: ComponentDef): Promise<ComponentValidation>;
|
|
@@ -1,32 +1,19 @@
|
|
|
1
1
|
import { parseComponentWithRawKeys } from './component-grammar.js';
|
|
2
|
+
import { fieldset } from '../content/fieldset.js';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
+
* Validate a serialized component directive against its definition: the attributes through the same
|
|
5
|
+
* `fieldset` validator a concept field uses (coercion, constraints, required, select domain, pattern,
|
|
6
|
+
* and any per-attribute `behavior.validate`), then the two component-only checks, an unknown attribute
|
|
7
|
+
* key and an unfilled required slot.
|
|
4
8
|
*/
|
|
5
9
|
export async function validateComponent(markdown, def) {
|
|
6
10
|
const { values, rawKeys } = await parseComponentWithRawKeys(markdown, def);
|
|
7
11
|
const errors = {};
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
errors[field.key] = `${field.label} is required.`;
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
if (field.type === 'select' && typeof v === 'string' && v !== '' && !(field.options ?? []).includes(v)) {
|
|
17
|
-
errors[field.key] = `${field.label} must be one of: ${(field.options ?? []).join(', ')}.`;
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
if (field.pattern && typeof v === 'string' && v !== '' && !new RegExp(field.pattern.source).test(v)) {
|
|
21
|
-
errors[field.key] = field.pattern.message;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (field.validate) {
|
|
25
|
-
const message = runFieldValidator(def, field.key, () => field.validate(v, values));
|
|
26
|
-
if (typeof message === 'string')
|
|
27
|
-
errors[field.key] = message;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
12
|
+
const schema = def.attributeSchema ?? fieldset(def.attributes ?? {}, { behavior: def.behavior });
|
|
13
|
+
const result = schema.validate(values.attributes, '');
|
|
14
|
+
if (!result.ok)
|
|
15
|
+
Object.assign(errors, result.errors);
|
|
16
|
+
const declared = new Set(Object.keys(def.attributes ?? {}));
|
|
30
17
|
for (const key of rawKeys) {
|
|
31
18
|
if (!declared.has(key))
|
|
32
19
|
errors[key] = `Unknown attribute "${key}".`;
|
|
@@ -41,15 +28,3 @@ export async function validateComponent(markdown, def) {
|
|
|
41
28
|
}
|
|
42
29
|
return Object.keys(errors).length ? { ok: false, errors } : { ok: true };
|
|
43
30
|
}
|
|
44
|
-
// Run a site-supplied attribute validator. The validator is author code, so a throw is contained:
|
|
45
|
-
// the field is treated as valid and a dev-time warning names the component and field so the author
|
|
46
|
-
// can find the bug. A returned string is the field error; anything else (null) is clean.
|
|
47
|
-
function runFieldValidator(def, key, call) {
|
|
48
|
-
try {
|
|
49
|
-
return call();
|
|
50
|
-
}
|
|
51
|
-
catch (err) {
|
|
52
|
-
console.warn(`cairn: validate() for component "${def.name}" field "${key}" threw; treating the field as valid.`, err);
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Root } from 'hast';
|
|
2
|
+
/**
|
|
3
|
+
* The Shiki rehype plugin. Highlights every fenced-code block into a `<pre class="shiki">` whose
|
|
4
|
+
* tokens carry the cairn-tok-* classes (no inline style), then leaves the rest of the tree
|
|
5
|
+
* untouched. It runs as an async transformer because Shiki tokenizes asynchronously. Because the
|
|
6
|
+
* output is class-only it needs no special placement; it is safe anywhere after `remarkRehype`.
|
|
7
|
+
* @returns A unified transformer that mutates the hast tree in place.
|
|
8
|
+
*/
|
|
9
|
+
export declare function rehypeCairnHighlight(): (tree: Root) => Promise<void>;
|