@glw907/cairn-cms 0.26.0 → 0.29.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 +49 -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.d.ts +0 -1
- package/dist/components/ComponentForm.svelte.d.ts +0 -1
- package/dist/components/ComponentInsertDialog.svelte.d.ts +0 -1
- package/dist/components/ConceptList.svelte.d.ts +0 -1
- package/dist/components/ConfirmPage.svelte.d.ts +0 -1
- package/dist/components/DeleteDialog.svelte.d.ts +0 -1
- 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.d.ts +0 -1
- package/dist/components/ManageEditors.svelte.d.ts +0 -1
- package/dist/components/MarkdownEditor.svelte.d.ts +0 -1
- package/dist/components/NavTree.svelte.d.ts +0 -1
- package/dist/components/RenameDialog.svelte.d.ts +0 -1
- 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 +1 -28
- package/dist/index.js +1 -23
- package/dist/nav/site-config.d.ts +0 -1
- 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 +0 -1
- package/dist/render/rehype-dispatch.d.ts +0 -1
- package/dist/render/remark-directives.d.ts +0 -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 +0 -1
- 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 +2 -1
- 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 +1 -56
- package/src/lib/render/pipeline.ts +4 -1
- package/src/lib/render/sanitize-schema.ts +97 -0
- 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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are recorded here, most recent first.
|
|
4
4
|
|
|
5
|
+
## 0.29.0
|
|
6
|
+
|
|
7
|
+
Consolidated the URL-identity model. A content entry's id, slug, date, and permalink are now derived in
|
|
8
|
+
one place (`entryIdentity`), so the content index and the manifest cannot drift on an entry's URL, and a
|
|
9
|
+
site's concept descriptors are resolved through one path shared by the admin runtime and the delivery
|
|
10
|
+
layer. No public surface changed.
|
|
11
|
+
|
|
12
|
+
The YAML URL policy is now validated at build. A permalink pattern must be root-relative and use only the
|
|
13
|
+
tokens `:slug`, `:year`, `:month`, and `:day`, a date token is valid only on a dated concept, a
|
|
14
|
+
`datePrefix` must be `year`, `month`, or `day`, and a policy keyed to an undeclared concept fails the
|
|
15
|
+
build.
|
|
16
|
+
|
|
17
|
+
Behavior note: a site whose `content:` URL policy was malformed and silently defaulted will now fail the
|
|
18
|
+
build with a named error. A valid policy is unaffected.
|
|
19
|
+
|
|
20
|
+
## 0.28.0
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
Closed the render attribute-sink residual by construction. A new post-dispatch guard runs last in
|
|
24
|
+
`createRenderer` and neutralizes the sinks a component `build()` could route a raw author attribute
|
|
25
|
+
value into, including the unsafe URL schemes `javascript:`, `data:`, and `vbscript:` in `href`,
|
|
26
|
+
`src`, `srcset`, `xlink:href`, `poster`, `formaction`, `action`, `object`'s `data`, and
|
|
27
|
+
`background`, the inline `on*` event handlers, and inline `style`, which is stripped wholesale. Safe
|
|
28
|
+
schemes, relative URLs, anchors, and the `cairn:` token are preserved. The guard is gated by the
|
|
29
|
+
existing `unsafeDisableSanitize` switch.
|
|
30
|
+
|
|
31
|
+
Behavior note: a site whose component `build()` emits a non-standard URL scheme, an `on*` handler,
|
|
32
|
+
or inline `style` will see that output neutralized. Route dynamic styling through a class or an
|
|
33
|
+
inert `data-*` attribute instead.
|
|
34
|
+
|
|
35
|
+
## 0.27.0
|
|
36
|
+
|
|
37
|
+
### Changed (breaking)
|
|
38
|
+
Narrowed the public export surface so each symbol has one canonical home. The `.` root and
|
|
39
|
+
`/sveltekit` no longer re-export another subpath's symbols, and the internal GitHub, signing, and
|
|
40
|
+
hast helpers left the public API. No symbol changed behavior; only where it exports from.
|
|
41
|
+
|
|
42
|
+
- Consumers must: import the delivery read helpers (`createContentIndex`, `createSiteIndexes`, the
|
|
43
|
+
feed, sitemap, robots, SEO, and pagination builders, `permalink`) from `@glw907/cairn-cms/delivery/data`
|
|
44
|
+
instead of the `.` root.
|
|
45
|
+
- Consumers must: import the public route loaders and the `*Response` helpers (`createPublicRoutes`,
|
|
46
|
+
`rssResponse`, `jsonFeedResponse`, `sitemapResponse`, `robotsResponse`) and the public route types
|
|
47
|
+
(`PublicRoutesDeps`, the public `ListData`, `TagData`, `TagIndexData`, `EntryData`) from
|
|
48
|
+
`@glw907/cairn-cms/delivery` instead of the `.` root or `/sveltekit`.
|
|
49
|
+
- Consumers must: stop importing the internal helpers that left the public API (`appJwt`,
|
|
50
|
+
`installationToken`, `signingSelfTest`, `appCredentials`, `treeUrl`, `contentsUrl`, `readRaw`,
|
|
51
|
+
`fileSha`, `listMarkdown`, `markdownFilesIn`, `commitFile`, `isElement`, `strProp`, `markFirstList`);
|
|
52
|
+
the engine wires GitHub token minting and the render pipeline internally, so no consumer needs them.
|
|
53
|
+
|
|
5
54
|
## 0.26.0
|
|
6
55
|
|
|
7
56
|
### Added
|
package/dist/auth/crypto.d.ts
CHANGED
|
@@ -16,4 +16,3 @@ export declare function generateToken(): string;
|
|
|
16
16
|
export declare function generateSessionId(): string;
|
|
17
17
|
/** The lowercase hex SHA-256 of a token, for storage and lookup. */
|
|
18
18
|
export declare function hashToken(token: string): Promise<string>;
|
|
19
|
-
//# sourceMappingURL=crypto.d.ts.map
|
package/dist/auth/store.d.ts
CHANGED
|
@@ -40,4 +40,3 @@ export declare function setEditorRole(db: D1Database, email: string, role: Role)
|
|
|
40
40
|
* `removeOwnerIfNotLast`). Returns false (and writes nothing) when this is the last owner.
|
|
41
41
|
*/
|
|
42
42
|
export declare function demoteOwnerIfNotLast(db: D1Database, email: string): Promise<boolean>;
|
|
43
|
-
//# sourceMappingURL=store.d.ts.map
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -17,4 +17,3 @@ interface Props {
|
|
|
17
17
|
declare const ComponentInsertDialog: import("svelte").Component<Props, {}, "">;
|
|
18
18
|
type ComponentInsertDialog = ReturnType<typeof ComponentInsertDialog>;
|
|
19
19
|
export default ComponentInsertDialog;
|
|
20
|
-
//# sourceMappingURL=ComponentInsertDialog.svelte.d.ts.map
|
|
@@ -12,4 +12,3 @@ export { default as NavTree } from './NavTree.svelte';
|
|
|
12
12
|
export { default as LinkPicker } from './LinkPicker.svelte';
|
|
13
13
|
export { default as DeleteDialog } from './DeleteDialog.svelte';
|
|
14
14
|
export { default as RenameDialog } from './RenameDialog.svelte';
|
|
15
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -13,4 +13,3 @@ export declare function linkCompletions(targets: LinkTarget[], query: string): C
|
|
|
13
13
|
* whole `[[query` with the chosen link, and sets filter:false because linkCompletions already
|
|
14
14
|
* filtered by the query (CodeMirror would otherwise re-filter against the literal `[[query`). */
|
|
15
15
|
export declare function cairnLinkCompletionSource(targets: LinkTarget[]): CompletionSource;
|
|
16
|
-
//# sourceMappingURL=link-completion.d.ts.map
|
|
@@ -30,4 +30,3 @@ export declare function unwrapCairnLink(doc: string, href: string): string;
|
|
|
30
30
|
* last to first, replacing only the `](oldHref` run so the label and title stay exact.
|
|
31
31
|
*/
|
|
32
32
|
export declare function rewriteCairnLink(doc: string, oldHref: string, newHref: string): string;
|
|
33
|
-
//# sourceMappingURL=markdown-format.d.ts.map
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type { CairnAdapter } from './types.js';
|
|
2
2
|
/** Declare a site's adapter while preserving each concept's concrete schema type for typed reads. */
|
|
3
3
|
export declare function defineAdapter<const A extends CairnAdapter>(adapter: A): A;
|
|
4
|
-
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CairnAdapter, CairnExtension, CairnRuntime } from './types.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { SiteConfig } from '../nav/site-config.js';
|
|
3
3
|
/** The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
|
|
4
4
|
* always derived from one source and can never be silently dropped. `extensions` fold in after the
|
|
5
5
|
* adapter's concepts. */
|
|
@@ -15,4 +15,3 @@ export interface ComposeInput {
|
|
|
15
15
|
* (seam 4) passes through untouched.
|
|
16
16
|
*/
|
|
17
17
|
export declare function composeRuntime({ adapter, siteConfig, extensions }: ComposeInput): CairnRuntime;
|
|
18
|
-
//# sourceMappingURL=compose.d.ts.map
|
package/dist/content/compose.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { urlPolicyFrom } from '../nav/site-config.js';
|
|
1
|
+
import { resolveConcepts } from './concepts.js';
|
|
3
2
|
/**
|
|
4
3
|
* Fold an adapter and any extensions into the composed runtime (seam 2). The per-concept URL policy
|
|
5
4
|
* is derived from the site config, the same source the delivery path uses, so the runtime and
|
|
@@ -24,7 +23,7 @@ export function composeRuntime({ adapter, siteConfig, extensions = [] }) {
|
|
|
24
23
|
}
|
|
25
24
|
return {
|
|
26
25
|
siteName: adapter.siteName,
|
|
27
|
-
concepts:
|
|
26
|
+
concepts: resolveConcepts(content, siteConfig),
|
|
28
27
|
backend: adapter.backend,
|
|
29
28
|
sender: adapter.sender,
|
|
30
29
|
render: adapter.render,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
|
|
2
|
+
import { type SiteConfig } from '../nav/site-config.js';
|
|
2
3
|
/**
|
|
3
4
|
* Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
|
|
4
5
|
* pages are plain navigable structure. Not in adapter config. A future Fragments adds one
|
|
@@ -13,6 +14,11 @@ export declare const CONCEPT_ROUTING: Readonly<Record<string, RoutingRule>>;
|
|
|
13
14
|
* a new concept attaches additively; production passes the default `CONCEPT_ROUTING`.
|
|
14
15
|
*/
|
|
15
16
|
export declare function normalizeConcepts(content: Record<string, ConceptConfig | undefined>, urlPolicy?: Record<string, ConceptUrlPolicy | undefined>, routing?: Readonly<Record<string, RoutingRule>>): ConceptDescriptor[];
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
|
|
19
|
+
* (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
|
|
20
|
+
* policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveConcepts(content: Record<string, ConceptConfig | undefined>, siteConfig: SiteConfig): ConceptDescriptor[];
|
|
16
23
|
/** Look up a normalized concept by id, or undefined when the site does not enable it. */
|
|
17
24
|
export declare function findConcept(concepts: ConceptDescriptor[], id: string): ConceptDescriptor | undefined;
|
|
18
|
-
//# sourceMappingURL=concepts.d.ts.map
|
package/dist/content/concepts.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { urlPolicyFrom } from '../nav/site-config.js';
|
|
1
2
|
/**
|
|
2
3
|
* Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
|
|
3
4
|
* pages are plain navigable structure. Not in adapter config. A future Fragments adds one
|
|
@@ -17,6 +18,37 @@ function defaultLabel(id) {
|
|
|
17
18
|
function defaultPermalink(id) {
|
|
18
19
|
return id === 'pages' ? '/:slug' : `/${id}/:slug`;
|
|
19
20
|
}
|
|
21
|
+
/** Permalink tokens the resolver understands. */
|
|
22
|
+
const KNOWN_TOKENS = new Set(['slug', 'year', 'month', 'day']);
|
|
23
|
+
/** The date-bearing tokens; valid only for a dated concept. */
|
|
24
|
+
const DATE_TOKENS = new Set(['year', 'month', 'day']);
|
|
25
|
+
/** The valid date-prefix granularities. A runtime check, since the YAML is untyped. */
|
|
26
|
+
const DATE_PREFIXES = new Set(['year', 'month', 'day']);
|
|
27
|
+
/**
|
|
28
|
+
* Validate one concept's URL policy at build, so a misconfigured permalink or datePrefix fails loudly
|
|
29
|
+
* here rather than emitting a wrong or defaulted URL at render. The permalink must be root-relative and
|
|
30
|
+
* use only known tokens, a date token requires a dated concept, and the datePrefix must be in range.
|
|
31
|
+
*/
|
|
32
|
+
function validateUrlPolicy(id, policy, dated) {
|
|
33
|
+
if (policy.permalink !== undefined) {
|
|
34
|
+
const pattern = policy.permalink;
|
|
35
|
+
if (!pattern.startsWith('/')) {
|
|
36
|
+
throw new Error(`cairn: concept "${id}" permalink "${pattern}" must start with "/"`);
|
|
37
|
+
}
|
|
38
|
+
for (const match of pattern.matchAll(/:(\w+)/g)) {
|
|
39
|
+
const token = match[1];
|
|
40
|
+
if (!KNOWN_TOKENS.has(token)) {
|
|
41
|
+
throw new Error(`cairn: concept "${id}" permalink "${pattern}" uses unknown token ":${token}"`);
|
|
42
|
+
}
|
|
43
|
+
if (DATE_TOKENS.has(token) && !dated) {
|
|
44
|
+
throw new Error(`cairn: concept "${id}" is not dated, so permalink "${pattern}" cannot use the date token ":${token}"`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (policy.datePrefix !== undefined && !DATE_PREFIXES.has(policy.datePrefix)) {
|
|
49
|
+
throw new Error(`cairn: concept "${id}" datePrefix "${policy.datePrefix}" must be one of year, month, day`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
20
52
|
/**
|
|
21
53
|
* Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
|
|
22
54
|
* (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
|
|
@@ -26,6 +58,12 @@ function defaultPermalink(id) {
|
|
|
26
58
|
*/
|
|
27
59
|
export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROUTING) {
|
|
28
60
|
const descriptors = [];
|
|
61
|
+
const declaredConcepts = new Set(Object.keys(content).filter((key) => content[key] !== undefined));
|
|
62
|
+
for (const key of Object.keys(urlPolicy)) {
|
|
63
|
+
if (!declaredConcepts.has(key)) {
|
|
64
|
+
throw new Error(`cairn: URL policy names concept "${key}", which is not declared under content`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
29
67
|
for (const [id, config] of Object.entries(content)) {
|
|
30
68
|
if (!config)
|
|
31
69
|
continue;
|
|
@@ -35,12 +73,14 @@ export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROU
|
|
|
35
73
|
if (undeclared !== undefined) {
|
|
36
74
|
throw new Error(`cairn: concept "${id}" summaryFields key "${undeclared}" is not a declared field`);
|
|
37
75
|
}
|
|
76
|
+
const conceptRouting = routing[id] ?? DEFAULT_ROUTING;
|
|
38
77
|
const policy = urlPolicy[id] ?? {};
|
|
78
|
+
validateUrlPolicy(id, policy, conceptRouting.dated);
|
|
39
79
|
descriptors.push({
|
|
40
80
|
id,
|
|
41
81
|
label: config.label ?? defaultLabel(id),
|
|
42
82
|
dir: config.dir,
|
|
43
|
-
routing:
|
|
83
|
+
routing: conceptRouting,
|
|
44
84
|
permalink: policy.permalink ?? defaultPermalink(id),
|
|
45
85
|
datePrefix: policy.datePrefix ?? 'day',
|
|
46
86
|
fields: config.schema.fields,
|
|
@@ -50,6 +90,14 @@ export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROU
|
|
|
50
90
|
}
|
|
51
91
|
return descriptors;
|
|
52
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
|
|
95
|
+
* (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
|
|
96
|
+
* policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveConcepts(content, siteConfig) {
|
|
99
|
+
return normalizeConcepts(content, urlPolicyFrom(siteConfig));
|
|
100
|
+
}
|
|
53
101
|
/** Look up a normalized concept by id, or undefined when the site does not enable it. */
|
|
54
102
|
export function findConcept(concepts, id) {
|
|
55
103
|
return concepts.find((concept) => concept.id === id);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ConceptDescriptor } from './types.js';
|
|
2
|
+
/** A content entry's resolved URL identity. */
|
|
3
|
+
export interface EntryIdentity {
|
|
4
|
+
id: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
date?: string;
|
|
7
|
+
permalink: string;
|
|
8
|
+
}
|
|
9
|
+
/** A present, non-empty string, else undefined. The read-model string coercion. */
|
|
10
|
+
export declare function asString(value: unknown): string | undefined;
|
|
11
|
+
/** A YYYY-MM-DD date. An unquoted YAML date parses as a JS Date; a string is sliced to its date head. */
|
|
12
|
+
export declare function asDate(value: unknown): string | undefined;
|
|
13
|
+
/** Tags as an array, empty when the file declares none. */
|
|
14
|
+
export declare function asTags(value: unknown): string[];
|
|
15
|
+
/** A content entry's id: its filename stem (the date prefix is part of a dated id). */
|
|
16
|
+
export declare function entryId(path: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a content entry's URL identity from its concept descriptor, its file path, and its parsed
|
|
19
|
+
* frontmatter. The slug strips the leading date prefix for a dated concept and is the id verbatim for
|
|
20
|
+
* an undated one. The permalink is the one resolver every reader shares. The caller parses the markdown
|
|
21
|
+
* once and passes the frontmatter, so there is no second parse here.
|
|
22
|
+
*/
|
|
23
|
+
export declare function entryIdentity(descriptor: ConceptDescriptor, path: string, frontmatter: Record<string, unknown>): EntryIdentity;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// cairn-cms: a content entry's URL identity in one place (engine-hardening pass 3). The id, the
|
|
2
|
+
// slug, the date, and the permalink are computed here, so the content index and the manifest cannot
|
|
3
|
+
// drift on what an entry's URL is. A cairn: link resolves through the manifest in the admin preview
|
|
4
|
+
// and through the content index in the public build, so the two must agree by construction.
|
|
5
|
+
import { idFromFilename, slugFromId } from './ids.js';
|
|
6
|
+
import { permalink } from './permalink.js';
|
|
7
|
+
/** The basename of a glob path: the segment after the last slash, or the whole path. */
|
|
8
|
+
function basename(path) {
|
|
9
|
+
const slash = path.lastIndexOf('/');
|
|
10
|
+
return slash >= 0 ? path.slice(slash + 1) : path;
|
|
11
|
+
}
|
|
12
|
+
/** A present, non-empty string, else undefined. The read-model string coercion. */
|
|
13
|
+
export function asString(value) {
|
|
14
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
/** A YYYY-MM-DD date. An unquoted YAML date parses as a JS Date; a string is sliced to its date head. */
|
|
17
|
+
export function asDate(value) {
|
|
18
|
+
if (value instanceof Date)
|
|
19
|
+
return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
|
|
20
|
+
if (typeof value === 'string')
|
|
21
|
+
return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Tags as an array, empty when the file declares none. */
|
|
25
|
+
export function asTags(value) {
|
|
26
|
+
return Array.isArray(value) ? value.map(String) : [];
|
|
27
|
+
}
|
|
28
|
+
/** A content entry's id: its filename stem (the date prefix is part of a dated id). */
|
|
29
|
+
export function entryId(path) {
|
|
30
|
+
return idFromFilename(basename(path));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a content entry's URL identity from its concept descriptor, its file path, and its parsed
|
|
34
|
+
* frontmatter. The slug strips the leading date prefix for a dated concept and is the id verbatim for
|
|
35
|
+
* an undated one. The permalink is the one resolver every reader shares. The caller parses the markdown
|
|
36
|
+
* once and passes the frontmatter, so there is no second parse here.
|
|
37
|
+
*/
|
|
38
|
+
export function entryIdentity(descriptor, path, frontmatter) {
|
|
39
|
+
const id = entryId(path);
|
|
40
|
+
const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
|
|
41
|
+
const date = asDate(frontmatter.date);
|
|
42
|
+
return { id, slug, date, permalink: permalink(descriptor, { id, slug, date }) };
|
|
43
|
+
}
|
package/dist/content/ids.d.ts
CHANGED
|
@@ -35,4 +35,3 @@ export declare function composeDatedId(date: string, slug: string, datePrefix: D
|
|
|
35
35
|
* newSlug. The caller validates newSlug with isValidId first.
|
|
36
36
|
*/
|
|
37
37
|
export declare function renameId(oldId: string, newSlug: string, datePrefix: DatePrefix | null): string;
|
|
38
|
-
//# sourceMappingURL=ids.d.ts.map
|
package/dist/content/links.d.ts
CHANGED
|
@@ -18,4 +18,3 @@ export declare function escapeLinkText(text: string): string;
|
|
|
18
18
|
/** The cairn links a markdown body points at, in first-occurrence order, deduped by concept/id.
|
|
19
19
|
* Parses the body as mdast, so a token inside a code span or fence is never matched. */
|
|
20
20
|
export declare function extractCairnLinks(body: string): CairnRef[];
|
|
21
|
-
//# sourceMappingURL=links.d.ts.map
|
|
@@ -24,7 +24,9 @@ export interface LinkTarget {
|
|
|
24
24
|
date?: string;
|
|
25
25
|
draft: boolean;
|
|
26
26
|
}
|
|
27
|
-
/** Build one manifest entry from a content file. Drafts are included and flagged.
|
|
27
|
+
/** Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
|
|
28
|
+
* permalink come from entryIdentity, the same source content-index uses, so a cairn: link resolves to
|
|
29
|
+
* one URL whether the admin preview reads the manifest or the public build reads the content index. */
|
|
28
30
|
export declare function manifestEntryFromFile(descriptor: ConceptDescriptor, file: {
|
|
29
31
|
path: string;
|
|
30
32
|
raw: string;
|
|
@@ -83,4 +85,3 @@ export declare function manifestLinkResolver(targets: {
|
|
|
83
85
|
id: string;
|
|
84
86
|
permalink: string;
|
|
85
87
|
}[]): LinkResolve;
|
|
86
|
-
//# sourceMappingURL=manifest.d.ts.map
|
package/dist/content/manifest.js
CHANGED
|
@@ -3,41 +3,21 @@
|
|
|
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 } from './links.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
/** Mirror content-index's frontmatter coercion: a present non-empty string, else undefined. */
|
|
15
|
-
function asString(value) {
|
|
16
|
-
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
17
|
-
}
|
|
18
|
-
/** Mirror content-index's date coercion: an unquoted YAML date is a JS Date, a string is sliced. */
|
|
19
|
-
function asDate(value) {
|
|
20
|
-
if (value instanceof Date)
|
|
21
|
-
return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
|
|
22
|
-
if (typeof value === 'string')
|
|
23
|
-
return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
/** Build one manifest entry from a content file. Drafts are included and flagged. */
|
|
9
|
+
/** Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
|
|
10
|
+
* permalink come from entryIdentity, the same source content-index uses, so a cairn: link resolves to
|
|
11
|
+
* one URL whether the admin preview reads the manifest or the public build reads the content index. */
|
|
27
12
|
export function manifestEntryFromFile(descriptor, file) {
|
|
28
|
-
const id = idFromFilename(basename(file.path));
|
|
29
|
-
// Use the same slug rule content-index uses, so the manifest's permalink for an entry always
|
|
30
|
-
// equals content-index's permalink for it. A cairn link must resolve to one URL whether the
|
|
31
|
-
// admin preview reads the manifest or the public build reads the content index.
|
|
32
|
-
const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
|
|
33
13
|
const { frontmatter, body } = parseMarkdown(file.raw);
|
|
34
|
-
const date =
|
|
14
|
+
const { id, date, permalink } = entryIdentity(descriptor, file.path, frontmatter);
|
|
35
15
|
return {
|
|
36
16
|
id,
|
|
37
17
|
concept: descriptor.id,
|
|
38
18
|
title: asString(frontmatter.title) ?? id,
|
|
39
19
|
date,
|
|
40
|
-
permalink
|
|
20
|
+
permalink,
|
|
41
21
|
draft: frontmatter.draft === true,
|
|
42
22
|
links: extractCairnLinks(body),
|
|
43
23
|
};
|
package/dist/content/schema.d.ts
CHANGED
|
@@ -72,4 +72,3 @@ export interface DefineFieldsOptions<F extends readonly FrontmatterField[]> {
|
|
|
72
72
|
/** Declare a concept's fields once. Returns the schema's faces derived from that one declaration. */
|
|
73
73
|
export declare function defineFields<const F extends readonly FrontmatterField[]>(fields: F, options?: DefineFieldsOptions<F>): ConceptSchema<F>;
|
|
74
74
|
export {};
|
|
75
|
-
//# sourceMappingURL=schema.d.ts.map
|
package/dist/content/types.d.ts
CHANGED
|
@@ -15,4 +15,3 @@ import type { FrontmatterField, ValidationResult } from './types.js';
|
|
|
15
15
|
* `Date` to `YYYY-MM-DD` so a valid parsed date is not mistaken for an empty one.
|
|
16
16
|
*/
|
|
17
17
|
export declare function validateFields(fields: FrontmatterField[], frontmatter: Record<string, unknown>): ValidationResult;
|
|
18
|
-
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -65,4 +65,3 @@ export interface ContentIndex<F = Record<string, unknown>> {
|
|
|
65
65
|
export declare function fromGlob(record: Record<string, string>): RawFile[];
|
|
66
66
|
/** Build a concept's index from its raw files and normalized descriptor. */
|
|
67
67
|
export declare function createContentIndex<F = Record<string, unknown>>(files: RawFile[], descriptor: ConceptDescriptor): ContentIndex<F>;
|
|
68
|
-
//# sourceMappingURL=content-index.d.ts.map
|
|
@@ -3,47 +3,30 @@
|
|
|
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
|
/** Map a Vite eager `?raw` glob record (`{ path: raw }`) to `RawFile[]`. */
|
|
10
9
|
export function fromGlob(record) {
|
|
11
10
|
return Object.entries(record).map(([path, raw]) => ({ path, raw }));
|
|
12
11
|
}
|
|
13
|
-
function basename(path) {
|
|
14
|
-
const slash = path.lastIndexOf('/');
|
|
15
|
-
return slash >= 0 ? path.slice(slash + 1) : path;
|
|
16
|
-
}
|
|
17
|
-
function asString(value) {
|
|
18
|
-
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
19
|
-
}
|
|
20
|
-
function asDate(value) {
|
|
21
|
-
if (value instanceof Date)
|
|
22
|
-
return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
|
|
23
|
-
if (typeof value === 'string')
|
|
24
|
-
return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
function asTags(value) {
|
|
28
|
-
return Array.isArray(value) ? value.map(String) : [];
|
|
29
|
-
}
|
|
30
12
|
/** Build a concept's index from its raw files and normalized descriptor. */
|
|
31
13
|
export function createContentIndex(files, descriptor) {
|
|
32
14
|
const problems = [];
|
|
33
15
|
const entries = [];
|
|
34
16
|
for (const file of files) {
|
|
35
|
-
const id = idFromFilename(basename(file.path));
|
|
36
|
-
const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
|
|
37
17
|
const { frontmatter: raw, body } = parseMarkdown(file.raw);
|
|
38
|
-
const
|
|
18
|
+
const id = entryId(file.path);
|
|
39
19
|
const draft = raw.draft === true;
|
|
40
|
-
// Validate
|
|
41
|
-
//
|
|
20
|
+
// Validate before resolving the permalink. A date-token permalink throws on an entry with no
|
|
21
|
+
// valid date; the validate gate records that as a content problem rather than aborting the whole
|
|
22
|
+
// index build, so one bad entry degrades to a skip, not a crash. A failure is also excluded from
|
|
23
|
+
// the typed read, so every readable entry's frontmatter is the validator's normalized output.
|
|
42
24
|
const result = descriptor.validate(raw, body);
|
|
43
25
|
if (!result.ok) {
|
|
44
26
|
problems.push({ id, draft, errors: result.errors });
|
|
45
27
|
continue;
|
|
46
28
|
}
|
|
29
|
+
const { slug, date, permalink } = entryIdentity(descriptor, file.path, raw);
|
|
47
30
|
const summaryFieldValues = {};
|
|
48
31
|
for (const key of descriptor.summaryFields) {
|
|
49
32
|
if (key in result.data)
|
|
@@ -53,7 +36,7 @@ export function createContentIndex(files, descriptor) {
|
|
|
53
36
|
concept: descriptor.id,
|
|
54
37
|
id,
|
|
55
38
|
slug,
|
|
56
|
-
permalink
|
|
39
|
+
permalink,
|
|
57
40
|
title: asString(raw.title) ?? id,
|
|
58
41
|
date,
|
|
59
42
|
updated: asDate(raw.updated),
|
package/dist/delivery/data.d.ts
CHANGED
|
@@ -21,4 +21,3 @@ export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from '
|
|
|
21
21
|
export { jsonLdScript } from './json-ld.js';
|
|
22
22
|
export { permalink } from '../content/permalink.js';
|
|
23
23
|
export { buildSiteManifest, buildLinkResolver } from './manifest.js';
|
|
24
|
-
//# sourceMappingURL=data.d.ts.map
|
package/dist/delivery/feeds.d.ts
CHANGED
|
@@ -24,4 +24,3 @@ export interface FeedItem {
|
|
|
24
24
|
export declare function buildRssFeed(channel: FeedChannel, items: FeedItem[]): string;
|
|
25
25
|
/** Build a JSON Feed 1.1 document. */
|
|
26
26
|
export declare function buildJsonFeed(channel: FeedChannel, items: FeedItem[]): string;
|
|
27
|
-
//# sourceMappingURL=feeds.d.ts.map
|
package/dist/delivery/head.d.ts
CHANGED
package/dist/delivery/index.d.ts
CHANGED
|
@@ -10,4 +10,3 @@ export declare function buildSiteManifest<A extends CairnAdapter>(adapter: A, co
|
|
|
10
10
|
/** A resolver backed by the site index, for the build. A miss throws, so a dangling cairn: token
|
|
11
11
|
* fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
|
|
12
12
|
export declare function buildLinkResolver(site: SiteIndex): LinkResolve;
|
|
13
|
-
//# sourceMappingURL=manifest.d.ts.map
|