@glw907/cairn-cms 0.26.0 → 0.33.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 +143 -0
- package/dist/auth/crypto.d.ts +0 -1
- package/dist/auth/store.d.ts +0 -1
- package/dist/auth/types.d.ts +0 -1
- package/dist/components/AdminLayout.svelte +372 -44
- package/dist/components/AdminLayout.svelte.d.ts +5 -5
- package/dist/components/CairnLogo.svelte +28 -0
- package/dist/components/CairnLogo.svelte.d.ts +15 -0
- package/dist/components/ComponentForm.svelte +1 -1
- package/dist/components/ComponentForm.svelte.d.ts +0 -1
- package/dist/components/ComponentInsertDialog.svelte.d.ts +0 -1
- package/dist/components/ConceptList.svelte +240 -45
- package/dist/components/ConceptList.svelte.d.ts +12 -3
- package/dist/components/ConfirmPage.svelte +20 -3
- package/dist/components/ConfirmPage.svelte.d.ts +0 -1
- package/dist/components/DeleteDialog.svelte.d.ts +0 -1
- package/dist/components/EditPage.svelte +12 -7
- package/dist/components/EditPage.svelte.d.ts +0 -1
- package/dist/components/EditorToolbar.svelte.d.ts +0 -1
- package/dist/components/IconPicker.svelte.d.ts +0 -1
- package/dist/components/LinkPicker.svelte.d.ts +0 -1
- package/dist/components/LoginPage.svelte +27 -5
- package/dist/components/LoginPage.svelte.d.ts +0 -1
- package/dist/components/ManageEditors.svelte +8 -5
- package/dist/components/ManageEditors.svelte.d.ts +0 -1
- package/dist/components/MarkdownEditor.svelte.d.ts +0 -1
- package/dist/components/NavTree.svelte +2 -2
- package/dist/components/NavTree.svelte.d.ts +0 -1
- package/dist/components/RenameDialog.svelte.d.ts +0 -1
- package/dist/components/admin-icons.d.ts +13 -0
- package/dist/components/admin-icons.js +15 -0
- package/dist/components/cairn-admin.css +5516 -37
- package/dist/components/cairn-favicon.d.ts +2 -0
- package/dist/components/cairn-favicon.js +7 -0
- package/dist/components/chrome-guard.d.ts +9 -0
- package/dist/components/chrome-guard.js +55 -0
- package/dist/components/fonts/BricolageGrotesque-OFL.txt +93 -0
- package/dist/components/fonts/Figtree-OFL.txt +93 -0
- package/dist/components/fonts/bricolage-grotesque.woff2 +0 -0
- package/dist/components/fonts/figtree.woff2 +0 -0
- package/dist/components/index.d.ts +0 -1
- package/dist/components/link-completion.d.ts +0 -1
- package/dist/components/markdown-format.d.ts +0 -1
- package/dist/content/adapter.d.ts +0 -1
- package/dist/content/compose.d.ts +1 -2
- package/dist/content/compose.js +2 -3
- package/dist/content/concepts.d.ts +7 -1
- package/dist/content/concepts.js +49 -1
- package/dist/content/frontmatter.d.ts +0 -1
- package/dist/content/identity.d.ts +23 -0
- package/dist/content/identity.js +43 -0
- package/dist/content/ids.d.ts +0 -1
- package/dist/content/links.d.ts +0 -1
- package/dist/content/manifest.d.ts +3 -2
- package/dist/content/manifest.js +6 -26
- package/dist/content/permalink.d.ts +0 -1
- package/dist/content/schema.d.ts +0 -1
- package/dist/content/types.d.ts +0 -1
- package/dist/content/validate.d.ts +0 -1
- package/dist/delivery/CairnHead.svelte.d.ts +0 -1
- package/dist/delivery/content-index.d.ts +0 -1
- package/dist/delivery/content-index.js +8 -25
- package/dist/delivery/data.d.ts +0 -1
- package/dist/delivery/excerpt.d.ts +0 -1
- package/dist/delivery/feeds.d.ts +0 -1
- package/dist/delivery/head.d.ts +0 -1
- package/dist/delivery/index.d.ts +0 -1
- package/dist/delivery/json-ld.d.ts +0 -1
- package/dist/delivery/manifest.d.ts +0 -1
- package/dist/delivery/paginate.d.ts +0 -1
- package/dist/delivery/responses.d.ts +0 -1
- package/dist/delivery/robots.d.ts +0 -1
- package/dist/delivery/seo-fields.d.ts +0 -1
- package/dist/delivery/seo.d.ts +0 -1
- package/dist/delivery/site-descriptors.d.ts +0 -1
- package/dist/delivery/site-descriptors.js +5 -6
- package/dist/delivery/site-index.d.ts +0 -1
- package/dist/delivery/site-indexes.d.ts +0 -1
- package/dist/delivery/sitemap.d.ts +0 -1
- package/dist/email.d.ts +0 -1
- package/dist/env.d.ts +0 -1
- package/dist/github/credentials.d.ts +0 -1
- package/dist/github/repo.d.ts +0 -1
- package/dist/github/signing.d.ts +0 -1
- package/dist/github/types.d.ts +0 -1
- package/dist/index.d.ts +0 -29
- package/dist/index.js +4 -23
- package/dist/nav/site-config.d.ts +0 -1
- package/dist/render/authoring.d.ts +3 -0
- package/dist/render/authoring.js +5 -0
- package/dist/render/component-grammar.d.ts +0 -1
- package/dist/render/component-insert.d.ts +0 -1
- package/dist/render/component-reference.d.ts +0 -1
- package/dist/render/component-validate.d.ts +0 -1
- package/dist/render/glyph.d.ts +0 -1
- package/dist/render/index.d.ts +0 -1
- package/dist/render/pipeline.d.ts +0 -1
- package/dist/render/pipeline.js +5 -1
- package/dist/render/registry.d.ts +2 -1
- package/dist/render/registry.js +15 -0
- package/dist/render/rehype-dispatch.d.ts +9 -7
- package/dist/render/rehype-dispatch.js +12 -6
- package/dist/render/remark-directives.d.ts +0 -1
- package/dist/render/remark-directives.js +1 -1
- package/dist/render/resolve-links.d.ts +0 -1
- package/dist/render/sanitize-schema.d.ts +14 -1
- package/dist/render/sanitize-schema.js +96 -0
- package/dist/sveltekit/auth-routes.d.ts +0 -1
- package/dist/sveltekit/content-routes.d.ts +12 -2
- package/dist/sveltekit/content-routes.js +37 -13
- package/dist/sveltekit/editors-routes.d.ts +0 -1
- package/dist/sveltekit/guard.d.ts +0 -1
- package/dist/sveltekit/health.d.ts +0 -1
- package/dist/sveltekit/index.d.ts +1 -3
- package/dist/sveltekit/index.js +0 -1
- package/dist/sveltekit/nav-routes.d.ts +0 -1
- package/dist/sveltekit/public-routes.d.ts +0 -1
- package/dist/sveltekit/types.d.ts +0 -1
- package/dist/vite/bin.d.ts +0 -1
- package/dist/vite/index.d.ts +0 -1
- package/package.json +16 -2
- package/src/lib/components/AdminLayout.svelte +372 -44
- package/src/lib/components/CairnLogo.svelte +28 -0
- package/src/lib/components/ComponentForm.svelte +1 -1
- package/src/lib/components/ConceptList.svelte +240 -45
- package/src/lib/components/ConfirmPage.svelte +20 -3
- package/src/lib/components/EditPage.svelte +12 -7
- package/src/lib/components/LoginPage.svelte +27 -5
- package/src/lib/components/ManageEditors.svelte +8 -5
- package/src/lib/components/NavTree.svelte +2 -2
- package/src/lib/components/admin-icons.ts +15 -0
- package/src/lib/components/cairn-admin.css +162 -7
- package/src/lib/components/cairn-favicon.ts +9 -0
- package/src/lib/components/chrome-guard.ts +62 -0
- package/src/lib/components/fonts/BricolageGrotesque-OFL.txt +93 -0
- package/src/lib/components/fonts/Figtree-OFL.txt +93 -0
- package/src/lib/components/fonts/bricolage-grotesque.woff2 +0 -0
- package/src/lib/components/fonts/figtree.woff2 +0 -0
- package/src/lib/content/compose.ts +3 -3
- package/src/lib/content/concepts.ts +61 -1
- package/src/lib/content/identity.ts +60 -0
- package/src/lib/content/manifest.ts +6 -27
- package/src/lib/delivery/content-index.ts +8 -27
- package/src/lib/delivery/site-descriptors.ts +5 -6
- package/src/lib/index.ts +4 -57
- package/src/lib/render/authoring.ts +7 -0
- package/src/lib/render/pipeline.ts +4 -1
- package/src/lib/render/registry.ts +20 -0
- package/src/lib/render/rehype-dispatch.ts +13 -6
- package/src/lib/render/remark-directives.ts +1 -1
- package/src/lib/render/sanitize-schema.ts +97 -0
- package/src/lib/sveltekit/content-routes.ts +51 -14
- package/src/lib/sveltekit/index.ts +2 -8
- package/dist/auth/crypto.d.ts.map +0 -1
- package/dist/auth/store.d.ts.map +0 -1
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/components/AdminLayout.svelte.d.ts.map +0 -1
- package/dist/components/ComponentForm.svelte.d.ts.map +0 -1
- package/dist/components/ComponentInsertDialog.svelte.d.ts.map +0 -1
- package/dist/components/ConceptList.svelte.d.ts.map +0 -1
- package/dist/components/ConfirmPage.svelte.d.ts.map +0 -1
- package/dist/components/DeleteDialog.svelte.d.ts.map +0 -1
- package/dist/components/EditPage.svelte.d.ts.map +0 -1
- package/dist/components/EditorToolbar.svelte.d.ts.map +0 -1
- package/dist/components/IconPicker.svelte.d.ts.map +0 -1
- package/dist/components/LinkPicker.svelte.d.ts.map +0 -1
- package/dist/components/LoginPage.svelte.d.ts.map +0 -1
- package/dist/components/ManageEditors.svelte.d.ts.map +0 -1
- package/dist/components/MarkdownEditor.svelte.d.ts.map +0 -1
- package/dist/components/NavTree.svelte.d.ts.map +0 -1
- package/dist/components/RenameDialog.svelte.d.ts.map +0 -1
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/link-completion.d.ts.map +0 -1
- package/dist/components/markdown-format.d.ts.map +0 -1
- package/dist/content/adapter.d.ts.map +0 -1
- package/dist/content/compose.d.ts.map +0 -1
- package/dist/content/concepts.d.ts.map +0 -1
- package/dist/content/frontmatter.d.ts.map +0 -1
- package/dist/content/ids.d.ts.map +0 -1
- package/dist/content/links.d.ts.map +0 -1
- package/dist/content/manifest.d.ts.map +0 -1
- package/dist/content/permalink.d.ts.map +0 -1
- package/dist/content/schema.d.ts.map +0 -1
- package/dist/content/types.d.ts.map +0 -1
- package/dist/content/validate.d.ts.map +0 -1
- package/dist/delivery/CairnHead.svelte.d.ts.map +0 -1
- package/dist/delivery/content-index.d.ts.map +0 -1
- package/dist/delivery/data.d.ts.map +0 -1
- package/dist/delivery/excerpt.d.ts.map +0 -1
- package/dist/delivery/feeds.d.ts.map +0 -1
- package/dist/delivery/head.d.ts.map +0 -1
- package/dist/delivery/index.d.ts.map +0 -1
- package/dist/delivery/json-ld.d.ts.map +0 -1
- package/dist/delivery/manifest.d.ts.map +0 -1
- package/dist/delivery/paginate.d.ts.map +0 -1
- package/dist/delivery/responses.d.ts.map +0 -1
- package/dist/delivery/robots.d.ts.map +0 -1
- package/dist/delivery/seo-fields.d.ts.map +0 -1
- package/dist/delivery/seo.d.ts.map +0 -1
- package/dist/delivery/site-descriptors.d.ts.map +0 -1
- package/dist/delivery/site-index.d.ts.map +0 -1
- package/dist/delivery/site-indexes.d.ts.map +0 -1
- package/dist/delivery/sitemap.d.ts.map +0 -1
- package/dist/email.d.ts.map +0 -1
- package/dist/env.d.ts.map +0 -1
- package/dist/github/credentials.d.ts.map +0 -1
- package/dist/github/repo.d.ts.map +0 -1
- package/dist/github/signing.d.ts.map +0 -1
- package/dist/github/types.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/nav/site-config.d.ts.map +0 -1
- package/dist/render/component-grammar.d.ts.map +0 -1
- package/dist/render/component-insert.d.ts.map +0 -1
- package/dist/render/component-reference.d.ts.map +0 -1
- package/dist/render/component-validate.d.ts.map +0 -1
- package/dist/render/glyph.d.ts.map +0 -1
- package/dist/render/index.d.ts.map +0 -1
- package/dist/render/pipeline.d.ts.map +0 -1
- package/dist/render/registry.d.ts.map +0 -1
- package/dist/render/rehype-dispatch.d.ts.map +0 -1
- package/dist/render/remark-directives.d.ts.map +0 -1
- package/dist/render/resolve-links.d.ts.map +0 -1
- package/dist/render/sanitize-schema.d.ts.map +0 -1
- package/dist/sveltekit/auth-routes.d.ts.map +0 -1
- package/dist/sveltekit/content-routes.d.ts.map +0 -1
- package/dist/sveltekit/editors-routes.d.ts.map +0 -1
- package/dist/sveltekit/guard.d.ts.map +0 -1
- package/dist/sveltekit/health.d.ts.map +0 -1
- package/dist/sveltekit/index.d.ts.map +0 -1
- package/dist/sveltekit/nav-routes.d.ts.map +0 -1
- package/dist/sveltekit/public-routes.d.ts.map +0 -1
- package/dist/sveltekit/types.d.ts.map +0 -1
- package/dist/vite/bin.d.ts.map +0 -1
- package/dist/vite/index.d.ts.map +0 -1
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
// code reads the content graph without an N+1 GitHub crawl. The build regenerates and verifies
|
|
4
4
|
// it; the save path patches one entry and commits it with the content in one commit. Each entry
|
|
5
5
|
// carries its identity and its outbound cairn: edges, so the manifest is the link graph.
|
|
6
|
-
import { idFromFilename, slugFromId } from './ids.js';
|
|
7
6
|
import { parseMarkdown } from './frontmatter.js';
|
|
8
|
-
import {
|
|
7
|
+
import { entryIdentity, asString } from './identity.js';
|
|
9
8
|
import { extractCairnLinks, type CairnRef, type LinkResolve } from './links.js';
|
|
10
9
|
import type { ConceptDescriptor } from './types.js';
|
|
11
10
|
|
|
@@ -36,38 +35,18 @@ export interface LinkTarget {
|
|
|
36
35
|
draft: boolean;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Mirror content-index's frontmatter coercion: a present non-empty string, else undefined. */
|
|
45
|
-
function asString(value: unknown): string | undefined {
|
|
46
|
-
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Mirror content-index's date coercion: an unquoted YAML date is a JS Date, a string is sliced. */
|
|
50
|
-
function asDate(value: unknown): string | undefined {
|
|
51
|
-
if (value instanceof Date) return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
|
|
52
|
-
if (typeof value === 'string') return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Build one manifest entry from a content file. Drafts are included and flagged. */
|
|
38
|
+
/** Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
|
|
39
|
+
* permalink come from entryIdentity, the same source content-index uses, so a cairn: link resolves to
|
|
40
|
+
* one URL whether the admin preview reads the manifest or the public build reads the content index. */
|
|
57
41
|
export function manifestEntryFromFile(descriptor: ConceptDescriptor, file: { path: string; raw: string }): ManifestEntry {
|
|
58
|
-
const id = idFromFilename(basename(file.path));
|
|
59
|
-
// Use the same slug rule content-index uses, so the manifest's permalink for an entry always
|
|
60
|
-
// equals content-index's permalink for it. A cairn link must resolve to one URL whether the
|
|
61
|
-
// admin preview reads the manifest or the public build reads the content index.
|
|
62
|
-
const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
|
|
63
42
|
const { frontmatter, body } = parseMarkdown(file.raw);
|
|
64
|
-
const date =
|
|
43
|
+
const { id, date, permalink } = entryIdentity(descriptor, file.path, frontmatter);
|
|
65
44
|
return {
|
|
66
45
|
id,
|
|
67
46
|
concept: descriptor.id,
|
|
68
47
|
title: asString(frontmatter.title) ?? id,
|
|
69
48
|
date,
|
|
70
|
-
permalink
|
|
49
|
+
permalink,
|
|
71
50
|
draft: frontmatter.draft === true,
|
|
72
51
|
links: extractCairnLinks(body),
|
|
73
52
|
};
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// returns cheap plain-data summaries plus an on-demand detail lookup. It is concept-generic:
|
|
4
4
|
// every operation reads the descriptor and its routing rule, never a hardcoded concept id.
|
|
5
5
|
import { parseMarkdown } from '../content/frontmatter.js';
|
|
6
|
-
import {
|
|
7
|
-
import { permalink } from '../content/permalink.js';
|
|
6
|
+
import { entryId, entryIdentity, asDate, asString, asTags } from '../content/identity.js';
|
|
8
7
|
import { deriveExcerpt, wordCount } from './excerpt.js';
|
|
9
8
|
import type { ConceptDescriptor } from '../content/types.js';
|
|
10
9
|
|
|
@@ -70,25 +69,6 @@ export function fromGlob(record: Record<string, string>): RawFile[] {
|
|
|
70
69
|
return Object.entries(record).map(([path, raw]) => ({ path, raw }));
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
function basename(path: string): string {
|
|
74
|
-
const slash = path.lastIndexOf('/');
|
|
75
|
-
return slash >= 0 ? path.slice(slash + 1) : path;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function asString(value: unknown): string | undefined {
|
|
79
|
-
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function asDate(value: unknown): string | undefined {
|
|
83
|
-
if (value instanceof Date) return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
|
|
84
|
-
if (typeof value === 'string') return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function asTags(value: unknown): string[] {
|
|
89
|
-
return Array.isArray(value) ? value.map(String) : [];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
72
|
/** Build a concept's index from its raw files and normalized descriptor. */
|
|
93
73
|
export function createContentIndex<F = Record<string, unknown>>(
|
|
94
74
|
files: RawFile[],
|
|
@@ -97,18 +77,19 @@ export function createContentIndex<F = Record<string, unknown>>(
|
|
|
97
77
|
const problems: ContentProblem[] = [];
|
|
98
78
|
const entries: ContentEntry<F>[] = [];
|
|
99
79
|
for (const file of files) {
|
|
100
|
-
const id = idFromFilename(basename(file.path));
|
|
101
|
-
const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
|
|
102
80
|
const { frontmatter: raw, body } = parseMarkdown(file.raw);
|
|
103
|
-
const
|
|
81
|
+
const id = entryId(file.path);
|
|
104
82
|
const draft = raw.draft === true;
|
|
105
|
-
// Validate
|
|
106
|
-
//
|
|
83
|
+
// Validate before resolving the permalink. A date-token permalink throws on an entry with no
|
|
84
|
+
// valid date; the validate gate records that as a content problem rather than aborting the whole
|
|
85
|
+
// index build, so one bad entry degrades to a skip, not a crash. A failure is also excluded from
|
|
86
|
+
// the typed read, so every readable entry's frontmatter is the validator's normalized output.
|
|
107
87
|
const result = descriptor.validate(raw, body);
|
|
108
88
|
if (!result.ok) {
|
|
109
89
|
problems.push({ id, draft, errors: result.errors });
|
|
110
90
|
continue;
|
|
111
91
|
}
|
|
92
|
+
const { slug, date, permalink } = entryIdentity(descriptor, file.path, raw);
|
|
112
93
|
const summaryFieldValues: Record<string, unknown> = {};
|
|
113
94
|
for (const key of descriptor.summaryFields) {
|
|
114
95
|
if (key in result.data) summaryFieldValues[key] = result.data[key];
|
|
@@ -117,7 +98,7 @@ export function createContentIndex<F = Record<string, unknown>>(
|
|
|
117
98
|
concept: descriptor.id,
|
|
118
99
|
id,
|
|
119
100
|
slug,
|
|
120
|
-
permalink
|
|
101
|
+
permalink,
|
|
121
102
|
title: asString(raw.title) ?? id,
|
|
122
103
|
date,
|
|
123
104
|
updated: asDate(raw.updated),
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
// cairn-cms: the one-call descriptor helper. A delivery site needs the same per-concept
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
import {
|
|
5
|
-
import { urlPolicyFrom } from '../nav/site-config.js';
|
|
1
|
+
// cairn-cms: the one-call descriptor helper. A delivery site needs the same per-concept descriptors
|
|
2
|
+
// the admin runtime uses; this delegates to the shared resolveConcepts so the pairing is one path, not
|
|
3
|
+
// tribal knowledge. The YAML URL policy stays the single source of truth.
|
|
4
|
+
import { resolveConcepts } from '../content/concepts.js';
|
|
6
5
|
import type { CairnAdapter, ConceptDescriptor } from '../content/types.js';
|
|
7
6
|
import type { SiteConfig } from '../nav/site-config.js';
|
|
8
7
|
|
|
9
8
|
/** Per-concept descriptors for a site, from its adapter content and its parsed site config. */
|
|
10
9
|
export function siteDescriptors(adapter: CairnAdapter, siteConfig: SiteConfig): ConceptDescriptor[] {
|
|
11
|
-
return
|
|
10
|
+
return resolveConcepts(adapter.content, siteConfig);
|
|
12
11
|
}
|
package/src/lib/index.ts
CHANGED
|
@@ -88,34 +88,16 @@ export type { ReferenceOptions } from './render/component-reference.js';
|
|
|
88
88
|
export { glyph } from './render/glyph.js';
|
|
89
89
|
export type { IconSet } from './render/glyph.js';
|
|
90
90
|
export { remarkDirectiveStamp } from './render/remark-directives.js';
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
iconSpan,
|
|
96
|
-
cardShell,
|
|
97
|
-
headRow,
|
|
98
|
-
markFirstList,
|
|
99
|
-
} from './render/rehype-dispatch.js';
|
|
100
|
-
export type { MakeIcon } from './render/rehype-dispatch.js';
|
|
91
|
+
// The component-authoring helpers (iconSpan, cardShell, headRow, isElement, strAttr) live on the
|
|
92
|
+
// @glw907/cairn-cms/render subpath, not the root barrel. rehypeDispatch is deliberately not public:
|
|
93
|
+
// createRenderer is the one public render pipeline, so the safe plugin ordering is the only public
|
|
94
|
+
// path. See docs/superpowers/specs/2026-06-05-cairn-render-authoring-surface-design.md.
|
|
101
95
|
export { createRenderer } from './render/pipeline.js';
|
|
102
96
|
export type { RendererOptions } from './render/pipeline.js';
|
|
103
97
|
|
|
104
98
|
// GitHub read-and-commit backend (Plan 03).
|
|
105
99
|
export type { RepoRef, RepoFile, CommitAuthor, AppCredentials } from './github/types.js';
|
|
106
100
|
export { CommitConflictError } from './github/types.js';
|
|
107
|
-
export { appJwt, installationToken, signingSelfTest } from './github/signing.js';
|
|
108
|
-
export {
|
|
109
|
-
treeUrl,
|
|
110
|
-
markdownFilesIn,
|
|
111
|
-
listMarkdown,
|
|
112
|
-
contentsUrl,
|
|
113
|
-
readRaw,
|
|
114
|
-
fileSha,
|
|
115
|
-
commitFile,
|
|
116
|
-
} from './github/repo.js';
|
|
117
|
-
export { appCredentials } from './github/credentials.js';
|
|
118
|
-
export type { GithubKeyEnv } from './github/credentials.js';
|
|
119
101
|
|
|
120
102
|
// Nav tree and site-config helpers (Plan 06).
|
|
121
103
|
export {
|
|
@@ -129,38 +111,3 @@ export {
|
|
|
129
111
|
SiteConfigError,
|
|
130
112
|
} from './nav/site-config.js';
|
|
131
113
|
export type { NavNode, SiteConfig } from './nav/site-config.js';
|
|
132
|
-
|
|
133
|
-
// Public content delivery (public-delivery design): the query index, syndication, and
|
|
134
|
-
// discovery surface that sites read. Pure builders plus the one permalink resolver; the
|
|
135
|
-
// SvelteKit loaders live under the /sveltekit subpath.
|
|
136
|
-
export { permalink } from './content/permalink.js';
|
|
137
|
-
export { createContentIndex, fromGlob } from './delivery/content-index.js';
|
|
138
|
-
export type {
|
|
139
|
-
RawFile,
|
|
140
|
-
ContentSummary,
|
|
141
|
-
ContentEntry,
|
|
142
|
-
ContentIndex,
|
|
143
|
-
ContentProblem,
|
|
144
|
-
} from './delivery/content-index.js';
|
|
145
|
-
export { createSiteIndex } from './delivery/site-index.js';
|
|
146
|
-
export type { SiteIndex, ConceptIndex } from './delivery/site-index.js';
|
|
147
|
-
export { createSiteIndexes } from './delivery/site-indexes.js';
|
|
148
|
-
export type { SiteIndexes, SiteGlobs } from './delivery/site-indexes.js';
|
|
149
|
-
export { deriveExcerpt, wordCount } from './delivery/excerpt.js';
|
|
150
|
-
export { buildRssFeed, buildJsonFeed } from './delivery/feeds.js';
|
|
151
|
-
export type { FeedChannel, FeedItem } from './delivery/feeds.js';
|
|
152
|
-
export { buildSitemap } from './delivery/sitemap.js';
|
|
153
|
-
export type { SitemapUrl } from './delivery/sitemap.js';
|
|
154
|
-
export { buildRobots } from './delivery/robots.js';
|
|
155
|
-
export { buildSeoMeta } from './delivery/seo.js';
|
|
156
|
-
export type { SeoInput, SeoMeta } from './delivery/seo.js';
|
|
157
|
-
export { readSeoFields, resolveImageUrl } from './delivery/seo-fields.js';
|
|
158
|
-
export type { SeoFields } from './delivery/seo-fields.js';
|
|
159
|
-
export { paginate } from './delivery/paginate.js';
|
|
160
|
-
export type { Page } from './delivery/paginate.js';
|
|
161
|
-
// Root superset of the delivery route surface: a wrong guess from root for a route loader or a
|
|
162
|
-
// response helper now resolves. The CairnHead component stays out of root so the root barrel stays
|
|
163
|
-
// node-importable for the unit suite; it resolves from @glw907/cairn-cms/delivery/head.
|
|
164
|
-
export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './delivery/responses.js';
|
|
165
|
-
export { createPublicRoutes } from './sveltekit/public-routes.js';
|
|
166
|
-
export type { PublicRoutesDeps, ListData, TagData, TagIndexData, EntryData } from './sveltekit/public-routes.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// cairn-cms: the component-authoring toolkit (@glw907/cairn-cms/render). A site authoring components
|
|
2
|
+
// through build(ctx) reaches for these hast builders and the string-attribute reader. Curated on
|
|
3
|
+
// purpose: the internal hast helpers (strProp, markFirstList, dataAttrProp) stay internal, and
|
|
4
|
+
// rehypeDispatch is deliberately omitted (createRenderer is the one public render pipeline).
|
|
5
|
+
export { iconSpan, cardShell, headRow, isElement, strAttr } from './rehype-dispatch.js';
|
|
6
|
+
export type { MakeIcon } from './rehype-dispatch.js';
|
|
7
|
+
export type { ComponentContext } from './registry.js';
|
|
@@ -9,7 +9,7 @@ import rehypeStringify from 'rehype-stringify';
|
|
|
9
9
|
import rehypeSanitize from 'rehype-sanitize';
|
|
10
10
|
import type { Schema } from 'hast-util-sanitize';
|
|
11
11
|
import { VFile } from 'vfile';
|
|
12
|
-
import { buildSanitizeSchema, rehypeAnchorRel } from './sanitize-schema.js';
|
|
12
|
+
import { buildSanitizeSchema, rehypeAnchorRel, rehypeSinkGuard } from './sanitize-schema.js';
|
|
13
13
|
import { remarkDirectiveStamp } from './remark-directives.js';
|
|
14
14
|
import { remarkResolveCairnLinks, CAIRN_RESOLVE } from './resolve-links.js';
|
|
15
15
|
import { rehypeDispatch } from './rehype-dispatch.js';
|
|
@@ -58,6 +58,9 @@ export function createRenderer(
|
|
|
58
58
|
rehypeSlug,
|
|
59
59
|
];
|
|
60
60
|
if (rel !== false) rehypePlugins.push([rehypeAnchorRel, rel]);
|
|
61
|
+
// The sink guard runs last, over the fully-built tree, so it neutralizes a sink a component
|
|
62
|
+
// build() emitted after the floor. Gated by the same switch as the floor.
|
|
63
|
+
if (!options.unsafeDisableSanitize) rehypePlugins.push(rehypeSinkGuard);
|
|
61
64
|
const processor = unified()
|
|
62
65
|
.use(remarkParse)
|
|
63
66
|
.use(remarkGfm)
|
|
@@ -82,6 +82,8 @@ export interface ComponentRegistry {
|
|
|
82
82
|
names: string[];
|
|
83
83
|
get(name: string): ComponentDef | undefined;
|
|
84
84
|
defaultIcon(name: string, role?: string): string | undefined;
|
|
85
|
+
/** The component's first `type:'icon'` attribute, or undefined when it declares none. */
|
|
86
|
+
iconField(name: string): AttributeField | undefined;
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/** The hast property name carrying one declared attribute from stamp to dispatch, e.g. `tone`
|
|
@@ -91,17 +93,35 @@ export function dataAttrProp(key: string): string {
|
|
|
91
93
|
return `dataAttr${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
/** A component's first `type:'icon'` attribute, or undefined when it declares none. Both the
|
|
97
|
+
* construction-time guard and the registry's `iconField` derive the icon field from this one
|
|
98
|
+
* predicate rather than spelling the `type === 'icon'` find twice. */
|
|
99
|
+
function findIconField(def: ComponentDef): AttributeField | undefined {
|
|
100
|
+
return def.attributes?.find((field) => field.type === 'icon');
|
|
101
|
+
}
|
|
102
|
+
|
|
94
103
|
/**
|
|
95
104
|
* Build a registry from a site's component definitions. The single source the render
|
|
96
105
|
* pipeline (directive stamp plus rehype dispatch) and the editor palette both read.
|
|
97
106
|
*/
|
|
98
107
|
export function defineRegistry({ components }: { components: ComponentDef[] }): ComponentRegistry {
|
|
108
|
+
for (const c of components) {
|
|
109
|
+
if (c.defaultIconByRole && Object.keys(c.defaultIconByRole).length > 0 && !findIconField(c)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`cairn: component "${c.name}" sets defaultIconByRole but declares no type:'icon' attribute, so the default icon can never render`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
99
115
|
const byName = new Map(components.map((c) => [c.name, c]));
|
|
100
116
|
return {
|
|
101
117
|
defs: components,
|
|
102
118
|
names: components.map((c) => c.name),
|
|
103
119
|
get: (name) => byName.get(name),
|
|
104
120
|
defaultIcon: (name, role) => (role ? byName.get(name)?.defaultIconByRole?.[role] : undefined),
|
|
121
|
+
iconField: (name) => {
|
|
122
|
+
const def = byName.get(name);
|
|
123
|
+
return def ? findIconField(def) : undefined;
|
|
124
|
+
},
|
|
105
125
|
};
|
|
106
126
|
}
|
|
107
127
|
|
|
@@ -6,6 +6,13 @@ export function isElement(node: ElementContent | undefined): node is Element {
|
|
|
6
6
|
return !!node && node.type === 'element';
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/** Read a declared string attribute off the component context, returning undefined for a boolean or
|
|
10
|
+
* absent value. Replaces the `typeof ctx.attributes[key] === 'string'` narrowing a build repeats. */
|
|
11
|
+
export function strAttr(ctx: ComponentContext, key: string): string | undefined {
|
|
12
|
+
const value = ctx.attributes[key];
|
|
13
|
+
return typeof value === 'string' ? value : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
// hast Properties values are PropertyValue (string | number | boolean | array | null).
|
|
10
17
|
// Directive markers (dataPrimitive/dataRole/dataAttr<Key>) are always stamped as strings;
|
|
11
18
|
// this reads them back with that guarantee instead of casting at each call site.
|
|
@@ -28,14 +35,14 @@ export function cardShell(classes: string[], body: ElementContent[]): Element {
|
|
|
28
35
|
return h('section', { className: classes }, [h('div', { className: ['card-body'] }, body)]);
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
/** Card head row: `<div class="ec-head">[icon]<
|
|
32
|
-
* Pass the title's inline children
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
export function headRow(title: ElementContent[], icon?: Element): Element {
|
|
38
|
+
/** Card head row: `<div class="ec-head">[icon]<hN class="card-title">{title}</hN></div>`.
|
|
39
|
+
* Pass the title's inline children, an optional pre-built icon element, and an optional heading
|
|
40
|
+
* 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). */
|
|
42
|
+
export function headRow(title: ElementContent[], icon?: Element, level: number = 2): Element {
|
|
36
43
|
const children: ElementContent[] = [];
|
|
37
44
|
if (icon) children.push(icon);
|
|
38
|
-
children.push(h(
|
|
45
|
+
children.push(h(`h${level}`, { className: ['card-title'] }, title));
|
|
39
46
|
return h('div', { className: ['ec-head'] }, children);
|
|
40
47
|
}
|
|
41
48
|
|
|
@@ -59,7 +59,7 @@ export function remarkDirectiveStamp(registry: ComponentRegistry) {
|
|
|
59
59
|
const def = registry.get(node.name);
|
|
60
60
|
const attrs = node.attributes ?? {};
|
|
61
61
|
const role = attrs.role || undefined;
|
|
62
|
-
const iconField =
|
|
62
|
+
const iconField = registry.iconField(node.name);
|
|
63
63
|
const iconKey = iconField?.key ?? 'icon';
|
|
64
64
|
let icon = attrs[iconKey] || undefined;
|
|
65
65
|
if (!icon && role) icon = registry.defaultIcon(node.name, role);
|
|
@@ -65,3 +65,100 @@ export function rehypeAnchorRel(rel: string) {
|
|
|
65
65
|
});
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
// URL-bearing hast properties the post-dispatch guard scheme-checks. hast camelCases attribute
|
|
70
|
+
// names through property-information (srcset -> srcSet, xlink:href -> xLinkHref with a capital L,
|
|
71
|
+
// formaction -> formAction). data is the <object data> URL attribute; data-* attributes camelCase
|
|
72
|
+
// to dataFoo and are not matched here.
|
|
73
|
+
const URL_PROPS = new Set([
|
|
74
|
+
'href',
|
|
75
|
+
'src',
|
|
76
|
+
'srcSet',
|
|
77
|
+
'xLinkHref',
|
|
78
|
+
'poster',
|
|
79
|
+
'formAction',
|
|
80
|
+
'action',
|
|
81
|
+
'data',
|
|
82
|
+
'background',
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
// The safe URL schemes: the union of every protocol list in defaultSchema, plus cairn. The
|
|
86
|
+
// floor admits these and strips the rest, so deriving from the same source keeps the floor and
|
|
87
|
+
// this guard from drifting on what a safe scheme is. javascript:/data:/vbscript: are never in
|
|
88
|
+
// defaultSchema, so they are never safe.
|
|
89
|
+
const SAFE_SCHEMES: Set<string> = (() => {
|
|
90
|
+
const protocols = defaultSchema.protocols ?? {};
|
|
91
|
+
const schemes = new Set<string>(['cairn']);
|
|
92
|
+
for (const list of Object.values(protocols)) {
|
|
93
|
+
for (const scheme of list ?? []) schemes.add(String(scheme).toLowerCase());
|
|
94
|
+
}
|
|
95
|
+
return schemes;
|
|
96
|
+
})();
|
|
97
|
+
|
|
98
|
+
// Read a URL value's scheme for the safety check, defeating the whitespace and control-character
|
|
99
|
+
// tricks a browser ignores inside a scheme (java\tscript:, a leading space). A value with no
|
|
100
|
+
// scheme (relative, anchor, query) returns undefined and is always safe.
|
|
101
|
+
function urlScheme(value: string): string | undefined {
|
|
102
|
+
const cleaned = value.replace(/[\x00-\x20]+/g, '');
|
|
103
|
+
const match = /^([a-z][a-z0-9+.-]*):/i.exec(cleaned);
|
|
104
|
+
return match ? match[1].toLowerCase() : undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isSafeUrl(value: string): boolean {
|
|
108
|
+
const scheme = urlScheme(value);
|
|
109
|
+
return scheme === undefined || SAFE_SCHEMES.has(scheme);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// srcset is "url descriptor, url descriptor, …". hast may store it as a string or, because
|
|
113
|
+
// property-information marks it comma-separated, as a string array. One unsafe candidate makes
|
|
114
|
+
// the whole attribute unsafe.
|
|
115
|
+
function isSafeSrcset(value: unknown): boolean {
|
|
116
|
+
const candidates = Array.isArray(value)
|
|
117
|
+
? value.map(String)
|
|
118
|
+
: typeof value === 'string'
|
|
119
|
+
? value.split(',')
|
|
120
|
+
: [];
|
|
121
|
+
return candidates.every((candidate) => {
|
|
122
|
+
const url = candidate.trim().split(/\s+/)[0];
|
|
123
|
+
return url === '' || isSafeUrl(url);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Decide whether one URL-bearing property value is safe to keep. srcset has its own
|
|
128
|
+
// multi-candidate grammar. A non-string value carries no scheme to abuse, so the floor's own
|
|
129
|
+
// handling stands and the guard leaves it alone.
|
|
130
|
+
function isSafeUrlProp(key: string, value: unknown): boolean {
|
|
131
|
+
if (key === 'srcSet') return isSafeSrcset(value);
|
|
132
|
+
if (typeof value !== 'string') return true;
|
|
133
|
+
return isSafeUrl(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Post-dispatch safety floor over the fully-built tree. The pre-dispatch rehype-sanitize floor
|
|
138
|
+
* cleans author content, but a component build() runs after it and can route a raw author
|
|
139
|
+
* attribute value into a sink. This guard runs last and neutralizes those sinks on every element
|
|
140
|
+
* no matter which plugin or which build() produced it: an unsafe URL scheme in a URL-bearing
|
|
141
|
+
* attribute, an inline on* event handler, or an inline style (stripped wholesale, matching the
|
|
142
|
+
* floor and cairn's class-driven styling). It is gated by the same unsafeDisableSanitize switch as
|
|
143
|
+
* the floor.
|
|
144
|
+
*
|
|
145
|
+
* The guard's boundary is the URL scheme check plus the on* and style strip. It does not remove a
|
|
146
|
+
* build()-emitted raw script, style, or iframe srcdoc element node. A build() that emits those is
|
|
147
|
+
* running site-developer code, and author markdown is cleaned by the pre-dispatch floor.
|
|
148
|
+
*/
|
|
149
|
+
export function rehypeSinkGuard() {
|
|
150
|
+
return (tree: Root) => {
|
|
151
|
+
visit(tree, 'element', (node: Element) => {
|
|
152
|
+
const props = node.properties;
|
|
153
|
+
if (!props) return;
|
|
154
|
+
for (const key of Object.keys(props)) {
|
|
155
|
+
if (/^on/i.test(key) || key === 'style') {
|
|
156
|
+
delete props[key];
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!URL_PROPS.has(key)) continue;
|
|
160
|
+
if (!isSafeUrlProp(key, props[key])) delete props[key];
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -22,15 +22,20 @@ export interface NavConcept {
|
|
|
22
22
|
label: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** The admin layout's data: site identity, the signed-in user, the nav,
|
|
25
|
+
/** The admin layout's data: site identity, the signed-in user, the nav, the active path, and theme. */
|
|
26
26
|
export interface LayoutData {
|
|
27
27
|
siteName: string;
|
|
28
|
-
user: { displayName: string; role: Role };
|
|
28
|
+
user: { displayName: string; email: string; role: Role };
|
|
29
29
|
concepts: NavConcept[];
|
|
30
30
|
pathname: string;
|
|
31
31
|
canManageEditors: boolean;
|
|
32
32
|
/** The nav menu's label when the site configures one; gates the Navigation nav entry. Null otherwise. */
|
|
33
33
|
navLabel: string | null;
|
|
34
|
+
/** The admin theme resolved for SSR: the persisted cookie choice, or the light default. */
|
|
35
|
+
theme: 'cairn-admin' | 'cairn-admin-dark';
|
|
36
|
+
/** The nav group labels the user has collapsed, from the persisted cookie. Read at SSR so a
|
|
37
|
+
* collapsed group renders collapsed with no flash. Empty when none are collapsed. */
|
|
38
|
+
collapsedNav: string[];
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
/** One row in a concept's list view. */
|
|
@@ -83,6 +88,8 @@ export interface ContentEvent {
|
|
|
83
88
|
request: Request;
|
|
84
89
|
locals: { editor?: Editor | null };
|
|
85
90
|
platform?: { env?: GithubKeyEnv };
|
|
91
|
+
/** SvelteKit's cookie jar; the layout load reads the persisted admin theme. Optional for non-route callers. */
|
|
92
|
+
cookies?: { get(name: string): string | undefined };
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
/** Injectable dependencies; tests stub the token mint to avoid signing a real key. */
|
|
@@ -109,16 +116,24 @@ export function createContentRoutes(runtime: CairnRuntime, deps: ContentRoutesDe
|
|
|
109
116
|
const mintToken =
|
|
110
117
|
deps.mintToken ?? ((env: GithubKeyEnv) => cachedInstallationToken(appCredentials(runtime.backend, env)));
|
|
111
118
|
|
|
112
|
-
/** Layout load for every admin page: the nav, the user, and the
|
|
119
|
+
/** Layout load for every admin page: the nav, the user, the active path, and the resolved theme. */
|
|
113
120
|
function layoutLoad(event: ContentEvent): LayoutData {
|
|
114
121
|
const editor = sessionOf(event);
|
|
122
|
+
const cookieTheme = event.cookies?.get('cairn-admin-theme');
|
|
123
|
+
const theme = cookieTheme === 'cairn-admin-dark' ? 'cairn-admin-dark' : 'cairn-admin';
|
|
124
|
+
const cookieCollapsed = event.cookies?.get('cairn-admin-nav-collapsed');
|
|
125
|
+
const collapsedNav = cookieCollapsed
|
|
126
|
+
? cookieCollapsed.split(',').map((part) => decodeURIComponent(part)).filter(Boolean)
|
|
127
|
+
: [];
|
|
115
128
|
return {
|
|
116
129
|
siteName: runtime.siteName,
|
|
117
|
-
user: { displayName: editor.displayName, role: editor.role },
|
|
130
|
+
user: { displayName: editor.displayName, email: editor.email, role: editor.role },
|
|
118
131
|
concepts: runtime.concepts.map((c) => ({ id: c.id, label: c.label })),
|
|
119
132
|
pathname: event.url.pathname,
|
|
120
133
|
canManageEditors: editor.role === 'owner',
|
|
121
134
|
navLabel: runtime.navMenu?.label ?? null,
|
|
135
|
+
theme,
|
|
136
|
+
collapsedNav,
|
|
122
137
|
};
|
|
123
138
|
}
|
|
124
139
|
|
|
@@ -338,14 +353,17 @@ export function createContentRoutes(runtime: CairnRuntime, deps: ContentRoutesDe
|
|
|
338
353
|
throw redirect(303, `/admin/${concept.id}/${id}?${savedQuery}`);
|
|
339
354
|
}
|
|
340
355
|
|
|
341
|
-
/**
|
|
342
|
-
* the file removal and the manifest patch in one commit. The inbound recheck here is the
|
|
343
|
-
* authoritative gate, closing the load-to-delete race.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
356
|
+
/** The shared delete core. Block-until-clean: refuse while inbound links exist (naming them), else
|
|
357
|
+
* commit the file removal and the manifest patch in one commit. The inbound recheck here is the
|
|
358
|
+
* authoritative gate, closing the load-to-delete race. Both the editor delete (id from params) and
|
|
359
|
+
* the list delete (id from the form body) call this with an already-validated id, so the guard is
|
|
360
|
+
* enforced once. */
|
|
361
|
+
async function deleteEntry(
|
|
362
|
+
event: ContentEvent,
|
|
363
|
+
concept: ConceptDescriptor,
|
|
364
|
+
id: string,
|
|
365
|
+
editor: Editor,
|
|
366
|
+
): Promise<ReturnType<typeof fail> | never> {
|
|
349
367
|
const path = `${concept.dir}/${filenameFromId(id)}`;
|
|
350
368
|
const token = await mintToken(event.platform?.env ?? {});
|
|
351
369
|
|
|
@@ -355,7 +373,7 @@ export function createContentRoutes(runtime: CairnRuntime, deps: ContentRoutesDe
|
|
|
355
373
|
const manifest = manifestRaw === null ? emptyManifest() : parseManifest(manifestRaw);
|
|
356
374
|
const inbound = inboundLinks(manifest, concept.id, id);
|
|
357
375
|
if (inbound.length) {
|
|
358
|
-
return fail(409, { inboundLinks: inbound });
|
|
376
|
+
return fail(409, { inboundLinks: inbound, id });
|
|
359
377
|
}
|
|
360
378
|
|
|
361
379
|
const nextManifest = serializeManifest(removeEntry(manifest, concept.id, id));
|
|
@@ -379,6 +397,25 @@ export function createContentRoutes(runtime: CairnRuntime, deps: ContentRoutesDe
|
|
|
379
397
|
throw redirect(303, `/admin/${concept.id}`);
|
|
380
398
|
}
|
|
381
399
|
|
|
400
|
+
/** Delete an entry from its editor. The id comes from the route param. */
|
|
401
|
+
async function deleteAction(event: ContentEvent): Promise<ReturnType<typeof fail> | never> {
|
|
402
|
+
const editor = sessionOf(event);
|
|
403
|
+
const concept = conceptOf(runtime, event.params);
|
|
404
|
+
const id = event.params.id ?? '';
|
|
405
|
+
if (!isValidId(id)) throw error(400, 'Invalid entry id');
|
|
406
|
+
return deleteEntry(event, concept, id, editor);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Delete an entry from the concept list. The id comes from the form body. */
|
|
410
|
+
async function listDeleteAction(event: ContentEvent): Promise<ReturnType<typeof fail> | never> {
|
|
411
|
+
const editor = sessionOf(event);
|
|
412
|
+
const concept = conceptOf(runtime, event.params);
|
|
413
|
+
const form = await event.request.formData();
|
|
414
|
+
const id = String(form.get('id') ?? '');
|
|
415
|
+
if (!isValidId(id)) throw error(400, 'Invalid entry id');
|
|
416
|
+
return deleteEntry(event, concept, id, editor);
|
|
417
|
+
}
|
|
418
|
+
|
|
382
419
|
/** Rename an entry: change its slug, move the file, and rewrite every inbound cairn token in one
|
|
383
420
|
* atomic commit, so no internal link breaks. The collision check and the inbound recompute here
|
|
384
421
|
* are the authoritative gate. The same last-writer-wins manifest race as save and delete applies,
|
|
@@ -466,5 +503,5 @@ export function createContentRoutes(runtime: CairnRuntime, deps: ContentRoutesDe
|
|
|
466
503
|
throw redirect(303, `/admin/${concept.id}/${newId}?renamed=1`);
|
|
467
504
|
}
|
|
468
505
|
|
|
469
|
-
return { layoutLoad, indexRedirect, listLoad, createAction, editLoad, saveAction, deleteAction, renameAction, mintToken };
|
|
506
|
+
return { layoutLoad, indexRedirect, listLoad, createAction, editLoad, saveAction, deleteAction, listDeleteAction, renameAction, mintToken };
|
|
470
507
|
}
|
|
@@ -17,11 +17,5 @@ export { createNavRoutes } from './nav-routes.js';
|
|
|
17
17
|
export type { NavLoadData, NavPageOption, NavRoutesDeps } from './nav-routes.js';
|
|
18
18
|
export { healthLoad, type HealthData } from './health.js';
|
|
19
19
|
export type { RequestContext, CookieJar, HandleInput } from './types.js';
|
|
20
|
-
|
|
21
|
-
export type {
|
|
22
|
-
PublicRoutesDeps,
|
|
23
|
-
ListData as PublicListData,
|
|
24
|
-
TagData,
|
|
25
|
-
TagIndexData,
|
|
26
|
-
EntryData,
|
|
27
|
-
} from './public-routes.js';
|
|
20
|
+
// Re-exported here, not from root, so the public ContentRoutesDeps consumer can name it.
|
|
21
|
+
export type { GithubKeyEnv } from '../github/credentials.js';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/lib/auth/crypto.ts"],"names":[],"mappings":"AAOA;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAEzD;AAED,yCAAyC;AACzC,eAAO,MAAM,YAAY,QAAiB,CAAC;AAE3C,6BAA6B;AAC7B,eAAO,MAAM,cAAc,QAA2B,CAAC;AAEvD,0FAA0F;AAC1F,eAAO,MAAM,gBAAgB,QAAY,CAAC;AAU1C,kDAAkD;AAClD,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,4CAA4C;AAC5C,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,oEAAoE;AACpE,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI9D"}
|
package/dist/auth/store.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/lib/auth/store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAQ/C,yCAAyC;AACzC,wBAAsB,UAAU,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMtF;AAED,2EAA2E;AAC3E,wBAAsB,UAAU,CAC9B,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,yGAAyG;AACzG,wBAAsB,cAAc,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMnG;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMzG;AAED,4BAA4B;AAC5B,wBAAsB,aAAa,CACjC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAUpG;AAED,iCAAiC;AACjC,wBAAsB,aAAa,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7E;AAED,2CAA2C;AAC3C,wBAAsB,WAAW,CAAC,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAKnE;AAED,sCAAsC;AACtC,wBAAsB,YAAY,CAChC,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,0FAA0F;AAC1F,wBAAsB,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM/E;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAe1F;AAED,iFAAiF;AACjF,wBAAsB,aAAa,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5F;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAU1F"}
|
package/dist/auth/types.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/auth/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEtC,0FAA0F;AAC1F,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,4FAA4F;AAC5F,MAAM,WAAW,OAAO;IACtB,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,qGAAqG;IACrG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wCAAwC;IACxC,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,OAAO,EAAE;YACZ,EAAE,EAAE,MAAM,CAAC;YACX,IAAI,EAAE,MAAM,CAAC;YACb,OAAO,EAAE,MAAM,CAAC;YAChB,IAAI,EAAE,MAAM,CAAC;YACb,IAAI,EAAE,MAAM,CAAC;SACd,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACnB,CAAC;CACH"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"AdminLayout.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AdminLayout.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,mBAAmB,CAAC;AAGzB,UAAU,KAAK;IACb,4FAA4F;IAC5F,IAAI,EAAE,UAAU,CAAC;IACjB,qBAAqB;IACrB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAyEH;;;;;GAKG;AACH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ComponentForm.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ComponentForm.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEvE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAIhD,UAAU,KAAK;IACb,GAAG,EAAE,YAAY,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mEAAmE;IACnE,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,4BAA4B;IAC5B,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAyJH;;;;;GAKG;AACH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ComponentInsertDialog.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ComponentInsertDialog.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAgB,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAIhD,UAAU,KAAK;IACb,qCAAqC;IACrC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,4CAA4C;IAC5C,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,4CAA4C;IAC5C,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAkFH;;;;;GAKG;AACH,QAAA,MAAM,qBAAqB,2CAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ConceptList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ConceptList.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAG7D,UAAU,KAAK;IACb,qFAAqF;IACrF,IAAI,EAAE,QAAQ,CAAC;CAChB;AA2EH;;;GAGG;AACH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ConfirmPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ConfirmPage.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,mBAAmB,CAAC;AAGzB,UAAU,KAAK;IACb,0FAA0F;IAC1F,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CACjE;AA8BH;;;GAGG;AACH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"DeleteDialog.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/DeleteDialog.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGxD,UAAU,KAAK;IACb,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B;AAqEH;;;;;GAKG;AACH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAE/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAIrD,UAAU,KAAK;IACb,gEAAgE;IAChE,IAAI,EAAE,QAAQ,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,qIAAqI;IACrI,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,WAAW,CAAA;KAAE,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvG,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;oFACgF;IAChF,IAAI,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,wBAAwB,EAAE,WAAW,EAAE,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC9I;AAyQH;;;;GAIG;AACH,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"EditorToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditorToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGrD,UAAU,KAAK;IACb,oEAAoE;IACpE,MAAM,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;CACpC;AAiDH;;;;;GAKG;AACH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|