svelte-docsmith 0.8.0 → 0.10.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/bin/svelte-docsmith.mjs +161 -0
- package/dist/buildtime/archive.d.ts +31 -0
- package/dist/buildtime/archive.js +50 -0
- 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/markdown-source.d.ts +31 -0
- package/dist/buildtime/markdown-source.js +90 -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.d.ts +4 -3
- package/dist/buildtime/vite/collect.js +31 -12
- package/dist/buildtime/vite/extract.js +9 -35
- package/dist/buildtime/vite/frontmatter.js +4 -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 +22 -0
- package/dist/buildtime/vite/index.js +27 -4
- 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/copy-button.svelte +3 -2
- package/dist/components/chrome/search.svelte +16 -4
- package/dist/components/chrome/search.svelte.d.ts +4 -1
- package/dist/components/chrome/version-banner.svelte +82 -0
- package/dist/components/chrome/version-banner.svelte.d.ts +10 -0
- package/dist/components/chrome/version-switcher.svelte +63 -0
- package/dist/components/chrome/version-switcher.svelte.d.ts +14 -0
- package/dist/components/docs/mermaid.svelte +213 -0
- package/dist/components/docs/mermaid.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 +61 -5
- package/dist/components/layouts/docs-header.svelte.d.ts +9 -1
- package/dist/components/layouts/docs-mobile-header.svelte +25 -2
- package/dist/components/layouts/docs-mobile-header.svelte.d.ts +9 -1
- package/dist/components/layouts/docs-shell.svelte +84 -66
- package/dist/components/layouts/docs-shell.svelte.d.ts +11 -2
- package/dist/components/layouts/error-page.svelte +23 -3
- package/dist/components/layouts/error-page.svelte.d.ts +8 -2
- package/dist/components/layouts/seo-head.svelte +8 -1
- package/dist/components/layouts/seo-head.svelte.d.ts +2 -0
- 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/content.d.ts +9 -0
- package/dist/core/docs-page.d.ts +70 -0
- package/dist/core/docs-page.js +55 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/version.d.ts +80 -0
- package/dist/core/version.js +85 -0
- package/dist/fallbacks/changelog.d.ts +10 -0
- package/dist/fallbacks/changelog.js +3 -0
- package/dist/fallbacks/content.d.ts +2 -0
- package/dist/fallbacks/content.js +1 -0
- package/dist/generate/feed.d.ts +35 -0
- package/dist/generate/feed.js +80 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +14 -0
- package/dist/search/context.svelte.d.ts +2 -0
- package/dist/search/context.svelte.js +2 -0
- package/dist/search/create-search.d.ts +8 -2
- package/dist/search/create-search.js +11 -3
- package/dist/theme.css +53 -0
- package/package.json +27 -3
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* doesn't care where it's registered). It must never import component code.
|
|
7
7
|
*/
|
|
8
8
|
import rehypeSectionize from '@hbsnow/rehype-sectionize';
|
|
9
|
-
import { transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight } from '@shikijs/transformers';
|
|
9
|
+
import { transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight, transformerMetaHighlight } from '@shikijs/transformers';
|
|
10
10
|
import { escapeSvelte, mdsvex } from 'mdsvex';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import rehypeSlug from 'rehype-slug';
|
|
13
13
|
import { DEFAULT_LANGS, DEFAULT_THEMES, lazyHighlighter } from './highlight.js';
|
|
14
|
+
import { parseFenceMeta, hasTwoslash } from './fence-meta.js';
|
|
15
|
+
import { twoslashTransformer, twoslashFailure } from './twoslash.js';
|
|
14
16
|
/**
|
|
15
17
|
* Build the svelte-docsmith preprocessor: mdsvex with Shiki highlighting (via
|
|
16
18
|
* the raw-code `highlight` hook, the correct mdsvex integration point), heading
|
|
@@ -31,25 +33,75 @@ export function docsmith(options = {}) {
|
|
|
31
33
|
extensions: options.extensions ?? ['.md'],
|
|
32
34
|
layout,
|
|
33
35
|
highlight: {
|
|
34
|
-
|
|
36
|
+
// mdsvex passes the fence's trailing metadata as the third argument,
|
|
37
|
+
// which is where `title=` and the line-number flags come from.
|
|
38
|
+
highlighter: async (code, lang, meta) => {
|
|
39
|
+
const fence = parseFenceMeta(meta);
|
|
40
|
+
// A ```mermaid fence renders as a diagram, not highlighted code. The
|
|
41
|
+
// component is dynamic-imported so `mermaid` (an optional peer dep) is
|
|
42
|
+
// only pulled into pages that actually use it; JSON.stringify safely
|
|
43
|
+
// carries the multi-line source into the attribute.
|
|
44
|
+
if (lang === 'mermaid') {
|
|
45
|
+
// The pending branch renders server-side, reserving the diagram's
|
|
46
|
+
// space at first paint so it doesn't shift the page in on load.
|
|
47
|
+
// The client component reuses the same skeleton class until the
|
|
48
|
+
// diagram is ready, so the two never disagree on height.
|
|
49
|
+
return (`{#await import('svelte-docsmith/mermaid')}` +
|
|
50
|
+
`<div class="docsmith-mermaid-skeleton not-prose" aria-hidden="true"></div>` +
|
|
51
|
+
`{:then { default: Mermaid }}` +
|
|
52
|
+
`<Mermaid code={${JSON.stringify(code)}} />` +
|
|
53
|
+
`{/await}`);
|
|
54
|
+
}
|
|
35
55
|
const highlighter = await getHighlighter();
|
|
36
56
|
const language = lang && highlighter.getLoadedLanguages().includes(lang) ? lang : 'text';
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
57
|
+
// Comment-driven annotations authors write inside the fence:
|
|
58
|
+
// line highlight, diff (++/--), focus, error/warning, and
|
|
59
|
+
// word highlight. Each strips its own marker comment.
|
|
60
|
+
const notation = [
|
|
61
|
+
// Line ranges from the fence meta (```svelte {4,7-9}). The only way
|
|
62
|
+
// to highlight a line in markup: Shiki's comment markers are not
|
|
63
|
+
// recognised inside a Svelte template region, where an HTML comment
|
|
64
|
+
// is stripped without applying anything.
|
|
65
|
+
transformerMetaHighlight(),
|
|
66
|
+
transformerNotationHighlight(),
|
|
67
|
+
transformerNotationDiff(),
|
|
68
|
+
transformerNotationFocus(),
|
|
69
|
+
transformerNotationErrorLevel(),
|
|
70
|
+
transformerNotationWordHighlight()
|
|
71
|
+
];
|
|
72
|
+
const render = (extra = []) => highlighter.codeToHtml(code, {
|
|
40
73
|
lang: language,
|
|
41
74
|
themes,
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
// transformerMetaHighlight reads the raw fence meta from here.
|
|
76
|
+
meta: { __raw: meta ?? '' },
|
|
77
|
+
transformers: [...extra, ...notation]
|
|
78
|
+
});
|
|
79
|
+
let rendered;
|
|
80
|
+
if (options.twoslash && hasTwoslash(meta)) {
|
|
81
|
+
const loaded = await twoslashTransformer();
|
|
82
|
+
if ('error' in loaded) {
|
|
83
|
+
console.warn(`[svelte-docsmith] ${loaded.error}`);
|
|
84
|
+
rendered = render();
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
try {
|
|
88
|
+
rendered = render([loaded.transformer]);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
// One snippet that doesn't typecheck must not take the site
|
|
92
|
+
// down; fall back to a plain highlight and say which it was.
|
|
93
|
+
console.warn(`[svelte-docsmith] twoslash could not annotate a ${language} block: ` +
|
|
94
|
+
twoslashFailure(error));
|
|
95
|
+
rendered = render();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
rendered = render();
|
|
101
|
+
}
|
|
102
|
+
// escapeSvelte makes the highlighted result safe to embed in a
|
|
103
|
+
// Svelte component.
|
|
104
|
+
const html = escapeSvelte(rendered);
|
|
53
105
|
// Without a layout there is no `Components.pre` to render through —
|
|
54
106
|
// keep Shiki's own <pre> as-is.
|
|
55
107
|
if (layout === undefined)
|
|
@@ -60,7 +112,15 @@ export function docsmith(options = {}) {
|
|
|
60
112
|
.replace(/^<pre[^>]*>/, '')
|
|
61
113
|
.replace(/<\/pre>\s*$/, '')
|
|
62
114
|
.trim();
|
|
63
|
-
|
|
115
|
+
const showNumbers = fence.lineNumbers ?? options.lineNumbers ?? false;
|
|
116
|
+
const attrs = [
|
|
117
|
+
fence.title ? `title=${JSON.stringify(fence.title)}` : '',
|
|
118
|
+
showNumbers ? 'lineNumbers' : '',
|
|
119
|
+
showNumbers && fence.startLine !== undefined ? `startLine={${fence.startLine}}` : ''
|
|
120
|
+
]
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.join(' ');
|
|
123
|
+
return `<Components.pre${attrs ? ' ' + attrs : ''}>{@html \`${inner}\`}</Components.pre>`;
|
|
64
124
|
}
|
|
65
125
|
},
|
|
66
126
|
remarkPlugins: options.remarkPlugins,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twoslash support for code fences that opt in with ```ts twoslash.
|
|
3
|
+
*
|
|
4
|
+
* Twoslash runs the snippet through the TypeScript compiler and annotates it
|
|
5
|
+
* with the real inferred types, which is what makes a hover show `string`
|
|
6
|
+
* rather than a guess. The cost is that it **throws** on anything that does not
|
|
7
|
+
* typecheck: a missing import, a type error, an incomplete fragment.
|
|
8
|
+
*
|
|
9
|
+
* That strictness is the point, but it must not be able to take a whole site
|
|
10
|
+
* down. {@link highlightWithTwoslash} therefore falls back to an ordinary
|
|
11
|
+
* highlight and warns, naming the file, rather than failing the build over one
|
|
12
|
+
* bad snippet.
|
|
13
|
+
*
|
|
14
|
+
* The packages are optional peer dependencies, loaded only when a fence asks
|
|
15
|
+
* for them, so sites that never use Twoslash pay nothing for it.
|
|
16
|
+
*/
|
|
17
|
+
import type { ShikiTransformer } from 'shiki';
|
|
18
|
+
type Loaded = {
|
|
19
|
+
transformer: ShikiTransformer;
|
|
20
|
+
} | {
|
|
21
|
+
error: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function twoslashTransformer(): Promise<Loaded>;
|
|
24
|
+
/** One-line reason a snippet could not be annotated, for the build warning. */
|
|
25
|
+
export declare function twoslashFailure(error: unknown): string;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
let cached;
|
|
2
|
+
/**
|
|
3
|
+
* Build the Twoslash transformer once per process. `langs` must include
|
|
4
|
+
* `svelte`, or Svelte fences are silently passed through unannotated: the
|
|
5
|
+
* transformer's default allowlist is TypeScript only.
|
|
6
|
+
*/
|
|
7
|
+
async function loadTransformer() {
|
|
8
|
+
try {
|
|
9
|
+
const [{ transformerTwoslash }, { createTwoslasher }] = await Promise.all([
|
|
10
|
+
import('@shikijs/twoslash'),
|
|
11
|
+
import('twoslash-svelte')
|
|
12
|
+
]);
|
|
13
|
+
return {
|
|
14
|
+
transformer: transformerTwoslash({
|
|
15
|
+
// The fence's meta is parsed by docsmith, which decides per block
|
|
16
|
+
// whether to include this transformer at all.
|
|
17
|
+
explicitTrigger: false,
|
|
18
|
+
langs: ['ts', 'tsx', 'js', 'svelte'],
|
|
19
|
+
twoslasher: createTwoslasher()
|
|
20
|
+
})
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {
|
|
25
|
+
error: "'twoslash' requires its optional peer dependencies. " +
|
|
26
|
+
'Install them with: npm i -D @shikijs/twoslash twoslash-svelte typescript'
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function twoslashTransformer() {
|
|
31
|
+
cached ??= loadTransformer();
|
|
32
|
+
return cached;
|
|
33
|
+
}
|
|
34
|
+
/** One-line reason a snippet could not be annotated, for the build warning. */
|
|
35
|
+
export function twoslashFailure(error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
// Twoslash errors are multi-line with a code frame; the first line carries
|
|
38
|
+
// the actual diagnostic.
|
|
39
|
+
return message.split('\n').find((line) => line.trim()) ?? 'unknown error';
|
|
40
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { DocsContentItem, LlmsDoc, SearchDoc } from '../../core/content.js';
|
|
2
|
+
import type { DocsVersions } from '../../core/version.js';
|
|
2
3
|
/**
|
|
3
4
|
* Scan `contentDir` for `+page.md`/`+page.svx` files and read the frontmatter
|
|
4
5
|
* fields the sidebar needs (plus the heading list for a server-rendered TOC and
|
|
5
6
|
* an estimated reading time), deriving each page's URL from its directory
|
|
6
7
|
* relative to `routesDir`. Pure and synchronous so it can be unit-tested.
|
|
7
8
|
*/
|
|
8
|
-
export declare function collectDocs(contentDir: string, routesDir: string): DocsContentItem[];
|
|
9
|
+
export declare function collectDocs(contentDir: string, routesDir: string, versions?: DocsVersions): DocsContentItem[];
|
|
9
10
|
/**
|
|
10
11
|
* Build the search records for every page under `contentDir`: title, section,
|
|
11
12
|
* description, heading list, and plain-text body. Served as the lazy-loaded
|
|
@@ -13,11 +14,11 @@ export declare function collectDocs(contentDir: string, routesDir: string): Docs
|
|
|
13
14
|
* bloating the eagerly-imported nav index. The missing-directory case is
|
|
14
15
|
* already reported by {@link collectDocs}, so this stays quiet.
|
|
15
16
|
*/
|
|
16
|
-
export declare function collectSearchDocs(contentDir: string, routesDir: string): SearchDoc[];
|
|
17
|
+
export declare function collectSearchDocs(contentDir: string, routesDir: string, versions?: DocsVersions): SearchDoc[];
|
|
17
18
|
/**
|
|
18
19
|
* Build the LLM records for every page: title, section, description, and the
|
|
19
20
|
* full markdown content. Served as the `svelte-docsmith/llms` virtual module and
|
|
20
21
|
* consumed server-side by `llms.txt` / `llms-full.txt` routes, so it never ships
|
|
21
22
|
* to the client.
|
|
22
23
|
*/
|
|
23
|
-
export declare function collectLlmsDocs(contentDir: string, routesDir: string): LlmsDoc[];
|
|
24
|
+
export declare function collectLlmsDocs(contentDir: string, routesDir: string, versions?: DocsVersions): LlmsDoc[];
|
|
@@ -38,7 +38,7 @@ function sectionKey(value) {
|
|
|
38
38
|
* frontmatter, derived URL, and title so every index (nav, search, llms) can be
|
|
39
39
|
* built from a single read of each file.
|
|
40
40
|
*/
|
|
41
|
-
function* eachTitledPage(contentDir, routesDir) {
|
|
41
|
+
function* eachTitledPage(contentDir, routesDir, versions) {
|
|
42
42
|
for (const file of listPageFiles(contentDir)) {
|
|
43
43
|
const source = fs.readFileSync(file, 'utf-8');
|
|
44
44
|
const front = parseFrontmatter(source, file);
|
|
@@ -46,7 +46,20 @@ function* eachTitledPage(contentDir, routesDir) {
|
|
|
46
46
|
continue;
|
|
47
47
|
const dir = path.dirname(file);
|
|
48
48
|
const url = '/' + path.relative(routesDir, dir).split(path.sep).join('/');
|
|
49
|
-
yield { source, front, url, title: front.title, file };
|
|
49
|
+
yield { source, front, url, title: front.title, file, version: versionOf(dir) };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A page belongs to an archived version when its first directory segment under
|
|
53
|
+
* the content dir is that archive's id; everything else is the current
|
|
54
|
+
* version, which lives unprefixed at the docs root. No versions ⇒ undefined,
|
|
55
|
+
* which keeps an unversioned site's index byte-for-byte what it was.
|
|
56
|
+
*/
|
|
57
|
+
function versionOf(dir) {
|
|
58
|
+
if (!versions)
|
|
59
|
+
return undefined;
|
|
60
|
+
const firstSegment = path.relative(contentDir, dir).split(path.sep)[0];
|
|
61
|
+
const archived = versions.archived?.find((v) => v.id === firstSegment);
|
|
62
|
+
return archived ? archived.id : versions.current.id;
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
65
|
/**
|
|
@@ -55,14 +68,14 @@ function* eachTitledPage(contentDir, routesDir) {
|
|
|
55
68
|
* an estimated reading time), deriving each page's URL from its directory
|
|
56
69
|
* relative to `routesDir`. Pure and synchronous so it can be unit-tested.
|
|
57
70
|
*/
|
|
58
|
-
export function collectDocs(contentDir, routesDir) {
|
|
71
|
+
export function collectDocs(contentDir, routesDir, versions) {
|
|
59
72
|
if (!fs.existsSync(contentDir)) {
|
|
60
73
|
console.warn(`[svelte-docsmith] content directory not found: ${contentDir}\n` +
|
|
61
74
|
` The sidebar will be empty. Create your doc pages there, or point docsmith() at the right place with \`content\`.`);
|
|
62
75
|
return [];
|
|
63
76
|
}
|
|
64
77
|
const items = [];
|
|
65
|
-
for (const { source, front, url, title, file } of eachTitledPage(contentDir, routesDir)) {
|
|
78
|
+
for (const { source, front, url, title, file, version } of eachTitledPage(contentDir, routesDir, versions)) {
|
|
66
79
|
items.push({
|
|
67
80
|
title,
|
|
68
81
|
path: url,
|
|
@@ -70,9 +83,13 @@ export function collectDocs(contentDir, routesDir) {
|
|
|
70
83
|
section: readSection(front.section),
|
|
71
84
|
order: typeof front.order === 'number' ? front.order : undefined,
|
|
72
85
|
sourcePath: path.relative(process.cwd(), file).split(path.sep).join('/'),
|
|
73
|
-
|
|
86
|
+
// Frontmatter wins over git so an archived page keeps the date it last
|
|
87
|
+
// really changed: archiving copies every page in one commit, which would
|
|
88
|
+
// otherwise stamp the whole archive with the day it was created.
|
|
89
|
+
lastUpdated: typeof front.lastUpdated === 'string' ? front.lastUpdated : lastCommitDate(file),
|
|
74
90
|
readingTime: readingMinutes(extractSearchText(source)),
|
|
75
|
-
toc: extractToc(source)
|
|
91
|
+
toc: extractToc(source),
|
|
92
|
+
version
|
|
76
93
|
});
|
|
77
94
|
}
|
|
78
95
|
if (items.length === 0) {
|
|
@@ -89,18 +106,19 @@ export function collectDocs(contentDir, routesDir) {
|
|
|
89
106
|
* bloating the eagerly-imported nav index. The missing-directory case is
|
|
90
107
|
* already reported by {@link collectDocs}, so this stays quiet.
|
|
91
108
|
*/
|
|
92
|
-
export function collectSearchDocs(contentDir, routesDir) {
|
|
109
|
+
export function collectSearchDocs(contentDir, routesDir, versions) {
|
|
93
110
|
if (!fs.existsSync(contentDir))
|
|
94
111
|
return [];
|
|
95
112
|
const docs = [];
|
|
96
|
-
for (const { source, front, url, title } of eachTitledPage(contentDir, routesDir)) {
|
|
113
|
+
for (const { source, front, url, title, version } of eachTitledPage(contentDir, routesDir, versions)) {
|
|
97
114
|
docs.push({
|
|
98
115
|
path: url,
|
|
99
116
|
title,
|
|
100
117
|
section: sectionLabel(front.section),
|
|
101
118
|
description: typeof front.description === 'string' ? front.description : undefined,
|
|
102
119
|
headings: extractToc(source).map((entry) => entry.title),
|
|
103
|
-
text: extractSearchText(source)
|
|
120
|
+
text: extractSearchText(source),
|
|
121
|
+
version
|
|
104
122
|
});
|
|
105
123
|
}
|
|
106
124
|
return docs.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -111,18 +129,19 @@ export function collectSearchDocs(contentDir, routesDir) {
|
|
|
111
129
|
* consumed server-side by `llms.txt` / `llms-full.txt` routes, so it never ships
|
|
112
130
|
* to the client.
|
|
113
131
|
*/
|
|
114
|
-
export function collectLlmsDocs(contentDir, routesDir) {
|
|
132
|
+
export function collectLlmsDocs(contentDir, routesDir, versions) {
|
|
115
133
|
if (!fs.existsSync(contentDir))
|
|
116
134
|
return [];
|
|
117
135
|
const docs = [];
|
|
118
|
-
for (const { source, front, url, title } of eachTitledPage(contentDir, routesDir)) {
|
|
136
|
+
for (const { source, front, url, title, version } of eachTitledPage(contentDir, routesDir, versions)) {
|
|
119
137
|
docs.push({
|
|
120
138
|
path: url,
|
|
121
139
|
title,
|
|
122
140
|
section: sectionKey(front.section),
|
|
123
141
|
order: typeof front.order === 'number' ? front.order : undefined,
|
|
124
142
|
description: typeof front.description === 'string' ? front.description : undefined,
|
|
125
|
-
content: extractLlmsContent(source, title)
|
|
143
|
+
content: extractLlmsContent(source, title),
|
|
144
|
+
version
|
|
126
145
|
});
|
|
127
146
|
}
|
|
128
147
|
return docs.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import GithubSlugger from 'github-slugger';
|
|
2
|
+
import { proseLines, splitFrontmatter } from '../markdown-source.js';
|
|
3
|
+
/** Drop a page's `<script>`/`<style>` blocks: component setup, not content. */
|
|
4
|
+
function withoutSetupBlocks(body) {
|
|
5
|
+
return body.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '');
|
|
6
|
+
}
|
|
2
7
|
/** Strip inline markdown so a heading's TOC label is plain text. */
|
|
3
8
|
export function stripInlineMarkdown(text) {
|
|
4
9
|
return text
|
|
@@ -17,23 +22,9 @@ export function stripInlineMarkdown(text) {
|
|
|
17
22
|
* samples are intentionally dropped to keep the index small and prose-focused.
|
|
18
23
|
*/
|
|
19
24
|
export function extractSearchText(source) {
|
|
20
|
-
|
|
21
|
-
// Component/setup blocks aren't prose; drop them whole before line scanning.
|
|
22
|
-
body = body.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '');
|
|
25
|
+
const body = withoutSetupBlocks(splitFrontmatter(source).body);
|
|
23
26
|
const out = [];
|
|
24
|
-
|
|
25
|
-
for (const line of body.split('\n')) {
|
|
26
|
-
const f = /^\s*(`{3,}|~{3,})/.exec(line);
|
|
27
|
-
if (f) {
|
|
28
|
-
const ch = f[1][0];
|
|
29
|
-
if (fence === null)
|
|
30
|
-
fence = ch;
|
|
31
|
-
else if (ch === fence)
|
|
32
|
-
fence = null;
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
if (fence !== null)
|
|
36
|
-
continue;
|
|
27
|
+
for (const line of proseLines(body)) {
|
|
37
28
|
// Skip table delimiter rows (`| --- | :--: |`) — pure structure, no words.
|
|
38
29
|
if (/^\s*\|?[\s:|-]+\|[\s:|-]*$/.test(line))
|
|
39
30
|
continue;
|
|
@@ -63,22 +54,9 @@ export function readingMinutes(text) {
|
|
|
63
54
|
* heading ids exactly, not just for common cases.
|
|
64
55
|
*/
|
|
65
56
|
export function extractToc(source) {
|
|
66
|
-
const body = source.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
|
|
67
57
|
const slugger = new GithubSlugger();
|
|
68
58
|
const toc = [];
|
|
69
|
-
|
|
70
|
-
for (const line of body.split('\n')) {
|
|
71
|
-
const f = /^\s*(`{3,}|~{3,})/.exec(line);
|
|
72
|
-
if (f) {
|
|
73
|
-
const ch = f[1][0];
|
|
74
|
-
if (fence === null)
|
|
75
|
-
fence = ch;
|
|
76
|
-
else if (ch === fence)
|
|
77
|
-
fence = null;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (fence !== null)
|
|
81
|
-
continue;
|
|
59
|
+
for (const line of proseLines(splitFrontmatter(source).body)) {
|
|
82
60
|
const m = /^(#{2,3})\s+(.+?)\s*#*\s*$/.exec(line);
|
|
83
61
|
if (!m)
|
|
84
62
|
continue;
|
|
@@ -95,10 +73,6 @@ export function extractToc(source) {
|
|
|
95
73
|
* with the frontmatter title prepended as an `h1` (pages start their body at h2).
|
|
96
74
|
*/
|
|
97
75
|
export function extractLlmsContent(source, title) {
|
|
98
|
-
const body = source
|
|
99
|
-
.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '')
|
|
100
|
-
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
101
|
-
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
102
|
-
.trim();
|
|
76
|
+
const body = withoutSetupBlocks(splitFrontmatter(source).body).trim();
|
|
103
77
|
return `# ${title}\n\n${body}`;
|
|
104
78
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import yaml from 'js-yaml';
|
|
2
|
+
import { splitFrontmatter } from '../markdown-source.js';
|
|
2
3
|
/**
|
|
3
4
|
* Parse a page's leading YAML frontmatter block into a plain object. Returns an
|
|
4
5
|
* empty object when there is no frontmatter; throws a located error on invalid
|
|
5
6
|
* YAML so a typo surfaces with its filename instead of a blank page.
|
|
6
7
|
*/
|
|
7
8
|
export function parseFrontmatter(source, file) {
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
9
|
+
const { frontmatter } = splitFrontmatter(source);
|
|
10
|
+
if (frontmatter === undefined)
|
|
10
11
|
return {};
|
|
11
12
|
let data;
|
|
12
13
|
try {
|
|
13
|
-
data = yaml.load(
|
|
14
|
+
data = yaml.load(frontmatter);
|
|
14
15
|
}
|
|
15
16
|
catch (err) {
|
|
16
17
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
/** Last git commit date (strict ISO) for a file, or undefined outside a repo. */
|
|
2
2
|
export declare function lastCommitDate(file: string): string | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Release dates keyed by version, read from the commit that introduced each
|
|
5
|
+
* `## <version>` heading in a changelog. Changesets does not write dates, and a
|
|
6
|
+
* tag lookup misses versions released before tagging was set up, so the file's
|
|
7
|
+
* own history is the most reliable source available.
|
|
8
|
+
*/
|
|
9
|
+
export declare function changelogDates(file: string): Map<string, string>;
|
|
@@ -9,3 +9,35 @@ export function lastCommitDate(file) {
|
|
|
9
9
|
const date = res.status === 0 ? res.stdout.trim() : '';
|
|
10
10
|
return date || undefined;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Release dates keyed by version, read from the commit that introduced each
|
|
14
|
+
* `## <version>` heading in a changelog. Changesets does not write dates, and a
|
|
15
|
+
* tag lookup misses versions released before tagging was set up, so the file's
|
|
16
|
+
* own history is the most reliable source available.
|
|
17
|
+
*/
|
|
18
|
+
export function changelogDates(file) {
|
|
19
|
+
const dates = new Map();
|
|
20
|
+
// `-L` would be per-line; instead walk the file's commits newest-first and
|
|
21
|
+
// record the first commit in which each version heading appears.
|
|
22
|
+
const log = spawnSync('git', ['log', '--format=%H %cI', '--reverse', '--follow', '--', path.basename(file)], { cwd: path.dirname(file), encoding: 'utf-8' });
|
|
23
|
+
if (log.status !== 0)
|
|
24
|
+
return dates;
|
|
25
|
+
for (const line of log.stdout.trim().split('\n').filter(Boolean)) {
|
|
26
|
+
const [hash, date] = line.split(' ');
|
|
27
|
+
if (!hash || !date)
|
|
28
|
+
continue;
|
|
29
|
+
const show = spawnSync('git', ['show', `${hash}:./${path.basename(file)}`], {
|
|
30
|
+
cwd: path.dirname(file),
|
|
31
|
+
encoding: 'utf-8'
|
|
32
|
+
});
|
|
33
|
+
if (show.status !== 0)
|
|
34
|
+
continue;
|
|
35
|
+
for (const match of show.stdout.matchAll(/^##\s+(?!#)(.+?)\s*$/gm)) {
|
|
36
|
+
const version = match[1].trim();
|
|
37
|
+
// First commit containing this heading wins, which is its release.
|
|
38
|
+
if (!dates.has(version))
|
|
39
|
+
dates.set(version, date);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return dates;
|
|
43
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
+
import { type DocsVersions } from '../../core/version.js';
|
|
2
3
|
export { collectDocs, collectLlmsDocs, collectSearchDocs } from './collect.js';
|
|
4
|
+
export { collectReleases } from './releases.js';
|
|
3
5
|
export interface DocsmithViteOptions {
|
|
4
6
|
/** Directory scanned for doc pages. Default: `'src/routes/docs'`. */
|
|
5
7
|
content?: string;
|
|
@@ -13,6 +15,26 @@ export interface DocsmithViteOptions {
|
|
|
13
15
|
light: string;
|
|
14
16
|
dark: string;
|
|
15
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Path to the `CHANGELOG.md` that feeds the generated
|
|
20
|
+
* `svelte-docsmith/changelog` index. Default: `'CHANGELOG.md'` in the app
|
|
21
|
+
* directory; point it at the package whose releases you publish. Set `false`
|
|
22
|
+
* to skip the changelog entirely.
|
|
23
|
+
*/
|
|
24
|
+
changelog?: string | false;
|
|
25
|
+
/**
|
|
26
|
+
* Route the changelog is served at, used to build feed links and to find
|
|
27
|
+
* hand-written per-release pages. Default: `'/changelog'`.
|
|
28
|
+
*/
|
|
29
|
+
changelogPath?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Declare documentation versions to enable versioned docs. `current` is the
|
|
32
|
+
* docs for the latest release: its pages sit directly under `<content>` and
|
|
33
|
+
* keep their unprefixed URLs. Each archived version's pages live under
|
|
34
|
+
* `<content>/<id>/` and are served at `/docs/<id>/…`. Omit for a single,
|
|
35
|
+
* unversioned docs tree (the default).
|
|
36
|
+
*/
|
|
37
|
+
versions?: DocsVersions;
|
|
16
38
|
}
|
|
17
39
|
/**
|
|
18
40
|
* The svelte-docsmith Vite plugin. Add it to `plugins` in `vite.config.ts`:
|
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
import fs from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import { DEFAULT_THEMES, lazyHighlighter } from '../highlight.js';
|
|
22
|
+
import { resolveVersions } from '../../core/version.js';
|
|
22
23
|
import { isPageFile, listPageFiles } from './pages.js';
|
|
23
24
|
import { collectDocs, collectLlmsDocs, collectSearchDocs } from './collect.js';
|
|
25
|
+
import { collectReleases } from './releases.js';
|
|
24
26
|
export { collectDocs, collectLlmsDocs, collectSearchDocs } from './collect.js';
|
|
27
|
+
export { collectReleases } from './releases.js';
|
|
25
28
|
/**
|
|
26
29
|
* The svelte-docsmith Vite plugin. Add it to `plugins` in `vite.config.ts`:
|
|
27
30
|
*
|
|
@@ -40,9 +43,18 @@ const SEARCH_SPECIFIER = 'svelte-docsmith/search';
|
|
|
40
43
|
const VIRTUAL_SEARCH_ID = '\0svelte-docsmith:search';
|
|
41
44
|
const LLMS_SPECIFIER = 'svelte-docsmith/llms';
|
|
42
45
|
const VIRTUAL_LLMS_ID = '\0svelte-docsmith:llms';
|
|
46
|
+
const CHANGELOG_SPECIFIER = 'svelte-docsmith/changelog';
|
|
47
|
+
const VIRTUAL_CHANGELOG_ID = '\0svelte-docsmith:changelog';
|
|
43
48
|
function contentIndexPlugin(options) {
|
|
44
49
|
const contentDir = path.resolve(options.content ?? 'src/routes/docs');
|
|
45
50
|
const routesDir = path.resolve(options.routes ?? 'src/routes');
|
|
51
|
+
const versions = options.versions;
|
|
52
|
+
// The docs URL base, e.g. `/docs`, derived from the content dir's location
|
|
53
|
+
// under the routes dir — the same mapping `collect.ts` uses for page URLs.
|
|
54
|
+
const docsBase = '/' + path.relative(routesDir, contentDir).split(path.sep).join('/');
|
|
55
|
+
const changelogFile = options.changelog === false ? undefined : path.resolve(options.changelog ?? 'CHANGELOG.md');
|
|
56
|
+
const changelogRoute = options.changelogPath ?? '/changelog';
|
|
57
|
+
const changelogOverrides = path.join(routesDir, changelogRoute.replace(/^\//, ''));
|
|
46
58
|
return {
|
|
47
59
|
name: 'docsmith-content',
|
|
48
60
|
enforce: 'pre',
|
|
@@ -53,6 +65,8 @@ function contentIndexPlugin(options) {
|
|
|
53
65
|
return VIRTUAL_SEARCH_ID;
|
|
54
66
|
if (id === LLMS_SPECIFIER)
|
|
55
67
|
return VIRTUAL_LLMS_ID;
|
|
68
|
+
if (id === CHANGELOG_SPECIFIER)
|
|
69
|
+
return VIRTUAL_CHANGELOG_ID;
|
|
56
70
|
},
|
|
57
71
|
load(id) {
|
|
58
72
|
// Watch each page file (not the directory) so editing frontmatter or
|
|
@@ -62,21 +76,30 @@ function contentIndexPlugin(options) {
|
|
|
62
76
|
if (id === VIRTUAL_CONTENT_ID) {
|
|
63
77
|
for (const file of listPageFiles(contentDir))
|
|
64
78
|
this.addWatchFile(file);
|
|
65
|
-
const docs = collectDocs(contentDir, routesDir);
|
|
66
|
-
|
|
79
|
+
const docs = collectDocs(contentDir, routesDir, versions);
|
|
80
|
+
const resolved = resolveVersions(versions, docsBase, docs);
|
|
81
|
+
return (`export const docs = ${JSON.stringify(docs, null, 2)};\n` +
|
|
82
|
+
`export const versions = ${JSON.stringify(resolved, null, 2)};\n`);
|
|
67
83
|
}
|
|
68
84
|
if (id === VIRTUAL_SEARCH_ID) {
|
|
69
85
|
for (const file of listPageFiles(contentDir))
|
|
70
86
|
this.addWatchFile(file);
|
|
71
|
-
const docs = collectSearchDocs(contentDir, routesDir);
|
|
87
|
+
const docs = collectSearchDocs(contentDir, routesDir, versions);
|
|
72
88
|
return `export const docs = ${JSON.stringify(docs)};\n`;
|
|
73
89
|
}
|
|
74
90
|
if (id === VIRTUAL_LLMS_ID) {
|
|
75
91
|
for (const file of listPageFiles(contentDir))
|
|
76
92
|
this.addWatchFile(file);
|
|
77
|
-
const docs = collectLlmsDocs(contentDir, routesDir);
|
|
93
|
+
const docs = collectLlmsDocs(contentDir, routesDir, versions);
|
|
78
94
|
return `export const docs = ${JSON.stringify(docs)};\n`;
|
|
79
95
|
}
|
|
96
|
+
if (id === VIRTUAL_CHANGELOG_ID) {
|
|
97
|
+
if (!changelogFile)
|
|
98
|
+
return `export const releases = [];\n`;
|
|
99
|
+
this.addWatchFile(changelogFile);
|
|
100
|
+
const releases = collectReleases(changelogFile, changelogOverrides, changelogRoute);
|
|
101
|
+
return `export const releases = ${JSON.stringify(releases)};\n`;
|
|
102
|
+
}
|
|
80
103
|
},
|
|
81
104
|
configureServer(server) {
|
|
82
105
|
server.watcher.add(contentDir);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ChangelogRelease } from '../../core/changelog.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read a Changesets `CHANGELOG.md` into releases, dated from git history, and
|
|
4
|
+
* linked to a hand-written page where one exists.
|
|
5
|
+
*
|
|
6
|
+
* `overridesDir` is scanned for directories named after a version
|
|
7
|
+
* (`src/routes/changelog/0.9.0/`), so a release worth a proper write-up can
|
|
8
|
+
* have one while the rest stay generated.
|
|
9
|
+
*/
|
|
10
|
+
export declare function collectReleases(changelogFile: string, overridesDir?: string, routePath?: string): ChangelogRelease[];
|
|
11
|
+
/** Default location of a monorepo package's changelog, relative to the app. */
|
|
12
|
+
export declare function defaultChangelogPath(cwd?: string): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
import { parseChangelog } from '../changelog.js';
|
|
5
|
+
import { changelogDates } from './git.js';
|
|
6
|
+
/**
|
|
7
|
+
* Render an entry's markdown to HTML at build time. Changesets entries use
|
|
8
|
+
* inline code, emphasis, links and nested lists, so shipping the raw markdown
|
|
9
|
+
* would mean every consuming site needed a runtime renderer.
|
|
10
|
+
*/
|
|
11
|
+
function renderItem(markdown) {
|
|
12
|
+
return marked.parse(markdown, { async: false, gfm: true }).trim();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Read a Changesets `CHANGELOG.md` into releases, dated from git history, and
|
|
16
|
+
* linked to a hand-written page where one exists.
|
|
17
|
+
*
|
|
18
|
+
* `overridesDir` is scanned for directories named after a version
|
|
19
|
+
* (`src/routes/changelog/0.9.0/`), so a release worth a proper write-up can
|
|
20
|
+
* have one while the rest stay generated.
|
|
21
|
+
*/
|
|
22
|
+
export function collectReleases(changelogFile, overridesDir, routePath = '/changelog') {
|
|
23
|
+
if (!fs.existsSync(changelogFile))
|
|
24
|
+
return [];
|
|
25
|
+
const releases = parseChangelog(fs.readFileSync(changelogFile, 'utf-8'));
|
|
26
|
+
const dates = changelogDates(changelogFile);
|
|
27
|
+
const overrides = new Set();
|
|
28
|
+
if (overridesDir && fs.existsSync(overridesDir)) {
|
|
29
|
+
for (const entry of fs.readdirSync(overridesDir, { withFileTypes: true })) {
|
|
30
|
+
if (entry.isDirectory())
|
|
31
|
+
overrides.add(entry.name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return releases.map((release) => {
|
|
35
|
+
// Directory names can't contain the dots a version has on every platform,
|
|
36
|
+
// so `0.9.0` may be written as `0-9-0`.
|
|
37
|
+
const slug = release.version.replace(/\./g, '-');
|
|
38
|
+
const override = overrides.has(release.version)
|
|
39
|
+
? release.version
|
|
40
|
+
: overrides.has(slug)
|
|
41
|
+
? slug
|
|
42
|
+
: undefined;
|
|
43
|
+
return {
|
|
44
|
+
...release,
|
|
45
|
+
groups: release.groups.map((group) => ({
|
|
46
|
+
...group,
|
|
47
|
+
items: group.items.map(renderItem)
|
|
48
|
+
})),
|
|
49
|
+
...(dates.get(release.version) ? { date: dates.get(release.version) } : {}),
|
|
50
|
+
...(override ? { path: `${routePath}/${override}` } : {})
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Default location of a monorepo package's changelog, relative to the app. */
|
|
55
|
+
export function defaultChangelogPath(cwd = process.cwd()) {
|
|
56
|
+
return path.resolve(cwd, 'CHANGELOG.md');
|
|
57
|
+
}
|