@glw907/cairn-cms 0.5.1 → 0.6.0-rc.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/dist/auth/crypto.d.ts +13 -0
- package/dist/auth/crypto.d.ts.map +1 -0
- package/dist/auth/crypto.js +31 -0
- package/dist/auth/store.d.ts +41 -0
- package/dist/auth/store.d.ts.map +1 -0
- package/dist/auth/store.js +115 -0
- package/dist/auth/types.d.ts +25 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +1 -0
- package/dist/components/AdminLayout.svelte +58 -164
- package/dist/components/AdminLayout.svelte.d.ts +14 -18
- package/dist/components/AdminLayout.svelte.d.ts.map +1 -1
- package/dist/components/ComponentPalette.svelte +36 -20
- package/dist/components/ComponentPalette.svelte.d.ts +11 -4
- package/dist/components/ComponentPalette.svelte.d.ts.map +1 -1
- package/dist/components/ConceptList.svelte +81 -0
- package/dist/components/ConceptList.svelte.d.ts +13 -0
- package/dist/components/ConceptList.svelte.d.ts.map +1 -0
- package/dist/components/ConfirmPage.svelte +23 -20
- package/dist/components/ConfirmPage.svelte.d.ts +6 -0
- package/dist/components/ConfirmPage.svelte.d.ts.map +1 -1
- package/dist/components/EditPage.svelte +155 -136
- package/dist/components/EditPage.svelte.d.ts +16 -8
- package/dist/components/EditPage.svelte.d.ts.map +1 -1
- package/dist/components/LoginPage.svelte +42 -52
- package/dist/components/LoginPage.svelte.d.ts +12 -0
- package/dist/components/LoginPage.svelte.d.ts.map +1 -1
- package/dist/components/ManageEditors.svelte +81 -0
- package/dist/components/ManageEditors.svelte.d.ts +23 -0
- package/dist/components/ManageEditors.svelte.d.ts.map +1 -0
- package/dist/components/MarkdownEditor.svelte +81 -0
- package/dist/components/MarkdownEditor.svelte.d.ts +20 -0
- package/dist/components/MarkdownEditor.svelte.d.ts.map +1 -0
- package/dist/components/NavTree.svelte +73 -63
- package/dist/components/NavTree.svelte.d.ts +13 -4
- package/dist/components/NavTree.svelte.d.ts.map +1 -1
- package/dist/components/cairn-admin.css +42 -0
- package/dist/components/index.d.ts +3 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +5 -4
- package/dist/content/compose.d.ts +7 -0
- package/dist/content/compose.d.ts.map +1 -0
- package/dist/content/compose.js +32 -0
- package/dist/content/concepts.d.ts +17 -0
- package/dist/content/concepts.d.ts.map +1 -0
- package/dist/content/concepts.js +41 -0
- package/dist/content/frontmatter.d.ts +18 -0
- package/dist/content/frontmatter.d.ts.map +1 -0
- package/dist/content/frontmatter.js +58 -0
- package/dist/content/ids.d.ts +17 -0
- package/dist/content/ids.d.ts.map +1 -0
- package/dist/content/ids.js +33 -0
- package/dist/content/types.d.ts +210 -0
- package/dist/content/types.d.ts.map +1 -0
- package/dist/content/types.js +1 -0
- package/dist/content/validate.d.ts +13 -0
- package/dist/content/validate.d.ts.map +1 -0
- package/dist/content/validate.js +45 -0
- package/dist/email.d.ts +25 -12
- package/dist/email.d.ts.map +1 -1
- package/dist/email.js +24 -24
- package/dist/env.d.ts +24 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +29 -0
- package/dist/github/credentials.d.ts +12 -0
- package/dist/github/credentials.d.ts.map +1 -0
- package/dist/github/credentials.js +11 -0
- package/dist/github/repo.d.ts +49 -0
- package/dist/github/repo.d.ts.map +1 -0
- package/dist/github/repo.js +123 -0
- package/dist/github/signing.d.ts +17 -0
- package/dist/github/signing.d.ts.map +1 -0
- package/dist/github/signing.js +79 -0
- package/dist/github/types.d.ts +35 -0
- package/dist/github/types.d.ts.map +1 -0
- package/dist/github/types.js +19 -0
- package/dist/index.d.ts +27 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -10
- package/dist/{nav.d.ts → nav/site-config.d.ts} +16 -24
- package/dist/nav/site-config.d.ts.map +1 -0
- package/dist/{nav.js → nav/site-config.js} +27 -13
- package/dist/render/glyph.d.ts +1 -1
- package/dist/render/glyph.d.ts.map +1 -1
- package/dist/render/index.d.ts +5 -5
- package/dist/render/index.d.ts.map +1 -1
- package/dist/render/index.js +6 -6
- package/dist/render/pipeline.d.ts +7 -6
- package/dist/render/pipeline.d.ts.map +1 -1
- package/dist/render/pipeline.js +5 -5
- package/dist/render/registry.d.ts +10 -6
- package/dist/render/registry.d.ts.map +1 -1
- package/dist/render/registry.js +8 -6
- package/dist/render/rehype-dispatch.d.ts +8 -7
- package/dist/render/rehype-dispatch.d.ts.map +1 -1
- package/dist/render/rehype-dispatch.js +16 -14
- package/dist/render/remark-directives.d.ts +1 -1
- package/dist/render/remark-directives.d.ts.map +1 -1
- package/dist/render/sanitize.d.ts +8 -0
- package/dist/render/sanitize.d.ts.map +1 -0
- package/dist/render/sanitize.js +26 -0
- package/dist/sveltekit/auth-routes.d.ts +23 -0
- package/dist/sveltekit/auth-routes.d.ts.map +1 -0
- package/dist/sveltekit/auth-routes.js +85 -0
- package/dist/sveltekit/content-routes.d.ts +80 -0
- package/dist/sveltekit/content-routes.d.ts.map +1 -0
- package/dist/sveltekit/content-routes.js +183 -0
- package/dist/sveltekit/editors-routes.d.ts +24 -0
- package/dist/sveltekit/editors-routes.d.ts.map +1 -0
- package/dist/sveltekit/editors-routes.js +73 -0
- package/dist/sveltekit/guard.d.ts +9 -0
- package/dist/sveltekit/guard.d.ts.map +1 -0
- package/dist/sveltekit/guard.js +43 -0
- package/dist/sveltekit/health.d.ts +19 -0
- package/dist/sveltekit/health.d.ts.map +1 -0
- package/dist/sveltekit/health.js +12 -0
- package/dist/sveltekit/index.d.ts +9 -173
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/index.js +8 -348
- package/dist/sveltekit/nav-routes.d.ts +30 -0
- package/dist/sveltekit/nav-routes.d.ts.map +1 -0
- package/dist/sveltekit/nav-routes.js +103 -0
- package/dist/sveltekit/types.d.ts +32 -0
- package/dist/sveltekit/types.d.ts.map +1 -0
- package/dist/sveltekit/types.js +1 -0
- package/package.json +33 -57
- package/src/lib/auth/crypto.ts +37 -0
- package/src/lib/auth/store.ts +158 -0
- package/src/lib/auth/types.ts +27 -0
- package/src/lib/components/AdminLayout.svelte +58 -164
- package/src/lib/components/ComponentPalette.svelte +36 -20
- package/src/lib/components/ConceptList.svelte +81 -0
- package/src/lib/components/ConfirmPage.svelte +23 -20
- package/src/lib/components/EditPage.svelte +155 -136
- package/src/lib/components/LoginPage.svelte +42 -52
- package/src/lib/components/ManageEditors.svelte +81 -0
- package/src/lib/components/MarkdownEditor.svelte +81 -0
- package/src/lib/components/NavTree.svelte +73 -63
- package/src/lib/components/cairn-admin.css +42 -0
- package/src/lib/components/index.ts +5 -4
- package/src/lib/content/compose.ts +39 -0
- package/src/lib/content/concepts.ts +57 -0
- package/src/lib/content/frontmatter.ts +71 -0
- package/src/lib/content/ids.ts +38 -0
- package/src/lib/content/types.ts +235 -0
- package/src/lib/content/validate.ts +51 -0
- package/src/lib/email.ts +52 -38
- package/src/lib/env.ts +32 -0
- package/src/lib/github/credentials.ts +27 -0
- package/src/lib/github/repo.ts +138 -0
- package/src/lib/github/signing.ts +97 -0
- package/src/lib/github/types.ts +46 -0
- package/src/lib/index.ts +86 -10
- package/src/lib/{nav.ts → nav/site-config.ts} +31 -24
- package/src/lib/render/glyph.ts +6 -6
- package/src/lib/render/index.ts +6 -6
- package/src/lib/render/pipeline.ts +23 -22
- package/src/lib/render/registry.ts +35 -26
- package/src/lib/render/rehype-dispatch.ts +58 -56
- package/src/lib/render/remark-directives.ts +46 -46
- package/src/lib/render/sanitize.ts +27 -0
- package/src/lib/sveltekit/auth-routes.ts +107 -0
- package/src/lib/sveltekit/content-routes.ts +261 -0
- package/src/lib/sveltekit/editors-routes.ts +82 -0
- package/src/lib/sveltekit/guard.ts +47 -0
- package/src/lib/sveltekit/health.ts +24 -0
- package/src/lib/sveltekit/index.ts +19 -512
- package/src/lib/sveltekit/nav-routes.ts +139 -0
- package/src/lib/sveltekit/types.ts +33 -0
- package/dist/adapter.d.ts +0 -93
- package/dist/adapter.d.ts.map +0 -1
- package/dist/adapter.js +0 -30
- package/dist/auth/admins.d.ts +0 -33
- package/dist/auth/admins.d.ts.map +0 -1
- package/dist/auth/admins.js +0 -90
- package/dist/auth/capabilities.d.ts +0 -7
- package/dist/auth/capabilities.d.ts.map +0 -1
- package/dist/auth/capabilities.js +0 -26
- package/dist/auth/config.d.ts +0 -2097
- package/dist/auth/config.d.ts.map +0 -1
- package/dist/auth/config.js +0 -78
- package/dist/auth/guard.d.ts +0 -34
- package/dist/auth/guard.d.ts.map +0 -1
- package/dist/auth/guard.js +0 -47
- package/dist/auth/index.d.ts +0 -5
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js +0 -7
- package/dist/auth/schema.d.ts +0 -750
- package/dist/auth/schema.d.ts.map +0 -1
- package/dist/auth/schema.js +0 -93
- package/dist/carta.d.ts +0 -39
- package/dist/carta.d.ts.map +0 -1
- package/dist/carta.js +0 -30
- package/dist/components/CollectionList.svelte +0 -96
- package/dist/components/CollectionList.svelte.d.ts +0 -8
- package/dist/components/CollectionList.svelte.d.ts.map +0 -1
- package/dist/components/ManageAdmins.svelte +0 -84
- package/dist/components/ManageAdmins.svelte.d.ts +0 -10
- package/dist/components/ManageAdmins.svelte.d.ts.map +0 -1
- package/dist/content.d.ts +0 -3
- package/dist/content.d.ts.map +0 -1
- package/dist/content.js +0 -10
- package/dist/editor.d.ts +0 -25
- package/dist/editor.d.ts.map +0 -1
- package/dist/editor.js +0 -20
- package/dist/frontmatter.d.ts +0 -3
- package/dist/frontmatter.d.ts.map +0 -1
- package/dist/frontmatter.js +0 -16
- package/dist/github.d.ts +0 -72
- package/dist/github.d.ts.map +0 -1
- package/dist/github.js +0 -171
- package/dist/nav.d.ts.map +0 -1
- package/dist/slug.d.ts +0 -7
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -15
- package/dist/utils.d.ts +0 -3
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -11
- package/src/lib/adapter.ts +0 -144
- package/src/lib/auth/admins.ts +0 -106
- package/src/lib/auth/capabilities.ts +0 -35
- package/src/lib/auth/config.ts +0 -108
- package/src/lib/auth/guard.ts +0 -60
- package/src/lib/auth/index.ts +0 -7
- package/src/lib/auth/schema.ts +0 -112
- package/src/lib/carta.ts +0 -59
- package/src/lib/components/CollectionList.svelte +0 -96
- package/src/lib/components/ManageAdmins.svelte +0 -84
- package/src/lib/content.ts +0 -11
- package/src/lib/editor.ts +0 -38
- package/src/lib/frontmatter.ts +0 -17
- package/src/lib/github.ts +0 -220
- package/src/lib/slug.ts +0 -16
- package/src/lib/utils.ts +0 -12
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/lib/github/types.ts
|
|
2
|
+
// cairn-cms: the GitHub backend's plain data types and its one typed error. The backend
|
|
3
|
+
// reads repo coordinates from the adapter's `BackendConfig` (spec §8); `RepoRef` is the
|
|
4
|
+
// `{ owner, repo, branch }` subset, so `backend` is assignable wherever a `RepoRef` is
|
|
5
|
+
// wanted with no conversion.
|
|
6
|
+
|
|
7
|
+
/** Repo coordinates pinned to a branch: the structural subset of `BackendConfig` the read and commit paths need. */
|
|
8
|
+
export interface RepoRef {
|
|
9
|
+
owner: string;
|
|
10
|
+
repo: string;
|
|
11
|
+
branch: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A markdown file in a concept directory. `id` is the filename without `.md`. */
|
|
15
|
+
export interface RepoFile {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A commit author: the signed-in editor (spec §7.4). The committer is left to the App. */
|
|
22
|
+
export interface CommitAuthor {
|
|
23
|
+
name: string;
|
|
24
|
+
email: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What the App signer needs: the app id, the installation, and the base64 PEM secret. */
|
|
28
|
+
export interface AppCredentials {
|
|
29
|
+
appId: string;
|
|
30
|
+
installationId: string;
|
|
31
|
+
/** The stored `GITHUB_APP_PRIVATE_KEY_B64`: base64 of the PEM, single line. */
|
|
32
|
+
privateKeyB64: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A concurrent edit lost the SHA race: the file changed between the read and the PUT, from
|
|
37
|
+
* another editor or the site's own CI. Thrown so the save fails safe (re-fetch and ask the
|
|
38
|
+
* editor to reapply) instead of surfacing a raw 409. Defined and caught inside the package
|
|
39
|
+
* so `instanceof` is reliable, unlike kit's `redirect`/`error` across the peer boundary.
|
|
40
|
+
*/
|
|
41
|
+
export class CommitConflictError extends Error {
|
|
42
|
+
constructor(public readonly path: string) {
|
|
43
|
+
super(`Commit conflict on ${path}: it changed since it was opened`);
|
|
44
|
+
this.name = 'CommitConflictError';
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/lib/index.ts
CHANGED
|
@@ -1,10 +1,86 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
|
|
1
|
+
// Engine entry. Auth landed in Plan 01, the content model and adapter in Plan 02, and the
|
|
2
|
+
// GitHub read-and-commit backend in Plan 03; render and nav follow.
|
|
3
|
+
export { requireOrigin } from './env.js';
|
|
4
|
+
export type { Role, Editor, AuthEnv } from './auth/types.js';
|
|
5
|
+
export type { AuthBranding, MagicLinkMessage, SendMagicLink } from './email.js';
|
|
6
|
+
export { buildMagicLinkMessage, cloudflareSend } from './email.js';
|
|
7
|
+
|
|
8
|
+
// Content model and adapter contract (Plan 02).
|
|
9
|
+
export type {
|
|
10
|
+
CairnAdapter,
|
|
11
|
+
ConceptConfig,
|
|
12
|
+
FrontmatterField,
|
|
13
|
+
TextField,
|
|
14
|
+
TextareaField,
|
|
15
|
+
DateField,
|
|
16
|
+
BooleanField,
|
|
17
|
+
TagsField,
|
|
18
|
+
FreeTagsField,
|
|
19
|
+
ValidationResult,
|
|
20
|
+
BackendConfig,
|
|
21
|
+
SenderConfig,
|
|
22
|
+
NavMenuConfig,
|
|
23
|
+
AssetConfig,
|
|
24
|
+
RoutingRule,
|
|
25
|
+
ConceptDescriptor,
|
|
26
|
+
CairnExtension,
|
|
27
|
+
CairnRuntime,
|
|
28
|
+
AdminPanel,
|
|
29
|
+
FieldTypeDef,
|
|
30
|
+
} from './content/types.js';
|
|
31
|
+
export { CONCEPT_ROUTING, normalizeConcepts, findConcept } from './content/concepts.js';
|
|
32
|
+
export { composeRuntime } from './content/compose.js';
|
|
33
|
+
export {
|
|
34
|
+
frontmatterFromForm,
|
|
35
|
+
dateInputValue,
|
|
36
|
+
serializeMarkdown,
|
|
37
|
+
parseMarkdown,
|
|
38
|
+
} from './content/frontmatter.js';
|
|
39
|
+
export { validateFields } from './content/validate.js';
|
|
40
|
+
export { isValidId, idFromFilename, filenameFromId, slugify } from './content/ids.js';
|
|
41
|
+
// Render engine (Plan 04): generic directive pipeline; sites own the component registry.
|
|
42
|
+
export { defineRegistry } from './render/registry.js';
|
|
43
|
+
export type { ComponentDef, ComponentRegistry } from './render/registry.js';
|
|
44
|
+
export { glyph } from './render/glyph.js';
|
|
45
|
+
export type { IconSet } from './render/glyph.js';
|
|
46
|
+
export { remarkDirectiveStamp } from './render/remark-directives.js';
|
|
47
|
+
export {
|
|
48
|
+
rehypeDispatch,
|
|
49
|
+
isElement,
|
|
50
|
+
strProp,
|
|
51
|
+
iconSpan,
|
|
52
|
+
splitHead,
|
|
53
|
+
cardShell,
|
|
54
|
+
markFirstList,
|
|
55
|
+
} from './render/rehype-dispatch.js';
|
|
56
|
+
export type { MakeIcon } from './render/rehype-dispatch.js';
|
|
57
|
+
export { createRenderer } from './render/pipeline.js';
|
|
58
|
+
export type { RendererOptions } from './render/pipeline.js';
|
|
59
|
+
|
|
60
|
+
// GitHub read-and-commit backend (Plan 03).
|
|
61
|
+
export type { RepoRef, RepoFile, CommitAuthor, AppCredentials } from './github/types.js';
|
|
62
|
+
export { CommitConflictError } from './github/types.js';
|
|
63
|
+
export { appJwt, installationToken, signingSelfTest } from './github/signing.js';
|
|
64
|
+
export {
|
|
65
|
+
treeUrl,
|
|
66
|
+
markdownFilesIn,
|
|
67
|
+
listMarkdown,
|
|
68
|
+
contentsUrl,
|
|
69
|
+
readRaw,
|
|
70
|
+
fileSha,
|
|
71
|
+
commitFile,
|
|
72
|
+
} from './github/repo.js';
|
|
73
|
+
export { appCredentials } from './github/credentials.js';
|
|
74
|
+
export type { GithubKeyEnv } from './github/credentials.js';
|
|
75
|
+
|
|
76
|
+
// Nav tree and site-config helpers (Plan 06).
|
|
77
|
+
export {
|
|
78
|
+
parseSiteConfig,
|
|
79
|
+
extractMenu,
|
|
80
|
+
setMenu,
|
|
81
|
+
validateNavTree,
|
|
82
|
+
MAX_NAV_NODES,
|
|
83
|
+
NavValidationError,
|
|
84
|
+
SiteConfigError,
|
|
85
|
+
} from './nav/site-config.js';
|
|
86
|
+
export type { NavNode, SiteConfig } from './nav/site-config.js';
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
// under `menus.<name>`, read at build time by the public layout and edited from
|
|
3
|
-
//
|
|
4
|
-
// site renders the tree with its own
|
|
5
|
-
|
|
1
|
+
// The navigation tree and its YAML site-config. A menu lives in the site's git-committed config
|
|
2
|
+
// under `menus.<name>`, read at build time by the public layout and edited from /admin/nav, which
|
|
3
|
+
// commits the file back through the GitHub-App pipeline. This module is pure: parse, validate, and
|
|
4
|
+
// rewrite only. The engine returns data; each site renders the tree with its own markup.
|
|
6
5
|
import { parse as parseYaml, parseDocument } from 'yaml';
|
|
7
6
|
|
|
8
|
-
/** One navigation node. `url`
|
|
7
|
+
/** One navigation node. An omitted or empty `url` is a label-only grouping header; no `children` is a leaf. */
|
|
9
8
|
export interface NavNode {
|
|
10
9
|
label: string;
|
|
11
10
|
url?: string;
|
|
@@ -15,6 +14,15 @@ export interface NavNode {
|
|
|
15
14
|
/** Total node cap across the whole tree, a guard against a runaway payload. */
|
|
16
15
|
export const MAX_NAV_NODES = 200;
|
|
17
16
|
|
|
17
|
+
/** Maximum character length for a node label. */
|
|
18
|
+
export const MAX_LABEL_LENGTH = 500;
|
|
19
|
+
|
|
20
|
+
/** Maximum character length for a node URL. */
|
|
21
|
+
export const MAX_URL_LENGTH = 2048;
|
|
22
|
+
|
|
23
|
+
/** Allowlist for safe URL schemes: site-relative, in-page anchors, http(s), mailto, and tel. */
|
|
24
|
+
const SAFE_URL = /^(\/|#|https?:\/\/|mailto:|tel:)/i;
|
|
25
|
+
|
|
18
26
|
export class NavValidationError extends Error {
|
|
19
27
|
constructor(message: string) {
|
|
20
28
|
super(message);
|
|
@@ -23,9 +31,9 @@ export class NavValidationError extends Error {
|
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
/**
|
|
26
|
-
* Validate and normalize an untrusted value into a NavNode[]: arrays only, non-empty labels,
|
|
27
|
-
*
|
|
28
|
-
*
|
|
34
|
+
* Validate and normalize an untrusted value into a NavNode[]: arrays only, non-empty labels, depth
|
|
35
|
+
* within `maxDepth` (1 is flat), a bounded node count, and only the three known keys kept. Throws
|
|
36
|
+
* NavValidationError on any violation. Used by navSave before writing.
|
|
29
37
|
*/
|
|
30
38
|
export function validateNavTree(value: unknown, maxDepth: number): NavNode[] {
|
|
31
39
|
let count = 0;
|
|
@@ -38,9 +46,15 @@ export function validateNavTree(value: unknown, maxDepth: number): NavNode[] {
|
|
|
38
46
|
const item = raw as Record<string, unknown>;
|
|
39
47
|
const label = typeof item.label === 'string' ? item.label.trim() : '';
|
|
40
48
|
if (!label) throw new NavValidationError('Each item needs a label');
|
|
49
|
+
if (label.length > MAX_LABEL_LENGTH) throw new NavValidationError('Label is too long (max 500 characters)');
|
|
41
50
|
if (++count > MAX_NAV_NODES) throw new NavValidationError('Too many navigation items');
|
|
42
51
|
const node: NavNode = { label };
|
|
43
|
-
if (typeof item.url === 'string' && item.url.trim())
|
|
52
|
+
if (typeof item.url === 'string' && item.url.trim()) {
|
|
53
|
+
const url = item.url.trim();
|
|
54
|
+
if (url.length > MAX_URL_LENGTH) throw new NavValidationError('URL is too long (max 2048 characters)');
|
|
55
|
+
if (!SAFE_URL.test(url)) throw new NavValidationError('URL must start with /, #, http(s)://, mailto:, or tel:');
|
|
56
|
+
node.url = url;
|
|
57
|
+
}
|
|
44
58
|
if (item.children !== undefined) {
|
|
45
59
|
const children = walk(item.children, depth + 1);
|
|
46
60
|
if (children.length) node.children = children;
|
|
@@ -53,8 +67,8 @@ export function validateNavTree(value: unknown, maxDepth: number): NavNode[] {
|
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
/**
|
|
56
|
-
* Shape of the YAML site-config file. Unknown keys are ignored so the file can grow without
|
|
57
|
-
*
|
|
70
|
+
* Shape of the YAML site-config file. Unknown keys are ignored so the file can grow without an
|
|
71
|
+
* engine change. Read at build time by the public site.
|
|
58
72
|
*/
|
|
59
73
|
export interface SiteConfig {
|
|
60
74
|
siteName: string;
|
|
@@ -64,14 +78,7 @@ export interface SiteConfig {
|
|
|
64
78
|
locale?: string;
|
|
65
79
|
/** Named navigation menus, each a NavNode[] (normalized by extractMenu). */
|
|
66
80
|
menus?: Record<string, unknown>;
|
|
67
|
-
|
|
68
|
-
footer?: { copyrightName?: string };
|
|
69
|
-
settings?: {
|
|
70
|
-
feedMaxItems?: number;
|
|
71
|
-
homepageFeaturedCount?: number;
|
|
72
|
-
postTags?: string[];
|
|
73
|
-
[key: string]: unknown;
|
|
74
|
-
};
|
|
81
|
+
[key: string]: unknown;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
export class SiteConfigError extends Error {
|
|
@@ -102,10 +109,10 @@ export function extractMenu(config: SiteConfig, name: string, maxDepth: number):
|
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
/**
|
|
105
|
-
* Replace one named menu in the YAML site-config text and
|
|
106
|
-
* top-level key (siteName, other menus, settings
|
|
107
|
-
*
|
|
108
|
-
*
|
|
112
|
+
* Replace one named menu in the YAML site-config text and reserialize, preserving every other
|
|
113
|
+
* top-level key (siteName, other menus, settings). Parses into a Document so the rest of the file
|
|
114
|
+
* round-trips. YAML comments are not preserved (an accepted trade); data keys are. A leaf node
|
|
115
|
+
* serializes without `url`/`children` keys.
|
|
109
116
|
*/
|
|
110
117
|
export function setMenu(raw: string, name: string, tree: NavNode[]): string {
|
|
111
118
|
const doc = parseDocument(raw);
|
package/src/lib/render/glyph.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { s } from 'hastscript';
|
|
2
2
|
import type { Element } from 'hast';
|
|
3
3
|
|
|
4
|
-
/** A glyph name
|
|
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
|
/** Inline SVG glyph as a real hast node: class ec-glyph, 256 viewBox, currentColor fill. */
|
|
8
8
|
export function glyph(name: string, icons: IconSet): Element {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
return s(
|
|
10
|
+
'svg',
|
|
11
|
+
{ className: ['ec-glyph'], viewBox: '0 0 256 256', fill: 'currentColor', ariaHidden: 'true' },
|
|
12
|
+
[s('path', { d: icons[name] })],
|
|
13
|
+
);
|
|
14
14
|
}
|
package/src/lib/render/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// cairn-cms render engine: a directive-driven markdown
|
|
1
|
+
// cairn-cms render engine: a directive-driven markdown to HTML pipeline whose
|
|
2
2
|
// component vocabulary is supplied by a site's component registry. The site owns the
|
|
3
3
|
// component builders, class names, icon set, and CSS; the engine owns the machinery.
|
|
4
|
-
export * from './registry';
|
|
5
|
-
export * from './glyph';
|
|
6
|
-
export * from './remark-directives';
|
|
7
|
-
export * from './rehype-dispatch';
|
|
8
|
-
export * from './pipeline';
|
|
4
|
+
export * from './registry.js';
|
|
5
|
+
export * from './glyph.js';
|
|
6
|
+
export * from './remark-directives.js';
|
|
7
|
+
export * from './rehype-dispatch.js';
|
|
8
|
+
export * from './pipeline.js';
|
|
@@ -6,32 +6,33 @@ import remarkRehype from 'remark-rehype';
|
|
|
6
6
|
import rehypeRaw from 'rehype-raw';
|
|
7
7
|
import rehypeSlug from 'rehype-slug';
|
|
8
8
|
import rehypeStringify from 'rehype-stringify';
|
|
9
|
-
import { remarkDirectiveStamp } from './remark-directives';
|
|
10
|
-
import { rehypeDispatch } from './rehype-dispatch';
|
|
11
|
-
import type { ComponentRegistry } from './registry';
|
|
9
|
+
import { remarkDirectiveStamp } from './remark-directives.js';
|
|
10
|
+
import { rehypeDispatch } from './rehype-dispatch.js';
|
|
11
|
+
import type { ComponentRegistry } from './registry.js';
|
|
12
12
|
|
|
13
13
|
export interface RendererOptions {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
/** Stamp a `data-rise` ordinal (0, 1, 2, …) on each top-level component so a site's
|
|
15
|
+
* CSS can drive an entrance-cascade delay off it. Omit for no stagger. The ordinal
|
|
16
|
+
* is inert, so a consumer's sanitize floor can keep `data-rise` and drop `style`. */
|
|
17
|
+
stagger?: boolean;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
/** Compose a site's render pipeline from its component registry: directive syntax
|
|
20
|
-
* stamped markers
|
|
20
|
+
/** Compose a site's render pipeline from its component registry: directive syntax to
|
|
21
|
+
* stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
|
|
21
22
|
* rehype plugin arrays (so the Carta editor preview can reuse the exact same set). */
|
|
22
23
|
export function createRenderer(registry: ComponentRegistry, options: RendererOptions = {}) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
const remarkPlugins: PluggableList = [remarkDirective, [remarkDirectiveStamp, registry]];
|
|
25
|
+
const rehypePlugins: PluggableList = [rehypeRaw, [rehypeDispatch, registry, options.stagger], rehypeSlug];
|
|
26
|
+
const processor = unified()
|
|
27
|
+
.use(remarkParse)
|
|
28
|
+
.use(remarkGfm)
|
|
29
|
+
.use(remarkPlugins)
|
|
30
|
+
.use(remarkRehype, { allowDangerousHtml: true })
|
|
31
|
+
.use(rehypePlugins)
|
|
32
|
+
.use(rehypeStringify);
|
|
33
|
+
return {
|
|
34
|
+
remarkPlugins,
|
|
35
|
+
rehypePlugins,
|
|
36
|
+
renderMarkdown: async (content: string): Promise<string> => String(await processor.process(content)),
|
|
37
|
+
};
|
|
37
38
|
}
|
|
@@ -1,36 +1,45 @@
|
|
|
1
|
+
// cairn-cms: the directive component registry (seam 3). One declaration per component,
|
|
2
|
+
// carrying how it inserts in the editor and how it renders in rehype. The render pipeline
|
|
3
|
+
// (Plan 04) and the future component palette both derive from this single source, so the
|
|
4
|
+
// parser, the render dispatch, and the editor never drift apart. The adapter references
|
|
5
|
+
// `ComponentRegistry` from here.
|
|
1
6
|
import type { Element } from 'hast';
|
|
2
7
|
|
|
3
8
|
/** A site component: how it inserts (editor) and how it renders (rehype). */
|
|
4
9
|
export interface ComponentDef {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
/** Directive name, e.g. 'card' (matches `:::card`). */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Palette label. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** Palette description. */
|
|
15
|
+
description: string;
|
|
16
|
+
/** Markdown scaffold inserted at the cursor by the editor palette. */
|
|
17
|
+
insertTemplate: string;
|
|
18
|
+
/** Build the final hast element from the stamped directive element. The engine
|
|
19
|
+
* stamps the entrance-stagger ordinal (`data-rise`) on the top-level result, so a
|
|
20
|
+
* build fn stays free of any motion concern. */
|
|
21
|
+
build: (node: Element) => Element;
|
|
22
|
+
/** Optional role-to-default-icon, e.g. `{ caution: 'warning' }`. */
|
|
23
|
+
defaultIconByRole?: Record<string, string>;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
export interface ComponentRegistry {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
defs: ComponentDef[];
|
|
28
|
+
names: string[];
|
|
29
|
+
get(name: string): ComponentDef | undefined;
|
|
30
|
+
defaultIcon(name: string, role?: string): string | undefined;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Build a registry from a site's component definitions. The single source the render
|
|
35
|
+
* pipeline (directive stamp plus rehype dispatch) and the editor palette both read.
|
|
36
|
+
*/
|
|
37
|
+
export function defineRegistry({ components }: { components: ComponentDef[] }): ComponentRegistry {
|
|
38
|
+
const byName = new Map(components.map((c) => [c.name, c]));
|
|
39
|
+
return {
|
|
40
|
+
defs: components,
|
|
41
|
+
names: components.map((c) => c.name),
|
|
42
|
+
get: (name) => byName.get(name),
|
|
43
|
+
defaultIcon: (name, role) => (role ? byName.get(name)?.defaultIconByRole?.[role] : undefined),
|
|
44
|
+
};
|
|
36
45
|
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import type { Root, Element, ElementContent
|
|
1
|
+
import type { Root, Element, ElementContent } from 'hast';
|
|
2
2
|
import { h } from 'hastscript';
|
|
3
|
-
import type { ComponentRegistry } from './registry';
|
|
3
|
+
import type { ComponentRegistry } from './registry.js';
|
|
4
4
|
|
|
5
5
|
export function isElement(node: ElementContent | undefined): node is Element {
|
|
6
|
-
|
|
6
|
+
return !!node && node.type === 'element';
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
// hast Properties values are PropertyValue (string | number | boolean | array | null).
|
|
10
10
|
// Directive markers (dataIcon/dataRole/dataPrimitive) are always stamped as strings;
|
|
11
11
|
// this reads them back with that guarantee instead of casting at each call site.
|
|
12
12
|
export function strProp(node: Element, name: string): string | undefined {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const value = node.properties?.[name];
|
|
14
|
+
return typeof value === 'string' ? value : undefined;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/** Wrap a pre-built glyph in an ec-icon span; secondary role adds the modifier. */
|
|
18
18
|
export function iconSpan(glyphEl: Element, role?: string): Element {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const className = role === 'secondary' ? ['ec-icon', 'ec-icon-secondary'] : ['ec-icon'];
|
|
20
|
+
return h('span', { className }, [glyphEl]);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** A site's icon factory: turn a stamped icon name + role into a hast element. */
|
|
@@ -28,70 +28,72 @@ export type MakeIcon = (name: string, role?: string) => Element;
|
|
|
28
28
|
// `makeIcon` (site-supplied) turns the stamped data-icon into an element; omit it
|
|
29
29
|
// for a head with no icon.
|
|
30
30
|
export function splitHead(node: Element, makeIcon?: MakeIcon): { head: Element; rest: ElementContent[] } {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
const children = node.children as ElementContent[];
|
|
32
|
+
const i = children.findIndex((c) => isElement(c) && c.tagName === 'h2');
|
|
33
|
+
const h2 = children[i] as Element;
|
|
34
|
+
h2.properties = { ...h2.properties, className: ['card-title'] };
|
|
35
|
+
const rest = children.filter((_, j) => j !== i);
|
|
36
|
+
const icon = strProp(node, 'dataIcon');
|
|
37
|
+
const role = strProp(node, 'dataRole');
|
|
38
|
+
const headKids: ElementContent[] = [];
|
|
39
|
+
if (makeIcon && icon) headKids.push(makeIcon(icon, role));
|
|
40
|
+
headKids.push(h2);
|
|
41
|
+
return { head: h('div', { className: ['ec-head'] }, headKids), rest };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** Section wrapper: `<section class=…><div class="card-body">…</div></section
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const properties: Properties = { className: classes };
|
|
48
|
-
if (rise) properties.style = rise;
|
|
49
|
-
return h('section', properties, [h('div', { className: ['card-body'] }, body)]);
|
|
44
|
+
/** Section wrapper: `<section class=…><div class="card-body">…</div></section>`. */
|
|
45
|
+
export function cardShell(classes: string[], body: ElementContent[]): Element {
|
|
46
|
+
return h('section', { className: classes }, [h('div', { className: ['card-body'] }, body)]);
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
/** Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
|
|
53
50
|
* text nodes so the bare list serializes without newlines. Returns that <ul>. */
|
|
54
51
|
export function markFirstList(children: ElementContent[]): Element | undefined {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
const ul = children.find((c) => isElement(c) && c.tagName === 'ul') as Element | undefined;
|
|
53
|
+
if (ul) {
|
|
54
|
+
ul.properties = { ...ul.properties, className: ['ec-grid'] };
|
|
55
|
+
ul.children = (ul.children as ElementContent[]).filter(
|
|
56
|
+
(c) => !(c.type === 'text' && /^\s*$/.test(c.value)),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return ul;
|
|
63
60
|
}
|
|
64
61
|
|
|
65
62
|
// Recurse into a node's children, transforming any nested primitive sections
|
|
66
|
-
// (a grid inside a card, panels inside a split)
|
|
63
|
+
// (a grid inside a card, panels inside a split). Nested primitives never carry the
|
|
64
|
+
// entrance stagger; only top-level ones do (stamped in the transformer below).
|
|
67
65
|
function transformChildren(children: ElementContent[], registry: ComponentRegistry): ElementContent[] {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
return children.map((c) => {
|
|
67
|
+
if (isElement(c) && c.properties?.dataPrimitive) return transformNode(c, registry);
|
|
68
|
+
if (isElement(c)) c.children = transformChildren(c.children as ElementContent[], registry);
|
|
69
|
+
return c;
|
|
70
|
+
});
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
function transformNode(node: Element, registry: ComponentRegistry
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
function transformNode(node: Element, registry: ComponentRegistry): Element {
|
|
74
|
+
node.children = transformChildren(node.children as ElementContent[], registry);
|
|
75
|
+
const name = strProp(node, 'dataPrimitive');
|
|
76
|
+
const def = name ? registry.get(name) : undefined;
|
|
77
|
+
return def ? def.build(node) : node;
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
/** Rehype transformer: dispatch each stamped element through its registry `build`
|
|
83
|
-
* fn.
|
|
84
|
-
*
|
|
81
|
+
* fn. When `stagger` is on, each top-level primitive gets a `data-rise` attribute
|
|
82
|
+
* carrying its document-order index (0, 1, 2, …); the site's CSS maps that ordinal
|
|
83
|
+
* to an entrance delay. The index is inert, so a consumer's sanitize floor can keep
|
|
84
|
+
* `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
|
|
85
85
|
* content (lede, intro paragraphs, the page-toc nav) passes through untouched. */
|
|
86
|
-
export function rehypeDispatch(registry: ComponentRegistry,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
86
|
+
export function rehypeDispatch(registry: ComponentRegistry, stagger?: boolean) {
|
|
87
|
+
return (tree: Root) => {
|
|
88
|
+
let idx = 0;
|
|
89
|
+
tree.children = (tree.children as ElementContent[]).map((child) => {
|
|
90
|
+
if (isElement(child) && child.properties?.dataPrimitive) {
|
|
91
|
+
const el = transformNode(child, registry);
|
|
92
|
+
if (stagger) el.properties = { ...el.properties, dataRise: String(idx++) };
|
|
93
|
+
return el;
|
|
94
|
+
}
|
|
95
|
+
if (isElement(child)) child.children = transformChildren(child.children as ElementContent[], registry);
|
|
96
|
+
return child;
|
|
97
|
+
});
|
|
98
|
+
};
|
|
97
99
|
}
|