@glw907/cairn-cms 0.60.1 → 0.62.2

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 (254) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/dist/components/AdminLayout.svelte +22 -0
  3. package/dist/components/CairnAdmin.svelte +3 -0
  4. package/dist/components/CairnTidySettings.svelte +2 -2
  5. package/dist/components/CairnTidySettings.svelte.d.ts +1 -1
  6. package/dist/components/EditPage.svelte +116 -39
  7. package/dist/components/HelpHome.svelte +824 -0
  8. package/dist/components/HelpHome.svelte.d.ts +22 -0
  9. package/dist/components/MarkdownHelpDialog.svelte +4 -15
  10. package/dist/components/client-ingest.d.ts +16 -8
  11. package/dist/components/client-ingest.js +12 -6
  12. package/dist/components/editor-media.js +16 -8
  13. package/dist/components/editor-placeholder.d.ts +4 -2
  14. package/dist/components/editor-tidy.d.ts +24 -12
  15. package/dist/components/editor-tidy.js +8 -4
  16. package/dist/components/index.d.ts +1 -0
  17. package/dist/components/index.js +1 -0
  18. package/dist/components/link-completion.d.ts +12 -6
  19. package/dist/components/link-completion.js +12 -6
  20. package/dist/components/markdown-directives.d.ts +9 -6
  21. package/dist/components/markdown-directives.js +9 -6
  22. package/dist/components/markdown-format.d.ts +7 -2
  23. package/dist/components/markdown-format.js +59 -28
  24. package/dist/components/markdown-reference.d.ts +8 -0
  25. package/dist/components/markdown-reference.js +22 -0
  26. package/dist/components/media-upload-outcome.d.ts +12 -6
  27. package/dist/components/objective-errors.d.ts +8 -4
  28. package/dist/components/objective-errors.js +8 -4
  29. package/dist/components/preview-doc.d.ts +4 -2
  30. package/dist/components/preview-doc.js +4 -2
  31. package/dist/components/spellcheck.d.ts +55 -29
  32. package/dist/components/spellcheck.js +39 -21
  33. package/dist/components/tidy-categorize.d.ts +20 -10
  34. package/dist/components/tidy-categorize.js +16 -8
  35. package/dist/components/tidy-validate.d.ts +12 -6
  36. package/dist/components/tidy-validate.js +20 -10
  37. package/dist/components/topbar-context.d.ts +4 -2
  38. package/dist/content/advisories.d.ts +56 -0
  39. package/dist/content/advisories.js +87 -0
  40. package/dist/content/compose.d.ts +4 -2
  41. package/dist/content/compose.js +1 -0
  42. package/dist/content/excerpt.js +4 -2
  43. package/dist/content/getting-started.d.ts +18 -0
  44. package/dist/content/getting-started.js +12 -0
  45. package/dist/content/links.d.ts +16 -8
  46. package/dist/content/links.js +12 -6
  47. package/dist/content/manifest.d.ts +36 -18
  48. package/dist/content/manifest.js +32 -16
  49. package/dist/content/media-refs.d.ts +4 -2
  50. package/dist/content/media-refs.js +4 -2
  51. package/dist/content/media-rewrite.d.ts +8 -4
  52. package/dist/content/media-rewrite.js +76 -38
  53. package/dist/content/schema.d.ts +20 -10
  54. package/dist/content/site-dictionary.d.ts +4 -2
  55. package/dist/content/site-dictionary.js +8 -4
  56. package/dist/content/types.d.ts +97 -42
  57. package/dist/delivery/content-index.d.ts +16 -8
  58. package/dist/delivery/feeds.js +4 -2
  59. package/dist/delivery/json-ld.d.ts +3 -0
  60. package/dist/delivery/json-ld.js +3 -0
  61. package/dist/delivery/manifest.d.ts +4 -2
  62. package/dist/delivery/manifest.js +4 -2
  63. package/dist/delivery/public-routes.d.ts +12 -6
  64. package/dist/delivery/public-routes.js +4 -2
  65. package/dist/delivery/seo-fields.d.ts +12 -6
  66. package/dist/delivery/seo-fields.js +8 -4
  67. package/dist/delivery/site-indexes.d.ts +4 -2
  68. package/dist/delivery/site-resolver.d.ts +4 -2
  69. package/dist/delivery/site-resolver.js +4 -2
  70. package/dist/doctor/cloudflare-api.d.ts +6 -0
  71. package/dist/doctor/cloudflare-api.js +6 -0
  72. package/dist/doctor/index.d.ts +12 -6
  73. package/dist/doctor/report.d.ts +3 -0
  74. package/dist/doctor/report.js +3 -0
  75. package/dist/doctor/run.d.ts +3 -0
  76. package/dist/doctor/run.js +3 -0
  77. package/dist/doctor/types.d.ts +10 -2
  78. package/dist/doctor/types.js +6 -0
  79. package/dist/doctor/wrangler-config.d.ts +7 -2
  80. package/dist/doctor/wrangler-config.js +3 -0
  81. package/dist/email.d.ts +4 -2
  82. package/dist/env.d.ts +0 -3
  83. package/dist/env.js +0 -3
  84. package/dist/github/branches.d.ts +4 -2
  85. package/dist/github/branches.js +4 -2
  86. package/dist/github/signing.d.ts +1 -1
  87. package/dist/github/signing.js +2 -2
  88. package/dist/log/events.d.ts +1 -1
  89. package/dist/media/bulk-delete-plan.d.ts +8 -4
  90. package/dist/media/config.d.ts +12 -6
  91. package/dist/media/config.js +16 -8
  92. package/dist/media/delivery-bucket.d.ts +4 -2
  93. package/dist/media/library-entry.d.ts +4 -2
  94. package/dist/media/library-entry.js +4 -2
  95. package/dist/media/manifest.d.ts +29 -15
  96. package/dist/media/manifest.js +29 -16
  97. package/dist/media/naming.d.ts +12 -6
  98. package/dist/media/naming.js +24 -12
  99. package/dist/media/orphan-scan.d.ts +4 -2
  100. package/dist/media/reconcile.d.ts +21 -11
  101. package/dist/media/reconcile.js +12 -6
  102. package/dist/media/reference.d.ts +8 -4
  103. package/dist/media/reference.js +12 -6
  104. package/dist/media/rewrite-plan.d.ts +12 -6
  105. package/dist/media/sniff.d.ts +4 -2
  106. package/dist/media/sniff.js +28 -14
  107. package/dist/media/store.d.ts +16 -8
  108. package/dist/media/store.js +4 -2
  109. package/dist/media/transform-url.d.ts +12 -6
  110. package/dist/media/transform-url.js +8 -4
  111. package/dist/media/usage.d.ts +8 -4
  112. package/dist/nav/site-config.d.ts +16 -8
  113. package/dist/render/component-grammar.d.ts +23 -10
  114. package/dist/render/component-grammar.js +19 -8
  115. package/dist/render/component-insert.d.ts +8 -4
  116. package/dist/render/component-insert.js +4 -2
  117. package/dist/render/component-reference.d.ts +4 -2
  118. package/dist/render/component-reference.js +4 -2
  119. package/dist/render/component-validate.d.ts +3 -0
  120. package/dist/render/component-validate.js +3 -0
  121. package/dist/render/glyph.d.ts +4 -2
  122. package/dist/render/glyph.js +4 -2
  123. package/dist/render/pipeline.d.ts +20 -10
  124. package/dist/render/pipeline.js +4 -2
  125. package/dist/render/registry.d.ts +40 -20
  126. package/dist/render/registry.js +16 -8
  127. package/dist/render/rehype-dispatch.d.ts +22 -8
  128. package/dist/render/rehype-dispatch.js +22 -8
  129. package/dist/render/remark-directives.d.ts +3 -0
  130. package/dist/render/remark-directives.js +3 -0
  131. package/dist/render/remark-figure.d.ts +4 -2
  132. package/dist/render/remark-figure.js +4 -2
  133. package/dist/render/resolve-links.d.ts +4 -2
  134. package/dist/render/resolve-links.js +4 -2
  135. package/dist/render/resolve-media.d.ts +16 -8
  136. package/dist/render/resolve-media.js +12 -6
  137. package/dist/sveltekit/admin-dispatch.d.ts +2 -0
  138. package/dist/sveltekit/admin-dispatch.js +9 -3
  139. package/dist/sveltekit/auth-routes.d.ts +3 -0
  140. package/dist/sveltekit/auth-routes.js +3 -0
  141. package/dist/sveltekit/cairn-admin.d.ts +16 -5
  142. package/dist/sveltekit/cairn-admin.js +26 -10
  143. package/dist/sveltekit/content-routes.d.ts +191 -86
  144. package/dist/sveltekit/content-routes.js +297 -107
  145. package/dist/sveltekit/editors-routes.d.ts +3 -0
  146. package/dist/sveltekit/editors-routes.js +3 -0
  147. package/dist/sveltekit/guard.d.ts +4 -2
  148. package/dist/sveltekit/guard.js +4 -2
  149. package/dist/sveltekit/https-required-page.d.ts +1 -1
  150. package/dist/sveltekit/https-required-page.js +1 -1
  151. package/dist/sveltekit/index.d.ts +1 -1
  152. package/dist/sveltekit/media-route.d.ts +1 -2
  153. package/dist/sveltekit/media-route.js +13 -8
  154. package/dist/sveltekit/nav-routes.d.ts +7 -2
  155. package/dist/sveltekit/nav-routes.js +3 -0
  156. package/dist/sveltekit/types.d.ts +4 -2
  157. package/dist/vite/index.d.ts +32 -16
  158. package/dist/vite/index.js +52 -26
  159. package/dist/vite/resolve-root.d.ts +8 -4
  160. package/dist/vite/resolve-root.js +4 -2
  161. package/package.json +7 -1
  162. package/src/lib/components/AdminLayout.svelte +22 -0
  163. package/src/lib/components/CairnAdmin.svelte +3 -0
  164. package/src/lib/components/CairnTidySettings.svelte +2 -2
  165. package/src/lib/components/ComponentForm.svelte +0 -1
  166. package/src/lib/components/EditPage.svelte +133 -41
  167. package/src/lib/components/HelpHome.svelte +850 -0
  168. package/src/lib/components/MarkdownHelpDialog.svelte +4 -15
  169. package/src/lib/components/client-ingest.ts +20 -10
  170. package/src/lib/components/editor-media.ts +20 -10
  171. package/src/lib/components/editor-placeholder.ts +12 -6
  172. package/src/lib/components/editor-tidy.ts +28 -14
  173. package/src/lib/components/index.ts +1 -0
  174. package/src/lib/components/link-completion.ts +12 -6
  175. package/src/lib/components/markdown-directives.ts +13 -8
  176. package/src/lib/components/markdown-format.ts +63 -30
  177. package/src/lib/components/markdown-reference.ts +30 -0
  178. package/src/lib/components/media-upload-outcome.ts +12 -6
  179. package/src/lib/components/objective-errors.ts +16 -8
  180. package/src/lib/components/preview-doc.ts +4 -2
  181. package/src/lib/components/spellcheck.ts +79 -41
  182. package/src/lib/components/tidy-categorize.ts +28 -14
  183. package/src/lib/components/tidy-validate.ts +28 -14
  184. package/src/lib/components/topbar-context.ts +4 -2
  185. package/src/lib/content/advisories.ts +150 -0
  186. package/src/lib/content/compose.ts +5 -2
  187. package/src/lib/content/excerpt.ts +4 -2
  188. package/src/lib/content/getting-started.ts +31 -0
  189. package/src/lib/content/links.ts +16 -8
  190. package/src/lib/content/manifest.ts +36 -18
  191. package/src/lib/content/media-refs.ts +4 -2
  192. package/src/lib/content/media-rewrite.ts +100 -50
  193. package/src/lib/content/schema.ts +20 -10
  194. package/src/lib/content/site-dictionary.ts +8 -4
  195. package/src/lib/content/types.ts +97 -42
  196. package/src/lib/delivery/content-index.ts +16 -8
  197. package/src/lib/delivery/feeds.ts +4 -2
  198. package/src/lib/delivery/json-ld.ts +3 -0
  199. package/src/lib/delivery/manifest.ts +4 -2
  200. package/src/lib/delivery/public-routes.ts +16 -8
  201. package/src/lib/delivery/seo-fields.ts +12 -6
  202. package/src/lib/delivery/site-indexes.ts +4 -2
  203. package/src/lib/delivery/site-resolver.ts +4 -2
  204. package/src/lib/doctor/cloudflare-api.ts +6 -0
  205. package/src/lib/doctor/index.ts +12 -6
  206. package/src/lib/doctor/report.ts +3 -0
  207. package/src/lib/doctor/run.ts +3 -0
  208. package/src/lib/doctor/types.ts +10 -2
  209. package/src/lib/doctor/wrangler-config.ts +7 -2
  210. package/src/lib/email.ts +4 -2
  211. package/src/lib/env.ts +0 -3
  212. package/src/lib/github/branches.ts +4 -2
  213. package/src/lib/github/signing.ts +2 -2
  214. package/src/lib/log/events.ts +1 -0
  215. package/src/lib/media/bulk-delete-plan.ts +8 -4
  216. package/src/lib/media/config.ts +24 -12
  217. package/src/lib/media/delivery-bucket.ts +4 -2
  218. package/src/lib/media/library-entry.ts +4 -2
  219. package/src/lib/media/manifest.ts +33 -18
  220. package/src/lib/media/naming.ts +24 -12
  221. package/src/lib/media/orphan-scan.ts +4 -2
  222. package/src/lib/media/reconcile.ts +21 -11
  223. package/src/lib/media/reference.ts +12 -6
  224. package/src/lib/media/rewrite-plan.ts +12 -6
  225. package/src/lib/media/sniff.ts +28 -14
  226. package/src/lib/media/store.ts +16 -8
  227. package/src/lib/media/transform-url.ts +12 -6
  228. package/src/lib/media/usage.ts +8 -4
  229. package/src/lib/nav/site-config.ts +16 -8
  230. package/src/lib/render/component-grammar.ts +23 -10
  231. package/src/lib/render/component-insert.ts +8 -4
  232. package/src/lib/render/component-reference.ts +4 -2
  233. package/src/lib/render/component-validate.ts +3 -0
  234. package/src/lib/render/glyph.ts +4 -2
  235. package/src/lib/render/pipeline.ts +20 -10
  236. package/src/lib/render/registry.ts +44 -22
  237. package/src/lib/render/rehype-dispatch.ts +22 -8
  238. package/src/lib/render/remark-directives.ts +3 -0
  239. package/src/lib/render/remark-figure.ts +4 -2
  240. package/src/lib/render/resolve-links.ts +4 -2
  241. package/src/lib/render/resolve-media.ts +16 -8
  242. package/src/lib/sveltekit/admin-dispatch.ts +10 -4
  243. package/src/lib/sveltekit/auth-routes.ts +3 -0
  244. package/src/lib/sveltekit/cairn-admin.ts +37 -15
  245. package/src/lib/sveltekit/content-routes.ts +494 -197
  246. package/src/lib/sveltekit/editors-routes.ts +3 -0
  247. package/src/lib/sveltekit/guard.ts +4 -2
  248. package/src/lib/sveltekit/https-required-page.ts +1 -1
  249. package/src/lib/sveltekit/index.ts +3 -0
  250. package/src/lib/sveltekit/media-route.ts +13 -8
  251. package/src/lib/sveltekit/nav-routes.ts +7 -2
  252. package/src/lib/sveltekit/types.ts +4 -2
  253. package/src/lib/vite/index.ts +60 -30
  254. package/src/lib/vite/resolve-root.ts +8 -4
