@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
@@ -49,10 +49,12 @@ interface Edit {
49
49
  kind: RepointPlacement['kind'];
50
50
  }
51
51
 
52
- /** Drop any span that overlaps a span already kept, in source order. A final safety net so two
52
+ /**
53
+ * Drop any span that overlaps a span already kept, in source order. A final safety net so two
53
54
  * splices can never target the same or overlapping bytes and clobber each other into a corrupt
54
55
  * result, no matter how the locating arms behaved. A pure-insert span (`start === end`) overlaps
55
- * another span only when it sits strictly inside it, so adjacent inserts and edits are kept. */
56
+ * another span only when it sits strictly inside it, so adjacent inserts and edits are kept.
57
+ */
56
58
  function dropOverlappingEdits<T extends { start: number; end: number }>(edits: T[]): T[] {
57
59
  const kept: T[] = [];
58
60
  for (const e of edits) {
@@ -62,32 +64,40 @@ function dropOverlappingEdits<T extends { start: number; end: number }>(edits: T
62
64
  return kept;
63
65
  }
64
66
 
65
- /** A locating scan for candidate `media:` token substrings. Deliberately broad (it accepts
67
+ /**
68
+ * A locating scan for candidate `media:` token substrings. Deliberately broad (it accepts
66
69
  * uppercase and other out-of-grammar characters) so a malformed token is still found and then
67
70
  * rejected by parseMediaToken, never silently skipped by the locator. The character class stops at
68
71
  * whitespace, a quote, or any YAML or markdown delimiter, so a frontmatter value or an image
69
- * destination ends the candidate. */
72
+ * destination ends the candidate.
73
+ */
70
74
  const MEDIA_TOKEN_SCAN = /media:[A-Za-z0-9._-]+/g;
71
75
 
72
- /** Split a leading frontmatter block off the markdown. `fmBlock` is the `---` fenced block including
76
+ /**
77
+ * Split a leading frontmatter block off the markdown. `fmBlock` is the `---` fenced block including
73
78
  * both fences and the trailing newline (empty when there is none); `body` is everything after it.
74
79
  * The block leads the document, so a frontmatter offset is already absolute and a body offset needs
75
- * `fmBlock.length` added. Shared by every arm so they agree on the boundary. */
80
+ * `fmBlock.length` added. Shared by every arm so they agree on the boundary.
81
+ */
76
82
  function splitFrontmatter(markdown: string): { fmBlock: string; body: string } {
77
83
  const m = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
78
84
  const fmBlock = m ? m[0] : '';
79
85
  return { fmBlock, body: markdown.slice(fmBlock.length) };
80
86
  }
81
87
 
82
- /** Parse a doc with the figure-aware pipeline, so the body arm agrees with what remarkFigure renders
83
- * and can see the enclosing `:::figure` container. Mirrors parseFigureDoc in markdown-format.ts. */
88
+ /**
89
+ * Parse a doc with the figure-aware pipeline, so the body arm agrees with what remarkFigure renders
90
+ * and can see the enclosing `:::figure` container. Mirrors parseFigureDoc in markdown-format.ts.
91
+ */
84
92
  function parseFigureDoc(doc: string): Root {
85
93
  return unified().use(remarkParse).use(remarkGfm).use(remarkDirective).parse(doc) as Root;
86
94
  }
87
95
 
88
- /** Whether `target` sits inside a `figure`-named container directive. Walks the tree to find the
96
+ /**
97
+ * Whether `target` sits inside a `figure`-named container directive. Walks the tree to find the
89
98
  * ancestor, since unist-util-visit's per-call ancestors are not retained across the traversal.
90
- * Mirrors enclosingFigure in markdown-format.ts, reduced to a boolean. */
99
+ * Mirrors enclosingFigure in markdown-format.ts, reduced to a boolean.
100
+ */
91
101
  function inFigure(tree: Root, target: Image): boolean {
92
102
  let found = false;
93
103
  visit(tree, 'containerDirective', (dir: ContainerDirective) => {
@@ -99,16 +109,20 @@ function inFigure(tree: Root, target: Image): boolean {
99
109
  return found;
100
110
  }
101
111
 
102
- /** The split of fmBlock into its lines, each with its block-relative start and end offsets (the end
112
+ /**
113
+ * The split of fmBlock into its lines, each with its block-relative start and end offsets (the end
103
114
  * is the index of the trailing newline, or the block length for the last line). Block offsets are
104
- * already absolute since the frontmatter leads the document. */
115
+ * already absolute since the frontmatter leads the document.
116
+ */
105
117
  interface FmLine {
106
118
  start: number;
107
119
  end: number;
108
120
  }
109
121
 
110
- /** Split fmBlock into lines once, so the locator helpers walk a shared structure instead of
111
- * re-scanning the block per call. */
122
+ /**
123
+ * Split fmBlock into lines once, so the locator helpers walk a shared structure instead of
124
+ * re-scanning the block per call.
125
+ */
112
126
  function fmLines(fmBlock: string): FmLine[] {
113
127
  const lines: FmLine[] = [];
114
128
  let pos = 0;
@@ -122,12 +136,14 @@ function fmLines(fmBlock: string): FmLine[] {
122
136
  return lines;
123
137
  }
124
138
 
125
- /** The inclusive line-index range `[lo, hi]` of the block-style mapping a top-level key opens: the
139
+ /**
140
+ * The inclusive line-index range `[lo, hi]` of the block-style mapping a top-level key opens: the
126
141
  * line `^<key>:` at indent 0 through the last line before the next top-level key (or the document
127
142
  * end). A flow-style value (`key: { ... }` all on one line) yields a single-line range. Returns null
128
143
  * when the key has no top-level line, which a malformed or non-canonical block can cause. Scoping the
129
144
  * per-key search to this range is what lets two image fields that share one hash, or an image field
130
- * whose hash also appears in a sibling text value, resolve to distinct, correct spans. */
145
+ * whose hash also appears in a sibling text value, resolve to distinct, correct spans.
146
+ */
131
147
  function frontmatterKeyRange(lines: FmLine[], fmBlock: string, key: string): [number, number] | null {
132
148
  const opener = new RegExp(`^${escapeForRegExp(key)}:`);
133
149
  const topLevelKey = /^[^\s#][^:]*:/;
@@ -153,8 +169,10 @@ function frontmatterKeyRange(lines: FmLine[], fmBlock: string, key: string): [nu
153
169
  return [lo, hi];
154
170
  }
155
171
 
156
- /** A located `src:` line inside a block-style mapping: the line's start and end, its leading indent,
157
- * and the exact `media:` token's block-relative offsets and text. */
172
+ /**
173
+ * A located `src:` line inside a block-style mapping: the line's start and end, its leading indent,
174
+ * and the exact `media:` token's block-relative offsets and text.
175
+ */
158
176
  interface SrcLineHit {
159
177
  lineStart: number;
160
178
  lineEnd: number;
@@ -164,10 +182,12 @@ interface SrcLineHit {
164
182
  token: string;
165
183
  }
166
184
 
167
- /** Find the block-style `src:` line within `[lo, hi]` whose value token parses to `hash`. The token
185
+ /**
186
+ * Find the block-style `src:` line within `[lo, hi]` whose value token parses to `hash`. The token
168
187
  * is located by the broad scan and validated through parseMediaToken (matching on hash), so a
169
188
  * malformed token is found then rejected. Returns null for a flow-style value (no own `src:` line),
170
- * which leaves that shape unanchorable rather than splicing a guessed span. */
189
+ * which leaves that shape unanchorable rather than splicing a guessed span.
190
+ */
171
191
  function findSrcLineInRange(
172
192
  lines: FmLine[],
173
193
  fmBlock: string,
@@ -199,11 +219,13 @@ function findSrcLineInRange(
199
219
  return null;
200
220
  }
201
221
 
202
- /** The image-like top-level frontmatter keys whose `src` parses to `hash`, in source order. A key is
222
+ /**
223
+ * The image-like top-level frontmatter keys whose `src` parses to `hash`, in source order. A key is
203
224
  * image-like when its value is an object carrying a string `src`; this is the same shape
204
225
  * extractMediaRefs reads, so a token in a plain-text value (a `title:`/`note:`) is never treated as a
205
226
  * reference. The bucket-classifying data comes from gray-matter (which handles every quoting form);
206
- * the byte edit is located structurally by the caller, keyed back to this key name. */
227
+ * the byte edit is located structurally by the caller, keyed back to this key name.
228
+ */
207
229
  function imageFieldKeys(data: Record<string, unknown>, hash: string): { key: string; obj: Record<string, unknown> }[] {
208
230
  const out: { key: string; obj: Record<string, unknown> }[] = [];
209
231
  for (const [key, value] of Object.entries(data)) {
@@ -217,11 +239,13 @@ function imageFieldKeys(data: Record<string, unknown>, hash: string): { key: str
217
239
  return out;
218
240
  }
219
241
 
220
- /** Collect hero src-token edits inside the frontmatter block. Only an image-field `src:` line is
242
+ /**
243
+ * Collect hero src-token edits inside the frontmatter block. Only an image-field `src:` line is
221
244
  * rewritten: the structure is read via gray-matter (image-like keys), and each key's `src:` line is
222
245
  * located structurally within that key's block. A `media:` token sitting in a plain-text value (a
223
246
  * `title:` or `description:`) is on no `src:` line, so it is left untouched, keeping the byte-exact
224
- * contract and agreeing with extractMediaRefs. A flow-style hero has no `src:` line and is skipped. */
247
+ * contract and agreeing with extractMediaRefs. A flow-style hero has no `src:` line and is skipped.
248
+ */
225
249
  function frontmatterEdits(markdown: string, fmBlock: string, oldHash: string): Edit[] {
226
250
  if (fmBlock === '') return [];
227
251
  const data = matter(markdown).data as Record<string, unknown>;
@@ -237,10 +261,12 @@ function frontmatterEdits(markdown: string, fmBlock: string, oldHash: string): E
237
261
  return edits;
238
262
  }
239
263
 
240
- /** Locate the exact `media:` token substring inside one image node's source span. The destination
264
+ /**
265
+ * Locate the exact `media:` token substring inside one image node's source span. The destination
241
266
  * begins at the `](` that follows the alt text, so the search starts there to avoid a false match on
242
267
  * a `media:`-like string inside the alt. Returns null when the token cannot be located, which leaves
243
- * the image untouched rather than splicing a guessed range. */
268
+ * the image untouched rather than splicing a guessed range.
269
+ */
244
270
  function locateImageToken(span: string, url: string): { start: number; end: number } | null {
245
271
  const destStart = span.indexOf('](');
246
272
  const from = destStart === -1 ? 0 : destStart + 2;
@@ -249,9 +275,11 @@ function locateImageToken(span: string, url: string): { start: number; end: numb
249
275
  return { start: at, end: at + url.length };
250
276
  }
251
277
 
252
- /** One body image whose url parses to the target hash, with its absolute node-span offsets (block
278
+ /**
279
+ * One body image whose url parses to the target hash, with its absolute node-span offsets (block
253
280
  * length added) and whether it sits inside a `:::figure`. The shared body-image find that both the
254
- * token-rewrite and alt-fill arms walk, so they agree on what an image is and how a figure is named. */
281
+ * token-rewrite and alt-fill arms walk, so they agree on what an image is and how a figure is named.
282
+ */
255
283
  interface MatchedBodyImage {
256
284
  node: Image;
257
285
  /** Absolute start offset of the `![...](...)` node in the whole markdown. */
@@ -261,9 +289,11 @@ interface MatchedBodyImage {
261
289
  kind: 'body' | 'figure';
262
290
  }
263
291
 
264
- /** Find every body image whose url parses to `hash`, in source order, with absolute offsets. Parses
292
+ /**
293
+ * Find every body image whose url parses to `hash`, in source order, with absolute offsets. Parses
265
294
  * with the figure-aware pipeline, so a `media:` token inside a code span or fence is not an image
266
- * node and is correctly skipped, matching extractMediaRefs. */
295
+ * node and is correctly skipped, matching extractMediaRefs.
296
+ */
267
297
  function matchedBodyImages(body: string, blockLength: number, hash: string): MatchedBodyImage[] {
268
298
  const tree = parseFigureDoc(body);
269
299
  const hits: MatchedBodyImage[] = [];
@@ -283,9 +313,11 @@ function matchedBodyImages(body: string, blockLength: number, hash: string): Mat
283
313
  return hits;
284
314
  }
285
315
 
286
- /** Collect body edits over the body slice. Each matching image is located within its own source span
316
+ /**
317
+ * Collect body edits over the body slice. Each matching image is located within its own source span
287
318
  * and recorded with an absolute offset. The kind is 'figure' when the image is inside a `:::figure`,
288
- * else 'body'. */
319
+ * else 'body'.
320
+ */
289
321
  function bodyEdits(body: string, blockLength: number, oldHash: string): Edit[] {
290
322
  const edits: Edit[] = [];
291
323
  for (const hit of matchedBodyImages(body, blockLength, oldHash)) {
@@ -337,13 +369,17 @@ export function repointMediaRef(markdown: string, oldHash: string, newToken: str
337
369
  return { markdown: out, placements };
338
370
  }
339
371
 
340
- /** Which alt bucket a placement falls in: an empty alt always gets filled, a non-empty (custom) alt is
341
- * reported and only overwritten on opt-in, and a decorative hero is never touched. */
372
+ /**
373
+ * Which alt bucket a placement falls in: an empty alt always gets filled, a non-empty (custom) alt is
374
+ * reported and only overwritten on opt-in, and a decorative hero is never touched.
375
+ */
342
376
  export type AltBucket = 'will-fill' | 'customized' | 'decorative-skipped';
343
377
 
344
- /** One placement of the target hash and what the alt-fill does to it: which surface it lives on, its
378
+ /**
379
+ * One placement of the target hash and what the alt-fill does to it: which surface it lives on, its
345
380
  * bucket, the existing alt, and the alt after the transform (unchanged for a customized alt left as
346
- * is and for a decorative hero). */
381
+ * is and for a decorative hero).
382
+ */
347
383
  export interface AltPlacement {
348
384
  kind: 'body' | 'figure' | 'hero';
349
385
  bucket: AltBucket;
@@ -359,10 +395,12 @@ export interface AltFillResult {
359
395
  placements: AltPlacement[];
360
396
  }
361
397
 
362
- /** A placement plus its optional byte edit. `apply` is false for a reported-but-unchanged placement
398
+ /**
399
+ * A placement plus its optional byte edit. `apply` is false for a reported-but-unchanged placement
363
400
  * (a kept custom alt, a decorative hero), which carries a diff entry but no splice. When `apply` is
364
401
  * true, `[start, end)` is the absolute source span to replace with `text` (a pure insert is
365
- * `start === end`). Keeping the placement here keeps the diff and the edits in step. */
402
+ * `start === end`). Keeping the placement here keeps the diff and the edits in step.
403
+ */
366
404
  interface AltEdit {
367
405
  apply: boolean;
368
406
  start: number;
@@ -371,25 +409,31 @@ interface AltEdit {
371
409
  placement: AltPlacement;
372
410
  }
373
411
 
374
- /** Classify an existing alt into its non-decorative bucket: an empty (or whitespace-only) alt is
412
+ /**
413
+ * Classify an existing alt into its non-decorative bucket: an empty (or whitespace-only) alt is
375
414
  * filled, a non-empty alt is a custom alt the caller may opt in to overwrite. Mirrors the empty-alt
376
- * test findMediaImagesNeedingAlt uses. */
415
+ * test findMediaImagesNeedingAlt uses.
416
+ */
377
417
  function classifyAlt(existing: string): 'will-fill' | 'customized' {
378
418
  return existing.trim() === '' ? 'will-fill' : 'customized';
379
419
  }
380
420
 
381
- /** Whether a bucket plus the overwrite choice means the alt text is actually rewritten. A will-fill
382
- * always writes; a customized alt writes only on opt-in; a decorative hero never writes. */
421
+ /**
422
+ * Whether a bucket plus the overwrite choice means the alt text is actually rewritten. A will-fill
423
+ * always writes; a customized alt writes only on opt-in; a decorative hero never writes.
424
+ */
383
425
  function altIsEdited(bucket: AltBucket, overwrite: boolean): boolean {
384
426
  if (bucket === 'will-fill') return true;
385
427
  if (bucket === 'customized') return overwrite;
386
428
  return false;
387
429
  }
388
430
 
389
- /** Collect the body and figure alt edits over the body slice. The alt source span sits between `![`
431
+ /**
432
+ * Collect the body and figure alt edits over the body slice. The alt source span sits between `![`
390
433
  * and the `](` inside the image node's span, so the new alt (escaped the way insertImage escapes it,
391
434
  * so a `]` cannot break the syntax) is spliced there. The existing alt is the parser's already
392
- * unescaped `node.alt`. A body image has no decorative slot, so an empty alt is always will-fill. */
435
+ * unescaped `node.alt`. A body image has no decorative slot, so an empty alt is always will-fill.
436
+ */
393
437
  function bodyAltEdits(body: string, blockLength: number, hash: string, defaultAlt: string, overwrite: boolean): AltEdit[] {
394
438
  const edits: AltEdit[] = [];
395
439
  for (const hit of matchedBodyImages(body, blockLength, hash)) {
@@ -426,18 +470,22 @@ function bodyAltEdits(body: string, blockLength: number, hash: string, defaultAl
426
470
  return edits;
427
471
  }
428
472
 
429
- /** Escape a literal string for safe interpolation into a RegExp source. A key name or an indent is
430
- * matched literally, so its characters must not act as metacharacters. */
473
+ /**
474
+ * Escape a literal string for safe interpolation into a RegExp source. A key name or an indent is
475
+ * matched literally, so its characters must not act as metacharacters.
476
+ */
431
477
  function escapeForRegExp(literal: string): string {
432
478
  return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
433
479
  }
434
480
 
435
- /** Find a sibling key line (`alt:` or `decorative:`) at exactly `indent` within the inclusive
481
+ /**
482
+ * Find a sibling key line (`alt:` or `decorative:`) at exactly `indent` within the inclusive
436
483
  * line-index range `[lo, hi]` of one mapping. The range is the mapping's own block, so the search
437
484
  * spans the whole mapping rather than a same-indent contiguous run: a blank line or a deeper-nested
438
485
  * child between `src:` and `alt:` no longer hides the existing key (which would otherwise insert a
439
486
  * duplicate key and break the YAML). Returns the key line's value span (after the key and its space,
440
- * to end of line) or null when the mapping has no such key at that indent. */
487
+ * to end of line) or null when the mapping has no such key at that indent.
488
+ */
441
489
  function findSiblingKeyValue(
442
490
  lines: FmLine[],
443
491
  fmBlock: string,
@@ -454,7 +502,8 @@ function findSiblingKeyValue(
454
502
  return null;
455
503
  }
456
504
 
457
- /** Collect the hero alt edits inside the frontmatter block. The image-field objects (and their
505
+ /**
506
+ * Collect the hero alt edits inside the frontmatter block. The image-field objects (and their
458
507
  * decorative and alt values) are read via gray-matter to classify the bucket robustly across quoting
459
508
  * forms; the byte edit is then located structurally, scoped to each field's own mapping block, keyed
460
509
  * back by the top-level field name. Iterating the fields in source order keeps the hero placements in
@@ -463,7 +512,8 @@ function findSiblingKeyValue(
463
512
  * blank line or a nested child) has its value replaced; an absent one is inserted right after the
464
513
  * `src:` line at the same indent. The new value is a JSON-quoted scalar, valid YAML that handles a
465
514
  * colon, a quote, or an empty string. A flow-style hero (`image: { ... }`, no own `src:` line) is
466
- * unanchorable, so it is reported from the gray-matter read but never spliced. */
515
+ * unanchorable, so it is reported from the gray-matter read but never spliced.
516
+ */
467
517
  function heroAltEdits(
468
518
  markdown: string,
469
519
  fmBlock: string,
@@ -11,8 +11,10 @@ export interface StandardInput {
11
11
  body: string;
12
12
  }
13
13
 
14
- /** A minimal local copy of the Standard Schema v1 interface (https://standardschema.dev), so the
15
- * schema is a drop-in where the ecosystem accepts a validator, with no runtime dependency. */
14
+ /**
15
+ * A minimal local copy of the Standard Schema v1 interface (https://standardschema.dev), so the
16
+ * schema is a drop-in where the ecosystem accepts a validator, with no runtime dependency.
17
+ */
16
18
  export interface StandardSchemaV1<Input = unknown, Output = Input> {
17
19
  readonly '~standard': {
18
20
  readonly version: 1;
@@ -25,9 +27,11 @@ type StandardResult<Output> =
25
27
  | { readonly value: Output; readonly issues?: undefined }
26
28
  | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray<PropertyKey> }> };
27
29
 
28
- /** Map one field descriptor to the TS type of its normalized value. text, textarea, and date
30
+ /**
31
+ * Map one field descriptor to the TS type of its normalized value. text, textarea, and date
29
32
  * normalize to a string; a closed-vocabulary `tags` field to the option-union array; an `image`
30
- * field to its nested object. */
33
+ * field to its nested object.
34
+ */
31
35
  type FieldValue<K extends FrontmatterField> = K extends { type: 'boolean' }
32
36
  ? boolean
33
37
  : K extends { type: 'image' }
@@ -41,16 +45,20 @@ type FieldValue<K extends FrontmatterField> = K extends { type: 'boolean' }
41
45
  /** Flatten an intersection into a single readable object type. */
42
46
  type Prettify<T> = { [K in keyof T]: T[K] } & {};
43
47
 
44
- /** The normalized frontmatter type inferred from a field tuple. A field declared
45
- * `required: true` is a required key; every other field is optional. */
48
+ /**
49
+ * The normalized frontmatter type inferred from a field tuple. A field declared
50
+ * `required: true` is a required key; every other field is optional.
51
+ */
46
52
  export type InferFields<F extends readonly FrontmatterField[]> = Prettify<
47
53
  { [K in F[number] as K extends { required: true } ? K['name'] : never]: FieldValue<K> } & {
48
54
  [K in F[number] as K extends { required: true } ? never : K['name']]?: FieldValue<K>;
49
55
  }
50
56
  >;
51
57
 
52
- /** A concept's schema: the plain-data field projection, the generated validator, and the
53
- * Standard Schema conformance property. */
58
+ /**
59
+ * A concept's schema: the plain-data field projection, the generated validator, and the
60
+ * Standard Schema conformance property.
61
+ */
54
62
  export interface ConceptSchema<F extends readonly FrontmatterField[] = readonly FrontmatterField[]> {
55
63
  /** The declared fields as plain serializable data, for the editor form. */
56
64
  readonly fields: FrontmatterField[];
@@ -82,9 +90,11 @@ function applyRules(field: FrontmatterField, value: unknown, errors: Record<stri
82
90
  }
83
91
  }
84
92
 
85
- /** Options for `defineFields`. `refine` runs after the per-field rules pass, for cross-field and
93
+ /**
94
+ * Options for `defineFields`. `refine` runs after the per-field rules pass, for cross-field and
86
95
  * body-dependent checks. It is validation-only: it returns field-keyed errors to merge, or
87
- * nothing, and never transforms the data. */
96
+ * nothing, and never transforms the data.
97
+ */
88
98
  export interface DefineFieldsOptions<F extends readonly FrontmatterField[]> {
89
99
  refine?: (data: InferFields<F>, body: string) => Record<string, string> | undefined;
90
100
  }
@@ -19,11 +19,13 @@ const HEADER = '# cairn personal dictionary: one word per line, sorted, kept in
19
19
  // inbound words through this before a merge.
20
20
  const WORD_RE = /^[^\s\p{Cc}]+$/u;
21
21
 
22
- /** True when a word is a single valid dictionary line (no whitespace, no control characters, non-empty
22
+ /**
23
+ * True when a word is a single valid dictionary line (no whitespace, no control characters, non-empty
23
24
  * and within the length bound). A leading "#" is rejected: parseDictionary re-reads such a line as a
24
25
  * comment, so committing it would silently drop the word on the next read. The action uses this to
25
26
  * reject untrusted input before the merge, so a newline or a control byte can never inject an extra
26
- * line into the committed file. */
27
+ * line into the committed file.
28
+ */
27
29
  export function isValidDictionaryWord(word: string, maxLength = 64): boolean {
28
30
  if (word.startsWith('#')) return false;
29
31
  return word.length > 0 && word.length <= maxLength && WORD_RE.test(word);
@@ -47,8 +49,10 @@ export function parseDictionary(text: string | null): string[] {
47
49
  return words;
48
50
  }
49
51
 
50
- /** Case-insensitive, locale-stable comparator for the canonical sort. Words are compared lowercased
51
- * so "Cairn" and "cairn" collapse to one entry, the same case-folding the Worker's merged set uses. */
52
+ /**
53
+ * Case-insensitive, locale-stable comparator for the canonical sort. Words are compared lowercased
54
+ * so "Cairn" and "cairn" collapse to one entry, the same case-folding the Worker's merged set uses.
55
+ */
52
56
  function byWord(a: string, b: string): number {
53
57
  return a.toLowerCase().localeCompare(b.toLowerCase());
54
58
  }