@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.
Files changed (281) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/components/AdminLayout.svelte +152 -229
  3. package/dist/components/CairnAdmin.svelte +13 -42
  4. package/dist/components/CairnLogo.svelte +1 -6
  5. package/dist/components/CairnMediaLibrary.svelte +821 -1210
  6. package/dist/components/CairnTidySettings.svelte +194 -261
  7. package/dist/components/CairnTidySettings.svelte.d.ts +1 -1
  8. package/dist/components/ComponentForm.svelte +110 -185
  9. package/dist/components/ComponentInsertDialog.svelte +163 -283
  10. package/dist/components/ConceptList.svelte +111 -191
  11. package/dist/components/ConfirmPage.svelte +5 -12
  12. package/dist/components/CsrfField.svelte +5 -11
  13. package/dist/components/DeleteDialog.svelte +15 -42
  14. package/dist/components/EditPage.svelte +781 -1205
  15. package/dist/components/EditorToolbar.svelte +108 -170
  16. package/dist/components/HelpHome.svelte +824 -0
  17. package/dist/components/HelpHome.svelte.d.ts +22 -0
  18. package/dist/components/IconPicker.svelte +23 -53
  19. package/dist/components/LinkPicker.svelte +34 -58
  20. package/dist/components/LoginPage.svelte +14 -27
  21. package/dist/components/ManageEditors.svelte +3 -15
  22. package/dist/components/MarkdownEditor.svelte +689 -957
  23. package/dist/components/MarkdownHelpDialog.svelte +12 -27
  24. package/dist/components/MediaCaptureCard.svelte +18 -57
  25. package/dist/components/MediaFigureControl.svelte +32 -71
  26. package/dist/components/MediaHeroField.svelte +210 -329
  27. package/dist/components/MediaInsertPopover.svelte +156 -283
  28. package/dist/components/MediaPicker.svelte +67 -131
  29. package/dist/components/NavTree.svelte +46 -78
  30. package/dist/components/RenameDialog.svelte +16 -43
  31. package/dist/components/ShortcutsDialog.svelte +9 -13
  32. package/dist/components/ShortcutsGrid.svelte +1 -2
  33. package/dist/components/TidyReview.svelte +140 -248
  34. package/dist/components/WebLinkDialog.svelte +19 -40
  35. package/dist/components/cairn-admin.css +4 -0
  36. package/dist/components/client-ingest.d.ts +16 -8
  37. package/dist/components/client-ingest.js +12 -6
  38. package/dist/components/editor-media.js +16 -8
  39. package/dist/components/editor-placeholder.d.ts +4 -2
  40. package/dist/components/editor-tidy.d.ts +24 -12
  41. package/dist/components/editor-tidy.js +8 -4
  42. package/dist/components/index.d.ts +1 -0
  43. package/dist/components/index.js +1 -0
  44. package/dist/components/link-completion.d.ts +12 -6
  45. package/dist/components/link-completion.js +12 -6
  46. package/dist/components/markdown-directives.d.ts +9 -6
  47. package/dist/components/markdown-directives.js +9 -6
  48. package/dist/components/markdown-format.d.ts +7 -2
  49. package/dist/components/markdown-format.js +59 -28
  50. package/dist/components/markdown-reference.d.ts +8 -0
  51. package/dist/components/markdown-reference.js +22 -0
  52. package/dist/components/media-upload-outcome.d.ts +12 -6
  53. package/dist/components/objective-errors.d.ts +8 -4
  54. package/dist/components/objective-errors.js +8 -4
  55. package/dist/components/preview-doc.d.ts +4 -2
  56. package/dist/components/preview-doc.js +4 -2
  57. package/dist/components/spellcheck.d.ts +57 -29
  58. package/dist/components/spellcheck.js +50 -20
  59. package/dist/components/tidy-categorize.d.ts +20 -10
  60. package/dist/components/tidy-categorize.js +16 -8
  61. package/dist/components/tidy-validate.d.ts +12 -6
  62. package/dist/components/tidy-validate.js +20 -10
  63. package/dist/components/topbar-context.d.ts +4 -2
  64. package/dist/content/advisories.d.ts +51 -0
  65. package/dist/content/advisories.js +79 -0
  66. package/dist/content/compose.d.ts +4 -2
  67. package/dist/content/compose.js +1 -0
  68. package/dist/content/excerpt.js +4 -2
  69. package/dist/content/getting-started.d.ts +18 -0
  70. package/dist/content/getting-started.js +12 -0
  71. package/dist/content/links.d.ts +16 -8
  72. package/dist/content/links.js +12 -6
  73. package/dist/content/manifest.d.ts +36 -18
  74. package/dist/content/manifest.js +32 -16
  75. package/dist/content/media-refs.d.ts +4 -2
  76. package/dist/content/media-refs.js +4 -2
  77. package/dist/content/media-rewrite.d.ts +8 -4
  78. package/dist/content/media-rewrite.js +76 -38
  79. package/dist/content/schema.d.ts +20 -10
  80. package/dist/content/site-dictionary.d.ts +4 -2
  81. package/dist/content/site-dictionary.js +8 -4
  82. package/dist/content/types.d.ts +97 -42
  83. package/dist/delivery/CairnHead.svelte +8 -11
  84. package/dist/delivery/content-index.d.ts +16 -8
  85. package/dist/delivery/feeds.js +4 -2
  86. package/dist/delivery/json-ld.d.ts +3 -0
  87. package/dist/delivery/json-ld.js +3 -0
  88. package/dist/delivery/manifest.d.ts +4 -2
  89. package/dist/delivery/manifest.js +4 -2
  90. package/dist/delivery/public-routes.d.ts +12 -6
  91. package/dist/delivery/public-routes.js +4 -2
  92. package/dist/delivery/seo-fields.d.ts +12 -6
  93. package/dist/delivery/seo-fields.js +8 -4
  94. package/dist/delivery/site-indexes.d.ts +4 -2
  95. package/dist/delivery/site-resolver.d.ts +4 -2
  96. package/dist/delivery/site-resolver.js +4 -2
  97. package/dist/doctor/cloudflare-api.d.ts +6 -0
  98. package/dist/doctor/cloudflare-api.js +6 -0
  99. package/dist/doctor/index.d.ts +12 -6
  100. package/dist/doctor/report.d.ts +3 -0
  101. package/dist/doctor/report.js +3 -0
  102. package/dist/doctor/run.d.ts +3 -0
  103. package/dist/doctor/run.js +3 -0
  104. package/dist/doctor/types.d.ts +10 -2
  105. package/dist/doctor/types.js +6 -0
  106. package/dist/doctor/wrangler-config.d.ts +7 -2
  107. package/dist/doctor/wrangler-config.js +3 -0
  108. package/dist/email.d.ts +4 -2
  109. package/dist/env.d.ts +0 -3
  110. package/dist/env.js +0 -3
  111. package/dist/github/branches.d.ts +4 -2
  112. package/dist/github/branches.js +4 -2
  113. package/dist/github/signing.d.ts +1 -1
  114. package/dist/github/signing.js +2 -2
  115. package/dist/log/events.d.ts +1 -1
  116. package/dist/media/bulk-delete-plan.d.ts +8 -4
  117. package/dist/media/config.d.ts +12 -6
  118. package/dist/media/config.js +16 -8
  119. package/dist/media/delivery-bucket.d.ts +4 -2
  120. package/dist/media/library-entry.d.ts +4 -2
  121. package/dist/media/library-entry.js +4 -2
  122. package/dist/media/manifest.d.ts +29 -15
  123. package/dist/media/manifest.js +29 -16
  124. package/dist/media/naming.d.ts +12 -6
  125. package/dist/media/naming.js +24 -12
  126. package/dist/media/orphan-scan.d.ts +4 -2
  127. package/dist/media/reconcile.d.ts +21 -11
  128. package/dist/media/reconcile.js +12 -6
  129. package/dist/media/reference.d.ts +8 -4
  130. package/dist/media/reference.js +12 -6
  131. package/dist/media/rewrite-plan.d.ts +12 -6
  132. package/dist/media/sniff.d.ts +4 -2
  133. package/dist/media/sniff.js +28 -14
  134. package/dist/media/store.d.ts +16 -8
  135. package/dist/media/store.js +4 -2
  136. package/dist/media/transform-url.d.ts +12 -6
  137. package/dist/media/transform-url.js +8 -4
  138. package/dist/media/usage.d.ts +8 -4
  139. package/dist/nav/site-config.d.ts +16 -8
  140. package/dist/render/component-grammar.d.ts +23 -10
  141. package/dist/render/component-grammar.js +19 -8
  142. package/dist/render/component-insert.d.ts +8 -4
  143. package/dist/render/component-insert.js +4 -2
  144. package/dist/render/component-reference.d.ts +4 -2
  145. package/dist/render/component-reference.js +4 -2
  146. package/dist/render/component-validate.d.ts +3 -0
  147. package/dist/render/component-validate.js +3 -0
  148. package/dist/render/glyph.d.ts +4 -2
  149. package/dist/render/glyph.js +4 -2
  150. package/dist/render/pipeline.d.ts +20 -10
  151. package/dist/render/pipeline.js +4 -2
  152. package/dist/render/registry.d.ts +40 -20
  153. package/dist/render/registry.js +16 -8
  154. package/dist/render/rehype-dispatch.d.ts +22 -8
  155. package/dist/render/rehype-dispatch.js +22 -8
  156. package/dist/render/remark-directives.d.ts +3 -0
  157. package/dist/render/remark-directives.js +3 -0
  158. package/dist/render/remark-figure.d.ts +4 -2
  159. package/dist/render/remark-figure.js +4 -2
  160. package/dist/render/resolve-links.d.ts +4 -2
  161. package/dist/render/resolve-links.js +4 -2
  162. package/dist/render/resolve-media.d.ts +16 -8
  163. package/dist/render/resolve-media.js +12 -6
  164. package/dist/sveltekit/admin-dispatch.d.ts +2 -0
  165. package/dist/sveltekit/admin-dispatch.js +9 -3
  166. package/dist/sveltekit/auth-routes.d.ts +3 -0
  167. package/dist/sveltekit/auth-routes.js +3 -0
  168. package/dist/sveltekit/cairn-admin.d.ts +16 -5
  169. package/dist/sveltekit/cairn-admin.js +26 -10
  170. package/dist/sveltekit/content-routes.d.ts +191 -86
  171. package/dist/sveltekit/content-routes.js +295 -107
  172. package/dist/sveltekit/editors-routes.d.ts +3 -0
  173. package/dist/sveltekit/editors-routes.js +3 -0
  174. package/dist/sveltekit/guard.d.ts +4 -2
  175. package/dist/sveltekit/guard.js +4 -2
  176. package/dist/sveltekit/https-required-page.d.ts +1 -1
  177. package/dist/sveltekit/https-required-page.js +1 -1
  178. package/dist/sveltekit/index.d.ts +1 -1
  179. package/dist/sveltekit/media-route.d.ts +1 -2
  180. package/dist/sveltekit/media-route.js +13 -8
  181. package/dist/sveltekit/nav-routes.d.ts +7 -2
  182. package/dist/sveltekit/nav-routes.js +3 -0
  183. package/dist/sveltekit/types.d.ts +4 -2
  184. package/dist/vite/index.d.ts +32 -16
  185. package/dist/vite/index.js +52 -26
  186. package/dist/vite/resolve-root.d.ts +8 -4
  187. package/dist/vite/resolve-root.js +4 -2
  188. package/package.json +8 -2
  189. package/src/lib/components/AdminLayout.svelte +22 -0
  190. package/src/lib/components/CairnAdmin.svelte +3 -0
  191. package/src/lib/components/CairnTidySettings.svelte +2 -2
  192. package/src/lib/components/ComponentForm.svelte +0 -1
  193. package/src/lib/components/EditPage.svelte +133 -41
  194. package/src/lib/components/HelpHome.svelte +850 -0
  195. package/src/lib/components/MarkdownHelpDialog.svelte +4 -15
  196. package/src/lib/components/client-ingest.ts +20 -10
  197. package/src/lib/components/editor-media.ts +20 -10
  198. package/src/lib/components/editor-placeholder.ts +12 -6
  199. package/src/lib/components/editor-tidy.ts +28 -14
  200. package/src/lib/components/index.ts +1 -0
  201. package/src/lib/components/link-completion.ts +12 -6
  202. package/src/lib/components/markdown-directives.ts +13 -8
  203. package/src/lib/components/markdown-format.ts +63 -30
  204. package/src/lib/components/markdown-reference.ts +30 -0
  205. package/src/lib/components/media-upload-outcome.ts +12 -6
  206. package/src/lib/components/objective-errors.ts +16 -8
  207. package/src/lib/components/preview-doc.ts +4 -2
  208. package/src/lib/components/spellcheck.ts +92 -40
  209. package/src/lib/components/tidy-categorize.ts +28 -14
  210. package/src/lib/components/tidy-validate.ts +28 -14
  211. package/src/lib/components/topbar-context.ts +4 -2
  212. package/src/lib/content/advisories.ts +141 -0
  213. package/src/lib/content/compose.ts +5 -2
  214. package/src/lib/content/excerpt.ts +4 -2
  215. package/src/lib/content/getting-started.ts +31 -0
  216. package/src/lib/content/links.ts +16 -8
  217. package/src/lib/content/manifest.ts +36 -18
  218. package/src/lib/content/media-refs.ts +4 -2
  219. package/src/lib/content/media-rewrite.ts +100 -50
  220. package/src/lib/content/schema.ts +20 -10
  221. package/src/lib/content/site-dictionary.ts +8 -4
  222. package/src/lib/content/types.ts +97 -42
  223. package/src/lib/delivery/content-index.ts +16 -8
  224. package/src/lib/delivery/feeds.ts +4 -2
  225. package/src/lib/delivery/json-ld.ts +3 -0
  226. package/src/lib/delivery/manifest.ts +4 -2
  227. package/src/lib/delivery/public-routes.ts +16 -8
  228. package/src/lib/delivery/seo-fields.ts +12 -6
  229. package/src/lib/delivery/site-indexes.ts +4 -2
  230. package/src/lib/delivery/site-resolver.ts +4 -2
  231. package/src/lib/doctor/cloudflare-api.ts +6 -0
  232. package/src/lib/doctor/index.ts +12 -6
  233. package/src/lib/doctor/report.ts +3 -0
  234. package/src/lib/doctor/run.ts +3 -0
  235. package/src/lib/doctor/types.ts +10 -2
  236. package/src/lib/doctor/wrangler-config.ts +7 -2
  237. package/src/lib/email.ts +4 -2
  238. package/src/lib/env.ts +0 -3
  239. package/src/lib/github/branches.ts +4 -2
  240. package/src/lib/github/signing.ts +2 -2
  241. package/src/lib/log/events.ts +1 -0
  242. package/src/lib/media/bulk-delete-plan.ts +8 -4
  243. package/src/lib/media/config.ts +24 -12
  244. package/src/lib/media/delivery-bucket.ts +4 -2
  245. package/src/lib/media/library-entry.ts +4 -2
  246. package/src/lib/media/manifest.ts +33 -18
  247. package/src/lib/media/naming.ts +24 -12
  248. package/src/lib/media/orphan-scan.ts +4 -2
  249. package/src/lib/media/reconcile.ts +21 -11
  250. package/src/lib/media/reference.ts +12 -6
  251. package/src/lib/media/rewrite-plan.ts +12 -6
  252. package/src/lib/media/sniff.ts +28 -14
  253. package/src/lib/media/store.ts +16 -8
  254. package/src/lib/media/transform-url.ts +12 -6
  255. package/src/lib/media/usage.ts +8 -4
  256. package/src/lib/nav/site-config.ts +16 -8
  257. package/src/lib/render/component-grammar.ts +23 -10
  258. package/src/lib/render/component-insert.ts +8 -4
  259. package/src/lib/render/component-reference.ts +4 -2
  260. package/src/lib/render/component-validate.ts +3 -0
  261. package/src/lib/render/glyph.ts +4 -2
  262. package/src/lib/render/pipeline.ts +20 -10
  263. package/src/lib/render/registry.ts +44 -22
  264. package/src/lib/render/rehype-dispatch.ts +22 -8
  265. package/src/lib/render/remark-directives.ts +3 -0
  266. package/src/lib/render/remark-figure.ts +4 -2
  267. package/src/lib/render/resolve-links.ts +4 -2
  268. package/src/lib/render/resolve-media.ts +16 -8
  269. package/src/lib/sveltekit/admin-dispatch.ts +10 -4
  270. package/src/lib/sveltekit/auth-routes.ts +3 -0
  271. package/src/lib/sveltekit/cairn-admin.ts +37 -15
  272. package/src/lib/sveltekit/content-routes.ts +492 -197
  273. package/src/lib/sveltekit/editors-routes.ts +3 -0
  274. package/src/lib/sveltekit/guard.ts +4 -2
  275. package/src/lib/sveltekit/https-required-page.ts +1 -1
  276. package/src/lib/sveltekit/index.ts +3 -0
  277. package/src/lib/sveltekit/media-route.ts +13 -8
  278. package/src/lib/sveltekit/nav-routes.ts +7 -2
  279. package/src/lib/sveltekit/types.ts +4 -2
  280. package/src/lib/vite/index.ts +60 -30
  281. 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
