@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
@@ -7,6 +7,7 @@ open(), so the component renders no trigger of its own.
7
7
  -->
8
8
  <script lang="ts">
9
9
  import ShortcutsGrid from './ShortcutsGrid.svelte';
10
+ import { markdownReference } from './markdown-reference.js';
10
11
 
11
12
  let dialog = $state<HTMLDialogElement | null>(null);
12
13
 
@@ -33,21 +34,9 @@ open(), so the component renders no trigger of its own.
33
34
  </tr>
34
35
  </thead>
35
36
  <tbody>
36
- <tr><td><code>## Heading</code></td><td>A heading</td></tr>
37
- <tr><td><code>### Heading</code></td><td>A smaller heading</td></tr>
38
- <tr><td><code>#### Heading</code></td><td>A fourth-level heading</td></tr>
39
- <tr><td><code>**bold**</code></td><td>Bold text</td></tr>
40
- <tr><td><code>*italic*</code></td><td>Italic text</td></tr>
41
- <tr><td><code>~~text~~</code></td><td>Crossed-out text</td></tr>
42
- <tr><td><code>[text](url)</code></td><td>A link</td></tr>
43
- <tr><td><code>[[page-name]]</code></td><td>A link to one of your pages</td></tr>
44
- <tr><td><code>- item</code></td><td>A bulleted list</td></tr>
45
- <tr><td><code>1. item</code></td><td>A numbered list</td></tr>
46
- <tr><td><code>- [ ] item</code></td><td>A checklist</td></tr>
47
- <tr><td><code>&gt; quote</code></td><td>A quote</td></tr>
48
- <tr><td><code>`code`</code></td><td>Code</td></tr>
49
- <tr><td>Table</td><td>The Table button in the toolbar inserts one</td></tr>
50
- <tr><td><code>---</code></td><td>A horizontal rule</td></tr>
37
+ {#each markdownReference as row (row.syntax)}
38
+ <tr><td><code>{row.syntax}</code></td><td>{row.makes}</td></tr>
39
+ {/each}
51
40
  </tbody>
52
41
  </table>
53
42
  <h3 class="mt-4 mb-2 text-sm font-semibold">Keyboard shortcuts</h3>
@@ -118,12 +118,16 @@ export function firstImageFile(dt: {
118
118
  return normalizeDataTransfer(dt)[0] ?? null;
119
119
  }
120
120
 
121
- /** The conservative canvas area budget, about 16.7M px (4096 x 4096). A source over this is scaled
122
- * down before any `drawImage`, never clipped. */
121
+ /**
122
+ * The conservative canvas area budget, about 16.7M px (4096 x 4096). A source over this is scaled
123
+ * down before any `drawImage`, never clipped.
124
+ */
123
125
  export const MAX_AREA = 16_777_216;
124
126
 
125
- /** The conservative short-side budget. A source whose smaller dimension exceeds this is scaled down so
126
- * the short side lands at the cap, even when its area is within MAX_AREA. */
127
+ /**
128
+ * The conservative short-side budget. A source whose smaller dimension exceeds this is scaled down so
129
+ * the short side lands at the cap, even when its area is within MAX_AREA.
130
+ */
127
131
  export const MAX_SHORT_SIDE = 4096;
128
132
 
129
133
  /**
@@ -151,9 +155,11 @@ export function budgetForDimensions(
151
155
  };
152
156
  }
153
157
 
154
- /** The ingest failure taxonomy. `decode-unsupported` is a format the browser and the HEIC decoder both
158
+ /**
159
+ * The ingest failure taxonomy. `decode-unsupported` is a format the browser and the HEIC decoder both
155
160
  * refuse; `transcode-failed` is a HEIC decode or a canvas re-encode that threw; `too-large` is a
156
- * source still over budget after a transcode; `network` is the upload fetch rejecting. */
161
+ * source still over budget after a transcode; `network` is the upload fetch rejecting.
162
+ */
157
163
  export type IngestFailureKind =
158
164
  | 'decode-unsupported'
159
165
  | 'transcode-failed'
@@ -254,8 +260,10 @@ export function buildUploadRequest(opts: UploadRequestOpts): { url: string; init
254
260
  // Browser-coupled glue (wired, proven live at 2b/site, not unit-tested here)
255
261
  // ---------------------------------------------------------------------------
256
262
 
257
- /** The structural shape of the heic-to module's `heicTo`, typed here so the dynamic import stays
258
- * lazy. A HEIC blob in, a decoded image blob out (PNG, with the HEIF orientation already applied). */
263
+ /**
264
+ * The structural shape of the heic-to module's `heicTo`, typed here so the dynamic import stays
265
+ * lazy. A HEIC blob in, a decoded image blob out (PNG, with the HEIF orientation already applied).
266
+ */
259
267
  type HeicTo = (args: { blob: Blob; type: 'image/png' }) => Promise<Blob>;
260
268
 
261
269
  /** A decoded source plus its dimensions, the input to the upload step. */
@@ -373,8 +381,10 @@ export async function sendUpload(url: string, init: RequestInit): Promise<Respon
373
381
  }
374
382
  }
375
383
 
376
- /** Guard a drop target: cancel the browser's default open-the-file behavior on `dragover` and `drop`
377
- * so a dropped image stays inside the editor rather than navigating the page to the file. */
384
+ /**
385
+ * Guard a drop target: cancel the browser's default open-the-file behavior on `dragover` and `drop`
386
+ * so a dropped image stays inside the editor rather than navigating the page to the file.
387
+ */
378
388
  export function guardDropTarget(event: DragEvent): void {
379
389
  event.preventDefault();
380
390
  }
@@ -35,10 +35,12 @@ import type { MediaLibrary, MediaLibraryEntry } from '../media/library-entry.js'
35
35
  // validation, so a non-media or malformed URL is dropped after the match.
36
36
  const MEDIA_IMAGE = /!\[([^\]]*)\]\((media:[^\s)]+)\)/g;
