@glw907/cairn-cms 0.60.0 → 0.62.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/dist/components/AdminLayout.svelte +152 -229
- package/dist/components/CairnAdmin.svelte +13 -42
- package/dist/components/CairnLogo.svelte +1 -6
- package/dist/components/CairnMediaLibrary.svelte +821 -1210
- package/dist/components/CairnTidySettings.svelte +194 -261
- package/dist/components/CairnTidySettings.svelte.d.ts +1 -1
- package/dist/components/ComponentForm.svelte +110 -185
- package/dist/components/ComponentInsertDialog.svelte +163 -283
- package/dist/components/ConceptList.svelte +111 -191
- package/dist/components/ConfirmPage.svelte +5 -12
- package/dist/components/CsrfField.svelte +5 -11
- package/dist/components/DeleteDialog.svelte +15 -42
- package/dist/components/EditPage.svelte +781 -1205
- package/dist/components/EditorToolbar.svelte +108 -170
- package/dist/components/HelpHome.svelte +824 -0
- package/dist/components/HelpHome.svelte.d.ts +22 -0
- package/dist/components/IconPicker.svelte +23 -53
- package/dist/components/LinkPicker.svelte +34 -58
- package/dist/components/LoginPage.svelte +14 -27
- package/dist/components/ManageEditors.svelte +3 -15
- package/dist/components/MarkdownEditor.svelte +689 -957
- package/dist/components/MarkdownHelpDialog.svelte +12 -27
- package/dist/components/MediaCaptureCard.svelte +18 -57
- package/dist/components/MediaFigureControl.svelte +32 -71
- package/dist/components/MediaHeroField.svelte +210 -329
- package/dist/components/MediaInsertPopover.svelte +156 -283
- package/dist/components/MediaPicker.svelte +67 -131
- package/dist/components/NavTree.svelte +46 -78
- package/dist/components/RenameDialog.svelte +16 -43
- package/dist/components/ShortcutsDialog.svelte +9 -13
- package/dist/components/ShortcutsGrid.svelte +1 -2
- package/dist/components/TidyReview.svelte +140 -248
- package/dist/components/WebLinkDialog.svelte +19 -40
- package/dist/components/cairn-admin.css +4 -0
- package/dist/components/client-ingest.d.ts +16 -8
- package/dist/components/client-ingest.js +12 -6
- package/dist/components/editor-media.js +16 -8
- package/dist/components/editor-placeholder.d.ts +4 -2
- package/dist/components/editor-tidy.d.ts +24 -12
- package/dist/components/editor-tidy.js +8 -4
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/link-completion.d.ts +12 -6
- package/dist/components/link-completion.js +12 -6
- package/dist/components/markdown-directives.d.ts +9 -6
- package/dist/components/markdown-directives.js +9 -6
- package/dist/components/markdown-format.d.ts +7 -2
- package/dist/components/markdown-format.js +59 -28
- package/dist/components/markdown-reference.d.ts +8 -0
- package/dist/components/markdown-reference.js +22 -0
- package/dist/components/media-upload-outcome.d.ts +12 -6
- package/dist/components/objective-errors.d.ts +8 -4
- package/dist/components/objective-errors.js +8 -4
- package/dist/components/preview-doc.d.ts +4 -2
- package/dist/components/preview-doc.js +4 -2
- package/dist/components/spellcheck.d.ts +57 -29
- package/dist/components/spellcheck.js +50 -20
- package/dist/components/tidy-categorize.d.ts +20 -10
- package/dist/components/tidy-categorize.js +16 -8
- package/dist/components/tidy-validate.d.ts +12 -6
- package/dist/components/tidy-validate.js +20 -10
- package/dist/components/topbar-context.d.ts +4 -2
- package/dist/content/advisories.d.ts +51 -0
- package/dist/content/advisories.js +79 -0
- package/dist/content/compose.d.ts +4 -2
- package/dist/content/compose.js +1 -0
- package/dist/content/excerpt.js +4 -2
- package/dist/content/getting-started.d.ts +18 -0
- package/dist/content/getting-started.js +12 -0
- package/dist/content/links.d.ts +16 -8
- package/dist/content/links.js +12 -6
- package/dist/content/manifest.d.ts +36 -18
- package/dist/content/manifest.js +32 -16
- package/dist/content/media-refs.d.ts +4 -2
- package/dist/content/media-refs.js +4 -2
- package/dist/content/media-rewrite.d.ts +8 -4
- package/dist/content/media-rewrite.js +76 -38
- package/dist/content/schema.d.ts +20 -10
- package/dist/content/site-dictionary.d.ts +4 -2
- package/dist/content/site-dictionary.js +8 -4
- package/dist/content/types.d.ts +97 -42
- package/dist/delivery/CairnHead.svelte +8 -11
- package/dist/delivery/content-index.d.ts +16 -8
- package/dist/delivery/feeds.js +4 -2
- package/dist/delivery/json-ld.d.ts +3 -0
- package/dist/delivery/json-ld.js +3 -0
- package/dist/delivery/manifest.d.ts +4 -2
- package/dist/delivery/manifest.js +4 -2
- package/dist/delivery/public-routes.d.ts +12 -6
- package/dist/delivery/public-routes.js +4 -2
- package/dist/delivery/seo-fields.d.ts +12 -6
- package/dist/delivery/seo-fields.js +8 -4
- package/dist/delivery/site-indexes.d.ts +4 -2
- package/dist/delivery/site-resolver.d.ts +4 -2
- package/dist/delivery/site-resolver.js +4 -2
- package/dist/doctor/cloudflare-api.d.ts +6 -0
- package/dist/doctor/cloudflare-api.js +6 -0
- package/dist/doctor/index.d.ts +12 -6
- package/dist/doctor/report.d.ts +3 -0
- package/dist/doctor/report.js +3 -0
- package/dist/doctor/run.d.ts +3 -0
- package/dist/doctor/run.js +3 -0
- package/dist/doctor/types.d.ts +10 -2
- package/dist/doctor/types.js +6 -0
- package/dist/doctor/wrangler-config.d.ts +7 -2
- package/dist/doctor/wrangler-config.js +3 -0
- package/dist/email.d.ts +4 -2
- package/dist/env.d.ts +0 -3
- package/dist/env.js +0 -3
- package/dist/github/branches.d.ts +4 -2
- package/dist/github/branches.js +4 -2
- package/dist/github/signing.d.ts +1 -1
- package/dist/github/signing.js +2 -2
- package/dist/log/events.d.ts +1 -1
- package/dist/media/bulk-delete-plan.d.ts +8 -4
- package/dist/media/config.d.ts +12 -6
- package/dist/media/config.js +16 -8
- package/dist/media/delivery-bucket.d.ts +4 -2
- package/dist/media/library-entry.d.ts +4 -2
- package/dist/media/library-entry.js +4 -2
- package/dist/media/manifest.d.ts +29 -15
- package/dist/media/manifest.js +29 -16
- package/dist/media/naming.d.ts +12 -6
- package/dist/media/naming.js +24 -12
- package/dist/media/orphan-scan.d.ts +4 -2
- package/dist/media/reconcile.d.ts +21 -11
- package/dist/media/reconcile.js +12 -6
- package/dist/media/reference.d.ts +8 -4
- package/dist/media/reference.js +12 -6
- package/dist/media/rewrite-plan.d.ts +12 -6
- package/dist/media/sniff.d.ts +4 -2
- package/dist/media/sniff.js +28 -14
- package/dist/media/store.d.ts +16 -8
- package/dist/media/store.js +4 -2
- package/dist/media/transform-url.d.ts +12 -6
- package/dist/media/transform-url.js +8 -4
- package/dist/media/usage.d.ts +8 -4
- package/dist/nav/site-config.d.ts +16 -8
- package/dist/render/component-grammar.d.ts +23 -10
- package/dist/render/component-grammar.js +19 -8
- package/dist/render/component-insert.d.ts +8 -4
- package/dist/render/component-insert.js +4 -2
- package/dist/render/component-reference.d.ts +4 -2
- package/dist/render/component-reference.js +4 -2
- package/dist/render/component-validate.d.ts +3 -0
- package/dist/render/component-validate.js +3 -0
- package/dist/render/glyph.d.ts +4 -2
- package/dist/render/glyph.js +4 -2
- package/dist/render/pipeline.d.ts +20 -10
- package/dist/render/pipeline.js +4 -2
- package/dist/render/registry.d.ts +40 -20
- package/dist/render/registry.js +16 -8
- package/dist/render/rehype-dispatch.d.ts +22 -8
- package/dist/render/rehype-dispatch.js +22 -8
- package/dist/render/remark-directives.d.ts +3 -0
- package/dist/render/remark-directives.js +3 -0
- package/dist/render/remark-figure.d.ts +4 -2
- package/dist/render/remark-figure.js +4 -2
- package/dist/render/resolve-links.d.ts +4 -2
- package/dist/render/resolve-links.js +4 -2
- package/dist/render/resolve-media.d.ts +16 -8
- package/dist/render/resolve-media.js +12 -6
- package/dist/sveltekit/admin-dispatch.d.ts +2 -0
- package/dist/sveltekit/admin-dispatch.js +9 -3
- package/dist/sveltekit/auth-routes.d.ts +3 -0
- package/dist/sveltekit/auth-routes.js +3 -0
- package/dist/sveltekit/cairn-admin.d.ts +16 -5
- package/dist/sveltekit/cairn-admin.js +26 -10
- package/dist/sveltekit/content-routes.d.ts +191 -86
- package/dist/sveltekit/content-routes.js +295 -107
- package/dist/sveltekit/editors-routes.d.ts +3 -0
- package/dist/sveltekit/editors-routes.js +3 -0
- package/dist/sveltekit/guard.d.ts +4 -2
- package/dist/sveltekit/guard.js +4 -2
- package/dist/sveltekit/https-required-page.d.ts +1 -1
- package/dist/sveltekit/https-required-page.js +1 -1
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/media-route.d.ts +1 -2
- package/dist/sveltekit/media-route.js +13 -8
- package/dist/sveltekit/nav-routes.d.ts +7 -2
- package/dist/sveltekit/nav-routes.js +3 -0
- package/dist/sveltekit/types.d.ts +4 -2
- package/dist/vite/index.d.ts +32 -16
- package/dist/vite/index.js +52 -26
- package/dist/vite/resolve-root.d.ts +8 -4
- package/dist/vite/resolve-root.js +4 -2
- package/package.json +8 -2
- package/src/lib/components/AdminLayout.svelte +22 -0
- package/src/lib/components/CairnAdmin.svelte +3 -0
- package/src/lib/components/CairnTidySettings.svelte +2 -2
- package/src/lib/components/ComponentForm.svelte +0 -1
- package/src/lib/components/EditPage.svelte +133 -41
- package/src/lib/components/HelpHome.svelte +850 -0
- package/src/lib/components/MarkdownHelpDialog.svelte +4 -15
- package/src/lib/components/client-ingest.ts +20 -10
- package/src/lib/components/editor-media.ts +20 -10
- package/src/lib/components/editor-placeholder.ts +12 -6
- package/src/lib/components/editor-tidy.ts +28 -14
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/link-completion.ts +12 -6
- package/src/lib/components/markdown-directives.ts +13 -8
- package/src/lib/components/markdown-format.ts +63 -30
- package/src/lib/components/markdown-reference.ts +30 -0
- package/src/lib/components/media-upload-outcome.ts +12 -6
- package/src/lib/components/objective-errors.ts +16 -8
- package/src/lib/components/preview-doc.ts +4 -2
- package/src/lib/components/spellcheck.ts +92 -40
- package/src/lib/components/tidy-categorize.ts +28 -14
- package/src/lib/components/tidy-validate.ts +28 -14
- package/src/lib/components/topbar-context.ts +4 -2
- package/src/lib/content/advisories.ts +141 -0
- package/src/lib/content/compose.ts +5 -2
- package/src/lib/content/excerpt.ts +4 -2
- package/src/lib/content/getting-started.ts +31 -0
- package/src/lib/content/links.ts +16 -8
- package/src/lib/content/manifest.ts +36 -18
- package/src/lib/content/media-refs.ts +4 -2
- package/src/lib/content/media-rewrite.ts +100 -50
- package/src/lib/content/schema.ts +20 -10
- package/src/lib/content/site-dictionary.ts +8 -4
- package/src/lib/content/types.ts +97 -42
- package/src/lib/delivery/content-index.ts +16 -8
- package/src/lib/delivery/feeds.ts +4 -2
- package/src/lib/delivery/json-ld.ts +3 -0
- package/src/lib/delivery/manifest.ts +4 -2
- package/src/lib/delivery/public-routes.ts +16 -8
- package/src/lib/delivery/seo-fields.ts +12 -6
- package/src/lib/delivery/site-indexes.ts +4 -2
- package/src/lib/delivery/site-resolver.ts +4 -2
- package/src/lib/doctor/cloudflare-api.ts +6 -0
- package/src/lib/doctor/index.ts +12 -6
- package/src/lib/doctor/report.ts +3 -0
- package/src/lib/doctor/run.ts +3 -0
- package/src/lib/doctor/types.ts +10 -2
- package/src/lib/doctor/wrangler-config.ts +7 -2
- package/src/lib/email.ts +4 -2
- package/src/lib/env.ts +0 -3
- package/src/lib/github/branches.ts +4 -2
- package/src/lib/github/signing.ts +2 -2
- package/src/lib/log/events.ts +1 -0
- package/src/lib/media/bulk-delete-plan.ts +8 -4
- package/src/lib/media/config.ts +24 -12
- package/src/lib/media/delivery-bucket.ts +4 -2
- package/src/lib/media/library-entry.ts +4 -2
- package/src/lib/media/manifest.ts +33 -18
- package/src/lib/media/naming.ts +24 -12
- package/src/lib/media/orphan-scan.ts +4 -2
- package/src/lib/media/reconcile.ts +21 -11
- package/src/lib/media/reference.ts +12 -6
- package/src/lib/media/rewrite-plan.ts +12 -6
- package/src/lib/media/sniff.ts +28 -14
- package/src/lib/media/store.ts +16 -8
- package/src/lib/media/transform-url.ts +12 -6
- package/src/lib/media/usage.ts +8 -4
- package/src/lib/nav/site-config.ts +16 -8
- package/src/lib/render/component-grammar.ts +23 -10
- package/src/lib/render/component-insert.ts +8 -4
- package/src/lib/render/component-reference.ts +4 -2
- package/src/lib/render/component-validate.ts +3 -0
- package/src/lib/render/glyph.ts +4 -2
- package/src/lib/render/pipeline.ts +20 -10
- package/src/lib/render/registry.ts +44 -22
- package/src/lib/render/rehype-dispatch.ts +22 -8
- package/src/lib/render/remark-directives.ts +3 -0
- package/src/lib/render/remark-figure.ts +4 -2
- package/src/lib/render/resolve-links.ts +4 -2
- package/src/lib/render/resolve-media.ts +16 -8
- package/src/lib/sveltekit/admin-dispatch.ts +10 -4
- package/src/lib/sveltekit/auth-routes.ts +3 -0
- package/src/lib/sveltekit/cairn-admin.ts +37 -15
- package/src/lib/sveltekit/content-routes.ts +492 -197
- package/src/lib/sveltekit/editors-routes.ts +3 -0
- package/src/lib/sveltekit/guard.ts +4 -2
- package/src/lib/sveltekit/https-required-page.ts +1 -1
- package/src/lib/sveltekit/index.ts +3 -0
- package/src/lib/sveltekit/media-route.ts +13 -8
- package/src/lib/sveltekit/nav-routes.ts +7 -2
- package/src/lib/sveltekit/types.ts +4 -2
- package/src/lib/vite/index.ts +60 -30
- package/src/lib/vite/resolve-root.ts +8 -4
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { listBranches } from '../github/branches.js';
|
|
2
|
+
import { readRaw } from '../github/repo.js';
|
|
3
|
+
import { PENDING_PREFIX, parsePendingBranch } from './pending.js';
|
|
4
|
+
import { findConcept } from './concepts.js';
|
|
5
|
+
import { isValidId, filenameFromId } from './ids.js';
|
|
6
|
+
import { parseMarkdown } from './frontmatter.js';
|
|
7
|
+
import { entryIdentity } from './identity.js';
|
|
8
|
+
/** Append a row under its permalink, creating the bucket on first use. */
|
|
9
|
+
function push(index, permalink, entry) {
|
|
10
|
+
const rows = index.get(permalink);
|
|
11
|
+
if (rows)
|
|
12
|
+
rows.push(entry);
|
|
13
|
+
else
|
|
14
|
+
index.set(permalink, [entry]);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build the permalink-keyed address index over main (from each manifest entry's resolved permalink)
|
|
18
|
+
* plus every open cairn/* branch (resolved from its edited markdown).
|
|
19
|
+
*
|
|
20
|
+
* The build fails open: a branch read that throws and a permalink that cannot resolve are both caught
|
|
21
|
+
* and skipped, so a transient failure degrades to a thinner index, never a thrown editor or a blocked
|
|
22
|
+
* publish. The branches are read in one Promise.all, the way buildUsageIndex reads them.
|
|
23
|
+
*/
|
|
24
|
+
export async function buildAddressIndex(repo, token, concepts, manifest) {
|
|
25
|
+
const index = new Map();
|
|
26
|
+
// The main arm: the manifest already carries each entry's resolved permalink, so this is a pure
|
|
27
|
+
// reverse map with no per-file read.
|
|
28
|
+
for (const entry of manifest.entries) {
|
|
29
|
+
push(index, entry.permalink, {
|
|
30
|
+
concept: entry.concept,
|
|
31
|
+
id: entry.id,
|
|
32
|
+
title: entry.title,
|
|
33
|
+
source: 'main',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// The branch arm: read each open cairn/* branch's one edited file and resolve its permalink. The
|
|
37
|
+
// path is derivable from the branch name, so no tree-listing is needed.
|
|
38
|
+
const names = await listBranches(repo, PENDING_PREFIX, token);
|
|
39
|
+
const perBranch = await Promise.all(names.map(async (name) => {
|
|
40
|
+
// Resolve the branch name with the branch tooling's guard: a malformed name, an id that fails
|
|
41
|
+
// the slug rule, or an unconfigured concept is skipped with no read attempted.
|
|
42
|
+
const ref = parsePendingBranch(name);
|
|
43
|
+
if (!ref || !isValidId(ref.id))
|
|
44
|
+
return null;
|
|
45
|
+
const concept = findConcept(concepts, ref.concept);
|
|
46
|
+
if (!concept)
|
|
47
|
+
return null;
|
|
48
|
+
const path = `${concept.dir}/${filenameFromId(ref.id)}`;
|
|
49
|
+
try {
|
|
50
|
+
const raw = await readRaw({ ...repo, branch: name }, path, token);
|
|
51
|
+
if (raw === null)
|
|
52
|
+
return null; // The file is absent on the branch: nothing to resolve.
|
|
53
|
+
const { frontmatter } = parseMarkdown(raw);
|
|
54
|
+
const fmTitle = frontmatter.title;
|
|
55
|
+
const title = typeof fmTitle === 'string' && fmTitle.trim() ? fmTitle : ref.id;
|
|
56
|
+
// entryIdentity throws for a dated entry with no date; that branch is caught and skipped.
|
|
57
|
+
const { permalink } = entryIdentity(concept, path, frontmatter);
|
|
58
|
+
return { permalink, entry: { concept: concept.id, id: ref.id, title, source: 'branch' } };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// A failed branch read or an unresolvable permalink degrades this one branch, fail open.
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
65
|
+
// Fold the per-branch rows back in, preserving the branch order so the index reads stably.
|
|
66
|
+
for (const row of perBranch) {
|
|
67
|
+
if (row)
|
|
68
|
+
push(index, row.permalink, row.entry);
|
|
69
|
+
}
|
|
70
|
+
return index;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Find the first other entry that already resolves to an address, or null when the address is free
|
|
74
|
+
* or holds only the entry itself. The self entry is identified by its concept and id together.
|
|
75
|
+
*/
|
|
76
|
+
export function addressCollision(index, self, address) {
|
|
77
|
+
const rows = index.get(address) ?? [];
|
|
78
|
+
return rows.find((row) => row.concept !== self.concept || row.id !== self.id) ?? null;
|
|
79
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { CairnAdapter, CairnExtension, CairnRuntime } from './types.js';
|
|
2
2
|
import { type SiteConfig } from '../nav/site-config.js';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
|
|
4
5
|
* always derived from one source and can never be silently dropped. `extensions` fold in after the
|
|
5
|
-
* adapter's concepts.
|
|
6
|
+
* adapter's concepts.
|
|
7
|
+
*/
|
|
6
8
|
export interface ComposeInput {
|
|
7
9
|
adapter: CairnAdapter;
|
|
8
10
|
siteConfig: SiteConfig;
|
package/dist/content/compose.js
CHANGED
|
@@ -28,6 +28,7 @@ export function composeRuntime({ adapter, siteConfig, extensions = [] }) {
|
|
|
28
28
|
concepts: resolveConcepts(content, siteConfig),
|
|
29
29
|
backend: adapter.backend,
|
|
30
30
|
sender: adapter.sender,
|
|
31
|
+
supportContact: adapter.supportContact,
|
|
31
32
|
render: adapter.render,
|
|
32
33
|
manifestPath: adapter.manifestPath ?? 'src/content/.cairn/index.json',
|
|
33
34
|
registry: adapter.registry,
|
package/dist/content/excerpt.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// cairn-cms: excerpt and word count for content summaries (public-delivery design, decision
|
|
2
2
|
// 5). A light markdown strip keeps summaries cheap, so a list card, an og:description, and a
|
|
3
3
|
// summary-mode feed read one derived excerpt without a full render.
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
4
|
+
/**
|
|
5
|
+
* Reduce markdown to readable plain text: drop fenced code, images, and markup; unwrap inline
|
|
6
|
+
* code and links to their text; collapse whitespace.
|
|
7
|
+
*/
|
|
6
8
|
function toPlainText(md) {
|
|
7
9
|
return md
|
|
8
10
|
.replace(/```[\s\S]*?```/g, ' ')
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Manifest } from './manifest.js';
|
|
2
|
+
/** The three getting-started steps, their completion count, and the fixed step total. */
|
|
3
|
+
export interface GettingStarted {
|
|
4
|
+
wrotePost: boolean;
|
|
5
|
+
publishedPost: boolean;
|
|
6
|
+
createdPage: boolean;
|
|
7
|
+
doneCount: number;
|
|
8
|
+
total: 3;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Map the manifest and the pending-branch list to the three getting-started step states. Writing a
|
|
12
|
+
* post (published or pending) completes the first step; publishing one completes the second; a page
|
|
13
|
+
* (published or pending) completes the third.
|
|
14
|
+
*/
|
|
15
|
+
export declare function deriveGettingStarted(manifest: Manifest, pending: {
|
|
16
|
+
concept: string;
|
|
17
|
+
id: string;
|
|
18
|
+
}[]): GettingStarted;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map the manifest and the pending-branch list to the three getting-started step states. Writing a
|
|
3
|
+
* post (published or pending) completes the first step; publishing one completes the second; a page
|
|
4
|
+
* (published or pending) completes the third.
|
|
5
|
+
*/
|
|
6
|
+
export function deriveGettingStarted(manifest, pending) {
|
|
7
|
+
const publishedPost = manifest.entries.some((e) => e.concept === 'posts');
|
|
8
|
+
const wrotePost = publishedPost || pending.some((p) => p.concept === 'posts');
|
|
9
|
+
const createdPage = manifest.entries.some((e) => e.concept === 'pages') || pending.some((p) => p.concept === 'pages');
|
|
10
|
+
const doneCount = Number(wrotePost) + Number(publishedPost) + Number(createdPage);
|
|
11
|
+
return { wrotePost, publishedPost, createdPage, doneCount, total: 3 };
|
|
12
|
+
}
|
package/dist/content/links.d.ts
CHANGED
|
@@ -3,20 +3,28 @@ export interface CairnRef {
|
|
|
3
3
|
concept: string;
|
|
4
4
|
id: string;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a reference to its live permalink. Returns undefined when the target is missing (the
|
|
8
|
+
* preview marks it); the build resolver throws instead, so a dangling token fails the build.
|
|
9
|
+
*/
|
|
8
10
|
export type LinkResolve = (ref: CairnRef) => string | undefined;
|
|
9
11
|
/** Parse a `cairn:<concept>/<id>` href, or null for any other href or a malformed token. */
|
|
10
12
|
export declare function parseCairnToken(href: string): CairnRef | null;
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
13
|
+
/**
|
|
14
|
+
* Write the `cairn:<concept>/<id>` token for a ref. The inverse of parseCairnToken, so the editor
|
|
15
|
+
* link picker and the autocomplete write exactly the form the resolver reads back.
|
|
16
|
+
*/
|
|
13
17
|
export declare function formatCairnToken(ref: CairnRef): string;
|
|
14
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Escape the characters that would break a markdown link's display text: a backslash and the
|
|
15
20
|
* square brackets that delimit the text. Used where a content title becomes link display text,
|
|
16
|
-
* so an unbalanced bracket in a title cannot truncate the generated link.
|
|
21
|
+
* so an unbalanced bracket in a title cannot truncate the generated link.
|
|
22
|
+
*/
|
|
17
23
|
export declare function escapeLinkText(text: string): string;
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
24
|
+
/**
|
|
25
|
+
* The cairn links a markdown body points at, in first-occurrence order, deduped by concept/id.
|
|
26
|
+
* Parses the body as mdast, so a token inside a code span or fence is never matched.
|
|
27
|
+
*/
|
|
20
28
|
export declare function extractCairnLinks(body: string): CairnRef[];
|
|
21
29
|
/**
|
|
22
30
|
* Rewrite every cairn: link whose href is exactly `oldHref` so its href becomes `newHref`, keeping
|
package/dist/content/links.js
CHANGED
|
@@ -21,19 +21,25 @@ export function parseCairnToken(href) {
|
|
|
21
21
|
return null;
|
|
22
22
|
return { concept, id };
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
24
|
+
/**
|
|
25
|
+
* Write the `cairn:<concept>/<id>` token for a ref. The inverse of parseCairnToken, so the editor
|
|
26
|
+
* link picker and the autocomplete write exactly the form the resolver reads back.
|
|
27
|
+
*/
|
|
26
28
|
export function formatCairnToken(ref) {
|
|
27
29
|
return `cairn:${ref.concept}/${ref.id}`;
|
|
28
30
|
}
|
|
29
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Escape the characters that would break a markdown link's display text: a backslash and the
|
|
30
33
|
* square brackets that delimit the text. Used where a content title becomes link display text,
|
|
31
|
-
* so an unbalanced bracket in a title cannot truncate the generated link.
|
|
34
|
+
* so an unbalanced bracket in a title cannot truncate the generated link.
|
|
35
|
+
*/
|
|
32
36
|
export function escapeLinkText(text) {
|
|
33
37
|
return text.replace(/[\\[\]]/g, (ch) => `\\${ch}`);
|
|
34
38
|
}
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
39
|
+
/**
|
|
40
|
+
* The cairn links a markdown body points at, in first-occurrence order, deduped by concept/id.
|
|
41
|
+
* Parses the body as mdast, so a token inside a code span or fence is never matched.
|
|
42
|
+
*/
|
|
37
43
|
export function extractCairnLinks(body) {
|
|
38
44
|
const tree = unified().use(remarkParse).use(remarkGfm).parse(body);
|
|
39
45
|
const seen = new Set();
|
|
@@ -10,9 +10,11 @@ export interface ManifestEntry {
|
|
|
10
10
|
summary?: string;
|
|
11
11
|
draft: boolean;
|
|
12
12
|
links: CairnRef[];
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The content hashes of the media this entry references (its hero plus its body images). The
|
|
14
15
|
* main side of the media where-used index. Additive and optional: an entry with no media omits
|
|
15
|
-
* the key, and a manifest committed before this field still parses (absent reads as no refs).
|
|
16
|
+
* the key, and a manifest committed before this field still parses (absent reads as no refs).
|
|
17
|
+
*/
|
|
16
18
|
mediaRefs?: string[];
|
|
17
19
|
}
|
|
18
20
|
/** The whole corpus as one committed file. `version` guards a future shape migration. */
|
|
@@ -29,22 +31,28 @@ export interface LinkTarget {
|
|
|
29
31
|
date?: string;
|
|
30
32
|
draft: boolean;
|
|
31
33
|
}
|
|
32
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
|
|
33
36
|
* permalink come from entryIdentity, the same source content-index uses, so a cairn: link resolves to
|
|
34
|
-
* one URL whether the admin preview reads the manifest or the public build reads the content index.
|
|
37
|
+
* one URL whether the admin preview reads the manifest or the public build reads the content index.
|
|
38
|
+
*/
|
|
35
39
|
export declare function manifestEntryFromFile(descriptor: ConceptDescriptor, file: {
|
|
36
40
|
path: string;
|
|
37
41
|
raw: string;
|
|
38
42
|
}): ManifestEntry;
|
|
39
43
|
/** An empty manifest, the starting point when no committed file exists yet. */
|
|
40
44
|
export declare function emptyManifest(): Manifest;
|
|
41
|
-
/**
|
|
42
|
-
*
|
|
45
|
+
/**
|
|
46
|
+
* Serialize canonically: entries sorted by concept then id, links sorted and deduped, a fixed key
|
|
47
|
+
* order, two-space pretty, and a trailing newline, so the committed file diffs cleanly in a PR.
|
|
48
|
+
*/
|
|
43
49
|
export declare function serializeManifest(manifest: Manifest): string;
|
|
44
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Parse a committed manifest. Throws on malformed JSON, a wrong version, or a malformed entry, so
|
|
45
52
|
* every reader (the save guard, the delete path, the preview) sees a well-formed graph or a clear
|
|
46
53
|
* error. The build regenerates the manifest, so a real file is always canonical; this guards a
|
|
47
|
-
* hand-edited or truncated one.
|
|
54
|
+
* hand-edited or truncated one.
|
|
55
|
+
*/
|
|
48
56
|
export declare function parseManifest(raw: string): Manifest;
|
|
49
57
|
/** A changed entry and the fields that differ between the built and committed manifests. */
|
|
50
58
|
export interface ManifestEntryDiff {
|
|
@@ -58,17 +66,23 @@ export interface ManifestDiff {
|
|
|
58
66
|
removed: ManifestEntry[];
|
|
59
67
|
changed: ManifestEntryDiff[];
|
|
60
68
|
}
|
|
61
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Compare a built manifest against a committed one, keyed by concept+id (the same identity
|
|
62
71
|
* upsertEntry and removeEntry use). A changed entry names the fields that differ. Pure, so it is
|
|
63
|
-
* unit-tested apart from any build.
|
|
72
|
+
* unit-tested apart from any build.
|
|
73
|
+
*/
|
|
64
74
|
export declare function diffManifests(built: Manifest, committed: Manifest): ManifestDiff;
|
|
65
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Throw if the committed manifest drifts from what the corpus says. The canonical serialized form
|
|
66
77
|
* is the fast-path equality guard, so semantic equality never spuriously fails. On a mismatch the
|
|
67
78
|
* error names the added, removed, and changed entries, so a raw-git content edit that leaves the
|
|
68
|
-
* committed manifest stale fails the build loudly with what drifted.
|
|
79
|
+
* committed manifest stale fails the build loudly with what drifted.
|
|
80
|
+
*/
|
|
69
81
|
export declare function verifyManifest(built: Manifest, committedRaw: string): void;
|
|
70
|
-
/**
|
|
71
|
-
*
|
|
82
|
+
/**
|
|
83
|
+
* Replace the entry with the same concept and id, or add it. Order does not matter, since
|
|
84
|
+
* serializeManifest sorts. This is the save path's incremental patch.
|
|
85
|
+
*/
|
|
72
86
|
export declare function upsertEntry(manifest: Manifest, entry: ManifestEntry): Manifest;
|
|
73
87
|
/** Drop the entry with the given concept and id, if present. The delete path's patch. */
|
|
74
88
|
export declare function removeEntry(manifest: Manifest, concept: string, id: string): Manifest;
|
|
@@ -79,12 +93,16 @@ export interface InboundLink {
|
|
|
79
93
|
title: string;
|
|
80
94
|
permalink: string;
|
|
81
95
|
}
|
|
82
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Every entry whose outbound edges point at the target, excluding the target itself. The delete
|
|
83
98
|
* guard reads this to name "what links here"; the backlinks panel will reuse it. Pure over the
|
|
84
|
-
* manifest, so the request-time delete path and a unit test call it the same way.
|
|
99
|
+
* manifest, so the request-time delete path and a unit test call it the same way.
|
|
100
|
+
*/
|
|
85
101
|
export declare function inboundLinks(manifest: Manifest, concept: string, id: string): InboundLink[];
|
|
86
|
-
/**
|
|
87
|
-
*
|
|
102
|
+
/**
|
|
103
|
+
* A resolver backed by manifest targets, for the admin preview. A miss returns undefined, so the
|
|
104
|
+
* render step marks the link broken rather than throwing. The build resolver throws instead.
|
|
105
|
+
*/
|
|
88
106
|
export declare function manifestLinkResolver(targets: {
|
|
89
107
|
concept: string;
|
|
90
108
|
id: string;
|
package/dist/content/manifest.js
CHANGED
|
@@ -8,9 +8,11 @@ import { deriveExcerpt } from './excerpt.js';
|
|
|
8
8
|
import { entryIdentity, asString } from './identity.js';
|
|
9
9
|
import { extractCairnLinks } from './links.js';
|
|
10
10
|
import { extractMediaRefs } from './media-refs.js';
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
|
|
12
13
|
* permalink come from entryIdentity, the same source content-index uses, so a cairn: link resolves to
|
|
13
|
-
* one URL whether the admin preview reads the manifest or the public build reads the content index.
|
|
14
|
+
* one URL whether the admin preview reads the manifest or the public build reads the content index.
|
|
15
|
+
*/
|
|
14
16
|
export function manifestEntryFromFile(descriptor, file) {
|
|
15
17
|
const { frontmatter, body } = parseMarkdown(file.raw);
|
|
16
18
|
const { id, date, permalink } = entryIdentity(descriptor, file.path, frontmatter);
|
|
@@ -38,8 +40,10 @@ export function emptyManifest() {
|
|
|
38
40
|
function compareRef(a, b) {
|
|
39
41
|
return a.concept.localeCompare(b.concept) || a.id.localeCompare(b.id);
|
|
40
42
|
}
|
|
41
|
-
/**
|
|
42
|
-
*
|
|
43
|
+
/**
|
|
44
|
+
* Serialize canonically: entries sorted by concept then id, links sorted and deduped, a fixed key
|
|
45
|
+
* order, two-space pretty, and a trailing newline, so the committed file diffs cleanly in a PR.
|
|
46
|
+
*/
|
|
43
47
|
export function serializeManifest(manifest) {
|
|
44
48
|
const entries = [...manifest.entries].sort(compareRef).map((e) => ({
|
|
45
49
|
id: e.id,
|
|
@@ -54,10 +58,12 @@ export function serializeManifest(manifest) {
|
|
|
54
58
|
}));
|
|
55
59
|
return `${JSON.stringify({ version: 1, entries }, null, 2)}\n`;
|
|
56
60
|
}
|
|
57
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Parse a committed manifest. Throws on malformed JSON, a wrong version, or a malformed entry, so
|
|
58
63
|
* every reader (the save guard, the delete path, the preview) sees a well-formed graph or a clear
|
|
59
64
|
* error. The build regenerates the manifest, so a real file is always canonical; this guards a
|
|
60
|
-
* hand-edited or truncated one.
|
|
65
|
+
* hand-edited or truncated one.
|
|
66
|
+
*/
|
|
61
67
|
export function parseManifest(raw) {
|
|
62
68
|
const data = JSON.parse(raw);
|
|
63
69
|
if (!data || typeof data !== 'object') {
|
|
@@ -108,9 +114,11 @@ export function parseManifest(raw) {
|
|
|
108
114
|
return { version: 1, entries: obj.entries };
|
|
109
115
|
}
|
|
110
116
|
const keyOf = (e) => `${e.concept}/${e.id}`;
|
|
111
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Compare a built manifest against a committed one, keyed by concept+id (the same identity
|
|
112
119
|
* upsertEntry and removeEntry use). A changed entry names the fields that differ. Pure, so it is
|
|
113
|
-
* unit-tested apart from any build.
|
|
120
|
+
* unit-tested apart from any build.
|
|
121
|
+
*/
|
|
114
122
|
export function diffManifests(built, committed) {
|
|
115
123
|
const builtByKey = new Map(built.entries.map((e) => [keyOf(e), e]));
|
|
116
124
|
const committedByKey = new Map(committed.entries.map((e) => [keyOf(e), e]));
|
|
@@ -141,10 +149,12 @@ function formatDiff(d) {
|
|
|
141
149
|
lines.push(` ~ ${e.concept}/${e.id} (${e.fields.join(', ')})`);
|
|
142
150
|
return lines.join('\n');
|
|
143
151
|
}
|
|
144
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* Throw if the committed manifest drifts from what the corpus says. The canonical serialized form
|
|
145
154
|
* is the fast-path equality guard, so semantic equality never spuriously fails. On a mismatch the
|
|
146
155
|
* error names the added, removed, and changed entries, so a raw-git content edit that leaves the
|
|
147
|
-
* committed manifest stale fails the build loudly with what drifted.
|
|
156
|
+
* committed manifest stale fails the build loudly with what drifted.
|
|
157
|
+
*/
|
|
148
158
|
export function verifyManifest(built, committedRaw) {
|
|
149
159
|
const builtRaw = serializeManifest(built);
|
|
150
160
|
if (committedRaw === builtRaw)
|
|
@@ -181,8 +191,10 @@ export function verifyManifest(built, committedRaw) {
|
|
|
181
191
|
formatDiff(diff) +
|
|
182
192
|
'\nRegenerate it (npm run cairn:manifest) and commit the result.');
|
|
183
193
|
}
|
|
184
|
-
/**
|
|
185
|
-
*
|
|
194
|
+
/**
|
|
195
|
+
* Replace the entry with the same concept and id, or add it. Order does not matter, since
|
|
196
|
+
* serializeManifest sorts. This is the save path's incremental patch.
|
|
197
|
+
*/
|
|
186
198
|
export function upsertEntry(manifest, entry) {
|
|
187
199
|
const entries = manifest.entries.filter((e) => !(e.concept === entry.concept && e.id === entry.id));
|
|
188
200
|
entries.push(entry);
|
|
@@ -192,17 +204,21 @@ export function upsertEntry(manifest, entry) {
|
|
|
192
204
|
export function removeEntry(manifest, concept, id) {
|
|
193
205
|
return { version: 1, entries: manifest.entries.filter((e) => !(e.concept === concept && e.id === id)) };
|
|
194
206
|
}
|
|
195
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Every entry whose outbound edges point at the target, excluding the target itself. The delete
|
|
196
209
|
* guard reads this to name "what links here"; the backlinks panel will reuse it. Pure over the
|
|
197
|
-
* manifest, so the request-time delete path and a unit test call it the same way.
|
|
210
|
+
* manifest, so the request-time delete path and a unit test call it the same way.
|
|
211
|
+
*/
|
|
198
212
|
export function inboundLinks(manifest, concept, id) {
|
|
199
213
|
return manifest.entries
|
|
200
214
|
.filter((e) => !(e.concept === concept && e.id === id))
|
|
201
215
|
.filter((e) => e.links.some((l) => l.concept === concept && l.id === id))
|
|
202
216
|
.map((e) => ({ concept: e.concept, id: e.id, title: e.title, permalink: e.permalink }));
|
|
203
217
|
}
|
|
204
|
-
/**
|
|
205
|
-
*
|
|
218
|
+
/**
|
|
219
|
+
* A resolver backed by manifest targets, for the admin preview. A miss returns undefined, so the
|
|
220
|
+
* render step marks the link broken rather than throwing. The build resolver throws instead.
|
|
221
|
+
*/
|
|
206
222
|
export function manifestLinkResolver(targets) {
|
|
207
223
|
const byKey = new Map(targets.map((t) => [`${t.concept}/${t.id}`, t.permalink]));
|
|
208
224
|
return (ref) => byKey.get(`${ref.concept}/${ref.id}`);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { FrontmatterField } from './types.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The content hashes one entry references, in first-occurrence order, deduped by hash. Reads the
|
|
3
4
|
* frontmatter hero `image.src` for each `image`-typed field plus every body image node. A
|
|
4
5
|
* non-media or malformed token is skipped, never thrown, so a stray `` does not break
|
|
5
6
|
* the manifest build. The body is parsed as mdast, so a `media:` token inside a code span or fence
|
|
6
|
-
* is never matched.
|
|
7
|
+
* is never matched.
|
|
8
|
+
*/
|
|
7
9
|
export declare function extractMediaRefs(frontmatter: Record<string, unknown>, body: string, fields: FrontmatterField[]): string[];
|
|
@@ -18,11 +18,13 @@ import remarkParse from 'remark-parse';
|
|
|
18
18
|
import remarkGfm from 'remark-gfm';
|
|
19
19
|
import { visit } from 'unist-util-visit';
|
|
20
20
|
import { parseMediaToken } from '../media/reference.js';
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* The content hashes one entry references, in first-occurrence order, deduped by hash. Reads the
|
|
22
23
|
* frontmatter hero `image.src` for each `image`-typed field plus every body image node. A
|
|
23
24
|
* non-media or malformed token is skipped, never thrown, so a stray `` does not break
|
|
24
25
|
* the manifest build. The body is parsed as mdast, so a `media:` token inside a code span or fence
|
|
25
|
-
* is never matched.
|
|
26
|
+
* is never matched.
|
|
27
|
+
*/
|
|
26
28
|
export function extractMediaRefs(frontmatter, body, fields) {
|
|
27
29
|
const seen = new Set();
|
|
28
30
|
const hashes = [];
|
|
@@ -24,12 +24,16 @@ export interface RepointResult {
|
|
|
24
24
|
* untouched. Pure and node-safe.
|
|
25
25
|
*/
|
|
26
26
|
export declare function repointMediaRef(markdown: string, oldHash: string, newToken: string): RepointResult;
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
27
|
+
/**
|
|
28
|
+
* Which alt bucket a placement falls in: an empty alt always gets filled, a non-empty (custom) alt is
|
|
29
|
+
* reported and only overwritten on opt-in, and a decorative hero is never touched.
|
|
30
|
+
*/
|
|
29
31
|
export type AltBucket = 'will-fill' | 'customized' | 'decorative-skipped';
|
|
30
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* One placement of the target hash and what the alt-fill does to it: which surface it lives on, its
|
|
31
34
|
* bucket, the existing alt, and the alt after the transform (unchanged for a customized alt left as
|
|
32
|
-
* is and for a decorative hero).
|
|
35
|
+
* is and for a decorative hero).
|
|
36
|
+
*/
|
|
33
37
|
export interface AltPlacement {
|
|
34
38
|
kind: 'body' | 'figure' | 'hero';
|
|
35
39
|
bucket: AltBucket;
|