svelte-docsmith 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/buildtime/changelog.d.ts +20 -0
- package/dist/buildtime/changelog.js +74 -0
- package/dist/buildtime/fence-meta.d.ts +20 -0
- package/dist/buildtime/fence-meta.js +48 -0
- package/dist/buildtime/markdown-layout.svelte.d.ts +3 -0
- package/dist/buildtime/preprocess.d.ts +13 -0
- package/dist/buildtime/preprocess.js +77 -17
- package/dist/buildtime/twoslash.d.ts +26 -0
- package/dist/buildtime/twoslash.js +40 -0
- package/dist/buildtime/vite/collect.js +31 -3
- package/dist/buildtime/vite/git.d.ts +7 -0
- package/dist/buildtime/vite/git.js +32 -0
- package/dist/buildtime/vite/index.d.ts +13 -0
- package/dist/buildtime/vite/index.js +16 -0
- package/dist/buildtime/vite/releases.d.ts +12 -0
- package/dist/buildtime/vite/releases.js +57 -0
- package/dist/components/changelog/changelog-entry.svelte +90 -0
- package/dist/components/changelog/changelog-entry.svelte.d.ts +7 -0
- package/dist/components/changelog/changelog.svelte +56 -0
- package/dist/components/changelog/changelog.svelte.d.ts +14 -0
- package/dist/components/chrome/announcement-bar.svelte +188 -0
- package/dist/components/chrome/announcement-bar.svelte.d.ts +7 -0
- package/dist/components/docs/mermaid.svelte +213 -0
- package/dist/components/docs/mermaid.svelte.d.ts +6 -0
- package/dist/components/docs/tabs-sync.svelte.d.ts +10 -0
- package/dist/components/docs/tabs-sync.svelte.js +59 -0
- package/dist/components/docs/tabs.svelte +42 -6
- package/dist/components/docs/tabs.svelte.d.ts +6 -0
- package/dist/components/landing/action.svelte +50 -0
- package/dist/components/landing/action.svelte.d.ts +13 -0
- package/dist/components/landing/cta.svelte +52 -0
- package/dist/components/landing/cta.svelte.d.ts +13 -0
- package/dist/components/landing/feature-grid.svelte +51 -0
- package/dist/components/landing/feature-grid.svelte.d.ts +19 -0
- package/dist/components/landing/feature.svelte +33 -0
- package/dist/components/landing/feature.svelte.d.ts +11 -0
- package/dist/components/landing/hero.svelte +70 -0
- package/dist/components/landing/hero.svelte.d.ts +19 -0
- package/dist/components/layouts/docs-header.svelte +36 -3
- package/dist/components/layouts/docs-shell.svelte +14 -8
- package/dist/components/layouts/docs-sidebar.svelte +52 -18
- package/dist/components/layouts/docs-sidebar.svelte.d.ts +1 -1
- package/dist/components/markdown/pre.svelte +238 -8
- package/dist/components/markdown/pre.svelte.d.ts +6 -0
- package/dist/core/changelog.d.ts +31 -0
- package/dist/core/changelog.js +7 -0
- package/dist/core/config.d.ts +21 -0
- package/dist/core/config.js +19 -0
- package/dist/core/content.d.ts +6 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/core/nav.d.ts +23 -6
- package/dist/core/nav.js +94 -15
- package/dist/fallbacks/changelog.d.ts +10 -0
- package/dist/fallbacks/changelog.js +3 -0
- package/dist/generate/feed.d.ts +35 -0
- package/dist/generate/feed.js +80 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/theme.css +32 -0
- package/package.json +21 -2
|
@@ -6,18 +6,102 @@
|
|
|
6
6
|
|
|
7
7
|
const clipboard = useClipboard();
|
|
8
8
|
|
|
9
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
title,
|
|
11
|
+
lineNumbers = false,
|
|
12
|
+
startLine = 1,
|
|
13
|
+
children
|
|
14
|
+
}: {
|
|
15
|
+
/** Filename or caption, from the fence's `title=` metadata. */
|
|
16
|
+
title?: string;
|
|
17
|
+
/** Number the lines, from `showLineNumbers`. */
|
|
18
|
+
lineNumbers?: boolean;
|
|
19
|
+
/** First line's number, from `startLine=`. */
|
|
20
|
+
startLine?: number;
|
|
21
|
+
children: Snippet;
|
|
22
|
+
} = $props();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Twoslash popups are absolutely positioned inside the code block, which is a
|
|
26
|
+
* scroll container, so anything reaching past its edges is clipped: a hover
|
|
27
|
+
* near the last line shows a sliver of the type and nothing else. CSS can't
|
|
28
|
+
* fix this, because a horizontally scrolling element must also clip
|
|
29
|
+
* vertically. So on hover the popup is promoted to fixed positioning, whose
|
|
30
|
+
* containing block is the viewport rather than the scroller.
|
|
31
|
+
*
|
|
32
|
+
* Without JavaScript the popup still opens on hover, clipped as before, so
|
|
33
|
+
* this only ever improves on the CSS-only behaviour.
|
|
34
|
+
*/
|
|
35
|
+
function unclipPopups(node: HTMLElement) {
|
|
36
|
+
const GAP = 6;
|
|
37
|
+
|
|
38
|
+
const place = (event: Event) => {
|
|
39
|
+
const target = event.target as HTMLElement | null;
|
|
40
|
+
const trigger = target?.closest?.('.twoslash-hover') as HTMLElement | null;
|
|
41
|
+
if (!trigger) return;
|
|
42
|
+
const popup = trigger.querySelector('.twoslash-popup-container') as HTMLElement | null;
|
|
43
|
+
if (!popup) return;
|
|
44
|
+
|
|
45
|
+
// Measure while visible but before committing a position, so the popup's
|
|
46
|
+
// own width decides whether it flips to stay on screen.
|
|
47
|
+
popup.style.position = 'fixed';
|
|
48
|
+
popup.style.left = '0px';
|
|
49
|
+
popup.style.top = '0px';
|
|
50
|
+
const rect = trigger.getBoundingClientRect();
|
|
51
|
+
const { width, height } = popup.getBoundingClientRect();
|
|
52
|
+
|
|
53
|
+
const left = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8));
|
|
54
|
+
// Below the token unless that would run off the bottom, then above it.
|
|
55
|
+
const below = rect.bottom + GAP;
|
|
56
|
+
const top = below + height > window.innerHeight - 8 ? rect.top - height - GAP : below;
|
|
57
|
+
|
|
58
|
+
popup.style.left = `${left}px`;
|
|
59
|
+
popup.style.top = `${Math.max(8, top)}px`;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const reset = (event: Event) => {
|
|
63
|
+
const trigger = (event.target as HTMLElement | null)?.closest?.(
|
|
64
|
+
'.twoslash-hover'
|
|
65
|
+
) as HTMLElement | null;
|
|
66
|
+
const popup = trigger?.querySelector('.twoslash-popup-container') as HTMLElement | null;
|
|
67
|
+
if (popup) popup.removeAttribute('style');
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
node.addEventListener('mouseover', place);
|
|
71
|
+
node.addEventListener('mouseout', reset);
|
|
72
|
+
return {
|
|
73
|
+
destroy() {
|
|
74
|
+
node.removeEventListener('mouseover', place);
|
|
75
|
+
node.removeEventListener('mouseout', reset);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
10
79
|
</script>
|
|
11
80
|
|
|
12
|
-
<div class="rounded-md overflow-hidden bg-muted mt-2 shadow-md text-sm relative">
|
|
81
|
+
<div class="rounded-md overflow-hidden bg-muted mt-2 shadow-md text-sm relative" use:unclipPopups>
|
|
82
|
+
{#if title}
|
|
83
|
+
<!-- The filename bar doubles as the copy button's row, so the button stops
|
|
84
|
+
floating over the first line of code when a title is present. -->
|
|
85
|
+
<div class="docsmith-code-title">
|
|
86
|
+
<span class="docsmith-code-title-text">{title}</span>
|
|
87
|
+
<CopyButton copied={clipboard.copied} onclick={() => clipboard.copy()} class="bg-muted" />
|
|
88
|
+
</div>
|
|
89
|
+
{/if}
|
|
90
|
+
|
|
13
91
|
<ScrollArea orientation="both" class="relative">
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
92
|
+
{#if !title}
|
|
93
|
+
<CopyButton
|
|
94
|
+
copied={clipboard.copied}
|
|
95
|
+
onclick={() => clipboard.copy()}
|
|
96
|
+
class="bg-muted absolute top-2 right-2 z-10"
|
|
97
|
+
/>
|
|
98
|
+
{/if}
|
|
19
99
|
|
|
20
|
-
<pre
|
|
100
|
+
<pre
|
|
101
|
+
class="not-prose shiki max-h-[32rem] flex shrink"
|
|
102
|
+
class:docsmith-line-numbers={lineNumbers}
|
|
103
|
+
style={lineNumbers && startLine !== 1 ? `--docsmith-line-start: ${startLine - 1}` : undefined}
|
|
104
|
+
use:clipboard.readText>
|
|
21
105
|
{@render children()}
|
|
22
106
|
</pre>
|
|
23
107
|
</ScrollArea>
|
|
@@ -44,6 +128,64 @@
|
|
|
44
128
|
padding-inline: 1rem;
|
|
45
129
|
}
|
|
46
130
|
|
|
131
|
+
/* --- Filename bar --- */
|
|
132
|
+
.docsmith-code-title {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: space-between;
|
|
136
|
+
gap: 0.75rem;
|
|
137
|
+
padding: 0.4rem 0.5rem 0.4rem 1rem;
|
|
138
|
+
border-bottom: 1px solid var(--border);
|
|
139
|
+
background: color-mix(in oklch, var(--muted) 60%, var(--background));
|
|
140
|
+
}
|
|
141
|
+
.docsmith-code-title-text {
|
|
142
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
143
|
+
font-size: 0.78rem;
|
|
144
|
+
color: var(--muted-foreground);
|
|
145
|
+
overflow-wrap: anywhere;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* --- Line numbers ---
|
|
149
|
+
A CSS counter on Shiki's existing per-line spans, so numbering adds no DOM
|
|
150
|
+
and stays out of the text the copy button reads. `--docsmith-line-start`
|
|
151
|
+
offsets the counter for a snippet lifted out of a larger file. */
|
|
152
|
+
:global(pre.docsmith-line-numbers code) {
|
|
153
|
+
counter-reset: docsmith-line var(--docsmith-line-start, 0);
|
|
154
|
+
}
|
|
155
|
+
:global(pre.docsmith-line-numbers span.line) {
|
|
156
|
+
counter-increment: docsmith-line;
|
|
157
|
+
position: relative;
|
|
158
|
+
padding-inline-start: 3.2rem;
|
|
159
|
+
}
|
|
160
|
+
:global(pre.docsmith-line-numbers span.line::before) {
|
|
161
|
+
content: counter(docsmith-line);
|
|
162
|
+
position: absolute;
|
|
163
|
+
left: 0;
|
|
164
|
+
width: 2.4rem;
|
|
165
|
+
text-align: right;
|
|
166
|
+
color: color-mix(in oklch, var(--muted-foreground) 65%, transparent);
|
|
167
|
+
/* Numbers are decoration, not content: keep them out of selections and
|
|
168
|
+
off the clipboard. */
|
|
169
|
+
user-select: none;
|
|
170
|
+
}
|
|
171
|
+
/* The diff gutter glyph would collide with the number, so shift it over. */
|
|
172
|
+
:global(pre.docsmith-line-numbers span.line.diff::before) {
|
|
173
|
+
content: counter(docsmith-line);
|
|
174
|
+
}
|
|
175
|
+
:global(pre.docsmith-line-numbers span.line.diff::after) {
|
|
176
|
+
position: absolute;
|
|
177
|
+
left: 2.7rem;
|
|
178
|
+
font-weight: 600;
|
|
179
|
+
}
|
|
180
|
+
:global(pre.docsmith-line-numbers span.line.diff.add::after) {
|
|
181
|
+
content: '+';
|
|
182
|
+
color: oklch(0.6 0.15 150);
|
|
183
|
+
}
|
|
184
|
+
:global(pre.docsmith-line-numbers span.line.diff.remove::after) {
|
|
185
|
+
content: '−';
|
|
186
|
+
color: oklch(0.58 0.19 25);
|
|
187
|
+
}
|
|
188
|
+
|
|
47
189
|
/* Two tiers of annotation. EMPHASIS (highlight, word) tints with the theme's
|
|
48
190
|
--primary, so it reads as the brand pointing at a line and recolors with
|
|
49
191
|
the active theme. SEMANTIC (diff, error, warning) uses fixed OKLCH hues so
|
|
@@ -125,6 +267,94 @@
|
|
|
125
267
|
}
|
|
126
268
|
}
|
|
127
269
|
|
|
270
|
+
/* --- Twoslash hovers ---
|
|
271
|
+
Restyled from @shikijs/twoslash's rich theme onto our tokens so the popup
|
|
272
|
+
belongs to the site in both colour schemes. Underline marks a token as
|
|
273
|
+
inspectable; the popup is a hidden sibling revealed on hover. `unclipPopups`
|
|
274
|
+
then promotes it to fixed positioning so the code block's scrolling does not
|
|
275
|
+
clip it — see the comment there. */
|
|
276
|
+
:global(pre .twoslash-hover) {
|
|
277
|
+
position: relative;
|
|
278
|
+
border-bottom: 1px dotted color-mix(in oklch, var(--muted-foreground) 55%, transparent);
|
|
279
|
+
}
|
|
280
|
+
:global(pre .twoslash-popup-container) {
|
|
281
|
+
position: absolute;
|
|
282
|
+
z-index: 40;
|
|
283
|
+
display: none;
|
|
284
|
+
left: 0;
|
|
285
|
+
top: 1.6em;
|
|
286
|
+
/* Shrink to the type's own width so a short signature isn't a wide slab;
|
|
287
|
+
the script measures this before choosing a position. */
|
|
288
|
+
width: max-content;
|
|
289
|
+
max-width: min(32rem, 78vw);
|
|
290
|
+
padding: 0.5rem 0.7rem;
|
|
291
|
+
border: 1px solid var(--border);
|
|
292
|
+
border-radius: var(--radius, 0.5rem);
|
|
293
|
+
background: var(--popover, var(--card));
|
|
294
|
+
color: var(--popover-foreground, var(--foreground));
|
|
295
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 0.18);
|
|
296
|
+
white-space: pre-wrap;
|
|
297
|
+
text-align: left;
|
|
298
|
+
font-size: 0.85em;
|
|
299
|
+
line-height: 1.5;
|
|
300
|
+
}
|
|
301
|
+
:global(pre .twoslash-hover:hover .twoslash-popup-container) {
|
|
302
|
+
display: block;
|
|
303
|
+
}
|
|
304
|
+
/* The signature keeps Shiki's own colours and monospace. */
|
|
305
|
+
:global(pre .twoslash-popup-code) {
|
|
306
|
+
display: block;
|
|
307
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
308
|
+
white-space: pre-wrap;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* Prose, by contrast, is prose. Inside a <pre> it would otherwise inherit
|
|
312
|
+
monospace and whatever token colour the hovered span happened to carry,
|
|
313
|
+
which reads as more code rather than an explanation of it. */
|
|
314
|
+
:global(pre .twoslash-popup-docs) {
|
|
315
|
+
/* The dark-theme swap repaints every span inside `.shiki` with
|
|
316
|
+
`--shiki-dark`, which these spans inherit from whichever token was
|
|
317
|
+
hovered — so docs took on the colour of the word above them. Redefining
|
|
318
|
+
the variable here resolves it to prose colours instead of fighting an
|
|
319
|
+
`!important` rule. */
|
|
320
|
+
--shiki-dark: var(--muted-foreground);
|
|
321
|
+
margin-top: 0.5rem;
|
|
322
|
+
padding-top: 0.5rem;
|
|
323
|
+
border-top: 1px solid var(--border);
|
|
324
|
+
font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif);
|
|
325
|
+
font-size: 0.92em;
|
|
326
|
+
line-height: 1.55;
|
|
327
|
+
color: var(--muted-foreground);
|
|
328
|
+
white-space: normal;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* Each @param is its own line: the tags are inline spans, so without this
|
|
332
|
+
they run together into one paragraph. */
|
|
333
|
+
:global(pre .twoslash-popup-docs-tags) {
|
|
334
|
+
display: flex;
|
|
335
|
+
flex-direction: column;
|
|
336
|
+
gap: 0.35rem;
|
|
337
|
+
}
|
|
338
|
+
:global(pre .twoslash-popup-docs-tag) {
|
|
339
|
+
display: block;
|
|
340
|
+
}
|
|
341
|
+
:global(pre .twoslash-popup-docs-tag-value) {
|
|
342
|
+
--shiki-dark: var(--muted-foreground);
|
|
343
|
+
color: var(--muted-foreground);
|
|
344
|
+
}
|
|
345
|
+
/* And the tag name needs a gap, or it reads as "@paramcallbackfn". */
|
|
346
|
+
:global(pre .twoslash-popup-docs-tag-name) {
|
|
347
|
+
--shiki-dark: var(--primary);
|
|
348
|
+
margin-right: 0.4em;
|
|
349
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
350
|
+
font-size: 0.92em;
|
|
351
|
+
color: var(--primary);
|
|
352
|
+
}
|
|
353
|
+
/* A snippet that deliberately shows an error (// @errors:) marks the line. */
|
|
354
|
+
:global(pre .twoslash-error) {
|
|
355
|
+
background: color-mix(in oklch, oklch(0.62 0.2 25) 14%, transparent);
|
|
356
|
+
}
|
|
357
|
+
|
|
128
358
|
/* --- Word highlight (// [!code word:name]): a primary-tinted inline pill --- */
|
|
129
359
|
:global(pre .highlighted-word) {
|
|
130
360
|
border-radius: 0.3rem;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
|
+
/** Filename or caption, from the fence's `title=` metadata. */
|
|
4
|
+
title?: string;
|
|
5
|
+
/** Number the lines, from `showLineNumbers`. */
|
|
6
|
+
lineNumbers?: boolean;
|
|
7
|
+
/** First line's number, from `startLine=`. */
|
|
8
|
+
startLine?: number;
|
|
3
9
|
children: Snippet;
|
|
4
10
|
};
|
|
5
11
|
declare const Pre: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record types for the generated changelog index. The `docsmith()` Vite
|
|
3
|
+
* plugin parses your package's `CHANGELOG.md` at build time and emits these as
|
|
4
|
+
* the `svelte-docsmith/changelog` virtual module, so every release becomes a
|
|
5
|
+
* page and a feed entry without being written twice.
|
|
6
|
+
*/
|
|
7
|
+
/** One group of changes within a release, e.g. "Minor Changes". */
|
|
8
|
+
export type ChangelogGroup = {
|
|
9
|
+
/** The group heading as written, e.g. `Minor Changes` or `Patch Changes`. */
|
|
10
|
+
kind: string;
|
|
11
|
+
/**
|
|
12
|
+
* Each entry's body as HTML, rendered from markdown at build time with the
|
|
13
|
+
* changeset's commit hash removed. HTML rather than markdown so a consuming
|
|
14
|
+
* site needs no runtime renderer.
|
|
15
|
+
*/
|
|
16
|
+
items: string[];
|
|
17
|
+
};
|
|
18
|
+
/** One released version. */
|
|
19
|
+
export type ChangelogRelease = {
|
|
20
|
+
/** Version string as it appears in the changelog, e.g. `0.8.0`. */
|
|
21
|
+
version: string;
|
|
22
|
+
/** Release date (ISO) from the git history, when it can be determined. */
|
|
23
|
+
date?: string;
|
|
24
|
+
groups: ChangelogGroup[];
|
|
25
|
+
/**
|
|
26
|
+
* A hand-written page for this release, when `src/routes/changelog/<version>/`
|
|
27
|
+
* exists. The generated entry is the fallback, so a release that deserves a
|
|
28
|
+
* proper write-up can have one without the rest going undocumented.
|
|
29
|
+
*/
|
|
30
|
+
path?: string;
|
|
31
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record types for the generated changelog index. The `docsmith()` Vite
|
|
3
|
+
* plugin parses your package's `CHANGELOG.md` at build time and emits these as
|
|
4
|
+
* the `svelte-docsmith/changelog` virtual module, so every release becomes a
|
|
5
|
+
* page and a feed entry without being written twice.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
package/dist/core/config.d.ts
CHANGED
|
@@ -14,6 +14,25 @@ export type DocsmithFooterColumn = {
|
|
|
14
14
|
title: string;
|
|
15
15
|
links: DocsmithLink[];
|
|
16
16
|
};
|
|
17
|
+
/** A thin announcement bar shown at the top of every page. */
|
|
18
|
+
export type DocsmithAnnouncement = {
|
|
19
|
+
/** The message text. */
|
|
20
|
+
text: string;
|
|
21
|
+
/** Optional leading pill label, e.g. `'New'`, `'Beta'`, or a version. */
|
|
22
|
+
tag?: string;
|
|
23
|
+
/** Optional link the bar points to. */
|
|
24
|
+
href?: string;
|
|
25
|
+
/** Open the link in a new tab with `rel="noopener"`. */
|
|
26
|
+
external?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Dismissal key. A dismissed bar stays dismissed until this value changes, so
|
|
29
|
+
* bump it (or edit `text`) when you post a new announcement. Defaults to the
|
|
30
|
+
* text itself.
|
|
31
|
+
*/
|
|
32
|
+
id?: string;
|
|
33
|
+
/** Let readers dismiss the bar. Defaults to `true`. */
|
|
34
|
+
dismissible?: boolean;
|
|
35
|
+
};
|
|
17
36
|
export type DocsmithConfig = {
|
|
18
37
|
/** Site title, shown in the header/sidebar and in the `<title>` suffix. */
|
|
19
38
|
title: string;
|
|
@@ -42,6 +61,8 @@ export type DocsmithConfig = {
|
|
|
42
61
|
version?: string;
|
|
43
62
|
/** Optional logo image src; falls back to the built-in book mark. */
|
|
44
63
|
logo?: string;
|
|
64
|
+
/** Optional announcement bar shown at the top of every page. */
|
|
65
|
+
announcement?: DocsmithAnnouncement;
|
|
45
66
|
/** Top-level header navigation links. */
|
|
46
67
|
nav?: DocsmithLink[];
|
|
47
68
|
/** Footer content, driven by data. */
|
package/dist/core/config.js
CHANGED
|
@@ -23,6 +23,25 @@ export function defineConfig(config) {
|
|
|
23
23
|
throw new Error(`[svelte-docsmith] config.${key} must be a string when set.`);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
if (config.announcement !== undefined) {
|
|
27
|
+
const a = config.announcement;
|
|
28
|
+
if (typeof a !== 'object' || a === null) {
|
|
29
|
+
throw new Error('[svelte-docsmith] config.announcement must be an object ({ text, href?, id?, external?, dismissible? }).');
|
|
30
|
+
}
|
|
31
|
+
if (typeof a.text !== 'string' || a.text.trim() === '') {
|
|
32
|
+
throw new Error('[svelte-docsmith] config.announcement.text is required and must be a string.');
|
|
33
|
+
}
|
|
34
|
+
for (const key of ['tag', 'href', 'id']) {
|
|
35
|
+
if (a[key] !== undefined && typeof a[key] !== 'string') {
|
|
36
|
+
throw new Error(`[svelte-docsmith] config.announcement.${key} must be a string when set.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const key of ['external', 'dismissible']) {
|
|
40
|
+
if (a[key] !== undefined && typeof a[key] !== 'boolean') {
|
|
41
|
+
throw new Error(`[svelte-docsmith] config.announcement.${key} must be a boolean when set.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
26
45
|
if (config.nav !== undefined) {
|
|
27
46
|
if (!Array.isArray(config.nav)) {
|
|
28
47
|
throw new Error('[svelte-docsmith] config.nav must be an array of { label, href } links.');
|
package/dist/core/content.d.ts
CHANGED
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
export type DocsContentItem = {
|
|
14
14
|
title: string;
|
|
15
15
|
path: string;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Sidebar group. A string places the page in a single top-level group; an
|
|
18
|
+
* array names a nested group path (e.g. `['Guides', 'Advanced']`), building
|
|
19
|
+
* collapsible subsections. Omitted pages fall under "Docs".
|
|
20
|
+
*/
|
|
21
|
+
section?: string | string[];
|
|
17
22
|
order?: number;
|
|
18
23
|
description?: string;
|
|
19
24
|
/** Source file path relative to the app cwd, for the "Edit this page" link. */
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { defineConfig, type DocsmithConfig, type DocsmithLink, type DocsmithFooterColumn } from './config.js';
|
|
1
|
+
export { defineConfig, type DocsmithConfig, type DocsmithLink, type DocsmithFooterColumn, type DocsmithAnnouncement } from './config.js';
|
|
2
2
|
export type { DocsContentItem, SearchDoc, LlmsDoc } from './content.js';
|
|
3
|
-
export { navFromContent, type NavItem, type NavGroup } from './nav.js';
|
|
3
|
+
export { navFromContent, flattenNav, navTrail, isNavGroup, type NavItem, type NavGroup, type NavNode } from './nav.js';
|
package/dist/core/index.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
// several of these (e.g. the shell) import from here; the public surface is
|
|
4
4
|
// re-exported through `src/lib/index.ts`.
|
|
5
5
|
export { defineConfig } from './config.js';
|
|
6
|
-
export { navFromContent } from './nav.js';
|
|
6
|
+
export { navFromContent, flattenNav, navTrail, isNavGroup } from './nav.js';
|
package/dist/core/nav.d.ts
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import type { DocsContentItem } from './content.js';
|
|
2
|
-
/** A single sidebar link. */
|
|
2
|
+
/** A single sidebar link (a leaf). */
|
|
3
3
|
export type NavItem = {
|
|
4
4
|
title: string;
|
|
5
5
|
url: string;
|
|
6
6
|
};
|
|
7
|
-
/** A titled group of sidebar
|
|
7
|
+
/** A titled group of sidebar entries, which may themselves be nested groups. */
|
|
8
8
|
export type NavGroup = {
|
|
9
9
|
title: string;
|
|
10
|
-
items:
|
|
10
|
+
items: NavNode[];
|
|
11
11
|
};
|
|
12
|
+
/** A sidebar entry: either a link or a nested group. */
|
|
13
|
+
export type NavNode = NavItem | NavGroup;
|
|
14
|
+
/** Narrow a {@link NavNode} to a {@link NavGroup} (an entry with children). */
|
|
15
|
+
export declare function isNavGroup(node: NavNode): node is NavGroup;
|
|
12
16
|
/**
|
|
13
|
-
* Derive sidebar nav from a content collection
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* Derive sidebar nav from a content collection. Each page's `section` (a string
|
|
18
|
+
* or a nested path) places it in the tree; within every level, entries order by
|
|
19
|
+
* `order` (then title), and a group inherits the smallest `order` of its
|
|
20
|
+
* descendants so groups sort by their earliest child. Top-level entries are
|
|
21
|
+
* always groups; pages without a section fall under "Docs".
|
|
16
22
|
*/
|
|
17
23
|
export declare function navFromContent(content: DocsContentItem[]): NavGroup[];
|
|
24
|
+
/**
|
|
25
|
+
* Flatten the nav tree to its leaf links in sidebar reading order (depth-first),
|
|
26
|
+
* for the prev/next pager.
|
|
27
|
+
*/
|
|
28
|
+
export declare function flattenNav(nodes: NavNode[]): NavItem[];
|
|
29
|
+
/**
|
|
30
|
+
* The trail of group titles leading to the page at `url` (top group first,
|
|
31
|
+
* excluding the page itself), or `undefined` if the page is not in the tree.
|
|
32
|
+
* Drives the breadcrumb trail.
|
|
33
|
+
*/
|
|
34
|
+
export declare function navTrail(nodes: NavNode[], url: string): string[] | undefined;
|
package/dist/core/nav.js
CHANGED
|
@@ -1,22 +1,101 @@
|
|
|
1
|
+
/** Narrow a {@link NavNode} to a {@link NavGroup} (an entry with children). */
|
|
2
|
+
export function isNavGroup(node) {
|
|
3
|
+
return 'items' in node;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a page's `section` to a group path. A string is a single level; an
|
|
7
|
+
* array is a nested path; blank or missing sections fall under "Docs". Empty
|
|
8
|
+
* strings inside an array are dropped so a stray `['Guides', '']` stays sane.
|
|
9
|
+
*/
|
|
10
|
+
function sectionPath(section) {
|
|
11
|
+
const segs = Array.isArray(section) ? section : section ? [section] : [];
|
|
12
|
+
const clean = segs.filter((s) => typeof s === 'string' && s.trim() !== '');
|
|
13
|
+
return clean.length ? clean : ['Docs'];
|
|
14
|
+
}
|
|
15
|
+
function makeBuilder(title) {
|
|
16
|
+
return { title, order: Infinity, groups: new Map(), leaves: [] };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Emit a group's children (nested groups and leaf links) as one list, sorted by
|
|
20
|
+
* `order` and then title. The title tiebreak keeps the output deterministic when
|
|
21
|
+
* pages share an order (e.g. all default to 0), instead of depending on the
|
|
22
|
+
* filesystem scan order.
|
|
23
|
+
*/
|
|
24
|
+
function emitItems(node) {
|
|
25
|
+
const entries = [];
|
|
26
|
+
for (const leaf of node.leaves) {
|
|
27
|
+
entries.push({
|
|
28
|
+
order: leaf.order,
|
|
29
|
+
title: leaf.title,
|
|
30
|
+
node: { title: leaf.title, url: leaf.url }
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
for (const group of node.groups.values()) {
|
|
34
|
+
entries.push({
|
|
35
|
+
order: group.order,
|
|
36
|
+
title: group.title,
|
|
37
|
+
node: { title: group.title, items: emitItems(group) }
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
entries.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
|
|
41
|
+
return entries.map((e) => e.node);
|
|
42
|
+
}
|
|
1
43
|
/**
|
|
2
|
-
* Derive sidebar nav from a content collection
|
|
3
|
-
*
|
|
4
|
-
*
|
|
44
|
+
* Derive sidebar nav from a content collection. Each page's `section` (a string
|
|
45
|
+
* or a nested path) places it in the tree; within every level, entries order by
|
|
46
|
+
* `order` (then title), and a group inherits the smallest `order` of its
|
|
47
|
+
* descendants so groups sort by their earliest child. Top-level entries are
|
|
48
|
+
* always groups; pages without a section fall under "Docs".
|
|
5
49
|
*/
|
|
6
50
|
export function navFromContent(content) {
|
|
7
|
-
const
|
|
51
|
+
const root = makeBuilder('');
|
|
8
52
|
for (const item of content) {
|
|
9
|
-
const section = item.section ?? 'Docs';
|
|
10
53
|
const order = item.order ?? 0;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
54
|
+
let node = root;
|
|
55
|
+
for (const seg of sectionPath(item.section)) {
|
|
56
|
+
let child = node.groups.get(seg);
|
|
57
|
+
if (!child) {
|
|
58
|
+
child = makeBuilder(seg);
|
|
59
|
+
node.groups.set(seg, child);
|
|
60
|
+
}
|
|
61
|
+
child.order = Math.min(child.order, order);
|
|
62
|
+
node = child;
|
|
63
|
+
}
|
|
64
|
+
node.leaves.push({ title: item.title, url: item.path, order });
|
|
65
|
+
}
|
|
66
|
+
// The root only ever holds groups (every page has at least one path segment),
|
|
67
|
+
// so its emitted items are all NavGroups.
|
|
68
|
+
return emitItems(root);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Flatten the nav tree to its leaf links in sidebar reading order (depth-first),
|
|
72
|
+
* for the prev/next pager.
|
|
73
|
+
*/
|
|
74
|
+
export function flattenNav(nodes) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const node of nodes) {
|
|
77
|
+
if (isNavGroup(node))
|
|
78
|
+
out.push(...flattenNav(node.items));
|
|
79
|
+
else
|
|
80
|
+
out.push(node);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The trail of group titles leading to the page at `url` (top group first,
|
|
86
|
+
* excluding the page itself), or `undefined` if the page is not in the tree.
|
|
87
|
+
* Drives the breadcrumb trail.
|
|
88
|
+
*/
|
|
89
|
+
export function navTrail(nodes, url) {
|
|
90
|
+
for (const node of nodes) {
|
|
91
|
+
if (isNavGroup(node)) {
|
|
92
|
+
const inner = navTrail(node.items, url);
|
|
93
|
+
if (inner)
|
|
94
|
+
return [node.title, ...inner];
|
|
95
|
+
}
|
|
96
|
+
else if (node.url === url) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
15
99
|
}
|
|
16
|
-
return
|
|
17
|
-
.sort((a, b) => a[1].minOrder - b[1].minOrder)
|
|
18
|
-
.map(([title, group]) => ({
|
|
19
|
-
title,
|
|
20
|
-
items: group.items.sort((a, b) => a.order - b.order).map(({ title, url }) => ({ title, url }))
|
|
21
|
-
}));
|
|
100
|
+
return undefined;
|
|
22
101
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `svelte-docsmith/changelog` — the generated release index.
|
|
3
|
+
*
|
|
4
|
+
* At build time the `docsmith()` Vite plugin (in `vite.config.ts`) intercepts
|
|
5
|
+
* this import and replaces it with your package's `CHANGELOG.md`, parsed into
|
|
6
|
+
* releases and dated from git. This file is only the fallback that runs when
|
|
7
|
+
* the plugin is missing — see {@link missingPluginError}.
|
|
8
|
+
*/
|
|
9
|
+
import type { ChangelogRelease } from '../core/changelog.js';
|
|
10
|
+
export declare const releases: ChangelogRelease[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ChangelogRelease } from '../core/changelog.js';
|
|
2
|
+
/** Site details the feed needs beyond the releases themselves. */
|
|
3
|
+
export type FeedSite = {
|
|
4
|
+
/** Canonical origin, e.g. `https://docs.example.com`. */
|
|
5
|
+
url: string;
|
|
6
|
+
/** Feed title, usually the site title. */
|
|
7
|
+
title: string;
|
|
8
|
+
/** One-line feed description. */
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Path the changelog lives at. Default: `/changelog`. */
|
|
11
|
+
path?: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Build an Atom feed body from parsed changelog releases. Framework-agnostic:
|
|
15
|
+
* wire it into a SvelteKit `src/routes/changelog.xml/+server.ts` over the
|
|
16
|
+
* generated `svelte-docsmith/changelog` index.
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { releases } from 'svelte-docsmith/changelog';
|
|
20
|
+
* import { generateFeed } from 'svelte-docsmith';
|
|
21
|
+
*
|
|
22
|
+
* export const prerender = true;
|
|
23
|
+
* export function GET() {
|
|
24
|
+
* const body = generateFeed(releases, {
|
|
25
|
+
* url: 'https://my-docs.dev',
|
|
26
|
+
* title: 'My Library'
|
|
27
|
+
* });
|
|
28
|
+
* return new Response(body, { headers: { 'content-type': 'application/atom+xml' } });
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Atom rather than RSS because it requires an unambiguous per-entry id and
|
|
33
|
+
* update timestamp, which is exactly what a version and its release date give.
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateFeed(releases: ChangelogRelease[], site: FeedSite): string;
|