37
37
 
38
- /** A matched media image in a line: the alt text and the URL token's character offsets within the
38
+ /**
39
+ * A matched media image in a line: the alt text and the URL token's character offsets within the
39
40
  * whole document, plus the parsed reference and the library entry (null when the hash is unknown).
40
41
  * figureRole carries the enclosing `:::figure` placement (the closed-set role, or `'figure'` for
41
- * the measure default), or null when the token is not in a figure: a bare token shows no role pill. */
42
+ * the measure default), or null when the token is not in a figure: a bare token shows no role pill.
43
+ */
42
44
  interface MediaImageMatch {
43
45
  alt: string;
44
46
  from: number;
@@ -138,8 +140,10 @@ class MediaChipWidget extends WidgetType {
138
140
  }
139
141
  }
140
142
 
141
- /** Scan one line's text for media image tokens, mapping each to its document offsets and resolving its
142
- * library entry. lineFrom is the line's document start, so the match offsets become absolute. */
143
+ /**
144
+ * Scan one line's text for media image tokens, mapping each to its document offsets and resolving its
145
+ * library entry. lineFrom is the line's document start, so the match offsets become absolute.
146
+ */
143
147
  function matchesInLine(text: string, lineFrom: number, library: MediaLibrary): MediaImageMatch[] {
144
148
  const out: MediaImageMatch[] = [];
145
149
  MEDIA_IMAGE.lastIndex = 0;
@@ -168,10 +172,12 @@ function matchesInLine(text: string, lineFrom: number, library: MediaLibrary): M
168
172
  return out;
169
173
  }
170
174
 
171
- /** Every media image match across the editor's visible ranges, in document order, each carrying its
175
+ /**
176
+ * Every media image match across the editor's visible ranges, in document order, each carrying its
172
177
  * enclosing figure role. One {@link fenceScan} over the whole document feeds the cheap per-token
173
178
  * figure detection (no remark parse on the per-rebuild chip path); the visible lines are scanned
174
- * for tokens, then each token's line index drives {@link figureRoleAtLine}. */
179
+ * for tokens, then each token's line index drives {@link figureRoleAtLine}.
180
+ */
175
181
  function visibleMatches(view: EditorView, library: MediaLibrary): MediaImageMatch[] {
176
182
  const lines = view.state.doc.toString().split('\n');
177
183
  const scan = fenceScan(lines);
@@ -189,8 +195,10 @@ function visibleMatches(view: EditorView, library: MediaLibrary): MediaImageMatc
189
195
  return out;
190
196
  }
191
197
 
