@glw907/cairn-cms 0.7.0 → 0.9.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/dist/components/ConceptList.svelte +8 -4
- package/dist/components/ConceptList.svelte.d.ts.map +1 -1
- package/dist/components/EditPage.svelte +4 -6
- package/dist/components/EditPage.svelte.d.ts +1 -3
- package/dist/components/EditPage.svelte.d.ts.map +1 -1
- package/dist/components/EditorToolbar.svelte +61 -0
- package/dist/components/EditorToolbar.svelte.d.ts +15 -0
- package/dist/components/EditorToolbar.svelte.d.ts.map +1 -0
- package/dist/components/MarkdownEditor.svelte +96 -57
- package/dist/components/MarkdownEditor.svelte.d.ts +5 -6
- package/dist/components/MarkdownEditor.svelte.d.ts.map +1 -1
- package/dist/components/markdown-format.d.ts +13 -0
- package/dist/components/markdown-format.d.ts.map +1 -0
- package/dist/components/markdown-format.js +23 -0
- package/dist/content/compose.d.ts +2 -2
- package/dist/content/compose.d.ts.map +1 -1
- package/dist/content/compose.js +2 -2
- package/dist/content/concepts.d.ts +7 -6
- package/dist/content/concepts.d.ts.map +1 -1
- package/dist/content/concepts.js +9 -6
- package/dist/content/ids.d.ts +14 -0
- package/dist/content/ids.d.ts.map +1 -1
- package/dist/content/ids.js +40 -0
- package/dist/content/permalink.d.ts +1 -0
- package/dist/content/permalink.d.ts.map +1 -1
- package/dist/content/permalink.js +1 -1
- package/dist/content/types.d.ts +12 -6
- package/dist/content/types.d.ts.map +1 -1
- package/dist/delivery/content-index.d.ts +1 -0
- package/dist/delivery/content-index.d.ts.map +1 -1
- package/dist/delivery/content-index.js +4 -2
- package/dist/delivery/site-index.d.ts +28 -0
- package/dist/delivery/site-index.d.ts.map +1 -0
- package/dist/delivery/site-index.js +38 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/nav/site-config.d.ts +5 -0
- package/dist/nav/site-config.d.ts.map +1 -1
- package/dist/nav/site-config.js +4 -0
- package/dist/render/pipeline.d.ts +1 -1
- package/dist/render/pipeline.js +1 -1
- package/dist/render/sanitize.js +2 -2
- package/dist/sveltekit/content-routes.d.ts.map +1 -1
- package/dist/sveltekit/content-routes.js +18 -8
- package/dist/sveltekit/public-routes.d.ts +11 -12
- package/dist/sveltekit/public-routes.d.ts.map +1 -1
- package/dist/sveltekit/public-routes.js +36 -35
- package/package.json +7 -3
- package/src/lib/components/ConceptList.svelte +8 -4
- package/src/lib/components/EditPage.svelte +4 -6
- package/src/lib/components/EditorToolbar.svelte +61 -0
- package/src/lib/components/MarkdownEditor.svelte +96 -57
- package/src/lib/components/markdown-format.ts +39 -0
- package/src/lib/content/compose.ts +3 -2
- package/src/lib/content/concepts.ts +10 -6
- package/src/lib/content/ids.ts +44 -0
- package/src/lib/content/permalink.ts +2 -2
- package/src/lib/content/types.ts +13 -6
- package/src/lib/delivery/content-index.ts +5 -2
- package/src/lib/delivery/site-index.ts +68 -0
- package/src/lib/index.ts +13 -1
- package/src/lib/nav/site-config.ts +8 -0
- package/src/lib/render/pipeline.ts +1 -1
- package/src/lib/render/sanitize.ts +2 -2
- package/src/lib/sveltekit/content-routes.ts +17 -7
- package/src/lib/sveltekit/public-routes.ts +38 -36
|
@@ -17,9 +17,14 @@ plus a new-entry form. The slug auto-derives from the title until the author edi
|
|
|
17
17
|
let title = $state('');
|
|
18
18
|
let slug = $state('');
|
|
19
19
|
let slugEdited = $state(false);
|
|
20
|
+
// Default the date client-side so the SSR pass and hydration agree across UTC midnight.
|
|
21
|
+
let dateDefault = $state('');
|
|
22
|
+
$effect(() => {
|
|
23
|
+
dateDefault = new Date().toISOString().slice(0, 10);
|
|
24
|
+
});
|
|
20
25
|
|
|
21
26
|
const derivedSlug = $derived(slugEdited ? slug : slugify(title));
|
|
22
|
-
const slugPlaceholder = $derived(data.dated ? '
|
|
27
|
+
const slugPlaceholder = $derived(data.dated ? 'my-entry' : 'about-us');
|
|
23
28
|
</script>
|
|
24
29
|
|
|
25
30
|
<header class="mb-4 flex items-center justify-between">
|
|
@@ -58,14 +63,13 @@ plus a new-entry form. The slug auto-derives from the title until the author edi
|
|
|
58
63
|
<h2 class="text-sm font-semibold">New entry</h2>
|
|
59
64
|
<label class="flex flex-col gap-1">
|
|
60
65
|
<span class="text-sm font-medium">Title</span>
|
|
61
|
-
<input class="input" name="title"
|
|
66
|
+
<input class="input" name="title" bind:value={title} required />
|
|
62
67
|
</label>
|
|
63
68
|
<label class="flex flex-col gap-1">
|
|
64
69
|
<span class="text-sm font-medium">Slug</span>
|
|
65
70
|
<input
|
|
66
71
|
class="input"
|
|
67
72
|
name="slug"
|
|
68
|
-
aria-label="Slug"
|
|
69
73
|
placeholder={slugPlaceholder}
|
|
70
74
|
value={derivedSlug}
|
|
71
75
|
oninput={(e) => { slugEdited = true; slug = e.currentTarget.value; }}
|
|
@@ -74,7 +78,7 @@ plus a new-entry form. The slug auto-derives from the title until the author edi
|
|
|
74
78
|
{#if data.dated}
|
|
75
79
|
<label class="flex flex-col gap-1">
|
|
76
80
|
<span class="text-sm font-medium">Date</span>
|
|
77
|
-
<input class="input" type="date" name="date"
|
|
81
|
+
<input class="input" type="date" name="date" value={dateDefault} />
|
|
78
82
|
</label>
|
|
79
83
|
{/if}
|
|
80
84
|
<button type="submit" class="btn btn-primary self-start">Create</button>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConceptList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ConceptList.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAG7D,UAAU,KAAK;IACb,qFAAqF;IACrF,IAAI,EAAE,QAAQ,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"ConceptList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ConceptList.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAG7D,UAAU,KAAK;IACb,qFAAqF;IACrF,IAAI,EAAE,QAAQ,CAAC;CAChB;AA2EH;;;GAGG;AACH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
|
-
The differentiated editor: the per-concept frontmatter form (from `data.fields`) beside the
|
|
3
|
+
The differentiated editor: the per-concept frontmatter form (from `data.fields`) beside the
|
|
4
4
|
markdown editor and a live, design-accurate preview. The whole surface is one form posting to the
|
|
5
5
|
`?/save` action; the preview toggle persists per user in localStorage (spec §7.6).
|
|
6
6
|
-->
|
|
@@ -18,13 +18,11 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
18
18
|
data: EditData & { siteName: string };
|
|
19
19
|
/** The site's component registry, for the insert palette. */
|
|
20
20
|
registry?: ComponentRegistry;
|
|
21
|
-
/** Carta preview plugins from the adapter, for the design-accurate preview. */
|
|
22
|
-
preview?: unknown[];
|
|
23
21
|
/** The site's design-accurate render pipeline; the preview pane sanitizes its output. */
|
|
24
22
|
render?: (md: string, opts?: { stagger?: boolean }) => string | Promise<string>;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
let { data, registry,
|
|
25
|
+
let { data, registry, render }: Props = $props();
|
|
28
26
|
|
|
29
27
|
// `body` is local editor state seeded once from the prop; it diverges as the user types.
|
|
30
28
|
// untrack() captures the initial value without subscribing to future prop changes.
|
|
@@ -46,7 +44,7 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
// Render the design-accurate preview as the body changes, debounced, and sanitize before the DOM.
|
|
49
|
-
// The sanitize is the one barrier between editor-authored markdown and the page (
|
|
47
|
+
// The sanitize is the one barrier between editor-authored markdown and the page (the editor is unsanitized).
|
|
50
48
|
// previewRun is a plain counter (not reactive state) used as a latest-wins guard: if a slow earlier
|
|
51
49
|
// async render call resolves after a newer one has started, the stale result is discarded.
|
|
52
50
|
let previewRun = 0;
|
|
@@ -103,7 +101,7 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
103
101
|
|
|
104
102
|
<div class="lg:order-1">
|
|
105
103
|
<div class="rounded-box border border-base-300 bg-base-100 overflow-hidden">
|
|
106
|
-
<MarkdownEditor bind:value={body} name="body"
|
|
104
|
+
<MarkdownEditor bind:value={body} name="body" registerInsert={(fn) => (insert = fn)} />
|
|
107
105
|
</div>
|
|
108
106
|
{#if showPreview}
|
|
109
107
|
<section
|
|
@@ -7,15 +7,13 @@ interface Props {
|
|
|
7
7
|
};
|
|
8
8
|
/** The site's component registry, for the insert palette. */
|
|
9
9
|
registry?: ComponentRegistry;
|
|
10
|
-
/** Carta preview plugins from the adapter, for the design-accurate preview. */
|
|
11
|
-
preview?: unknown[];
|
|
12
10
|
/** The site's design-accurate render pipeline; the preview pane sanitizes its output. */
|
|
13
11
|
render?: (md: string, opts?: {
|
|
14
12
|
stagger?: boolean;
|
|
15
13
|
}) => string | Promise<string>;
|
|
16
14
|
}
|
|
17
15
|
/**
|
|
18
|
-
* The differentiated editor: the per-concept frontmatter form (from `data.fields`) beside the
|
|
16
|
+
* The differentiated editor: the per-concept frontmatter form (from `data.fields`) beside the
|
|
19
17
|
* markdown editor and a live, design-accurate preview. The whole surface is one form posting to the
|
|
20
18
|
* `?/save` action; the preview toggle persists per user in localStorage (spec §7.6).
|
|
21
19
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAK7D,UAAU,KAAK;IACb,gEAAgE;IAChE,IAAI,EAAE,QAAQ,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B
|
|
1
|
+
{"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAK7D,UAAU,KAAK;IACb,gEAAgE;IAChE,IAAI,EAAE,QAAQ,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,yFAAyF;IACzF,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACjF;AAqJH;;;;GAIG;AACH,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The editor's formatting toolbar: bold, italic, heading, link, bulleted list, quote, code. Each button
|
|
4
|
+
asks the host to apply a markdown transform to the current selection. Carta supplied this row before;
|
|
5
|
+
cairn owns it now so the edit surface stays swappable. The glyphs are stroke SVG icons in the admin's
|
|
6
|
+
house style (24x24 viewBox, `currentColor`, round caps), so the row matches the rest of the surface.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import type { FormatKind } from './markdown-format.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Apply a markdown transform to the editor's current selection. */
|
|
13
|
+
format: (kind: FormatKind) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { format }: Props = $props();
|
|
17
|
+
|
|
18
|
+
// Each icon is a set of stroke `<path>` d-strings rendered into the shared 24x24 svg below, so the
|
|
19
|
+
// markup stays declarative (no per-icon raw html). Paths follow the house outline style.
|
|
20
|
+
const buttons: { kind: FormatKind; label: string; paths: string[] }[] = [
|
|
21
|
+
{ kind: 'bold', label: 'Bold', paths: ['M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8'] },
|
|
22
|
+
{ kind: 'italic', label: 'Italic', paths: ['M19 4h-9', 'M14 20H5', 'M15 4 9 20'] },
|
|
23
|
+
{ kind: 'heading', label: 'Heading', paths: ['M6 4v16', 'M18 4v16', 'M6 12h12'] },
|
|
24
|
+
{
|
|
25
|
+
kind: 'link',
|
|
26
|
+
label: 'Link',
|
|
27
|
+
paths: [
|
|
28
|
+
'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71',
|
|
29
|
+
'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71',
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{ kind: 'ul', label: 'Bulleted list', paths: ['M8 6h13', 'M8 12h13', 'M8 18h13', 'M3 6h.01', 'M3 12h.01', 'M3 18h.01'] },
|
|
33
|
+
{
|
|
34
|
+
kind: 'quote',
|
|
35
|
+
label: 'Quote',
|
|
36
|
+
paths: [
|
|
37
|
+
'M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1 1 1 0 0 0 1 1 4 4 0 0 0 4-4V5a2 2 0 0 0-2-2z',
|
|
38
|
+
'M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1 1 1 0 0 0 1 1 4 4 0 0 0 4-4V5a2 2 0 0 0-2-2z',
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{ kind: 'code', label: 'Code', paths: ['M16 18l6-6-6-6', 'M8 6l-6 6 6 6'] },
|
|
42
|
+
];
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<div class="border-base-300 bg-base-200 flex gap-1 border-b p-1" role="toolbar" aria-label="Formatting">
|
|
46
|
+
{#each buttons as button (button.kind)}
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
class="btn btn-ghost btn-sm btn-square"
|
|
50
|
+
aria-label={button.label}
|
|
51
|
+
title={button.label}
|
|
52
|
+
onclick={() => format(button.kind)}
|
|
53
|
+
>
|
|
54
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
55
|
+
{#each button.paths as d (d)}
|
|
56
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={d} />
|
|
57
|
+
{/each}
|
|
58
|
+
</svg>
|
|
59
|
+
</button>
|
|
60
|
+
{/each}
|
|
61
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FormatKind } from './markdown-format.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Apply a markdown transform to the editor's current selection. */
|
|
4
|
+
format: (kind: FormatKind) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The editor's formatting toolbar: bold, italic, heading, link, bulleted list, quote, code. Each button
|
|
8
|
+
* asks the host to apply a markdown transform to the current selection. Carta supplied this row before;
|
|
9
|
+
* cairn owns it now so the edit surface stays swappable. The glyphs are stroke SVG icons in the admin's
|
|
10
|
+
* house style (24x24 viewBox, `currentColor`, round caps), so the row matches the rest of the surface.
|
|
11
|
+
*/
|
|
12
|
+
declare const EditorToolbar: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type EditorToolbar = ReturnType<typeof EditorToolbar>;
|
|
14
|
+
export default EditorToolbar;
|
|
15
|
+
//# sourceMappingURL=EditorToolbar.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EditorToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditorToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGrD,UAAU,KAAK;IACb,oEAAoE;IACpE,MAAM,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;CACpC;AAiDH;;;;;GAKG;AACH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -1,81 +1,120 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
|
-
The `MarkdownEditor` seam (spec §6, seam 5): a thin wrapper over
|
|
4
|
-
and a cursor-insert callback.
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The `MarkdownEditor` seam (spec §6, seam 5): a thin wrapper over CodeMirror 6 exposing a bindable
|
|
4
|
+
value and a cursor-insert callback. CodeMirror is client-only, so it mounts after the component does
|
|
5
|
+
through a dynamic import; until then a plain textarea carries the value so the form still submits, and
|
|
6
|
+
the hidden field mirrors the value throughout. The edit surface owns its toolbar; the design-accurate
|
|
7
|
+
preview lives in EditPage through the adapter's render. Swapping the editor stays a one-file change.
|
|
7
8
|
-->
|
|
8
9
|
<script lang="ts">
|
|
9
|
-
import { onMount } from 'svelte';
|
|
10
|
+
import { onMount, onDestroy } from 'svelte';
|
|
11
|
+
import EditorToolbar from './EditorToolbar.svelte';
|
|
12
|
+
import { applyMarkdownFormat, type FormatKind } from './markdown-format.js';
|
|
10
13
|
|
|
11
14
|
interface Props {
|
|
12
15
|
/** The markdown source; bindable so the parent reads edits back. */
|
|
13
16
|
value: string;
|
|
14
17
|
/** The hidden field name the value is mirrored to for form submit. */
|
|
15
18
|
name: string;
|
|
16
|
-
/** Carta extensions from the adapter, for the design-accurate preview. */
|
|
17
|
-
plugins?: unknown[];
|
|
18
19
|
/** Receives a `(text) => void` that inserts at the cursor; the palette calls it. */
|
|
19
20
|
registerInsert?: (insert: (text: string) => void) => void;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
let { value = $bindable(), name,
|
|
23
|
-
|
|
24
|
-
// Local structural type for the Carta editing surface this seam uses. carta-md re-exports its
|
|
25
|
-
// Svelte components from the package entry, so its `Carta` class is not reachable as a named
|
|
26
|
-
// export under NodeNext; a structural type stays compatible without naming it (the shape
|
|
27
|
-
// legacy/src/lib/editor.ts relied on, verified against carta-md@4.11).
|
|
28
|
-
interface CartaInput {
|
|
29
|
-
getSelection(): { start: number };
|
|
30
|
-
insertAt(position: number, text: string): void;
|
|
31
|
-
update(): boolean;
|
|
32
|
-
}
|
|
33
|
-
interface CartaLike {
|
|
34
|
-
input?: CartaInput;
|
|
35
|
-
}
|
|
23
|
+
let { value = $bindable(), name, registerInsert }: Props = $props();
|
|
36
24
|
|
|
25
|
+
let host = $state<HTMLDivElement | null>(null);
|
|
37
26
|
let mounted = $state(false);
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
let Editor = $state<(typeof import('carta-md'))['MarkdownEditor'] | null>(null);
|
|
43
|
-
let carta = $state<CartaLike | null>(null);
|
|
27
|
+
// The CodeMirror view, untyped at the runtime boundary because @codemirror/* loads only in the
|
|
28
|
+
// browser. The type-only `import(...)` annotation is erased; the value import is dynamic in onMount,
|
|
29
|
+
// so the server bundle never pulls CodeMirror (guarded by the editor-boundary test).
|
|
30
|
+
let view: import('@codemirror/view').EditorView | null = null;
|
|
44
31
|
|
|
45
32
|
onMount(async () => {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
33
|
+
const viewMod = await import('@codemirror/view');
|
|
34
|
+
const stateMod = await import('@codemirror/state');
|
|
35
|
+
const markdownMod = await import('@codemirror/lang-markdown');
|
|
36
|
+
const commandsMod = await import('@codemirror/commands');
|
|
37
|
+
const languageMod = await import('@codemirror/language');
|
|
38
|
+
|
|
39
|
+
if (!host) return;
|
|
40
|
+
|
|
41
|
+
const { EditorView, keymap } = viewMod;
|
|
42
|
+
const theme = EditorView.theme(
|
|
43
|
+
{
|
|
44
|
+
'&': { backgroundColor: 'var(--color-base-100)', color: 'var(--color-base-content)', fontSize: '0.875rem' },
|
|
45
|
+
'.cm-content': { fontFamily: 'ui-monospace, monospace', padding: '0.75rem', lineHeight: '1.7' },
|
|
46
|
+
'.cm-cursor': { borderLeftColor: 'var(--color-primary)' },
|
|
47
|
+
'&.cm-focused': { outline: '2px solid var(--color-primary)', outlineOffset: '-2px' },
|
|
48
|
+
'.cm-line': { padding: '0' },
|
|
49
|
+
},
|
|
50
|
+
{ dark: false },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
view = new EditorView({
|
|
54
|
+
parent: host,
|
|
55
|
+
state: stateMod.EditorState.create({
|
|
56
|
+
doc: value,
|
|
57
|
+
extensions: [
|
|
58
|
+
commandsMod.history(),
|
|
59
|
+
keymap.of([...commandsMod.defaultKeymap, ...commandsMod.historyKeymap]),
|
|
60
|
+
markdownMod.markdown(),
|
|
61
|
+
EditorView.lineWrapping,
|
|
62
|
+
languageMod.syntaxHighlighting(languageMod.defaultHighlightStyle, { fallback: true }),
|
|
63
|
+
theme,
|
|
64
|
+
EditorView.updateListener.of((update) => {
|
|
65
|
+
if (update.docChanged) value = update.state.doc.toString();
|
|
66
|
+
}),
|
|
67
|
+
],
|
|
68
|
+
}),
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
registerInsert?.(insertAtCursor);
|
|
70
72
|
mounted = true;
|
|
71
73
|
});
|
|
74
|
+
|
|
75
|
+
onDestroy(() => view?.destroy());
|
|
76
|
+
|
|
77
|
+
// Reconcile an externally reassigned `value` into the mounted editor. A no-op until `view` exists,
|
|
78
|
+
// and the doc-equality guard ignores the updateListener's own writes so the two never feed back.
|
|
79
|
+
$effect(() => {
|
|
80
|
+
const incoming = value;
|
|
81
|
+
if (!view) return;
|
|
82
|
+
const current = view.state.doc.toString();
|
|
83
|
+
if (incoming === current) return;
|
|
84
|
+
view.dispatch({ changes: { from: 0, to: current.length, insert: incoming } });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function insertAtCursor(text: string) {
|
|
88
|
+
if (!view) {
|
|
89
|
+
value = value ? `${value}\n\n${text}` : text;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const pos = view.state.selection.main.head;
|
|
93
|
+
const prefix = pos > 0 ? '\n\n' : '';
|
|
94
|
+
const insert = `${prefix}${text}`;
|
|
95
|
+
view.dispatch({ changes: { from: pos, insert }, selection: { anchor: pos + insert.length } });
|
|
96
|
+
view.focus();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function applyFormat(kind: FormatKind) {
|
|
100
|
+
if (!view) return;
|
|
101
|
+
const { from, to } = view.state.selection.main;
|
|
102
|
+
const doc = view.state.doc.toString();
|
|
103
|
+
const next = applyMarkdownFormat(doc, from, to, kind);
|
|
104
|
+
view.dispatch({
|
|
105
|
+
changes: { from: 0, to: doc.length, insert: next.doc },
|
|
106
|
+
selection: { anchor: next.from, head: next.to },
|
|
107
|
+
});
|
|
108
|
+
view.focus();
|
|
109
|
+
}
|
|
72
110
|
</script>
|
|
73
111
|
|
|
74
|
-
<input type="hidden" {name}
|
|
112
|
+
<input type="hidden" {name} {value} />
|
|
75
113
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
{/if}
|
|
114
|
+
<div class="border-base-300 overflow-hidden rounded-box border">
|
|
115
|
+
<EditorToolbar format={applyFormat} />
|
|
116
|
+
<div bind:this={host}></div>
|
|
117
|
+
{#if !mounted}
|
|
118
|
+
<textarea class="textarea min-h-64 w-full font-mono text-sm" bind:value aria-label="Markdown source"></textarea>
|
|
119
|
+
{/if}
|
|
120
|
+
</div>
|
|
@@ -3,16 +3,15 @@ interface Props {
|
|
|
3
3
|
value: string;
|
|
4
4
|
/** The hidden field name the value is mirrored to for form submit. */
|
|
5
5
|
name: string;
|
|
6
|
-
/** Carta extensions from the adapter, for the design-accurate preview. */
|
|
7
|
-
plugins?: unknown[];
|
|
8
6
|
/** Receives a `(text) => void` that inserts at the cursor; the palette calls it. */
|
|
9
7
|
registerInsert?: (insert: (text: string) => void) => void;
|
|
10
8
|
}
|
|
11
9
|
/**
|
|
12
|
-
* The `MarkdownEditor` seam (spec §6, seam 5): a thin wrapper over
|
|
13
|
-
* and a cursor-insert callback.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* The `MarkdownEditor` seam (spec §6, seam 5): a thin wrapper over CodeMirror 6 exposing a bindable
|
|
11
|
+
* value and a cursor-insert callback. CodeMirror is client-only, so it mounts after the component does
|
|
12
|
+
* through a dynamic import; until then a plain textarea carries the value so the form still submits, and
|
|
13
|
+
* the hidden field mirrors the value throughout. The edit surface owns its toolbar; the design-accurate
|
|
14
|
+
* preview lives in EditPage through the adapter's render. Swapping the editor stays a one-file change.
|
|
16
15
|
*/
|
|
17
16
|
declare const MarkdownEditor: import("svelte").Component<Props, {}, "value">;
|
|
18
17
|
type MarkdownEditor = ReturnType<typeof MarkdownEditor>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MarkdownEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MarkdownEditor.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MarkdownEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MarkdownEditor.svelte.ts"],"names":[],"mappings":"AAQE,UAAU,KAAK;IACb,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,IAAI,CAAC;CAC3D;AA6GH;;;;;;GAMG;AACH,QAAA,MAAM,cAAc,gDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure markdown selection transforms for the editor toolbar. Each call maps a document and a
|
|
3
|
+
* selection range to a new document and a new selection, with no DOM. The MarkdownEditor view
|
|
4
|
+
* dispatches the result; keeping the logic here lets it unit-test without a browser.
|
|
5
|
+
*/
|
|
6
|
+
export type FormatKind = 'bold' | 'italic' | 'code' | 'heading' | 'quote' | 'ul' | 'link';
|
|
7
|
+
export interface FormatResult {
|
|
8
|
+
doc: string;
|
|
9
|
+
from: number;
|
|
10
|
+
to: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function applyMarkdownFormat(doc: string, from: number, to: number, kind: FormatKind): FormatResult;
|
|
13
|
+
//# sourceMappingURL=markdown-format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-format.d.ts","sourceRoot":"","sources":["../../src/lib/components/markdown-format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC;AAE1F,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAKD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,YAAY,CAsBzG"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const WRAP = { bold: '**', italic: '_', code: '`' };
|
|
2
|
+
const LINE_PREFIX = { heading: '# ', quote: '> ', ul: '- ' };
|
|
3
|
+
export function applyMarkdownFormat(doc, from, to, kind) {
|
|
4
|
+
if (kind === 'bold' || kind === 'italic' || kind === 'code') {
|
|
5
|
+
const marker = WRAP[kind];
|
|
6
|
+
const next = doc.slice(0, from) + marker + doc.slice(from, to) + marker + doc.slice(to);
|
|
7
|
+
return { doc: next, from: from + marker.length, to: to + marker.length };
|
|
8
|
+
}
|
|
9
|
+
if (kind === 'link') {
|
|
10
|
+
const text = doc.slice(from, to);
|
|
11
|
+
const placeholder = 'url';
|
|
12
|
+
const lead = `[${text}](`; // everything before the url placeholder
|
|
13
|
+
const inserted = `${lead}${placeholder})`;
|
|
14
|
+
const urlStart = from + lead.length;
|
|
15
|
+
return { doc: doc.slice(0, from) + inserted + doc.slice(to), from: urlStart, to: urlStart + placeholder.length };
|
|
16
|
+
}
|
|
17
|
+
const prefix = LINE_PREFIX[kind];
|
|
18
|
+
const lineStart = doc.lastIndexOf('\n', from - 1) + 1; // 0 when the selection is on the first line
|
|
19
|
+
const region = doc.slice(lineStart, to);
|
|
20
|
+
const prefixed = region.replace(/^/gm, prefix);
|
|
21
|
+
const added = prefixed.length - region.length;
|
|
22
|
+
return { doc: doc.slice(0, lineStart) + prefixed + doc.slice(to), from: from + prefix.length, to: to + added };
|
|
23
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { CairnAdapter, CairnExtension, CairnRuntime } from './types.js';
|
|
1
|
+
import type { CairnAdapter, CairnExtension, CairnRuntime, ConceptUrlPolicy } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Fold an adapter and any extensions into the composed runtime (seam 2). Extension concepts
|
|
4
4
|
* merge after the adapter's. The asset slot (seam 4) passes through untouched.
|
|
5
5
|
*/
|
|
6
|
-
export declare function composeRuntime(adapter: CairnAdapter, extensions?: CairnExtension[]): CairnRuntime;
|
|
6
|
+
export declare function composeRuntime(adapter: CairnAdapter, extensions?: CairnExtension[], urlPolicy?: Record<string, ConceptUrlPolicy | undefined>): CairnRuntime;
|
|
7
7
|
//# sourceMappingURL=compose.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../src/lib/content/compose.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAc,YAAY,EAAE,cAAc,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../src/lib/content/compose.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAc,YAAY,EAAE,cAAc,EAAE,YAAY,EAAiB,gBAAgB,EAAgB,MAAM,YAAY,CAAC;AAGxI;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,YAAY,EACrB,UAAU,GAAE,cAAc,EAAO,EACjC,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,SAAS,CAAM,GAC3D,YAAY,CAuBd"}
|
package/dist/content/compose.js
CHANGED
|
@@ -3,7 +3,7 @@ import { normalizeConcepts } from './concepts.js';
|
|
|
3
3
|
* Fold an adapter and any extensions into the composed runtime (seam 2). Extension concepts
|
|
4
4
|
* merge after the adapter's. The asset slot (seam 4) passes through untouched.
|
|
5
5
|
*/
|
|
6
|
-
export function composeRuntime(adapter, extensions = []) {
|
|
6
|
+
export function composeRuntime(adapter, extensions = [], urlPolicy = {}) {
|
|
7
7
|
const content = { ...adapter.content };
|
|
8
8
|
const adminPanels = [];
|
|
9
9
|
const fieldTypes = [];
|
|
@@ -19,7 +19,7 @@ export function composeRuntime(adapter, extensions = []) {
|
|
|
19
19
|
}
|
|
20
20
|
return {
|
|
21
21
|
siteName: adapter.siteName,
|
|
22
|
-
concepts: normalizeConcepts(content),
|
|
22
|
+
concepts: normalizeConcepts(content, urlPolicy),
|
|
23
23
|
backend: adapter.backend,
|
|
24
24
|
sender: adapter.sender,
|
|
25
25
|
render: adapter.render,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConceptConfig, ConceptDescriptor, RoutingRule } from './types.js';
|
|
1
|
+
import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
|
|
4
4
|
* pages are plain navigable structure. Not in adapter config. A future Fragments adds one
|
|
@@ -6,12 +6,13 @@ import type { ConceptConfig, ConceptDescriptor, RoutingRule } from './types.js';
|
|
|
6
6
|
*/
|
|
7
7
|
export declare const CONCEPT_ROUTING: Readonly<Record<string, RoutingRule>>;
|
|
8
8
|
/**
|
|
9
|
-
* Normalize an adapter's declared concepts into uniform descriptors (seam 1).
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
|
|
10
|
+
* (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
|
|
11
|
+
* concept id; each value defaults when the YAML omits it (`/:slug` for Pages, `/<id>/:slug`
|
|
12
|
+
* otherwise; `datePrefix` defaults to `day`). `routing` is injectable so a contract test can prove
|
|
13
|
+
* a new concept attaches additively; production passes the default `CONCEPT_ROUTING`.
|
|
13
14
|
*/
|
|
14
|
-
export declare function normalizeConcepts(content: Record<string, ConceptConfig | undefined>, routing?: Readonly<Record<string, RoutingRule>>): ConceptDescriptor[];
|
|
15
|
+
export declare function normalizeConcepts(content: Record<string, ConceptConfig | undefined>, urlPolicy?: Record<string, ConceptUrlPolicy | undefined>, routing?: Readonly<Record<string, RoutingRule>>): ConceptDescriptor[];
|
|
15
16
|
/** Look up a normalized concept by id, or undefined when the site does not enable it. */
|
|
16
17
|
export declare function findConcept(concepts: ConceptDescriptor[], id: string): ConceptDescriptor | undefined;
|
|
17
18
|
//# sourceMappingURL=concepts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"concepts.d.ts","sourceRoot":"","sources":["../../src/lib/content/concepts.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"concepts.d.ts","sourceRoot":"","sources":["../../src/lib/content/concepts.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAElG;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAGjE,CAAC;AAeF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,CAAC,EAClD,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,SAAS,CAAM,EAC5D,OAAO,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAmB,GAC/D,iBAAiB,EAAE,CAiBrB;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CACzB,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,EAAE,EAAE,MAAM,GACT,iBAAiB,GAAG,SAAS,CAE/B"}
|
package/dist/content/concepts.js
CHANGED
|
@@ -18,22 +18,25 @@ function defaultPermalink(id) {
|
|
|
18
18
|
return id === 'pages' ? '/:slug' : `/${id}/:slug`;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* Normalize an adapter's declared concepts into uniform descriptors (seam 1).
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
|
|
22
|
+
* (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
|
|
23
|
+
* concept id; each value defaults when the YAML omits it (`/:slug` for Pages, `/<id>/:slug`
|
|
24
|
+
* otherwise; `datePrefix` defaults to `day`). `routing` is injectable so a contract test can prove
|
|
25
|
+
* a new concept attaches additively; production passes the default `CONCEPT_ROUTING`.
|
|
25
26
|
*/
|
|
26
|
-
export function normalizeConcepts(content, routing = CONCEPT_ROUTING) {
|
|
27
|
+
export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROUTING) {
|
|
27
28
|
const descriptors = [];
|
|
28
29
|
for (const [id, config] of Object.entries(content)) {
|
|
29
30
|
if (!config)
|
|
30
31
|
continue;
|
|
32
|
+
const policy = urlPolicy[id] ?? {};
|
|
31
33
|
descriptors.push({
|
|
32
34
|
id,
|
|
33
35
|
label: config.label ?? defaultLabel(id),
|
|
34
36
|
dir: config.dir,
|
|
35
37
|
routing: routing[id] ?? DEFAULT_ROUTING,
|
|
36
|
-
permalink:
|
|
38
|
+
permalink: policy.permalink ?? defaultPermalink(id),
|
|
39
|
+
datePrefix: policy.datePrefix ?? 'day',
|
|
37
40
|
fields: config.fields,
|
|
38
41
|
validate: config.validate,
|
|
39
42
|
});
|
package/dist/content/ids.d.ts
CHANGED
|
@@ -14,4 +14,18 @@ export declare function filenameFromId(id: string): string;
|
|
|
14
14
|
* single hyphen; leading and trailing hyphens are trimmed.
|
|
15
15
|
*/
|
|
16
16
|
export declare function slugify(title: string): string;
|
|
17
|
+
/** Filename date-prefix granularity for a dated concept: the leading `YYYY[-MM[-DD]]-` on the stem. */
|
|
18
|
+
export type DatePrefix = 'year' | 'month' | 'day';
|
|
19
|
+
/**
|
|
20
|
+
* The URL slug for an id. A dated concept passes its `datePrefix` and the leading date prefix is
|
|
21
|
+
* stripped when present; a non-dated concept passes `null` and the id is returned verbatim. Only
|
|
22
|
+
* the leading prefix is removed, so a year-like tail (a post titled "2024 Recap") stays in the slug.
|
|
23
|
+
*/
|
|
24
|
+
export declare function slugFromId(id: string, datePrefix: DatePrefix | null): string;
|
|
25
|
+
/**
|
|
26
|
+
* Compose a dated entry's id from a `YYYY-MM-DD` date, a date-free slug, and the concept's
|
|
27
|
+
* granularity: the date truncated to the granularity, a hyphen, then the slug. Throws on a
|
|
28
|
+
* malformed date so a bad create fails before touching git.
|
|
29
|
+
*/
|
|
30
|
+
export declare function composeDatedId(date: string, slug: string, datePrefix: DatePrefix): string;
|
|
17
31
|
//# sourceMappingURL=ids.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../../src/lib/content/ids.ts"],"names":[],"mappings":"AAOA,qGAAqG;AACrG,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE7C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C"}
|
|
1
|
+
{"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../../src/lib/content/ids.ts"],"names":[],"mappings":"AAOA,qGAAqG;AACrG,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE7C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C;AAED,uGAAuG;AACvG,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AASlD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI,GAAG,MAAM,CAG5E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,CAiBzF"}
|
package/dist/content/ids.js
CHANGED
|
@@ -31,3 +31,43 @@ export function slugify(title) {
|
|
|
31
31
|
.replace(/[^a-z0-9]+/g, '-')
|
|
32
32
|
.replace(/^-+|-+$/g, '');
|
|
33
33
|
}
|
|
34
|
+
/** The leading date-prefix shape for each granularity. */
|
|
35
|
+
const DATE_PREFIX_RE = {
|
|
36
|
+
year: /^\d{4}-/,
|
|
37
|
+
month: /^\d{4}-\d{2}-/,
|
|
38
|
+
day: /^\d{4}-\d{2}-\d{2}-/,
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The URL slug for an id. A dated concept passes its `datePrefix` and the leading date prefix is
|
|
42
|
+
* stripped when present; a non-dated concept passes `null` and the id is returned verbatim. Only
|
|
43
|
+
* the leading prefix is removed, so a year-like tail (a post titled "2024 Recap") stays in the slug.
|
|
44
|
+
*/
|
|
45
|
+
export function slugFromId(id, datePrefix) {
|
|
46
|
+
if (!datePrefix)
|
|
47
|
+
return id;
|
|
48
|
+
return id.replace(DATE_PREFIX_RE[datePrefix], '');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Compose a dated entry's id from a `YYYY-MM-DD` date, a date-free slug, and the concept's
|
|
52
|
+
* granularity: the date truncated to the granularity, a hyphen, then the slug. Throws on a
|
|
53
|
+
* malformed date so a bad create fails before touching git.
|
|
54
|
+
*/
|
|
55
|
+
export function composeDatedId(date, slug, datePrefix) {
|
|
56
|
+
const m = date.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
57
|
+
if (!m)
|
|
58
|
+
throw new Error(`composeDatedId: malformed date "${date}"`);
|
|
59
|
+
const [, year, month, day] = m;
|
|
60
|
+
let prefix;
|
|
61
|
+
switch (datePrefix) {
|
|
62
|
+
case 'year':
|
|
63
|
+
prefix = year;
|
|
64
|
+
break;
|
|
65
|
+
case 'month':
|
|
66
|
+
prefix = `${year}-${month}`;
|
|
67
|
+
break;
|
|
68
|
+
case 'day':
|
|
69
|
+
prefix = `${year}-${month}-${day}`;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
return `${prefix}-${slug}`;
|
|
73
|
+
}
|