@glw907/cairn-cms 0.60.1 → 0.62.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/dist/components/AdminLayout.svelte +22 -0
- package/dist/components/CairnAdmin.svelte +3 -0
- package/dist/components/CairnTidySettings.svelte +2 -2
- package/dist/components/CairnTidySettings.svelte.d.ts +1 -1
- package/dist/components/EditPage.svelte +116 -39
- package/dist/components/HelpHome.svelte +824 -0
- package/dist/components/HelpHome.svelte.d.ts +22 -0
- package/dist/components/MarkdownHelpDialog.svelte +4 -15
- package/dist/components/client-ingest.d.ts +16 -8
- package/dist/components/client-ingest.js +12 -6
- package/dist/components/editor-media.js +16 -8
- package/dist/components/editor-placeholder.d.ts +4 -2
- package/dist/components/editor-tidy.d.ts +24 -12
- package/dist/components/editor-tidy.js +8 -4
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/link-completion.d.ts +12 -6
- package/dist/components/link-completion.js +12 -6
- package/dist/components/markdown-directives.d.ts +9 -6
- package/dist/components/markdown-directives.js +9 -6
- package/dist/components/markdown-format.d.ts +7 -2
- package/dist/components/markdown-format.js +59 -28
- package/dist/components/markdown-reference.d.ts +8 -0
- package/dist/components/markdown-reference.js +22 -0
- package/dist/components/media-upload-outcome.d.ts +12 -6
- package/dist/components/objective-errors.d.ts +8 -4
- package/dist/components/objective-errors.js +8 -4
- package/dist/components/preview-doc.d.ts +4 -2
- package/dist/components/preview-doc.js +4 -2
- package/dist/components/spellcheck.d.ts +55 -29
- package/dist/components/spellcheck.js +39 -21
- package/dist/components/tidy-categorize.d.ts +20 -10
- package/dist/components/tidy-categorize.js +16 -8
- package/dist/components/tidy-validate.d.ts +12 -6
- package/dist/components/tidy-validate.js +20 -10
- package/dist/components/topbar-context.d.ts +4 -2
- package/dist/content/advisories.d.ts +56 -0
- package/dist/content/advisories.js +87 -0
- package/dist/content/compose.d.ts +4 -2
- package/dist/content/compose.js +1 -0
- package/dist/content/excerpt.js +4 -2
- package/dist/content/getting-started.d.ts +18 -0
- package/dist/content/getting-started.js +12 -0
- package/dist/content/links.d.ts +16 -8
- package/dist/content/links.js +12 -6
- package/dist/content/manifest.d.ts +36 -18
- package/dist/content/manifest.js +32 -16
- package/dist/content/media-refs.d.ts +4 -2
- package/dist/content/media-refs.js +4 -2
- package/dist/content/media-rewrite.d.ts +8 -4
- package/dist/content/media-rewrite.js +76 -38
- package/dist/content/schema.d.ts +20 -10
- package/dist/content/site-dictionary.d.ts +4 -2
- package/dist/content/site-dictionary.js +8 -4
- package/dist/content/types.d.ts +97 -42
- package/dist/delivery/content-index.d.ts +16 -8
- package/dist/delivery/feeds.js +4 -2
- package/dist/delivery/json-ld.d.ts +3 -0
- package/dist/delivery/json-ld.js +3 -0
- package/dist/delivery/manifest.d.ts +4 -2
- package/dist/delivery/manifest.js +4 -2
- package/dist/delivery/public-routes.d.ts +12 -6
- package/dist/delivery/public-routes.js +4 -2
- package/dist/delivery/seo-fields.d.ts +12 -6
- package/dist/delivery/seo-fields.js +8 -4
- package/dist/delivery/site-indexes.d.ts +4 -2
- package/dist/delivery/site-resolver.d.ts +4 -2
- package/dist/delivery/site-resolver.js +4 -2
- package/dist/doctor/cloudflare-api.d.ts +6 -0
- package/dist/doctor/cloudflare-api.js +6 -0
- package/dist/doctor/index.d.ts +12 -6
- package/dist/doctor/report.d.ts +3 -0
- package/dist/doctor/report.js +3 -0
- package/dist/doctor/run.d.ts +3 -0
- package/dist/doctor/run.js +3 -0
- package/dist/doctor/types.d.ts +10 -2
- package/dist/doctor/types.js +6 -0
- package/dist/doctor/wrangler-config.d.ts +7 -2
- package/dist/doctor/wrangler-config.js +3 -0
- package/dist/email.d.ts +4 -2
- package/dist/env.d.ts +0 -3
- package/dist/env.js +0 -3
- package/dist/github/branches.d.ts +4 -2
- package/dist/github/branches.js +4 -2
- package/dist/github/signing.d.ts +1 -1
- package/dist/github/signing.js +2 -2
- package/dist/log/events.d.ts +1 -1
- package/dist/media/bulk-delete-plan.d.ts +8 -4
- package/dist/media/config.d.ts +12 -6
- package/dist/media/config.js +16 -8
- package/dist/media/delivery-bucket.d.ts +4 -2
- package/dist/media/library-entry.d.ts +4 -2
- package/dist/media/library-entry.js +4 -2
- package/dist/media/manifest.d.ts +29 -15
- package/dist/media/manifest.js +29 -16
- package/dist/media/naming.d.ts +12 -6
- package/dist/media/naming.js +24 -12
- package/dist/media/orphan-scan.d.ts +4 -2
- package/dist/media/reconcile.d.ts +21 -11
- package/dist/media/reconcile.js +12 -6
- package/dist/media/reference.d.ts +8 -4
- package/dist/media/reference.js +12 -6
- package/dist/media/rewrite-plan.d.ts +12 -6
- package/dist/media/sniff.d.ts +4 -2
- package/dist/media/sniff.js +28 -14
- package/dist/media/store.d.ts +16 -8
- package/dist/media/store.js +4 -2
- package/dist/media/transform-url.d.ts +12 -6
- package/dist/media/transform-url.js +8 -4
- package/dist/media/usage.d.ts +8 -4
- package/dist/nav/site-config.d.ts +16 -8
- package/dist/render/component-grammar.d.ts +23 -10
- package/dist/render/component-grammar.js +19 -8
- package/dist/render/component-insert.d.ts +8 -4
- package/dist/render/component-insert.js +4 -2
- package/dist/render/component-reference.d.ts +4 -2
- package/dist/render/component-reference.js +4 -2
- package/dist/render/component-validate.d.ts +3 -0
- package/dist/render/component-validate.js +3 -0
- package/dist/render/glyph.d.ts +4 -2
- package/dist/render/glyph.js +4 -2
- package/dist/render/pipeline.d.ts +20 -10
- package/dist/render/pipeline.js +4 -2
- package/dist/render/registry.d.ts +40 -20
- package/dist/render/registry.js +16 -8
- package/dist/render/rehype-dispatch.d.ts +22 -8
- package/dist/render/rehype-dispatch.js +22 -8
- package/dist/render/remark-directives.d.ts +3 -0
- package/dist/render/remark-directives.js +3 -0
- package/dist/render/remark-figure.d.ts +4 -2
- package/dist/render/remark-figure.js +4 -2
- package/dist/render/resolve-links.d.ts +4 -2
- package/dist/render/resolve-links.js +4 -2
- package/dist/render/resolve-media.d.ts +16 -8
- package/dist/render/resolve-media.js +12 -6
- package/dist/sveltekit/admin-dispatch.d.ts +2 -0
- package/dist/sveltekit/admin-dispatch.js +9 -3
- package/dist/sveltekit/auth-routes.d.ts +3 -0
- package/dist/sveltekit/auth-routes.js +3 -0
- package/dist/sveltekit/cairn-admin.d.ts +16 -5
- package/dist/sveltekit/cairn-admin.js +26 -10
- package/dist/sveltekit/content-routes.d.ts +191 -86
- package/dist/sveltekit/content-routes.js +297 -107
- package/dist/sveltekit/editors-routes.d.ts +3 -0
- package/dist/sveltekit/editors-routes.js +3 -0
- package/dist/sveltekit/guard.d.ts +4 -2
- package/dist/sveltekit/guard.js +4 -2
- package/dist/sveltekit/https-required-page.d.ts +1 -1
- package/dist/sveltekit/https-required-page.js +1 -1
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/media-route.d.ts +1 -2
- package/dist/sveltekit/media-route.js +13 -8
- package/dist/sveltekit/nav-routes.d.ts +7 -2
- package/dist/sveltekit/nav-routes.js +3 -0
- package/dist/sveltekit/types.d.ts +4 -2
- package/dist/vite/index.d.ts +32 -16
- package/dist/vite/index.js +52 -26
- package/dist/vite/resolve-root.d.ts +8 -4
- package/dist/vite/resolve-root.js +4 -2
- package/package.json +7 -1
- package/src/lib/components/AdminLayout.svelte +22 -0
- package/src/lib/components/CairnAdmin.svelte +3 -0
- package/src/lib/components/CairnTidySettings.svelte +2 -2
- package/src/lib/components/ComponentForm.svelte +0 -1
- package/src/lib/components/EditPage.svelte +133 -41
- package/src/lib/components/HelpHome.svelte +850 -0
- package/src/lib/components/MarkdownHelpDialog.svelte +4 -15
- package/src/lib/components/client-ingest.ts +20 -10
- package/src/lib/components/editor-media.ts +20 -10
- package/src/lib/components/editor-placeholder.ts +12 -6
- package/src/lib/components/editor-tidy.ts +28 -14
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/link-completion.ts +12 -6
- package/src/lib/components/markdown-directives.ts +13 -8
- package/src/lib/components/markdown-format.ts +63 -30
- package/src/lib/components/markdown-reference.ts +30 -0
- package/src/lib/components/media-upload-outcome.ts +12 -6
- package/src/lib/components/objective-errors.ts +16 -8
- package/src/lib/components/preview-doc.ts +4 -2
- package/src/lib/components/spellcheck.ts +79 -41
- package/src/lib/components/tidy-categorize.ts +28 -14
- package/src/lib/components/tidy-validate.ts +28 -14
- package/src/lib/components/topbar-context.ts +4 -2
- package/src/lib/content/advisories.ts +150 -0
- package/src/lib/content/compose.ts +5 -2
- package/src/lib/content/excerpt.ts +4 -2
- package/src/lib/content/getting-started.ts +31 -0
- package/src/lib/content/links.ts +16 -8
- package/src/lib/content/manifest.ts +36 -18
- package/src/lib/content/media-refs.ts +4 -2
- package/src/lib/content/media-rewrite.ts +100 -50
- package/src/lib/content/schema.ts +20 -10
- package/src/lib/content/site-dictionary.ts +8 -4
- package/src/lib/content/types.ts +97 -42
- package/src/lib/delivery/content-index.ts +16 -8
- package/src/lib/delivery/feeds.ts +4 -2
- package/src/lib/delivery/json-ld.ts +3 -0
- package/src/lib/delivery/manifest.ts +4 -2
- package/src/lib/delivery/public-routes.ts +16 -8
- package/src/lib/delivery/seo-fields.ts +12 -6
- package/src/lib/delivery/site-indexes.ts +4 -2
- package/src/lib/delivery/site-resolver.ts +4 -2
- package/src/lib/doctor/cloudflare-api.ts +6 -0
- package/src/lib/doctor/index.ts +12 -6
- package/src/lib/doctor/report.ts +3 -0
- package/src/lib/doctor/run.ts +3 -0
- package/src/lib/doctor/types.ts +10 -2
- package/src/lib/doctor/wrangler-config.ts +7 -2
- package/src/lib/email.ts +4 -2
- package/src/lib/env.ts +0 -3
- package/src/lib/github/branches.ts +4 -2
- package/src/lib/github/signing.ts +2 -2
- package/src/lib/log/events.ts +1 -0
- package/src/lib/media/bulk-delete-plan.ts +8 -4
- package/src/lib/media/config.ts +24 -12
- package/src/lib/media/delivery-bucket.ts +4 -2
- package/src/lib/media/library-entry.ts +4 -2
- package/src/lib/media/manifest.ts +33 -18
- package/src/lib/media/naming.ts +24 -12
- package/src/lib/media/orphan-scan.ts +4 -2
- package/src/lib/media/reconcile.ts +21 -11
- package/src/lib/media/reference.ts +12 -6
- package/src/lib/media/rewrite-plan.ts +12 -6
- package/src/lib/media/sniff.ts +28 -14
- package/src/lib/media/store.ts +16 -8
- package/src/lib/media/transform-url.ts +12 -6
- package/src/lib/media/usage.ts +8 -4
- package/src/lib/nav/site-config.ts +16 -8
- package/src/lib/render/component-grammar.ts +23 -10
- package/src/lib/render/component-insert.ts +8 -4
- package/src/lib/render/component-reference.ts +4 -2
- package/src/lib/render/component-validate.ts +3 -0
- package/src/lib/render/glyph.ts +4 -2
- package/src/lib/render/pipeline.ts +20 -10
- package/src/lib/render/registry.ts +44 -22
- package/src/lib/render/rehype-dispatch.ts +22 -8
- package/src/lib/render/remark-directives.ts +3 -0
- package/src/lib/render/remark-figure.ts +4 -2
- package/src/lib/render/resolve-links.ts +4 -2
- package/src/lib/render/resolve-media.ts +16 -8
- package/src/lib/sveltekit/admin-dispatch.ts +10 -4
- package/src/lib/sveltekit/auth-routes.ts +3 -0
- package/src/lib/sveltekit/cairn-admin.ts +37 -15
- package/src/lib/sveltekit/content-routes.ts +494 -197
- package/src/lib/sveltekit/editors-routes.ts +3 -0
- package/src/lib/sveltekit/guard.ts +4 -2
- package/src/lib/sveltekit/https-required-page.ts +1 -1
- package/src/lib/sveltekit/index.ts +3 -0
- package/src/lib/sveltekit/media-route.ts +13 -8
- package/src/lib/sveltekit/nav-routes.ts +7 -2
- package/src/lib/sveltekit/types.ts +4 -2
- package/src/lib/vite/index.ts +60 -30
- package/src/lib/vite/resolve-root.ts +8 -4
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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>> 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
|
-
/**
|
|
122
|
-
*
|
|
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
|
-
/**
|
|
126
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
258
|
-
*
|
|
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
|
-
/**
|
|
377
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
142
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
193
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
28
|
-
*
|
|
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
|
-
/**
|
|
91
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
179
|
-
*
|
|
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
|
-
/**
|
|
182
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
204
|
-
*
|
|
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
|
-
/**
|
|
25
|
-
*
|
|
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
|
-
/**
|
|
32
|
-
*
|
|
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
|
-
/**
|
|
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{...}`
|
|
22
|
-
* `::::cta[Book]`
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
165
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
63
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
101
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
254
|
-
*
|
|
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
|
-
/**
|
|
285
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
326
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
348
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
389
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
+
];
|