192
- /** Replace decorations for each visible media image's reference token: the chip widget over the URL
193
- * token, the alt left untouched. The same spans seed the atomic-range set. */
198
+ /**
199
+ * Replace decorations for each visible media image's reference token: the chip widget over the URL
200
+ * token, the alt left untouched. The same spans seed the atomic-range set.
201
+ */
194
202
  function buildMediaDecorations(view: EditorView, library: MediaLibrary): DecorationSet {
195
203
  const builder = new RangeSetBuilder<Decoration>();
196
204
  for (const match of visibleMatches(view, library)) {
@@ -199,9 +207,11 @@ function buildMediaDecorations(view: EditorView, library: MediaLibrary): Decorat
199
207
  return builder.finish();
200
208
  }
201
209
 
202
- /** The atomic ranges for the visible media reference tokens: a caret or selection edit treats each
210
+ /**
211
+ * The atomic ranges for the visible media reference tokens: a caret or selection edit treats each
203
212
  * token as one unit, so a stray keystroke replaces the whole reference rather than corrupting a hex
204
- * digit. Built from the same matches the decorations use, so the two never disagree. */
213
+ * digit. Built from the same matches the decorations use, so the two never disagree.
214
+ */
205
215
  function buildAtomicRanges(view: EditorView, library: MediaLibrary): DecorationSet {
206
216
  const ranges: Range<Decoration>[] = [];
207
217
  for (const match of visibleMatches(view, library)) {
@@ -24,8 +24,10 @@ import {
24
24
  import { StateEffect, StateField, type Extension } from '@codemirror/state';
25
25
  import { insertImage as insertImageFormat } from './markdown-format.js';
26
26
 
27
- /** One active placeholder's data: its stable id, the object URL for the thumbnail, and the upload
28
- * progress as a 0..1 fraction. The widget reads this to render the thumbnail and the bar. */
27
+ /**
28
+ * One active placeholder's data: its stable id, the object URL for the thumbnail, and the upload
29
+ * progress as a 0..1 fraction. The widget reads this to render the thumbnail and the bar.
30
+ */
29
31
  interface PlaceholderData {
30
32
  id: number;
31
33
  url: string;
@@ -87,8 +89,10 @@ class PlaceholderWidget extends WidgetType {
87
89
  }
88
90
  }
89
91
 
90
- /** The active placeholders as a decoration set plus the per-id position map, so a resolve can find
91
- * the mapped position to insert at and the field can rebuild after a position shift. */
92
+ /**
93
+ * The active placeholders as a decoration set plus the per-id position map, so a resolve can find
94
+ * the mapped position to insert at and the field can rebuild after a position shift.
95
+ */
92
96
  interface PlaceholderState {
93
97
  set: DecorationSet;
94
98
  // Each active placeholder's current data and its mapped document position.
@@ -149,9 +153,11 @@ const placeholderField = StateField.define<PlaceholderState>({
149
153
  provide: (f) => EditorView.decorations.from(f, (v) => v.set),
150
154
  });
151
155
 
152
- /** The seam the host drives: begin lands a placeholder and returns its id; progress moves its bar;
156
+ /**
157
+ * The seam the host drives: begin lands a placeholder and returns its id; progress moves its bar;
153
158
  * resolveTo swaps it for the committed image text; cancel removes it leaving the source untouched.
154
- * Mirrors the register-callback idiom MarkdownEditor uses for its other editor ops. */
159
+ * Mirrors the register-callback idiom MarkdownEditor uses for its other editor ops.
160
+ */
155
161
  export interface ImagePlaceholderApi {
156
162
  /** Land an optimistic placeholder at the current caret from a local object URL; returns its id. */
157
163
  begin(objectUrl: string): number;
@@ -20,10 +20,12 @@ import { Decoration, EditorView, WidgetType, type DecorationSet } from '@codemir
20
20
  import { StateEffect, StateField, RangeSet, type Extension, type Range } from '@codemirror/state';
21
21
  import type { Change } from './tidy-diff.js';
22
22
 
23
- /** A change plus its live disposition and current mapped span. `pending` is undecided-in-the-buffer:
23
+ /**
24
+ * A change plus its live disposition and current mapped span. `pending` is undecided-in-the-buffer:
24
25
  * it still carries decorations. `accepted` has been written (its edit dispatched), so it carries no
25
26
  * decoration. `rejected` was dropped, so it also carries no decoration and never writes. The `from`
26
- * and `to` are the change's current offsets, mapped across every accepted edit since tidy opened. */
27
+ * and `to` are the change's current offsets, mapped across every accepted edit since tidy opened.
28
+ */
27
29
  interface TidyEntry {
28
30
  index: number;
29
31
  from: number;
@@ -171,21 +173,29 @@ const tidyField = StateField.define<TidyState>({
171
173
  provide: (f) => EditorView.decorations.from(f, (v) => buildDecorations(v)),
172
174
  });
173
175
 
174
- /** The api the host drives over one editor view (spec 2.5). Mirrors imagePlaceholderApi: the host
176
+ /**
177
+ * The api the host drives over one editor view (spec 2.5). Mirrors imagePlaceholderApi: the host
175
178
  * registers it through registerTidy, and the review surface calls it as the author works the list.
176
- * Every accept lands as a CodeMirror transaction; reject and reject-all write no text. */
179
+ * Every accept lands as a CodeMirror transaction; reject and reject-all write no text.
180
+ */
177
181
  export interface TidyApi {
178
- /** Open tidy with the validated change set: seed the field, show the decorations. The buffer is
179
- * untouched; the originals stay until an accept writes. */
182
+ /**
183
+ * Open tidy with the validated change set: seed the field, show the decorations. The buffer is
184
+ * untouched; the originals stay until an accept writes.
185
+ */
180
186
  enter(changes: Change[]): void;
181
- /** Accept one change: dispatch its replacement over its current span in one transaction and mark it
182
- * accepted. The other pending changes map across the edit. */
187
+ /**
188
+ * Accept one change: dispatch its replacement over its current span in one transaction and mark it
189
+ * accepted. The other pending changes map across the edit.
190
+ */
183
191
  acceptOne(index: number): void;
184
192
  /** Reject one change: mark it rejected so its decorations clear, leaving the original untouched. */
185
193
  rejectOne(index: number): void;
186
- /** Accept many changes (the bulk action) in ONE transaction: the whole edit is one undoable step.
194
+ /**
195
+ * Accept many changes (the bulk action) in ONE transaction: the whole edit is one undoable step.
187
196
  * The caller passes ONLY the indexes it has decided to keep; this never sweeps an index the caller
188
- * did not name, which is how Accept-fixes confines itself to objective hunks. */
197
+ * did not name, which is how Accept-fixes confines itself to objective hunks.
198
+ */
189
199
  acceptMany(indexes: number[]): void;
190
200
  /** Reject every remaining pending change, leaving the document byte-identical. */
191
201
  rejectAll(): void;
@@ -193,15 +203,19 @@ export interface TidyApi {
193
203
  exit(): void;
194
204
  }
195
205
 
196
- /** The tidy extension: the StateField holding the change set and its decorations. The host adds it to
206
+ /**
207
+ * The tidy extension: the StateField holding the change set and its decorations. The host adds it to
197
208
  * the initial editor state (in its own compartment beside media and folding), then builds the driving
198
- * api with tidyApi once the view exists. */
209
+ * api with tidyApi once the view exists.
210
+ */
199
211
  export function cairnTidy(): Extension {
200
212
  return tidyField;
201
213
  }
202
214
 
203
- /** Build the api that drives tidy against one editor view. The host registers it through registerTidy;
204
- * the review surface calls enter, the per-hunk and bulk accept/reject, and exit. */
215
+ /**
216
+ * Build the api that drives tidy against one editor view. The host registers it through registerTidy;
217
+ * the review surface calls enter, the per-hunk and bulk accept/reject, and exit.
218
+ */
205
219
  export function tidyApi(view: EditorView): TidyApi {
206
220
  // Dispatch the named changes' replacements over their CURRENT mapped spans in one transaction, mark
207
221
  // them accepted, and let the field map any remaining pending entries. The changes are read from the
@@ -8,6 +8,7 @@ export { default as CsrfField } from './CsrfField.svelte';
8
8
  export { default as ConceptList } from './ConceptList.svelte';
9
9
  export { default as CairnMediaLibrary } from './CairnMediaLibrary.svelte';
10
10
  export { default as CairnTidySettings } from './CairnTidySettings.svelte';
11
+ export { default as HelpHome } from './HelpHome.svelte';
11
12
  export { default as EditPage } from './EditPage.svelte';
12
13
  export { default as ManageEditors } from './ManageEditors.svelte';
13
14
  export { default as MarkdownEditor } from './MarkdownEditor.svelte';
@@ -21,15 +21,19 @@ function sectionFor(concept: string): { name: string; rank: number } {
21
21
  return CONCEPT_SECTIONS[concept] ?? { name: concept.charAt(0).toUpperCase() + concept.slice(1), rank: 2 };
22
22
  }
23
23
 
24
- /** The open `[[query` before the cursor, or null. The query stops at a closing bracket or a newline,
25
- * so a finished `[[x]]` link and ordinary prose never trigger. `from` is the index of the `[[`. */
24
+ /**
25
+ * The open `[[query` before the cursor, or null. The query stops at a closing bracket or a newline,
26
+ * so a finished `[[x]]` link and ordinary prose never trigger. `from` is the index of the `[[`.
27
+ */
26
28
  export function matchCairnTrigger(before: string): { query: string; from: number } | null {
27
29
  const match = /\[\[([^[\]\n]*)$/.exec(before);
28
30
  return match ? { query: match[1], from: match.index } : null;
29
31
  }
30
32
 
31
- /** The completion options for a query: a case-insensitive title substring match, each option grouped
32
- * by concept, a draft marked and a post date shown in the detail, and the apply text the full link. */
33
+ /**
34
+ * The completion options for a query: a case-insensitive title substring match, each option grouped
35
+ * by concept, a draft marked and a post date shown in the detail, and the apply text the full link.
36
+ */
33
37
  export function linkCompletions(targets: LinkTarget[], query: string): Completion[] {
34
38
  const q = query.trim().toLowerCase();
35
39
  const matched = q ? targets.filter((t) => t.title.toLowerCase().includes(q)) : targets;
@@ -41,9 +45,11 @@ export function linkCompletions(targets: LinkTarget[], query: string): Completio
41
45
  }));
42
46
  }
43
47
 
44
- /** A CodeMirror CompletionSource over the site's link targets, triggered by `[[`. It replaces the
48
+ /**
49
+ * A CodeMirror CompletionSource over the site's link targets, triggered by `[[`. It replaces the
45
50
  * whole `[[query` with the chosen link, and sets filter:false because linkCompletions already
46
- * filtered by the query (CodeMirror would otherwise re-filter against the literal `[[query`). */
51
+ * filtered by the query (CodeMirror would otherwise re-filter against the literal `[[query`).
52
+ */
47
53
  export function cairnLinkCompletionSource(targets: LinkTarget[]): CompletionSource {
48
54
  return async (context: CompletionContext): Promise<CompletionResult | null> => {
49
55
  const line = context.state.doc.lineAt(context.pos);
@@ -18,8 +18,8 @@ const INLINE = /(?<![:\w]):[\w-]+\[[^\]]*\](\{[^}]*\})?/g;
18
18
  const CODE_FENCE = /^\s{0,3}(`{3,}|~{3,})/;
19
19
 
20
20
  /**
21
- * The directive name from a container opener line (`:::callout{...}` -> `callout`,
22
- * `::::cta[Book]` -> `cta`), or null when the line is not a named container opener. Reads the
21
+ * The directive name from a container opener line (`:::callout{...}` gives `callout`,
22
+ * `::::cta[Book]` gives `cta`), or null when the line is not a named container opener. Reads the
23
23
  * same FENCE match the scan uses: group 2 is the name, empty on a bare closer, so a closer or a
24
24
  * non-fence line returns null.
25
25
  */
@@ -39,9 +39,11 @@ export function directiveLineKind(line: string): 'fence' | 'leaf' | null {
39
39
  export interface FenceScan {
40
40
  /** The 1-based container depth per line, or null outside any container. */
41
41
  depths: (number | null)[];
42
- /** Whether a line opened or closed a container, or null for everything else. A fence-shaped
42
+ /**
43
+ * Whether a line opened or closed a container, or null for everything else. A fence-shaped
43
44
  * line the code-block tracking disowned is null too, so the role array is the one source of
44
- * truth for pairing and no caller re-parses a line the scan already judged. */
45
+ * truth for pairing and no caller re-parses a line the scan already judged.
46
+ */
45
47
  roles: ('opener' | 'closer' | null)[];
46
48
  }
47
49
 
@@ -161,8 +163,10 @@ export function containerRanges(scan: FenceScan): ContainerRange[] {
161
163
  return out;
162
164
  }
163
165
 
164
- /** The closed placement-role set for the reserved `figure` directive, mirroring remark-figure.ts.
165
- * A class outside this set is the measure default, never passed through as a role. */
166
+ /**
167
+ * The closed placement-role set for the reserved `figure` directive, mirroring remark-figure.ts.
168
+ * A class outside this set is the measure default, never passed through as a role.
169
+ */
166
170
  const FIGURE_ROLES = new Set(['center', 'wide', 'full']);
167
171
 
168
172
  // The directive `{attrs}` brace, and the `.class` shorthands inside it. mdast-util-directive folds
@@ -174,7 +178,8 @@ const ATTR_BRACE = /\{([^}]*)\}/;
174
178
  const CLASS_SHORTHAND = /\.([\w-]+)/g;
175
179
  const CLASS_ATTR = /class\s*=\s*"([^"]*)"/;
176
180
 
177
- /** The figure placement role for a media token sitting on `lineIndex`, derived from the editor's
181
+ /**
182
+ * The figure placement role for a media token sitting on `lineIndex`, derived from the editor's
178
183
  * line scan without a remark parse (the chip rebuild runs on every doc and viewport change).
179
184
  *
180
185
  * Returns the closed-set role (`center`/`wide`/`full`) when the innermost container holding the
@@ -216,7 +221,7 @@ export interface FenceToken {
216
221
 
217
222
  /**
218
223
  * Split a fence line into machinery and meaning. The colon run, the label's brackets, and the
219
- * whole {attrs} group are machinery; the directive name and the label text are meaning, the
224
+ * whole `{attrs}` group are machinery; the directive name and the label text are meaning, the
220
225
  * parts an editor reads. A bare closer is a single machinery span, and a non-fence line yields
221
226
  * no spans at all.
222
227
  */
@@ -59,8 +59,10 @@ const LINE: Record<LineKind, { prefix: (i: number) => string; exact?: RegExp; st
59
59
  const TABLE_GRID =
60
60
  '| Column 1 | Column 2 |\n| -------- | -------- |\n| | |\n| | |';
61
61
 
62
- /** Wrap the selection in `marker`, or unwrap when the markers are already there (inside or just
63
- * outside the selection). The returned range covers the text without its markers either way. */
62
+ /**
63
+ * Wrap the selection in `marker`, or unwrap when the markers are already there (inside or just
64
+ * outside the selection). The returned range covers the text without its markers either way.
65
+ */
64
66
  function toggleWrap(doc: string, from: number, to: number, marker: string): FormatResult {
65
67
  const m = marker.length;
66
68
  const sel = doc.slice(from, to);
@@ -75,10 +77,12 @@ function toggleWrap(doc: string, from: number, to: number, marker: string): Form
75
77
  return { doc: next, from: from + m, to: to + m };
76
78
  }
77
79
 
78
- /** Apply a line-prefix kind to every selected line. When the kind toggles and every line already
80
+ /**
81
+ * Apply a line-prefix kind to every selected line. When the kind toggles and every line already
79
82
  * carries its marker, the markers come off; otherwise competing markers are replaced and each
80
83
  * line gains the kind's prefix. The selection shifts with the first line's edit and stretches
81
- * by the total length change, the same mechanics the original single-prefix version had. */
84
+ * by the total length change, the same mechanics the original single-prefix version had.
85
+ */
82
86
  function applyLinePrefix(doc: string, from: number, to: number, kind: LineKind): FormatResult {
83
87
  const { prefix, exact, strip } = LINE[kind];
84
88
  const lineStart = doc.lastIndexOf('\n', from - 1) + 1; // 0 when the selection is on the first line
@@ -97,8 +101,10 @@ function applyLinePrefix(doc: string, from: number, to: number, kind: LineKind):
97
101
  };
98
102
  }
99
103
 
100
- /** Fence the selected lines in triple backticks on their own lines, or remove the fences when the
101
- * lines just above and below the selection already are fences. */
104
+ /**
105
+ * Fence the selected lines in triple backticks on their own lines, or remove the fences when the
106
+ * lines just above and below the selection already are fences.
107
+ */
102
108
  function toggleCodeFence(doc: string, from: number, to: number): FormatResult {
103
109
  const lineStart = doc.lastIndexOf('\n', from - 1) + 1;
104
110
  const lineEndRaw = doc.indexOf('\n', to);
@@ -118,6 +124,9 @@ function toggleCodeFence(doc: string, from: number, to: number): FormatResult {
118
124
  return { doc: next, from: from + open.length, to: to + open.length };
119
125
  }
120
126
 
127
+ /**
128
+ *
129
+ */
121
130
  export function applyMarkdownFormat(doc: string, from: number, to: number, kind: FormatKind): FormatResult {
122
131
  if (kind === 'bold' || kind === 'italic' || kind === 'code' || kind === 'strike') {
123
132
  return toggleWrap(doc, from, to, WRAP[kind]);
@@ -215,10 +224,12 @@ export function findMediaImagesNeedingAlt(doc: string): MediaImageNeedingAlt[] {
215
224
  return hits;
216
225
  }
217
226
 
218
- /** Concatenate a link node's text-child values. The parser has already unescaped them, so a source
227
+ /**
228
+ * Concatenate a link node's text-child values. The parser has already unescaped them, so a source
219
229
  * `Notes \[draft\]` yields `Notes [draft]`. Used instead of mdast-util-to-string, which is not a
220
230
  * direct dependency. Non-text children (a nested emphasis, say) contribute no value, which is fine
221
- * for the picker-produced links this fix targets. */
231
+ * for the picker-produced links this fix targets.
232
+ */
222
233
  function linkText(node: Link): string {
223
234
  return node.children.map((c) => ('value' in c ? c.value : '')).join('');
224
235
  }
@@ -250,8 +261,10 @@ export function unwrapCairnLink(doc: string, href: string): string {
250
261
  return out;
251
262
  }
252
263
 
253
- /** The closed placement role set the figure render step honors. A role outside it is the measure
254
- * default (null), so the control never writes one. Mirrors the set in render/remark-figure.ts. */
264
+ /**
265
+ * The closed placement role set the figure render step honors. A role outside it is the measure
266
+ * default (null), so the control never writes one. Mirrors the set in render/remark-figure.ts.
267
+ */
255
268
  export type FigureRole = 'center' | 'wide' | 'full';
256
269
  const FIGURE_ROLES = new Set<string>(['center', 'wide', 'full']);
257
270
 
@@ -281,15 +294,19 @@ export interface FigureAtImage {
281
294
  } | null;
282
295
  }
283
296
 
284
- /** Parse a doc with the figure-aware pipeline (the render step's grammar), so the editor transforms
285
- * agree with what renders. Container directives need remark-directive on top of the markdown base. */
297
+ /**
298
+ * Parse a doc with the figure-aware pipeline (the render step's grammar), so the editor transforms
299
+ * agree with what renders. Container directives need remark-directive on top of the markdown base.
300
+ */
286
301
  function parseFigureDoc(doc: string): Root {
287
302
  return unified().use(remarkParse).use(remarkGfm).use(remarkDirective).parse(doc) as Root;
288
303
  }
289
304
 
290
- /** Find the media `image` node whose source range contains `pos`, or whose enclosing figure contains
305
+ /**
306
+ * Find the media `image` node whose source range contains `pos`, or whose enclosing figure contains
291
307
  * `pos`, along with its enclosing `figure` directive when there is one. Returns null when `pos` is
292
- * not on a media image nor inside a figure that wraps one. */
308
+ * not on a media image nor inside a figure that wraps one.
309
+ */
293
310
  function locateMediaImage(
294
311
  tree: Root,
295
312
  pos: number,
@@ -322,8 +339,10 @@ function locateMediaImage(
322
339
  return figureHit ?? bareHit;
323
340
  }
324
341
 
325
- /** The `figure`-named container directive that encloses `node`, or null. Walks the tree to find the
326
- * ancestor, since unist-util-visit's per-call ancestors are not retained across the traversal. */
342
+ /**
343
+ * The `figure`-named container directive that encloses `node`, or null. Walks the tree to find the
344
+ * ancestor, since unist-util-visit's per-call ancestors are not retained across the traversal.
345
+ */
327
346
  function enclosingFigure(tree: Root, target: Image): ContainerDirective | null {
328
347
  let found: ContainerDirective | null = null;
329
348
  visit(tree, 'containerDirective', (dir: ContainerDirective) => {
@@ -337,26 +356,32 @@ function enclosingFigure(tree: Root, target: Image): ContainerDirective | null {
337
356
  return found;
338
357
  }
339
358
 
340
- /** Strip one leading backslash sitting immediately before a colon, the inverse of the fence-escape
359
+ /**
360
+ * Strip one leading backslash sitting immediately before a colon, the inverse of the fence-escape
341
361
  * wrapImageInFigure/updateFigure apply, so a caption that began with a directive-opening colon run
342
- * round-trips to the author's original text. */
362
+ * round-trips to the author's original text.
363
+ */
343
364
  function unescapeCaption(raw: string): string {
344
365
  return raw.replace(/^\\(?=:)/, '');
345
366
  }
346
367
 
347
- /** Collapse a raw caption source span to the single-line value the control edits: internal newlines
348
- * to single spaces, trimmed, with the leading-colon fence escape stripped. */
368
+ /**
369
+ * Collapse a raw caption source span to the single-line value the control edits: internal newlines
370
+ * to single spaces, trimmed, with the leading-colon fence escape stripped.
371
+ */
349
372
  function finishCaption(raw: string): string {
350
373
  return unescapeCaption(raw.replace(/\s*\n\s*/g, ' ').trim());
351
374
  }
352
375
 
353
- /** Read the raw caption source from a figure directive, mirroring the render step's caption: the first
376
+ /**
377
+ * Read the raw caption source from a figure directive, mirroring the render step's caption: the first
354
378
  * text-bearing content after the image. The render step (remark-figure.ts) handles both caption
355
379
  * forms, so the read must too. In the no-blank-line form the caption shares the image's paragraph,
356
380
  * trailing the token, so it is read from the token end to that block's end; in the blank-line form it
357
381
  * is the first text-bearing block after the image's paragraph. Only the first such content is the
358
382
  * caption (a later block is a stray paragraph the render leaves outside the figcaption). Empty when
359
- * the figure has no caption. */
383
+ * the figure has no caption.
384
+ */
360
385
  function readCaption(doc: string, figure: ContainerDirective, image: Image): string {
361
386
  const imageStart = image.position?.start?.offset;
362
387
  const imageEnd = image.position?.end?.offset;
@@ -385,8 +410,10 @@ function readCaption(doc: string, figure: ContainerDirective, image: Image): str
385
410
  return '';
386
411
  }
387
412
 
388
- /** Whether a block's subtree carries any non-whitespace text, the caption-candidate test the render
389
- * step uses (a bare image paragraph has no text node, so it is never read as a caption). */
413
+ /**
414
+ * Whether a block's subtree carries any non-whitespace text, the caption-candidate test the render
415
+ * step uses (a bare image paragraph has no text node, so it is never read as a caption).
416
+ */
390
417
  function blockHasText(node: RootContent): boolean {
391
418
  let found = false;
392
419
  visit(node, 'text', (text) => {
@@ -418,19 +445,23 @@ export function figureAtImage(doc: string, pos: number): FigureAtImage | null {
418
445
  return { imageFrom, imageTo, figure: { from, to, caption: readCaption(doc, dir, hit.image), role } };
419
446
  }
420
447
 
421
- /** Sanitize a caption into a single safe body line: collapse internal newlines to single spaces,
448
+ /**
449
+ * Sanitize a caption into a single safe body line: collapse internal newlines to single spaces,
422
450
  * trim, and neutralize ONLY the directive-fence hazard (a leading colon would open a directive at
423
451
  * line start) by prefixing one backslash. The author's inline markdown is preserved otherwise, so
424
- * emphasis and links survive. figureAtImage strips the backslash on read for a clean round-trip. */
452
+ * emphasis and links survive. figureAtImage strips the backslash on read for a clean round-trip.
453
+ */
425
454
  function sanitizeCaption(caption: string): string {
426
455
  const line = caption.replace(/\s*\n\s*/g, ' ').trim();
427
456
  return line.startsWith(':') ? '\\' + line : line;
428
457
  }
429
458
 
430
- /** Build the canonical figure block source: the opener (with the role brace only for a non-null
459
+ /**
460
+ * Build the canonical figure block source: the opener (with the role brace only for a non-null
431
461
  * role), the image token verbatim on its own line, then a blank line and the sanitized caption when
432
462
  * the caption is non-empty, and the closing fence. This is the blank-line form remarkFigure reads as
433
- * its primary path, and it reads cleanly when hand-edited. */
463
+ * its primary path, and it reads cleanly when hand-edited.
464
+ */
434
465
  function buildFigureBlock(imageSrc: string, caption: string, role: FigureRole | null): string {
435
466
  const opener = role ? `:::figure{.${role}}` : ':::figure';
436
467
  const cap = sanitizeCaption(caption);
@@ -466,9 +497,11 @@ export function wrapImageInFigure(
466
497
  return { doc: before + inserted + after, from: end, to: end };
467
498
  }
468
499
 
469
- /** The inner image token of the figure at `figureRange.from`, sliced verbatim from the source so it
500
+ /**
501
+ * The inner image token of the figure at `figureRange.from`, sliced verbatim from the source so it
470
502
  * is reused byte-for-byte (open risk 3). Empty when no media image is found there, which leaves the
471
- * rebuild image-less rather than throwing. Shared by updateFigure and unwrapFigure. */
503
+ * rebuild image-less rather than throwing. Shared by updateFigure and unwrapFigure.
504
+ */
472
505
  function figureImageSrc(doc: string, figureRange: { from: number; to: number }): string {
473
506
  const info = figureAtImage(doc, figureRange.from);
474
507
  return info ? doc.slice(info.imageFrom, info.imageTo) : '';
@@ -0,0 +1,30 @@
1
+ // The one Markdown cheat-sheet table, the single source the editor's Markdown help dialog and the
2
+ // Help home both render, so the two surfaces cannot drift. Each row pairs the literal syntax an
3
+ // author types with a plain gloss of what it makes, grouped so the Help home can show the everyday
4
+ // rows (text and links) and the dialog can show every group. Mirrors editor-shortcuts.ts.
5
+
6
+ /** One cheat-sheet row: the literal syntax, a plain gloss, and the group it belongs to. */
7
+ export interface MarkdownReferenceRow {
8
+ syntax: string;
9
+ makes: string;
10
+ group: 'text' | 'links' | 'blocks';
11
+ }
12
+
13
+ /** The cheat-sheet vocabulary, everyday rows first: the five text, the four links, then the blocks. */
14
+ export const markdownReference: MarkdownReferenceRow[] = [
15
+ { syntax: '## Heading', makes: 'A heading', group: 'text' },
16
+ { syntax: '**bold**', makes: 'Bold text', group: 'text' },
17
+ { syntax: '*italic*', makes: 'Italic text', group: 'text' },
18
+ { syntax: '> quote', makes: 'A quote', group: 'text' },
19
+ { syntax: '`code`', makes: 'Inline code', group: 'text' },
20
+ { syntax: '[text](url)', makes: 'A link', group: 'links' },
21
+ { syntax: '[[page-name]]', makes: 'A link to one of your pages', group: 'links' },
22
+ { syntax: '- item', makes: 'A bulleted list', group: 'links' },
23
+ { syntax: '1. item', makes: 'A numbered list', group: 'links' },
24
+ { syntax: '### Heading', makes: 'A smaller heading', group: 'blocks' },
25
+ { syntax: '#### Heading', makes: 'A fourth-level heading', group: 'blocks' },
26
+ { syntax: '~~text~~', makes: 'Crossed-out text', group: 'blocks' },
27
+ { syntax: '- [ ] item', makes: 'A checklist', group: 'blocks' },
28
+ { syntax: 'Table', makes: 'The Table button in the toolbar inserts one', group: 'blocks' },
29
+ { syntax: '---', makes: 'A horizontal rule', group: 'blocks' },
30
+ ];