@@ -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;
@@ -23,10 +23,12 @@ import remarkDirective from 'remark-directive';
23
23
  import { visit } from 'unist-util-visit';
24
24
  import { parseMediaToken } from '../media/reference.js';
25
25
  import { escapeLinkText } from './links.js';
26
- /** Drop any span that overlaps a span already kept, in source order. A final safety net so two
26
+ /**
27
+ * Drop any span that overlaps a span already kept, in source order. A final safety net so two
27
28
  * splices can never target the same or overlapping bytes and clobber each other into a corrupt
28
29
  * result, no matter how the locating arms behaved. A pure-insert span (`start === end`) overlaps
29
- * another span only when it sits strictly inside it, so adjacent inserts and edits are kept. */
30
+ * another span only when it sits strictly inside it, so adjacent inserts and edits are kept.
31
+ */
30
32
  function dropOverlappingEdits(edits) {
31
33
  const kept = [];
32
34
  for (const e of edits) {
@@ -36,29 +38,37 @@ function dropOverlappingEdits(edits) {
36
38
  }
37
39
  return kept;
38
40
  }
39
- /** A locating scan for candidate `media:` token substrings. Deliberately broad (it accepts
41
+ /**
42
+ * A locating scan for candidate `media:` token substrings. Deliberately broad (it accepts
40
43
  * uppercase and other out-of-grammar characters) so a malformed token is still found and then
41
44
  * rejected by parseMediaToken, never silently skipped by the locator. The character class stops at
42
45
  * whitespace, a quote, or any YAML or markdown delimiter, so a frontmatter value or an image
43
- * destination ends the candidate. */
46
+ * destination ends the candidate.
47
+ */
44
48
  const MEDIA_TOKEN_SCAN = /media:[A-Za-z0-9._-]+/g;
45
- /** Split a leading frontmatter block off the markdown. `fmBlock` is the `---` fenced block including
49
+ /**
50
+ * Split a leading frontmatter block off the markdown. `fmBlock` is the `---` fenced block including
46
51
  * both fences and the trailing newline (empty when there is none); `body` is everything after it.
47
52
  * The block leads the document, so a frontmatter offset is already absolute and a body offset needs
48
- * `fmBlock.length` added. Shared by every arm so they agree on the boundary. */
53
+ * `fmBlock.length` added. Shared by every arm so they agree on the boundary.
54
+ */
49
55
  function splitFrontmatter(markdown) {
50
56
  const m = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
51
57
  const fmBlock = m ? m[0] : '';
52
58
  return { fmBlock, body: markdown.slice(fmBlock.length) };
53
59
  }
54
- /** Parse a doc with the figure-aware pipeline, so the body arm agrees with what remarkFigure renders
55
- * and can see the enclosing `:::figure` container. Mirrors parseFigureDoc in markdown-format.ts. */
60
+ /**
61
+ * Parse a doc with the figure-aware pipeline, so the body arm agrees with what remarkFigure renders
62
+ * and can see the enclosing `:::figure` container. Mirrors parseFigureDoc in markdown-format.ts.
63
+ */
56
64
  function parseFigureDoc(doc) {
57
65
  return unified().use(remarkParse).use(remarkGfm).use(remarkDirective).parse(doc);
58
66
  }
59
- /** Whether `target` sits inside a `figure`-named container directive. Walks the tree to find the
67
+ /**
68
+ * Whether `target` sits inside a `figure`-named container directive. Walks the tree to find the
60
69
  * ancestor, since unist-util-visit's per-call ancestors are not retained across the traversal.
61
- * Mirrors enclosingFigure in markdown-format.ts, reduced to a boolean. */
70
+ * Mirrors enclosingFigure in markdown-format.ts, reduced to a boolean.
71
+ */
62
72
  function inFigure(tree, target) {
63
73
  let found = false;
64
74
  visit(tree, 'containerDirective', (dir) => {
@@ -71,8 +81,10 @@ function inFigure(tree, target) {
71
81
  });
72
82
  return found;
73
83
  }
74
- /** Split fmBlock into lines once, so the locator helpers walk a shared structure instead of
75
- * re-scanning the block per call. */
84
+ /**
85
+ * Split fmBlock into lines once, so the locator helpers walk a shared structure instead of
86
+ * re-scanning the block per call.
87
+ */
76
88
  function fmLines(fmBlock) {
77
89
  const lines = [];
78
90
  let pos = 0;
@@ -86,12 +98,14 @@ function fmLines(fmBlock) {
86
98
  }
87
99
  return lines;
88
100
  }
89
- /** The inclusive line-index range `[lo, hi]` of the block-style mapping a top-level key opens: the
101
+ /**
102
+ * The inclusive line-index range `[lo, hi]` of the block-style mapping a top-level key opens: the
90
103
  * line `^<key>:` at indent 0 through the last line before the next top-level key (or the document
91
104
  * end). A flow-style value (`key: { ... }` all on one line) yields a single-line range. Returns null
92
105
  * when the key has no top-level line, which a malformed or non-canonical block can cause. Scoping the
93
106
  * per-key search to this range is what lets two image fields that share one hash, or an image field
94
- * whose hash also appears in a sibling text value, resolve to distinct, correct spans. */
107
+ * whose hash also appears in a sibling text value, resolve to distinct, correct spans.
108
+ */
95
109
  function frontmatterKeyRange(lines, fmBlock, key) {
96
110
  const opener = new RegExp(`^${escapeForRegExp(key)}:`);
97
111
  const topLevelKey = /^[^\s#][^:]*:/;
@@ -118,10 +132,12 @@ function frontmatterKeyRange(lines, fmBlock, key) {
118
132
  }
119
133
  return [lo, hi];
120
134
  }
121
- /** Find the block-style `src:` line within `[lo, hi]` whose value token parses to `hash`. The token
135
+ /**
136
+ * Find the block-style `src:` line within `[lo, hi]` whose value token parses to `hash`. The token
122
137
  * is located by the broad scan and validated through parseMediaToken (matching on hash), so a
123
138
  * malformed token is found then rejected. Returns null for a flow-style value (no own `src:` line),
124
- * which leaves that shape unanchorable rather than splicing a guessed span. */
139
+ * which leaves that shape unanchorable rather than splicing a guessed span.
140
+ */
125
141
  function findSrcLineInRange(lines, fmBlock, range, hash) {
126
142
  const srcKeyRe = /^(\s*)src:[ \t]?/;
127
143
  for (let i = range[0]; i <= range[1]; i += 1) {
@@ -149,11 +165,13 @@ function findSrcLineInRange(lines, fmBlock, range, hash) {
149
165
  }
150
166
  return null;
151
167
  }
152
- /** The image-like top-level frontmatter keys whose `src` parses to `hash`, in source order. A key is
168
+ /**
169
+ * The image-like top-level frontmatter keys whose `src` parses to `hash`, in source order. A key is
153
170
  * image-like when its value is an object carrying a string `src`; this is the same shape
154
171
  * extractMediaRefs reads, so a token in a plain-text value (a `title:`/`note:`) is never treated as a
155
172
  * reference. The bucket-classifying data comes from gray-matter (which handles every quoting form);
156
- * the byte edit is located structurally by the caller, keyed back to this key name. */
173
+ * the byte edit is located structurally by the caller, keyed back to this key name.
174
+ */
157
175
  function imageFieldKeys(data, hash) {
158
176
  const out = [];
159
177
  for (const [key, value] of Object.entries(data)) {
@@ -169,11 +187,13 @@ function imageFieldKeys(data, hash) {
169
187
  }
170
188
  return out;
171
189
  }
172
- /** Collect hero src-token edits inside the frontmatter block. Only an image-field `src:` line is
190
+ /**
191
+ * Collect hero src-token edits inside the frontmatter block. Only an image-field `src:` line is
173
192
  * rewritten: the structure is read via gray-matter (image-like keys), and each key's `src:` line is
174
193
  * located structurally within that key's block. A `media:` token sitting in a plain-text value (a
175
194
  * `title:` or `description:`) is on no `src:` line, so it is left untouched, keeping the byte-exact
176
- * contract and agreeing with extractMediaRefs. A flow-style hero has no `src:` line and is skipped. */
195
+ * contract and agreeing with extractMediaRefs. A flow-style hero has no `src:` line and is skipped.
196
+ */
177
197
  function frontmatterEdits(markdown, fmBlock, oldHash) {
178
198
  if (fmBlock === '')
179
199
  return [];
@@ -191,10 +211,12 @@ function frontmatterEdits(markdown, fmBlock, oldHash) {
191
211
  }
192
212
  return edits;
193
213
  }
194
- /** Locate the exact `media:` token substring inside one image node's source span. The destination
214
+ /**
215
+ * Locate the exact `media:` token substring inside one image node's source span. The destination
195
216
  * begins at the `](` that follows the alt text, so the search starts there to avoid a false match on
196
217
  * a `media:`-like string inside the alt. Returns null when the token cannot be located, which leaves
197
- * the image untouched rather than splicing a guessed range. */
218
+ * the image untouched rather than splicing a guessed range.
219
+ */
198
220
  function locateImageToken(span, url) {
199
221
  const destStart = span.indexOf('](');
200
222
  const from = destStart === -1 ? 0 : destStart + 2;
@@ -203,9 +225,11 @@ function locateImageToken(span, url) {
203
225
  return null;
204
226
  return { start: at, end: at + url.length };
205
227
  }
206
- /** Find every body image whose url parses to `hash`, in source order, with absolute offsets. Parses
228
+ /**
229
+ * Find every body image whose url parses to `hash`, in source order, with absolute offsets. Parses
207
230
  * with the figure-aware pipeline, so a `media:` token inside a code span or fence is not an image
208
- * node and is correctly skipped, matching extractMediaRefs. */
231
+ * node and is correctly skipped, matching extractMediaRefs.
232
+ */
209
233
  function matchedBodyImages(body, blockLength, hash) {
210
234
  const tree = parseFigureDoc(body);
211
235
  const hits = [];
@@ -226,9 +250,11 @@ function matchedBodyImages(body, blockLength, hash) {
226
250
  });
227
251
  return hits;
228
252
  }
229
- /** Collect body edits over the body slice. Each matching image is located within its own source span
253
+ /**
254
+ * Collect body edits over the body slice. Each matching image is located within its own source span
230
255
  * and recorded with an absolute offset. The kind is 'figure' when the image is inside a `:::figure`,
231
- * else 'body'. */
256
+ * else 'body'.
257
+ */
232
258
  function bodyEdits(body, blockLength, oldHash) {
233
259
  const edits = [];
234
260
  for (const hit of matchedBodyImages(body, blockLength, oldHash)) {
@@ -276,14 +302,18 @@ export function repointMediaRef(markdown, oldHash, newToken) {
276
302
  }
277
303
  return { markdown: out, placements };
278
304
  }
279
- /** Classify an existing alt into its non-decorative bucket: an empty (or whitespace-only) alt is
305
+ /**
306
+ * Classify an existing alt into its non-decorative bucket: an empty (or whitespace-only) alt is
280
307
  * filled, a non-empty alt is a custom alt the caller may opt in to overwrite. Mirrors the empty-alt
281
- * test findMediaImagesNeedingAlt uses. */
308
+ * test findMediaImagesNeedingAlt uses.
309
+ */
282
310
  function classifyAlt(existing) {
283
311
  return existing.trim() === '' ? 'will-fill' : 'customized';
284
312
  }
285
- /** Whether a bucket plus the overwrite choice means the alt text is actually rewritten. A will-fill
286
- * always writes; a customized alt writes only on opt-in; a decorative hero never writes. */
313
+ /**
314
+ * Whether a bucket plus the overwrite choice means the alt text is actually rewritten. A will-fill
315
+ * always writes; a customized alt writes only on opt-in; a decorative hero never writes.
316
+ */
287
317
  function altIsEdited(bucket, overwrite) {
288
318
  if (bucket === 'will-fill')
289
319
  return true;
@@ -291,10 +321,12 @@ function altIsEdited(bucket, overwrite) {
291
321
  return overwrite;
292
322
  return false;
293
323
  }
294
- /** Collect the body and figure alt edits over the body slice. The alt source span sits between `![`
324
+ /**
325
+ * Collect the body and figure alt edits over the body slice. The alt source span sits between `![`
295
326
  * and the `](` inside the image node's span, so the new alt (escaped the way insertImage escapes it,
296
327
  * so a `]` cannot break the syntax) is spliced there. The existing alt is the parser's already
297
- * unescaped `node.alt`. A body image has no decorative slot, so an empty alt is always will-fill. */
328
+ * unescaped `node.alt`. A body image has no decorative slot, so an empty alt is always will-fill.
329
+ */
298
330
  function bodyAltEdits(body, blockLength, hash, defaultAlt, overwrite) {
299
331
  const edits = [];
300
332
  for (const hit of matchedBodyImages(body, blockLength, hash)) {
@@ -333,17 +365,21 @@ function bodyAltEdits(body, blockLength, hash, defaultAlt, overwrite) {
333
365
  }
334
366
  return edits;
335
367
  }
336
- /** Escape a literal string for safe interpolation into a RegExp source. A key name or an indent is
337
- * matched literally, so its characters must not act as metacharacters. */
368
+ /**
369
+ * Escape a literal string for safe interpolation into a RegExp source. A key name or an indent is
370
+ * matched literally, so its characters must not act as metacharacters.
371
+ */
338
372
  function escapeForRegExp(literal) {
339
373
  return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
340
374
  }
341
- /** Find a sibling key line (`alt:` or `decorative:`) at exactly `indent` within the inclusive
375
+ /**
376
+ * Find a sibling key line (`alt:` or `decorative:`) at exactly `indent` within the inclusive
342
377
  * line-index range `[lo, hi]` of one mapping. The range is the mapping's own block, so the search
343
378
  * spans the whole mapping rather than a same-indent contiguous run: a blank line or a deeper-nested
344
379
  * child between `src:` and `alt:` no longer hides the existing key (which would otherwise insert a
345
380
  * duplicate key and break the YAML). Returns the key line's value span (after the key and its space,
346
- * to end of line) or null when the mapping has no such key at that indent. */
381
+ * to end of line) or null when the mapping has no such key at that indent.
382
+ */
347
383
  function findSiblingKeyValue(lines, fmBlock, range, indent, key) {
348
384
  const keyRe = new RegExp(`^${escapeForRegExp(indent)}${escapeForRegExp(key)}:[ \\t]?`);
349
385
  for (let i = range[0]; i <= range[1]; i += 1) {
@@ -354,7 +390,8 @@ function findSiblingKeyValue(lines, fmBlock, range, indent, key) {
354
390
  }
355
391
  return null;
356
392
  }
357
- /** Collect the hero alt edits inside the frontmatter block. The image-field objects (and their
393
+ /**
394
+ * Collect the hero alt edits inside the frontmatter block. The image-field objects (and their
358
395
  * decorative and alt values) are read via gray-matter to classify the bucket robustly across quoting
359
396
  * forms; the byte edit is then located structurally, scoped to each field's own mapping block, keyed
360
397
  * back by the top-level field name. Iterating the fields in source order keeps the hero placements in
@@ -363,7 +400,8 @@ function findSiblingKeyValue(lines, fmBlock, range, indent, key) {
363
400
  * blank line or a nested child) has its value replaced; an absent one is inserted right after the
364
401
  * `src:` line at the same indent. The new value is a JSON-quoted scalar, valid YAML that handles a
365
402
  * colon, a quote, or an empty string. A flow-style hero (`image: { ... }`, no own `src:` line) is
366
- * unanchorable, so it is reported from the gray-matter read but never spliced. */
403
+ * unanchorable, so it is reported from the gray-matter read but never spliced.
404
+ */
367
405
  function heroAltEdits(markdown, fmBlock, hash, defaultAlt, overwrite) {
368
406
  if (fmBlock === '')
369
407
  return [];
@@ -4,8 +4,10 @@ export interface StandardInput {
4
4
  frontmatter: Record<string, unknown>;
5
5
  body: string;
6
6
  }
7
- /** A minimal local copy of the Standard Schema v1 interface (https://standardschema.dev), so the
8
- * schema is a drop-in where the ecosystem accepts a validator, with no runtime dependency. */
7
+ /**
8
+ * A minimal local copy of the Standard Schema v1 interface (https://standardschema.dev), so the
9
+ * schema is a drop-in where the ecosystem accepts a validator, with no runtime dependency.
10
+ */
9
11
  export interface StandardSchemaV1<Input = unknown, Output = Input> {
10
12
  readonly '~standard': {
11
13
  readonly version: 1;
@@ -26,9 +28,11 @@ type StandardResult<Output> = {
26
28
  readonly path?: ReadonlyArray<PropertyKey>;
27
29
  }>;
28
30
  };
29
- /** Map one field descriptor to the TS type of its normalized value. text, textarea, and date
31
+ /**
32
+ * Map one field descriptor to the TS type of its normalized value. text, textarea, and date
30
33
  * normalize to a string; a closed-vocabulary `tags` field to the option-union array; an `image`
31
- * field to its nested object. */
34
+ * field to its nested object.
35
+ */
32
36
  type FieldValue<K extends FrontmatterField> = K extends {
33
37
  type: 'boolean';
34
38
  } ? boolean : K extends {
@@ -43,8 +47,10 @@ type FieldValue<K extends FrontmatterField> = K extends {
43
47
  type Prettify<T> = {
44
48
  [K in keyof T]: T[K];
45
49
  } & {};
46
- /** The normalized frontmatter type inferred from a field tuple. A field declared
47
- * `required: true` is a required key; every other field is optional. */
50
+ /**
51
+ * The normalized frontmatter type inferred from a field tuple. A field declared
52
+ * `required: true` is a required key; every other field is optional.
53
+ */
48
54
  export type InferFields<F extends readonly FrontmatterField[]> = Prettify<{
49
55
  [K in F[number] as K extends {
50
56
  required: true;
@@ -54,8 +60,10 @@ export type InferFields<F extends readonly FrontmatterField[]> = Prettify<{
54
60
  required: true;
55
61
  } ? never : K['name']]?: FieldValue<K>;
56
62
  }>;
57
- /** A concept's schema: the plain-data field projection, the generated validator, and the
58
- * Standard Schema conformance property. */
63
+ /**
64
+ * A concept's schema: the plain-data field projection, the generated validator, and the
65
+ * Standard Schema conformance property.
66
+ */
59
67
  export interface ConceptSchema<F extends readonly FrontmatterField[] = readonly FrontmatterField[]> {
60
68
  /** The declared fields as plain serializable data, for the editor form. */
61
69
  readonly fields: FrontmatterField[];
@@ -66,9 +74,11 @@ export interface ConceptSchema<F extends readonly FrontmatterField[] = readonly
66
74
  }
67
75
  /** Extract the inferred frontmatter type from a `ConceptSchema`. */
68
76
  export type Infer<S> = S extends ConceptSchema<infer F> ? InferFields<F> : never;
69
- /** Options for `defineFields`. `refine` runs after the per-field rules pass, for cross-field and
77
+ /**
78
+ * Options for `defineFields`. `refine` runs after the per-field rules pass, for cross-field and
70
79
  * body-dependent checks. It is validation-only: it returns field-keyed errors to merge, or
71
- * nothing, and never transforms the data. */
80
+ * nothing, and never transforms the data.
81
+ */
72
82
  export interface DefineFieldsOptions<F extends readonly FrontmatterField[]> {
73
83
  refine?: (data: InferFields<F>, body: string) => Record<string, string> | undefined;
74
84
  }
@@ -1,8 +1,10 @@
1
- /** True when a word is a single valid dictionary line (no whitespace, no control characters, non-empty
1
+ /**
2
+ * True when a word is a single valid dictionary line (no whitespace, no control characters, non-empty
2
3
  * and within the length bound). A leading "#" is rejected: parseDictionary re-reads such a line as a
3
4
  * comment, so committing it would silently drop the word on the next read. The action uses this to
4
5
  * reject untrusted input before the merge, so a newline or a control byte can never inject an extra
5
- * line into the committed file. */
6
+ * line into the committed file.
7
+ */
6
8
  export declare function isValidDictionaryWord(word: string, maxLength?: number): boolean;
7
9
  /**
8
10
  * Parse the committed dictionary file text into its word list. Comment lines (a `#` after optional