@glw907/cairn-cms 0.41.0 → 0.51.0
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 +82 -0
- package/README.md +2 -2
- package/dist/ambient.d.ts +9 -0
- package/dist/ambient.js +1 -0
- package/dist/components/AdminLayout.svelte +6 -8
- package/dist/components/CairnAdmin.svelte +67 -0
- package/dist/components/CairnAdmin.svelte.d.ts +35 -0
- package/dist/components/ConceptList.svelte +4 -5
- package/dist/components/ConceptList.svelte.d.ts +4 -8
- package/dist/components/ConfirmPage.svelte +1 -1
- package/dist/components/EditPage.svelte +107 -25
- package/dist/components/EditPage.svelte.d.ts +8 -10
- package/dist/components/EditorToolbar.svelte +79 -8
- package/dist/components/EditorToolbar.svelte.d.ts +10 -2
- package/dist/components/LoginPage.svelte +2 -2
- package/dist/components/LoginPage.svelte.d.ts +1 -1
- package/dist/components/ManageEditors.svelte +4 -3
- package/dist/components/ManageEditors.svelte.d.ts +2 -1
- package/dist/components/MarkdownEditor.svelte +20 -2
- package/dist/components/cairn-admin.css +57 -9
- package/dist/components/editor-highlight.d.ts +1 -0
- package/dist/components/editor-highlight.js +31 -8
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/markdown-directives.d.ts +10 -0
- package/dist/components/markdown-directives.js +54 -1
- package/dist/components/markdown-format.d.ts +0 -8
- package/dist/components/markdown-format.js +0 -28
- package/dist/components/preview-doc.d.ts +27 -0
- package/dist/components/preview-doc.js +64 -0
- package/dist/content/compose.js +1 -0
- package/dist/content/links.d.ts +8 -0
- package/dist/content/links.js +28 -0
- package/dist/content/types.d.ts +35 -2
- package/dist/delivery/data.d.ts +3 -5
- package/dist/delivery/data.js +2 -3
- package/dist/delivery/feeds.js +1 -7
- package/dist/delivery/index.d.ts +2 -2
- package/dist/delivery/index.js +1 -1
- package/dist/delivery/manifest.d.ts +0 -5
- package/dist/delivery/manifest.js +5 -16
- package/dist/{sveltekit → delivery}/public-routes.d.ts +4 -4
- package/dist/{sveltekit → delivery}/public-routes.js +7 -7
- package/dist/delivery/site-indexes.d.ts +3 -3
- package/dist/delivery/site-indexes.js +3 -3
- package/dist/delivery/{site-index.d.ts → site-resolver.d.ts} +7 -3
- package/dist/delivery/{site-index.js → site-resolver.js} +13 -3
- package/dist/delivery/sitemap.js +1 -3
- package/dist/delivery/xml.d.ts +2 -0
- package/dist/delivery/xml.js +11 -0
- package/dist/diagnostics/conditions.js +24 -0
- package/dist/doctor/bin.js +30 -12
- package/dist/doctor/check-floors.d.ts +15 -0
- package/dist/doctor/check-floors.js +107 -0
- package/dist/doctor/check-probe.d.ts +3 -0
- package/dist/doctor/check-probe.js +123 -0
- package/dist/doctor/checks-github.js +1 -1
- package/dist/doctor/checks-local.d.ts +1 -0
- package/dist/doctor/checks-local.js +28 -2
- package/dist/doctor/cloudflare-api.js +2 -2
- package/dist/doctor/index.d.ts +28 -3
- package/dist/doctor/index.js +47 -6
- package/dist/doctor/types.d.ts +2 -0
- package/dist/doctor/wrangler-config.d.ts +4 -0
- package/dist/doctor/wrangler-config.js +11 -0
- package/dist/email.js +4 -11
- package/dist/env.d.ts +3 -2
- package/dist/env.js +12 -6
- package/dist/escape.d.ts +2 -0
- package/dist/escape.js +11 -0
- package/dist/github/credentials.d.ts +2 -1
- package/dist/github/credentials.js +10 -2
- package/dist/github/types.d.ts +2 -0
- package/dist/github/types.js +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/log/events.d.ts +1 -1
- package/dist/nav/site-config.d.ts +2 -0
- package/dist/nav/site-config.js +2 -0
- package/dist/sveltekit/admin-dispatch.d.ts +28 -0
- package/dist/sveltekit/admin-dispatch.js +62 -0
- package/dist/sveltekit/cairn-admin.d.ts +94 -0
- package/dist/sveltekit/cairn-admin.js +126 -0
- package/dist/sveltekit/condition-response.d.ts +1 -0
- package/dist/sveltekit/condition-response.js +25 -0
- package/dist/sveltekit/content-routes.d.ts +39 -15
- package/dist/sveltekit/content-routes.js +84 -50
- package/dist/sveltekit/guard.d.ts +8 -2
- package/dist/sveltekit/guard.js +18 -4
- package/dist/sveltekit/https-required-page.js +2 -1
- package/dist/sveltekit/index.d.ts +3 -1
- package/dist/sveltekit/index.js +2 -0
- package/dist/sveltekit/nav-routes.d.ts +3 -1
- package/dist/sveltekit/nav-routes.js +22 -19
- package/dist/sveltekit/static-admin-page.d.ts +0 -2
- package/dist/sveltekit/static-admin-page.js +1 -8
- package/dist/sveltekit/types.d.ts +18 -11
- package/dist/vite/index.d.ts +16 -0
- package/dist/vite/index.js +57 -13
- package/package.json +6 -2
- package/src/lib/ambient.ts +19 -0
- package/src/lib/components/AdminLayout.svelte +6 -8
- package/src/lib/components/CairnAdmin.svelte +67 -0
- package/src/lib/components/ConceptList.svelte +4 -5
- package/src/lib/components/ConfirmPage.svelte +1 -1
- package/src/lib/components/EditPage.svelte +107 -25
- package/src/lib/components/EditorToolbar.svelte +79 -8
- package/src/lib/components/LoginPage.svelte +2 -2
- package/src/lib/components/ManageEditors.svelte +4 -3
- package/src/lib/components/MarkdownEditor.svelte +20 -2
- package/src/lib/components/cairn-admin.css +59 -0
- package/src/lib/components/editor-highlight.ts +32 -7
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdown-directives.ts +51 -1
- package/src/lib/components/markdown-format.ts +0 -27
- package/src/lib/components/preview-doc.ts +82 -0
- package/src/lib/content/compose.ts +1 -0
- package/src/lib/content/links.ts +28 -0
- package/src/lib/content/types.ts +34 -2
- package/src/lib/delivery/data.ts +3 -5
- package/src/lib/delivery/feeds.ts +1 -8
- package/src/lib/delivery/index.ts +2 -2
- package/src/lib/delivery/manifest.ts +5 -18
- package/src/lib/{sveltekit → delivery}/public-routes.ts +11 -11
- package/src/lib/delivery/site-indexes.ts +6 -6
- package/src/lib/delivery/{site-index.ts → site-resolver.ts} +20 -8
- package/src/lib/delivery/sitemap.ts +1 -4
- package/src/lib/delivery/xml.ts +12 -0
- package/src/lib/diagnostics/conditions.ts +24 -0
- package/src/lib/doctor/bin.ts +35 -10
- package/src/lib/doctor/check-floors.ts +124 -0
- package/src/lib/doctor/check-probe.ts +138 -0
- package/src/lib/doctor/checks-github.ts +3 -1
- package/src/lib/doctor/checks-local.ts +28 -2
- package/src/lib/doctor/cloudflare-api.ts +4 -2
- package/src/lib/doctor/index.ts +67 -6
- package/src/lib/doctor/types.ts +2 -0
- package/src/lib/doctor/wrangler-config.ts +11 -0
- package/src/lib/email.ts +4 -11
- package/src/lib/env.ts +12 -6
- package/src/lib/escape.ts +12 -0
- package/src/lib/github/credentials.ts +6 -2
- package/src/lib/github/types.ts +5 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/log/events.ts +1 -0
- package/src/lib/nav/site-config.ts +3 -0
- package/src/lib/sveltekit/admin-dispatch.ts +75 -0
- package/src/lib/sveltekit/cairn-admin.ts +177 -0
- package/src/lib/sveltekit/condition-response.ts +27 -1
- package/src/lib/sveltekit/content-routes.ts +131 -62
- package/src/lib/sveltekit/guard.ts +20 -5
- package/src/lib/sveltekit/https-required-page.ts +2 -1
- package/src/lib/sveltekit/index.ts +6 -0
- package/src/lib/sveltekit/nav-routes.ts +24 -21
- package/src/lib/sveltekit/static-admin-page.ts +1 -9
- package/src/lib/sveltekit/types.ts +16 -7
- package/src/lib/vite/index.ts +71 -17
- package/dist/delivery/paginate.d.ts +0 -12
- package/dist/delivery/paginate.js +0 -20
- package/dist/render/index.d.ts +0 -5
- package/dist/render/index.js +0 -8
- package/src/lib/delivery/paginate.ts +0 -32
- package/src/lib/render/index.ts +0 -8
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
The editor card's instrument strip. Three button groups divided by hairlines (Text, Structure with a
|
|
4
4
|
More overflow menu, then the host's Insert controls) and the Write/Preview segmented control pinned
|
|
5
5
|
right. Format buttons ask the host to transform the editor's current selection; the host supplies the
|
|
6
|
-
Insert group through the `insertControls` snippet so the strip stays free of picker wiring.
|
|
7
|
-
|
|
6
|
+
Insert group through the `insertControls` snippet so the strip stays free of picker wiring. While
|
|
7
|
+
Preview shows, a device trigger joins the segmented capsule and opens a popover menu of preview
|
|
8
|
+
widths, reported to the host through `onDevice`. The glyphs are stroke SVG icons in the admin's
|
|
9
|
+
house style (24x24 viewBox, `currentColor`, round caps).
|
|
8
10
|
-->
|
|
9
11
|
<script lang="ts">
|
|
10
12
|
import type { Snippet } from 'svelte';
|
|
11
13
|
import type { FormatKind } from './markdown-format.js';
|
|
14
|
+
import { deviceLabel, previewDevice, previewDevices, type PreviewDeviceId } from './preview-doc.js';
|
|
12
15
|
|
|
13
16
|
interface Props {
|
|
14
17
|
/** Apply a markdown transform to the editor's current selection. */
|
|
@@ -17,11 +20,16 @@ are stroke SVG icons in the admin's house style (24x24 viewBox, `currentColor`,
|
|
|
17
20
|
mode: 'write' | 'preview';
|
|
18
21
|
/** Ask the host to switch panes. */
|
|
19
22
|
onMode: (m: 'write' | 'preview') => void;
|
|
23
|
+
/** The active preview-frame device, shown on the device trigger. Desktop when absent. */
|
|
24
|
+
device?: PreviewDeviceId;
|
|
25
|
+
/** Pick a preview-frame width. When set, a device trigger joins the Write/Preview capsule
|
|
26
|
+
* while Preview shows. */
|
|
27
|
+
onDevice?: (id: PreviewDeviceId) => void;
|
|
20
28
|
/** The host's Insert controls (link picker, component insert, image), rendered in the Insert group. */
|
|
21
29
|
insertControls?: Snippet;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
let { format, mode, onMode, insertControls }: Props = $props();
|
|
32
|
+
let { format, mode, onMode, device = 'desktop', onDevice, insertControls }: Props = $props();
|
|
25
33
|
|
|
26
34
|
// Each icon is a set of stroke `<path>` d-strings rendered into the shared 24x24 svg below, so the
|
|
27
35
|
// markup stays declarative (no per-icon raw html). Paths follow the house outline style.
|
|
@@ -89,6 +97,19 @@ are stroke SVG icons in the admin's house style (24x24 viewBox, `currentColor`,
|
|
|
89
97
|
if (moreMenu?.matches(':popover-open')) moreMenu.hidePopover();
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
// The device menu's popover element and its open state, mirrored from the toggle event into
|
|
101
|
+
// aria-expanded on the trigger (the More menu's pattern).
|
|
102
|
+
let deviceMenu = $state<HTMLUListElement | null>(null);
|
|
103
|
+
let deviceOpen = $state(false);
|
|
104
|
+
const activeDevice = $derived(previewDevice(device));
|
|
105
|
+
// Whether the device trigger renders as the capsule's third segment.
|
|
106
|
+
const showDeviceTrigger = $derived(mode === 'preview' && !!onDevice);
|
|
107
|
+
|
|
108
|
+
function pickDevice(id: PreviewDeviceId) {
|
|
109
|
+
onDevice?.(id);
|
|
110
|
+
if (deviceMenu?.matches(':popover-open')) deviceMenu.hidePopover();
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
let toolbarEl = $state<HTMLDivElement | null>(null);
|
|
93
114
|
// The roving tab stop's position among the strip's enabled top-level controls. The Write/Preview
|
|
94
115
|
// tabs join the toolbar's roving order instead of managing their own arrow keys: the ARIA toolbar
|
|
@@ -156,13 +177,20 @@ are stroke SVG icons in the admin's house style (24x24 viewBox, `currentColor`,
|
|
|
156
177
|
{/snippet}
|
|
157
178
|
|
|
158
179
|
{#snippet tab(m: 'write' | 'preview', label: string)}
|
|
180
|
+
<!-- The capsule look is manual rounding, not daisyUI's .join: join radii follow direct
|
|
181
|
+
children, and the device trigger must sit outside the tablist (ARIA required children),
|
|
182
|
+
so the segments square their shared edges themselves. Preview squares its right edge only
|
|
183
|
+
while the trigger extends the capsule. -->
|
|
159
184
|
<button
|
|
160
185
|
type="button"
|
|
161
186
|
role="tab"
|
|
162
187
|
id={`cairn-tab-${m}`}
|
|
163
188
|
aria-selected={mode === m}
|
|
164
189
|
aria-controls={`cairn-pane-${m}`}
|
|
165
|
-
class="
|
|
190
|
+
class="btn btn-sm {mode === m ? 'btn-active' : 'btn-ghost'}"
|
|
191
|
+
class:rounded-r-none={m === 'write' || showDeviceTrigger}
|
|
192
|
+
class:rounded-l-none={m === 'preview'}
|
|
193
|
+
class:-ml-px={m === 'preview'}
|
|
166
194
|
onclick={() => onMode(m)}
|
|
167
195
|
>
|
|
168
196
|
{label}
|
|
@@ -230,9 +258,52 @@ are stroke SVG icons in the admin's house style (24x24 viewBox, `currentColor`,
|
|
|
230
258
|
{/if}
|
|
231
259
|
|
|
232
260
|
<!-- The host renders the matching tabpanels (#cairn-pane-write and #cairn-pane-preview) below
|
|
233
|
-
the strip inside the same editor card.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
261
|
+
the strip inside the same editor card. The tablist wrapper holds ONLY the two tabs (ARIA
|
|
262
|
+
required children: anything else in a tablist makes assistive tech miscount the tabs).
|
|
263
|
+
While Preview shows, the device trigger reads as the capsule's third segment from the
|
|
264
|
+
flex row right after the wrapper; it is a plain button, not a tab. -->
|
|
265
|
+
<div class="ml-auto flex items-center">
|
|
266
|
+
<div role="tablist" aria-label="Editor view" class="flex items-center">
|
|
267
|
+
{@render tab('write', 'Write')}
|
|
268
|
+
{@render tab('preview', 'Preview')}
|
|
269
|
+
</div>
|
|
270
|
+
{#if showDeviceTrigger}
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
class="btn btn-sm btn-ghost gap-1 rounded-l-none -ml-px"
|
|
274
|
+
title="Preview width"
|
|
275
|
+
aria-expanded={deviceOpen}
|
|
276
|
+
popovertarget="cairn-preview-device-menu"
|
|
277
|
+
style="anchor-name:--cairn-preview-device"
|
|
278
|
+
>
|
|
279
|
+
<span class="sr-only">Preview width:</span>
|
|
280
|
+
{activeDevice.label}
|
|
281
|
+
{@render strokeIcon(['m6 9 6 6 6-6'])}
|
|
282
|
+
</button>
|
|
283
|
+
{/if}
|
|
237
284
|
</div>
|
|
285
|
+
{#if showDeviceTrigger}
|
|
286
|
+
<!-- The device list mirrors the More menu exactly: a DaisyUI v5 popover dropdown of plain
|
|
287
|
+
buttons, with the active pick carried by aria-pressed and the check glyph. Deliberately
|
|
288
|
+
NOT the ARIA menu pattern: menu roles promise interactions this list does not have. -->
|
|
289
|
+
<ul
|
|
290
|
+
bind:this={deviceMenu}
|
|
291
|
+
popover="auto"
|
|
292
|
+
id="cairn-preview-device-menu"
|
|
293
|
+
style="position-anchor:--cairn-preview-device"
|
|
294
|
+
ontoggle={(e) => (deviceOpen = e.newState === 'open')}
|
|
295
|
+
class="dropdown dropdown-end menu menu-sm bg-base-100 rounded-box w-44 border border-[var(--cairn-card-border)] p-1 shadow-[var(--cairn-shadow)]"
|
|
296
|
+
>
|
|
297
|
+
{#each previewDevices as d (d.id)}
|
|
298
|
+
<li>
|
|
299
|
+
<button type="button" aria-pressed={device === d.id} onclick={() => pickDevice(d.id)}>
|
|
300
|
+
<span class="grow">{deviceLabel(d)}</span>
|
|
301
|
+
{#if device === d.id}
|
|
302
|
+
{@render strokeIcon(['M20 6 9 17l-5-5'])}
|
|
303
|
+
{/if}
|
|
304
|
+
</button>
|
|
305
|
+
</li>
|
|
306
|
+
{/each}
|
|
307
|
+
</ul>
|
|
308
|
+
{/if}
|
|
238
309
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { FormatKind } from './markdown-format.js';
|
|
3
|
+
import { type PreviewDeviceId } from './preview-doc.js';
|
|
3
4
|
interface Props {
|
|
4
5
|
/** Apply a markdown transform to the editor's current selection. */
|
|
5
6
|
format: (kind: FormatKind) => void;
|
|
@@ -7,6 +8,11 @@ interface Props {
|
|
|
7
8
|
mode: 'write' | 'preview';
|
|
8
9
|
/** Ask the host to switch panes. */
|
|
9
10
|
onMode: (m: 'write' | 'preview') => void;
|
|
11
|
+
/** The active preview-frame device, shown on the device trigger. Desktop when absent. */
|
|
12
|
+
device?: PreviewDeviceId;
|
|
13
|
+
/** Pick a preview-frame width. When set, a device trigger joins the Write/Preview capsule
|
|
14
|
+
* while Preview shows. */
|
|
15
|
+
onDevice?: (id: PreviewDeviceId) => void;
|
|
10
16
|
/** The host's Insert controls (link picker, component insert, image), rendered in the Insert group. */
|
|
11
17
|
insertControls?: Snippet;
|
|
12
18
|
}
|
|
@@ -14,8 +20,10 @@ interface Props {
|
|
|
14
20
|
* The editor card's instrument strip. Three button groups divided by hairlines (Text, Structure with a
|
|
15
21
|
* More overflow menu, then the host's Insert controls) and the Write/Preview segmented control pinned
|
|
16
22
|
* right. Format buttons ask the host to transform the editor's current selection; the host supplies the
|
|
17
|
-
* Insert group through the `insertControls` snippet so the strip stays free of picker wiring.
|
|
18
|
-
*
|
|
23
|
+
* Insert group through the `insertControls` snippet so the strip stays free of picker wiring. While
|
|
24
|
+
* Preview shows, a device trigger joins the segmented capsule and opens a popover menu of preview
|
|
25
|
+
* widths, reported to the host through `onDevice`. The glyphs are stroke SVG icons in the admin's
|
|
26
|
+
* house style (24x24 viewBox, `currentColor`, round caps).
|
|
19
27
|
*/
|
|
20
28
|
declare const EditorToolbar: import("svelte").Component<Props, {}, "">;
|
|
21
29
|
type EditorToolbar = ReturnType<typeof EditorToolbar>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
|
-
The magic-link sign-in page. A plain form POST to the
|
|
3
|
+
The magic-link sign-in page. A plain form POST to the named `?/request` action (the engine's
|
|
4
4
|
`requestAction`); no client SDK. The success message is identical whether or not the email is on
|
|
5
5
|
the allowlist, so the page never leaks membership (spec §7.1).
|
|
6
6
|
-->
|
|
@@ -101,7 +101,7 @@ the allowlist, so the page never leaks membership (spec §7.1).
|
|
|
101
101
|
{#if data.error && !form?.status}
|
|
102
102
|
<div role="alert" class="alert alert-error mb-3 text-sm">That link expired. Request a new one below.</div>
|
|
103
103
|
{/if}
|
|
104
|
-
<form method="POST" class="flex flex-col gap-3">
|
|
104
|
+
<form method="POST" action="?/request" class="flex flex-col gap-3">
|
|
105
105
|
<CsrfField token={data.csrf} />
|
|
106
106
|
<label class="flex flex-col gap-1">
|
|
107
107
|
<span class="text-sm font-medium">Email</span>
|
|
@@ -14,7 +14,7 @@ interface Props {
|
|
|
14
14
|
} | null;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* The magic-link sign-in page. A plain form POST to the
|
|
17
|
+
* The magic-link sign-in page. A plain form POST to the named `?/request` action (the engine's
|
|
18
18
|
* `requestAction`); no client SDK. The success message is identical whether or not the email is on
|
|
19
19
|
* the allowlist, so the page never leaks membership (spec §7.1).
|
|
20
20
|
*/
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
The owner-gated editor management surface: a table of editors with role-flip and remove actions,
|
|
4
4
|
and an add-editor form. The acting owner's own row disables its destructive controls; the
|
|
5
5
|
last-owner anti-lockout rule itself is enforced server-side (editors-routes). Actions post to the
|
|
6
|
-
named `?/setRole`, `?/
|
|
6
|
+
named `?/setRole`, `?/removeEditor`, and `?/addEditor` actions, the names the single-mount
|
|
7
|
+
dispatcher defines.
|
|
7
8
|
-->
|
|
8
9
|
<script lang="ts">
|
|
9
10
|
import CsrfField from './CsrfField.svelte';
|
|
@@ -53,7 +54,7 @@ named `?/setRole`, `?/remove`, and `?/add` actions.
|
|
|
53
54
|
{editor.role === 'owner' ? 'Make editor' : 'Make owner'}
|
|
54
55
|
</button>
|
|
55
56
|
</form>
|
|
56
|
-
<form method="POST" action="?/
|
|
57
|
+
<form method="POST" action="?/removeEditor">
|
|
57
58
|
<CsrfField />
|
|
58
59
|
<input type="hidden" name="email" value={editor.email} />
|
|
59
60
|
<button type="submit" class="btn btn-ghost btn-xs text-error" disabled={isSelf} aria-label={`Remove ${editor.displayName}`}>
|
|
@@ -67,7 +68,7 @@ named `?/setRole`, `?/remove`, and `?/add` actions.
|
|
|
67
68
|
</table>
|
|
68
69
|
</div>
|
|
69
70
|
|
|
70
|
-
<form method="POST" action="?/
|
|
71
|
+
<form method="POST" action="?/addEditor" class="rounded-box border border-[var(--cairn-card-border)] bg-base-100 grid gap-3 p-4 shadow-[var(--cairn-shadow)] sm:grid-cols-[1fr_1fr_auto_auto] sm:items-end">
|
|
71
72
|
<CsrfField />
|
|
72
73
|
<label class="flex flex-col gap-1">
|
|
73
74
|
<span class="text-sm font-medium">Name</span>
|
|
@@ -15,7 +15,8 @@ interface Props {
|
|
|
15
15
|
* The owner-gated editor management surface: a table of editors with role-flip and remove actions,
|
|
16
16
|
* and an add-editor form. The acting owner's own row disables its destructive controls; the
|
|
17
17
|
* last-owner anti-lockout rule itself is enforced server-side (editors-routes). Actions post to the
|
|
18
|
-
* named `?/setRole`, `?/
|
|
18
|
+
* named `?/setRole`, `?/removeEditor`, and `?/addEditor` actions, the names the single-mount
|
|
19
|
+
* dispatcher defines.
|
|
19
20
|
*/
|
|
20
21
|
declare const ManageEditors: import("svelte").Component<Props, {}, "">;
|
|
21
22
|
type ManageEditors = ReturnType<typeof ManageEditors>;
|
|
@@ -61,9 +61,16 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
61
61
|
// Mirror the admin theme into CodeMirror's own dark flag, so its base chrome (the autocomplete
|
|
62
62
|
// tooltip above all) renders dark-on-dark instead of light-on-dark.
|
|
63
63
|
const isDark = host.closest('[data-theme]')?.getAttribute('data-theme')?.includes('dark') ?? false;
|
|
64
|
-
// The directive machinery
|
|
64
|
+
// The directive machinery treatment. The fence bands and content rails step their alpha by
|
|
65
|
+
// nesting depth through the per-theme vars in cairn-admin.css; the fallbacks are the light
|
|
66
|
+
// values, so the editor still renders sensibly outside an admin theme wrapper. The deeper
|
|
67
|
+
// bands swap in a darker ink (--cairn-directive-ink-N) to hold AA on their own tint.
|
|
68
|
+
const band = (depth: number, fallback: string) =>
|
|
69
|
+
`color-mix(in oklab, var(--color-accent) var(--cairn-directive-band-${depth}, ${fallback}), transparent)`;
|
|
70
|
+
const rail = (depth: number, fallback: string) =>
|
|
71
|
+
`inset 2px 0 0 0 color-mix(in oklab, var(--color-accent) var(--cairn-directive-rail-${depth}, ${fallback}), transparent)`;
|
|
65
72
|
const directiveInk = {
|
|
66
|
-
backgroundColor:
|
|
73
|
+
backgroundColor: band(1, '8%'),
|
|
67
74
|
color: 'var(--color-accent)',
|
|
68
75
|
};
|
|
69
76
|
const theme = EditorView.theme(
|
|
@@ -90,6 +97,17 @@ through the adapter's render. Swapping the editor stays a one-file change.
|
|
|
90
97
|
},
|
|
91
98
|
'.cm-line': { padding: '0' },
|
|
92
99
|
'.cm-cairn-directive-fence': directiveInk,
|
|
100
|
+
'.cm-cairn-directive-fence.cm-cairn-depth-2': {
|
|
101
|
+
backgroundColor: band(2, '14%'),
|
|
102
|
+
color: 'var(--cairn-directive-ink-2, oklch(50% 0.16 300))',
|
|
103
|
+
},
|
|
104
|
+
'.cm-cairn-directive-fence.cm-cairn-depth-3': {
|
|
105
|
+
backgroundColor: band(3, '20%'),
|
|
106
|
+
color: 'var(--cairn-directive-ink-3, oklch(48% 0.16 300))',
|
|
107
|
+
},
|
|
108
|
+
'.cm-cairn-directive-content.cm-cairn-depth-1': { boxShadow: rail(1, '75%') },
|
|
109
|
+
'.cm-cairn-directive-content.cm-cairn-depth-2': { boxShadow: rail(2, '82%') },
|
|
110
|
+
'.cm-cairn-directive-content.cm-cairn-depth-3': { boxShadow: rail(3, '90%') },
|
|
93
111
|
'.cm-cairn-directive-leaf': directiveInk,
|
|
94
112
|
'.cm-cairn-directive-inline': directiveInk,
|
|
95
113
|
},
|
|
@@ -176,6 +176,14 @@
|
|
|
176
176
|
outline-offset: -1px;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
:where([data-theme="cairn-admin"], [data-theme="cairn-admin-dark"]) .menu li > button:not(.btn) {
|
|
180
|
+
font: inherit;
|
|
181
|
+
color: inherit;
|
|
182
|
+
text-align: start;
|
|
183
|
+
background-color: #0000;
|
|
184
|
+
border: 0 solid;
|
|
185
|
+
}
|
|
186
|
+
|
|
179
187
|
:where([data-theme="cairn-admin"], [data-theme="cairn-admin-dark"]) :is(a, button, input, textarea, select, summary, [tabindex]) {
|
|
180
188
|
scroll-margin-top: 8.5rem;
|
|
181
189
|
}
|
|
@@ -3400,15 +3408,6 @@
|
|
|
3400
3408
|
}
|
|
3401
3409
|
}
|
|
3402
3410
|
|
|
3403
|
-
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .join-item:where(:not(:first-child, :disabled, [disabled], .btn-disabled)) {
|
|
3404
|
-
margin-block-start: 0;
|
|
3405
|
-
margin-inline-start: calc(var(--border, 1px) * -1);
|
|
3406
|
-
}
|
|
3407
|
-
|
|
3408
|
-
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .join-item:where(:is(:disabled, [disabled], .btn-disabled)) {
|
|
3409
|
-
border-width: var(--border, 1px) 0 var(--border, 1px) var(--border, 1px);
|
|
3410
|
-
}
|
|
3411
|
-
|
|
3412
3411
|
@layer daisyui.l1.l2.l3 {
|
|
3413
3412
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .modal-action {
|
|
3414
3413
|
justify-content: flex-end;
|
|
@@ -3503,6 +3502,10 @@
|
|
|
3503
3502
|
margin-bottom: calc(var(--spacing) * 7);
|
|
3504
3503
|
}
|
|
3505
3504
|
|
|
3505
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .-ml-px {
|
|
3506
|
+
margin-left: -1px;
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3506
3509
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .ml-1 {
|
|
3507
3510
|
margin-left: calc(var(--spacing) * 1);
|
|
3508
3511
|
}
|
|
@@ -3919,6 +3922,10 @@
|
|
|
3919
3922
|
height: calc(var(--spacing) * 16);
|
|
3920
3923
|
}
|
|
3921
3924
|
|
|
3925
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .h-\[70vh\] {
|
|
3926
|
+
height: 70vh;
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3922
3929
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-h-\[60vh\] {
|
|
3923
3930
|
max-height: 60vh;
|
|
3924
3931
|
}
|
|
@@ -4037,6 +4044,10 @@
|
|
|
4037
4044
|
max-width: 30%;
|
|
4038
4045
|
}
|
|
4039
4046
|
|
|
4047
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-full {
|
|
4048
|
+
max-width: 100%;
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4040
4051
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-md {
|
|
4041
4052
|
max-width: var(--container-md);
|
|
4042
4053
|
}
|
|
@@ -4337,6 +4348,16 @@
|
|
|
4337
4348
|
border-radius: var(--radius-xl);
|
|
4338
4349
|
}
|
|
4339
4350
|
|
|
4351
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .rounded-l-none {
|
|
4352
|
+
border-top-left-radius: 0;
|
|
4353
|
+
border-bottom-left-radius: 0;
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4356
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .rounded-r-none {
|
|
4357
|
+
border-top-right-radius: 0;
|
|
4358
|
+
border-bottom-right-radius: 0;
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4340
4361
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border {
|
|
4341
4362
|
border-style: var(--tw-border-style);
|
|
4342
4363
|
border-width: 1px;
|
|
@@ -4996,6 +5017,12 @@
|
|
|
4996
5017
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
|
4997
5018
|
}
|
|
4998
5019
|
|
|
5020
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .transition-\[width\] {
|
|
5021
|
+
transition-property: width;
|
|
5022
|
+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
5023
|
+
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
|
5024
|
+
}
|
|
5025
|
+
|
|
4999
5026
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .transition-all {
|
|
5000
5027
|
transition-property: all;
|
|
5001
5028
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
@@ -5418,6 +5445,14 @@
|
|
|
5418
5445
|
--color-secondary-content: oklch(98% .004 75);
|
|
5419
5446
|
--color-accent: oklch(54% .16 300);
|
|
5420
5447
|
--color-accent-content: oklch(98% .012 300);
|
|
5448
|
+
--cairn-directive-band-1: 8%;
|
|
5449
|
+
--cairn-directive-band-2: 14%;
|
|
5450
|
+
--cairn-directive-band-3: 20%;
|
|
5451
|
+
--cairn-directive-ink-2: oklch(50% .16 300);
|
|
5452
|
+
--cairn-directive-ink-3: oklch(48% .16 300);
|
|
5453
|
+
--cairn-directive-rail-1: 75%;
|
|
5454
|
+
--cairn-directive-rail-2: 82%;
|
|
5455
|
+
--cairn-directive-rail-3: 90%;
|
|
5421
5456
|
--color-neutral: oklch(32% .012 75);
|
|
5422
5457
|
--color-neutral-content: oklch(96% .004 75);
|
|
5423
5458
|
--color-info: oklch(52% .12 240);
|
|
@@ -5459,6 +5494,14 @@
|
|
|
5459
5494
|
--color-secondary-content: oklch(20% .008 75);
|
|
5460
5495
|
--color-accent: oklch(70% .14 300);
|
|
5461
5496
|
--color-accent-content: oklch(20% .04 300);
|
|
5497
|
+
--cairn-directive-band-1: 10%;
|
|
5498
|
+
--cairn-directive-band-2: 16%;
|
|
5499
|
+
--cairn-directive-band-3: 22%;
|
|
5500
|
+
--cairn-directive-ink-2: oklch(74% .14 300);
|
|
5501
|
+
--cairn-directive-ink-3: oklch(78% .14 300);
|
|
5502
|
+
--cairn-directive-rail-1: 65%;
|
|
5503
|
+
--cairn-directive-rail-2: 75%;
|
|
5504
|
+
--cairn-directive-rail-3: 85%;
|
|
5462
5505
|
--color-neutral: oklch(80% .01 75);
|
|
5463
5506
|
--color-neutral-content: oklch(22% .008 75);
|
|
5464
5507
|
--color-info: oklch(72% .12 240);
|
|
@@ -5487,6 +5530,11 @@
|
|
|
5487
5530
|
box-sizing: border-box;
|
|
5488
5531
|
}
|
|
5489
5532
|
|
|
5533
|
+
:where([data-theme="cairn-admin"], [data-theme="cairn-admin-dark"]) .menu li > :is(button, a):focus-visible {
|
|
5534
|
+
outline: 2px solid var(--color-primary);
|
|
5535
|
+
outline-offset: -2px;
|
|
5536
|
+
}
|
|
5537
|
+
|
|
5490
5538
|
@media (prefers-reduced-motion: reduce) {
|
|
5491
5539
|
[data-theme="cairn-admin"] *, [data-theme="cairn-admin-dark"] * {
|
|
5492
5540
|
scroll-behavior: auto !important;
|
|
@@ -5,5 +5,6 @@ export declare function cairnHighlightStyle(): HighlightStyle;
|
|
|
5
5
|
/** Line and mark decorations flagging remark-directive machinery. */
|
|
6
6
|
export declare function cairnDirectivePlugin(): ViewPlugin<{
|
|
7
7
|
decorations: DecorationSet;
|
|
8
|
+
depths: (number | null)[];
|
|
8
9
|
update(update: ViewUpdate): void;
|
|
9
10
|
}, undefined>;
|
|
@@ -5,7 +5,7 @@ import { HighlightStyle } from '@codemirror/language';
|
|
|
5
5
|
import { tags } from '@lezer/highlight';
|
|
6
6
|
import { Decoration, ViewPlugin } from '@codemirror/view';
|
|
7
7
|
import { RangeSetBuilder } from '@codemirror/state';
|
|
8
|
-
import { directiveLineKind, findInlineDirectives } from './markdown-directives.js';
|
|
8
|
+
import { directiveLineKind, fenceDepths, findInlineDirectives } from './markdown-directives.js';
|
|
9
9
|
/** Markdown token colors over the admin theme variables. */
|
|
10
10
|
export function cairnHighlightStyle() {
|
|
11
11
|
return HighlightStyle.define([
|
|
@@ -24,20 +24,39 @@ export function cairnHighlightStyle() {
|
|
|
24
24
|
// The machinery lines explain themselves on hover, so an editor who has never seen ::: syntax
|
|
25
25
|
// learns what the line is without leaving the page.
|
|
26
26
|
const MACHINERY_HINT = 'Layout marker. Edit the text between these lines and leave this line as it is.';
|
|
27
|
-
|
|
27
|
+
// Nesting deeper than three steps shares the third visual step; the depth model itself is unbounded.
|
|
28
|
+
const DEPTH_STEPS = [1, 2, 3];
|
|
29
|
+
const fenceLines = DEPTH_STEPS.map((d) => Decoration.line({ class: `cm-cairn-directive-fence cm-cairn-depth-${d}`, attributes: { title: MACHINERY_HINT } }));
|
|
30
|
+
const contentLines = DEPTH_STEPS.map((d) => Decoration.line({ class: `cm-cairn-directive-content cm-cairn-depth-${d}` }));
|
|
28
31
|
const leafLine = Decoration.line({ class: 'cm-cairn-directive-leaf', attributes: { title: MACHINERY_HINT } });
|
|
29
32
|
const inlineMark = Decoration.mark({ class: 'cm-cairn-directive-inline' });
|
|
30
|
-
|
|
33
|
+
// Depth needs the whole document, since a visible line's containers can open above the viewport.
|
|
34
|
+
// One regex pass per line, linear in the document; at admin entry sizes (tens of kilobytes) that
|
|
35
|
+
// is well under a millisecond. The plugin caches the result, so the scan reruns only when the
|
|
36
|
+
// document changes and a scroll rebuilds the viewport decorations from the cached array.
|
|
37
|
+
function docDepths(view) {
|
|
38
|
+
const doc = view.state.doc;
|
|
39
|
+
const lines = [];
|
|
40
|
+
for (let n = 1; n <= doc.lines; n++)
|
|
41
|
+
lines.push(doc.line(n).text);
|
|
42
|
+
return fenceDepths(lines);
|
|
43
|
+
}
|
|
44
|
+
function buildDirectiveDecorations(view, depths) {
|
|
31
45
|
const builder = new RangeSetBuilder();
|
|
32
46
|
for (const { from, to } of view.visibleRanges) {
|
|
33
47
|
for (let pos = from; pos <= to;) {
|
|
34
48
|
const line = view.state.doc.lineAt(pos);
|
|
35
49
|
const kind = directiveLineKind(line.text);
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
const depth = Math.min(depths[line.number - 1] ?? 0, DEPTH_STEPS.length);
|
|
51
|
+
// A fence-shaped line at depth 0 is one the depth scan disowned (a documented example
|
|
52
|
+
// inside a code block, outside any container); it gets no machinery treatment.
|
|
53
|
+
if (kind === 'fence' && depth > 0)
|
|
54
|
+
builder.add(line.from, line.from, fenceLines[depth - 1]);
|
|
38
55
|
else if (kind === 'leaf')
|
|
39
56
|
builder.add(line.from, line.from, leafLine);
|
|
40
|
-
else {
|
|
57
|
+
else if (kind === null) {
|
|
58
|
+
if (depth > 0)
|
|
59
|
+
builder.add(line.from, line.from, contentLines[depth - 1]);
|
|
41
60
|
for (const r of findInlineDirectives(line.text)) {
|
|
42
61
|
builder.add(line.from + r.from, line.from + r.to, inlineMark);
|
|
43
62
|
}
|
|
@@ -51,12 +70,16 @@ function buildDirectiveDecorations(view) {
|
|
|
51
70
|
export function cairnDirectivePlugin() {
|
|
52
71
|
return ViewPlugin.fromClass(class {
|
|
53
72
|
decorations;
|
|
73
|
+
depths;
|
|
54
74
|
constructor(view) {
|
|
55
|
-
this.
|
|
75
|
+
this.depths = docDepths(view);
|
|
76
|
+
this.decorations = buildDirectiveDecorations(view, this.depths);
|
|
56
77
|
}
|
|
57
78
|
update(update) {
|
|
79
|
+
if (update.docChanged)
|
|
80
|
+
this.depths = docDepths(update.view);
|
|
58
81
|
if (update.docChanged || update.viewportChanged)
|
|
59
|
-
this.decorations = buildDirectiveDecorations(update.view);
|
|
82
|
+
this.decorations = buildDirectiveDecorations(update.view, this.depths);
|
|
60
83
|
}
|
|
61
84
|
}, { decorations: (v) => v.decorations });
|
|
62
85
|
}
|
package/dist/components/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Admin Svelte components (Plan 05). The Warm Stone theme ships as a CSS side effect imported
|
|
2
2
|
// by the components that set `data-theme="cairn-admin"`.
|
|
3
|
+
export { default as CairnAdmin } from './CairnAdmin.svelte';
|
|
3
4
|
export { default as AdminLayout } from './AdminLayout.svelte';
|
|
4
5
|
export { default as LoginPage } from './LoginPage.svelte';
|
|
5
6
|
export { default as ConfirmPage } from './ConfirmPage.svelte';
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
/** Classify a whole line as a container fence, a leaf directive, or neither. */
|
|
2
2
|
export declare function directiveLineKind(line: string): 'fence' | 'leaf' | null;
|
|
3
|
+
/**
|
|
4
|
+
* The 1-based container depth each line sits at, or null outside any container. A named fence
|
|
5
|
+
* opens a container; a bare fence closes the most recent one (colon counts are not trusted for
|
|
6
|
+
* pairing, since authors vary them). An opener and its closer share the opener's depth, and a
|
|
7
|
+
* line between them carries the depth of its innermost container. Lines inside a fenced code
|
|
8
|
+
* block are plain content, so a documented ::: example cannot open a phantom container running
|
|
9
|
+
* to end of document. Author errors are tolerated: an unmatched closer reads as depth 1 and the
|
|
10
|
+
* count never goes below zero.
|
|
11
|
+
*/
|
|
12
|
+
export declare function fenceDepths(lines: string[]): (number | null)[];
|
|
3
13
|
/** Inline directive ranges (`:name[...]{...}`) within a line of text. */
|
|
4
14
|
export declare function findInlineDirectives(text: string): {
|
|
5
15
|
from: number;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
// Remark-directive detection for the editor's machinery highlighting (spec: directive syntax is
|
|
2
2
|
// styled distinctly so an editor can tell component scaffolding from prose). Pure functions; the
|
|
3
3
|
// CodeMirror decoration plugin wraps them.
|
|
4
|
-
|
|
4
|
+
// A container fence: three or more colons, then an optional name, an optional [label], and
|
|
5
|
+
// optional {attrs}, in remark-directive order. The name is captured so the depth scan below can
|
|
6
|
+
// tell an opener (named) from a closer (bare colons). Matching is tolerant of stray whitespace,
|
|
7
|
+
// the same posture as the leaf form: a slightly off fence should still read as machinery.
|
|
8
|
+
const FENCE = /^\s{0,3}:{3,}\s*([\w-]*)\s*(\[[^\]]*\])?\s*(\{[^}]*\})?\s*$/;
|
|
5
9
|
const LEAF = /^\s{0,3}::[\w-]+(\[[^\]]*\])?(\{[^}]*\})?\s*$/;
|
|
6
10
|
const INLINE = /(?<![:\w]):[\w-]+\[[^\]]*\](\{[^}]*\})?/g;
|
|
11
|
+
// A fenced code block's delimiter: three or more backticks or tildes, indent-tolerant like the
|
|
12
|
+
// directive forms. The depth scan tracks these so a documented ::: example inside a code block
|
|
13
|
+
// never opens a real container.
|
|
14
|
+
const CODE_FENCE = /^\s{0,3}(`{3,}|~{3,})/;
|
|
7
15
|
/** Classify a whole line as a container fence, a leaf directive, or neither. */
|
|
8
16
|
export function directiveLineKind(line) {
|
|
9
17
|
if (FENCE.test(line))
|
|
@@ -12,6 +20,51 @@ export function directiveLineKind(line) {
|
|
|
12
20
|
return 'leaf';
|
|
13
21
|
return null;
|
|
14
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* The 1-based container depth each line sits at, or null outside any container. A named fence
|
|
25
|
+
* opens a container; a bare fence closes the most recent one (colon counts are not trusted for
|
|
26
|
+
* pairing, since authors vary them). An opener and its closer share the opener's depth, and a
|
|
27
|
+
* line between them carries the depth of its innermost container. Lines inside a fenced code
|
|
28
|
+
* block are plain content, so a documented ::: example cannot open a phantom container running
|
|
29
|
+
* to end of document. Author errors are tolerated: an unmatched closer reads as depth 1 and the
|
|
30
|
+
* count never goes below zero.
|
|
31
|
+
*/
|
|
32
|
+
export function fenceDepths(lines) {
|
|
33
|
+
const depths = [];
|
|
34
|
+
let open = 0;
|
|
35
|
+
// The marker character that opened the current code block, or null outside one. Only a line
|
|
36
|
+
// opening with the same character closes it, so tildes inside a backtick block stay literal.
|
|
37
|
+
let codeMarker = null;
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
const code = CODE_FENCE.exec(line);
|
|
40
|
+
if (code) {
|
|
41
|
+
if (codeMarker === null)
|
|
42
|
+
codeMarker = code[1][0];
|
|
43
|
+
else if (code[1][0] === codeMarker)
|
|
44
|
+
codeMarker = null;
|
|
45
|
+
depths.push(open > 0 ? open : null);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (codeMarker !== null) {
|
|
49
|
+
depths.push(open > 0 ? open : null);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const fence = FENCE.exec(line);
|
|
53
|
+
if (!fence) {
|
|
54
|
+
depths.push(open > 0 ? open : null);
|
|
55
|
+
}
|
|
56
|
+
else if (fence[1]) {
|
|
57
|
+
open += 1;
|
|
58
|
+
depths.push(open);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
depths.push(Math.max(open, 1));
|
|
62
|
+
if (open > 0)
|
|
63
|
+
open -= 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return depths;
|
|
67
|
+
}
|
|
15
68
|
/** Inline directive ranges (`:name[...]{...}`) within a line of text. */
|
|
16
69
|
export function findInlineDirectives(text) {
|
|
17
70
|
const out = [];
|
|
@@ -22,11 +22,3 @@ export declare function insertInlineLink(doc: string, from: number, to: number,
|
|
|
22
22
|
* is left in place.
|
|
23
23
|
*/
|
|
24
24
|
export declare function unwrapCairnLink(doc: string, href: string): string;
|
|
25
|
-
/**
|
|
26
|
-
* Rewrite every cairn: link whose href is exactly `oldHref` so its href becomes `newHref`, keeping
|
|
27
|
-
* the display text and any link title byte-for-byte. Rename calls this to repoint a renamed entry's
|
|
28
|
-
* inbound tokens. Parsed with the same remark pipeline as extractCairnLinks, so a token inside a code
|
|
29
|
-
* span is not a link node and is never touched. Each matching node's source span is rewritten from
|
|
30
|
-
* last to first, replacing only the `](oldHref` run so the label and title stay exact.
|
|
31
|
-
*/
|
|
32
|
-
export declare function rewriteCairnLink(doc: string, oldHref: string, newHref: string): string;
|
|
@@ -157,31 +157,3 @@ export function unwrapCairnLink(doc, href) {
|
|
|
157
157
|
}
|
|
158
158
|
return out;
|
|
159
159
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Rewrite every cairn: link whose href is exactly `oldHref` so its href becomes `newHref`, keeping
|
|
162
|
-
* the display text and any link title byte-for-byte. Rename calls this to repoint a renamed entry's
|
|
163
|
-
* inbound tokens. Parsed with the same remark pipeline as extractCairnLinks, so a token inside a code
|
|
164
|
-
* span is not a link node and is never touched. Each matching node's source span is rewritten from
|
|
165
|
-
* last to first, replacing only the `](oldHref` run so the label and title stay exact.
|
|
166
|
-
*/
|
|
167
|
-
export function rewriteCairnLink(doc, oldHref, newHref) {
|
|
168
|
-
const tree = unified().use(remarkParse).use(remarkGfm).parse(doc);
|
|
169
|
-
const spans = [];
|
|
170
|
-
visit(tree, 'link', (node) => {
|
|
171
|
-
if (node.url !== oldHref)
|
|
172
|
-
return;
|
|
173
|
-
const start = node.position?.start?.offset;
|
|
174
|
-
const end = node.position?.end?.offset;
|
|
175
|
-
if (start == null || end == null)
|
|
176
|
-
return;
|
|
177
|
-
spans.push({ start, end });
|
|
178
|
-
});
|
|
179
|
-
spans.sort((a, b) => b.start - a.start);
|
|
180
|
-
let out = doc;
|
|
181
|
-
for (const span of spans) {
|
|
182
|
-
const src = out.slice(span.start, span.end);
|
|
183
|
-
const rewritten = src.replace(`](${oldHref}`, `](${newHref}`);
|
|
184
|
-
out = out.slice(0, span.start) + rewritten + out.slice(span.end);
|
|
185
|
-
}
|
|
186
|
-
return out;
|
|
187
|
-
}
|