- /** The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
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;
@@ -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,
@@ -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
- /** Reduce markdown to readable plain text: drop fenced code, images, and markup; unwrap inline
5
- * code and links to their text; collapse whitespace. */
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
+ }
@@ -3,20 +3,28 @@ export interface CairnRef {
3
3
  concept: string;
4
4
  id: string;
5
5
  }
6
- /** Resolve a reference to its live permalink. Returns undefined when the target is missing (the
7
- * preview marks it); the build resolver throws instead, so a dangling token fails the build. */
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
- /** Write the `cairn:<concept>/<id>` token for a ref. The inverse of parseCairnToken, so the editor
12
- * link picker and the autocomplete write exactly the form the resolver reads back. */
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
- /** Escape the characters that would break a markdown link's display text: a backslash and the
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
- /** The cairn links a markdown body points at, in first-occurrence order, deduped by concept/id.
19
- * Parses the body as mdast, so a token inside a code span or fence is never matched. */
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
@@ -21,19 +21,25 @@ export function parseCairnToken(href) {
21
21
  return null;
22
22
  return { concept, id };
23
23
  }
24
- /** Write the `cairn:<concept>/<id>` token for a ref. The inverse of parseCairnToken, so the editor
25
- * link picker and the autocomplete write exactly the form the resolver reads back. */
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
- /** Escape the characters that would break a markdown link's display text: a backslash and the
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
- /** The cairn links a markdown body points at, in first-occurrence order, deduped by concept/id.
36
- * Parses the body as mdast, so a token inside a code span or fence is never matched. */
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
- /** The content hashes of the media this entry references (its hero plus its body images). The
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
- /** Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
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
- /** Serialize canonically: entries sorted by concept then id, links sorted and deduped, a fixed key
42
- * order, two-space pretty, and a trailing newline, so the committed file diffs cleanly in a PR. */
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
- /** Parse a committed manifest. Throws on malformed JSON, a wrong version, or a malformed entry, so
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
- /** Compare a built manifest against a committed one, keyed by concept+id (the same identity
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
- /** Throw if the committed manifest drifts from what the corpus says. The canonical serialized form
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
- /** Replace the entry with the same concept and id, or add it. Order does not matter, since
71
- * serializeManifest sorts. This is the save path's incremental patch. */
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
- /** Every entry whose outbound edges point at the target, excluding the target itself. The delete
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
- /** A resolver backed by manifest targets, for the admin preview. A miss returns undefined, so the
87
- * render step marks the link broken rather than throwing. The build resolver throws instead. */
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;
@@ -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
- /** Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
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
- /** Serialize canonically: entries sorted by concept then id, links sorted and deduped, a fixed key
42
- * order, two-space pretty, and a trailing newline, so the committed file diffs cleanly in a PR. */
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
- /** Parse a committed manifest. Throws on malformed JSON, a wrong version, or a malformed entry, so
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
- /** Compare a built manifest against a committed one, keyed by concept+id (the same identity
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
- /** Throw if the committed manifest drifts from what the corpus says. The canonical serialized form
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
- /** Replace the entry with the same concept and id, or add it. Order does not matter, since
185
- * serializeManifest sorts. This is the save path's incremental patch. */
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
- /** Every entry whose outbound edges point at the target, excluding the target itself. The delete
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
- /** A resolver backed by manifest targets, for the admin preview. A miss returns undefined, so the
205
- * render step marks the link broken rather than throwing. The build resolver throws instead. */
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
- /** The content hashes one entry references, in first-occurrence order, deduped by hash. Reads the
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 `![](/x.png)` 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
- /** The content hashes one entry references, in first-occurrence order, deduped by hash. Reads the
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 `![](/x.png)` 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
- /** Which alt bucket a placement falls in: an empty alt always gets filled, a non-empty (custom) alt is
28
- * reported and only overwritten on opt-in, and a decorative hero is never touched. */
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
- /** One placement of the target hash and what the alt-fill does to it: which surface it lives on, its
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;