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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The svelte-docsmith maintenance CLI. Currently one command: `archive-version`,
|
|
3
|
+
// which freezes today's docs into an archived version folder so they stay live
|
|
4
|
+
// after a breaking release.
|
|
5
|
+
//
|
|
6
|
+
// The current version is served unprefixed from the docs root and is the folder
|
|
7
|
+
// you keep editing; archives are copies under their own prefix. See
|
|
8
|
+
// docs/adr/0001-unprefixed-current-docs.md. The text transforms live in
|
|
9
|
+
// `src/lib/buildtime/archive.ts` so they are typechecked and unit-tested; this
|
|
10
|
+
// file is the filesystem wrapper around them.
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
import {
|
|
15
|
+
rewriteDocsLinks,
|
|
16
|
+
freezeLastUpdated,
|
|
17
|
+
isInheritedRouteFile
|
|
18
|
+
} from '../dist/buildtime/archive.js';
|
|
19
|
+
|
|
20
|
+
const HELP = `svelte-docsmith — docs maintenance CLI
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
svelte-docsmith archive-version <id> [options]
|
|
24
|
+
|
|
25
|
+
Freeze the current docs into an archived version folder. The archive keeps
|
|
26
|
+
serving the release it documents while you go on editing the docs root.
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--label <label> switcher label for the archive (default: the id)
|
|
30
|
+
--content <dir> docs content directory (default: src/routes/docs)
|
|
31
|
+
--routes <dir> SvelteKit routes directory (default: src/routes)
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
// Written into every archive this command creates, so later runs know which
|
|
35
|
+
// directories are already archives and must not be copied into the new one.
|
|
36
|
+
const MARKER = '.docsmith-archive';
|
|
37
|
+
|
|
38
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
39
|
+
|
|
40
|
+
if (command !== 'archive-version') {
|
|
41
|
+
console.log(HELP);
|
|
42
|
+
process.exit(command ? 1 : 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const opts = { content: 'src/routes/docs', routes: 'src/routes', label: undefined };
|
|
46
|
+
let id;
|
|
47
|
+
for (let i = 0; i < rest.length; i++) {
|
|
48
|
+
const arg = rest[i];
|
|
49
|
+
if (arg === '--label') opts.label = rest[++i];
|
|
50
|
+
else if (arg === '--content') opts.content = rest[++i];
|
|
51
|
+
else if (arg === '--routes') opts.routes = rest[++i];
|
|
52
|
+
else if (!arg.startsWith('--') && !id) id = arg;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fail = (message) => {
|
|
56
|
+
console.error(`svelte-docsmith archive-version: ${message}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (!id) fail('missing <id>. e.g. `svelte-docsmith archive-version v1`');
|
|
61
|
+
// Must start alphanumeric: the id is both a directory name and a URL segment, so
|
|
62
|
+
// `.`, `..` and dotfiles would escape the docs root or make a hidden, dead route.
|
|
63
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) {
|
|
64
|
+
fail(`invalid id: ${id}. Start with a letter or digit; it is used as a URL segment.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const contentDir = path.resolve(opts.content);
|
|
68
|
+
const routesDir = path.resolve(opts.routes);
|
|
69
|
+
const toDir = path.join(contentDir, id);
|
|
70
|
+
const rel = (p) => path.relative(process.cwd(), p);
|
|
71
|
+
|
|
72
|
+
if (!fs.existsSync(contentDir)) fail(`content directory not found: ${rel(contentDir)}`);
|
|
73
|
+
if (fs.existsSync(toDir)) fail(`target already exists: ${rel(toDir)}`);
|
|
74
|
+
|
|
75
|
+
// The docs URL base (e.g. `/docs`), derived exactly as the vite plugin does.
|
|
76
|
+
const docsBase = '/' + path.relative(routesDir, contentDir).split(path.sep).join('/');
|
|
77
|
+
|
|
78
|
+
/** Directories directly under the docs root that are already archives. */
|
|
79
|
+
const archivedIds = new Set(
|
|
80
|
+
fs
|
|
81
|
+
.readdirSync(contentDir, { withFileTypes: true })
|
|
82
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(contentDir, e.name, MARKER)))
|
|
83
|
+
.map((e) => e.name)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const isPage = (name) => name.endsWith('+page.md') || name.endsWith('+page.svx');
|
|
87
|
+
|
|
88
|
+
/** Copy the docs root into the archive, skipping what the archive shouldn't hold. */
|
|
89
|
+
function copyCurrent(from, to) {
|
|
90
|
+
fs.mkdirSync(to, { recursive: true });
|
|
91
|
+
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
92
|
+
const src = path.join(from, entry.name);
|
|
93
|
+
const dest = path.join(to, entry.name);
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
// Never descend into the archive being written (it lives inside the docs
|
|
96
|
+
// root) or into an archive written by an earlier run.
|
|
97
|
+
if (src === toDir) continue;
|
|
98
|
+
if (from === contentDir && archivedIds.has(entry.name)) continue;
|
|
99
|
+
copyCurrent(src, dest);
|
|
100
|
+
} else {
|
|
101
|
+
// The root layout and error page already apply to the archive, which is
|
|
102
|
+
// nested inside them; copying them would nest a second DocsShell.
|
|
103
|
+
if (from === contentDir && isInheritedRouteFile(entry.name)) continue;
|
|
104
|
+
fs.copyFileSync(src, dest);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Every file in the archive, paired with the source file it was copied from. */
|
|
110
|
+
function* eachCopiedFile(dir = toDir) {
|
|
111
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
112
|
+
const file = path.join(dir, entry.name);
|
|
113
|
+
if (entry.isDirectory()) yield* eachCopiedFile(file);
|
|
114
|
+
else yield { file, source: path.join(contentDir, path.relative(toDir, file)) };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The date a page last really changed, so the archive doesn't claim today. */
|
|
119
|
+
function lastCommitDate(file) {
|
|
120
|
+
try {
|
|
121
|
+
const out = execFileSync('git', ['log', '-1', '--format=%cs', '--', file], {
|
|
122
|
+
cwd: path.dirname(file),
|
|
123
|
+
encoding: 'utf-8',
|
|
124
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
125
|
+
}).trim();
|
|
126
|
+
return out || undefined;
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
copyCurrent(contentDir, toDir);
|
|
133
|
+
fs.writeFileSync(
|
|
134
|
+
path.join(toDir, MARKER),
|
|
135
|
+
`Archived docs for ${id}, created by \`svelte-docsmith archive-version\`.\n` +
|
|
136
|
+
`This folder is frozen: edit the docs root instead.\n`
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
let pages = 0;
|
|
140
|
+
for (const { file, source } of eachCopiedFile()) {
|
|
141
|
+
if (!isPage(path.basename(file))) continue;
|
|
142
|
+
pages++;
|
|
143
|
+
const text = fs.readFileSync(file, 'utf-8');
|
|
144
|
+
fs.writeFileSync(
|
|
145
|
+
file,
|
|
146
|
+
freezeLastUpdated(
|
|
147
|
+
rewriteDocsLinks(text, { docsBase, versionId: id, archivedIds }),
|
|
148
|
+
lastCommitDate(source)
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const label = opts.label ?? id;
|
|
154
|
+
console.log(`\n✓ Archived the current docs into ${rel(toDir)} (${pages} pages)\n`);
|
|
155
|
+
console.log('Links were rewritten to stay inside the archive, and each page kept');
|
|
156
|
+
console.log('its real last-updated date. Review the diff, then update');
|
|
157
|
+
console.log('docsmith({ versions }) so the archive is served:\n');
|
|
158
|
+
console.log(' versions: {');
|
|
159
|
+
console.log(" current: { id: '<new release>', label: '<new release>' },");
|
|
160
|
+
console.log(` archived: [{ id: '${id}', label: '${label}' }${archivedIds.size ? ', …' : ''}]`);
|
|
161
|
+
console.log(' }\n');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure text transforms behind `svelte-docsmith archive-version`. Archiving
|
|
3
|
+
* copies the docs root into a frozen version folder, and a verbatim copy gets
|
|
4
|
+
* two things wrong: its links keep resolving to the current docs, and every page
|
|
5
|
+
* looks like it changed on the day the archive was made. These fix both.
|
|
6
|
+
*
|
|
7
|
+
* Kept here rather than in `bin/` so they are typechecked and unit-tested; the
|
|
8
|
+
* CLI is a thin wrapper over them. See `docs/adr/0002-archives-are-rewritten-source-copies.md`.
|
|
9
|
+
*/
|
|
10
|
+
/** Route files at the docs root that an archive nested inside it already inherits. */
|
|
11
|
+
export declare function isInheritedRouteFile(name: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Point a page's docs links at the archive it is being copied into. An absolute
|
|
14
|
+
* link like `](/docs/theming)` resolves to the *current* docs forever, so
|
|
15
|
+
* without this an archive silently walks readers into newer content.
|
|
16
|
+
*
|
|
17
|
+
* Links already targeting a version folder are left alone, as are links inside
|
|
18
|
+
* fenced code, which are sample code rather than navigation.
|
|
19
|
+
*/
|
|
20
|
+
export declare function rewriteDocsLinks(text: string, options: {
|
|
21
|
+
docsBase: string;
|
|
22
|
+
versionId: string;
|
|
23
|
+
archivedIds?: Iterable<string>;
|
|
24
|
+
}): string;
|
|
25
|
+
/**
|
|
26
|
+
* Write a page's real last-updated date into its frontmatter. The collector
|
|
27
|
+
* prefers frontmatter over the git date, so an archive keeps the date each page
|
|
28
|
+
* was actually accurate on instead of the day the archive was created. Leaves an
|
|
29
|
+
* existing `lastUpdated` and any page without frontmatter alone.
|
|
30
|
+
*/
|
|
31
|
+
export declare function freezeLastUpdated(text: string, date: string | undefined): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure text transforms behind `svelte-docsmith archive-version`. Archiving
|
|
3
|
+
* copies the docs root into a frozen version folder, and a verbatim copy gets
|
|
4
|
+
* two things wrong: its links keep resolving to the current docs, and every page
|
|
5
|
+
* looks like it changed on the day the archive was made. These fix both.
|
|
6
|
+
*
|
|
7
|
+
* Kept here rather than in `bin/` so they are typechecked and unit-tested; the
|
|
8
|
+
* CLI is a thin wrapper over them. See `docs/adr/0002-archives-are-rewritten-source-copies.md`.
|
|
9
|
+
*/
|
|
10
|
+
import { outsideCodeFences, withFrontmatter } from './markdown-source.js';
|
|
11
|
+
/** Route files at the docs root that an archive nested inside it already inherits. */
|
|
12
|
+
export function isInheritedRouteFile(name) {
|
|
13
|
+
return /^\+(layout|error)\./.test(name);
|
|
14
|
+
}
|
|
15
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16
|
+
/**
|
|
17
|
+
* Point a page's docs links at the archive it is being copied into. An absolute
|
|
18
|
+
* link like `](/docs/theming)` resolves to the *current* docs forever, so
|
|
19
|
+
* without this an archive silently walks readers into newer content.
|
|
20
|
+
*
|
|
21
|
+
* Links already targeting a version folder are left alone, as are links inside
|
|
22
|
+
* fenced code, which are sample code rather than navigation.
|
|
23
|
+
*/
|
|
24
|
+
export function rewriteDocsLinks(text, options) {
|
|
25
|
+
const { docsBase, versionId } = options;
|
|
26
|
+
// Never double-prefix: skip links into an existing archive, and into the one
|
|
27
|
+
// being created (a page may already point at the id we're about to write).
|
|
28
|
+
const skip = new Set([...(options.archivedIds ?? []), versionId]);
|
|
29
|
+
const pattern = new RegExp(`(\\]\\(|href="|href='|\\]:[ \\t]+)(${escapeRe(docsBase)})([^)"'\\s]*)`, 'g');
|
|
30
|
+
return outsideCodeFences(text, (chunk) => chunk.replace(pattern, (match, prefix, base, rest) => {
|
|
31
|
+
// Guard the segment boundary: `/docsmith` must not become `/docs/v1mith`.
|
|
32
|
+
if (rest && !/^[/#?]/.test(rest))
|
|
33
|
+
return match;
|
|
34
|
+
const firstSegment = rest.startsWith('/') ? rest.slice(1).split(/[/#?]/)[0] : '';
|
|
35
|
+
if (firstSegment && skip.has(firstSegment))
|
|
36
|
+
return match;
|
|
37
|
+
return `${prefix}${base}/${versionId}${rest}`;
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Write a page's real last-updated date into its frontmatter. The collector
|
|
42
|
+
* prefers frontmatter over the git date, so an archive keeps the date each page
|
|
43
|
+
* was actually accurate on instead of the day the archive was created. Leaves an
|
|
44
|
+
* existing `lastUpdated` and any page without frontmatter alone.
|
|
45
|
+
*/
|
|
46
|
+
export function freezeLastUpdated(text, date) {
|
|
47
|
+
if (!date)
|
|
48
|
+
return text;
|
|
49
|
+
return withFrontmatter(text, (front) => /^lastUpdated:/m.test(front) ? front : `${front}\nlastUpdated: '${date}'`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a Changesets-generated `CHANGELOG.md` into structured releases.
|
|
3
|
+
*
|
|
4
|
+
* The shape Changesets writes is stable:
|
|
5
|
+
*
|
|
6
|
+
* # package-name
|
|
7
|
+
*
|
|
8
|
+
* ## 0.8.0
|
|
9
|
+
*
|
|
10
|
+
* ### Minor Changes
|
|
11
|
+
*
|
|
12
|
+
* - 4a1b3d4: Add an announcement bar. …
|
|
13
|
+
*
|
|
14
|
+
* A second paragraph, indented two spaces.
|
|
15
|
+
*
|
|
16
|
+
* Parsing that rather than asking authors to write release notes twice means
|
|
17
|
+
* the changelog can never fall behind what actually shipped.
|
|
18
|
+
*/
|
|
19
|
+
import type { ChangelogRelease } from '../core/changelog.js';
|
|
20
|
+
export declare function parseChangelog(markdown: string): ChangelogRelease[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** `- <7-or-more hex chars>: ` prefix that Changesets puts on each entry. */
|
|
2
|
+
const COMMIT_PREFIX = /^([-*])\s+[0-9a-f]{7,40}:\s*/;
|
|
3
|
+
/** Strip the leading `- ` (and any commit hash) from an entry's first line. */
|
|
4
|
+
function stripBullet(line) {
|
|
5
|
+
const withoutHash = line.replace(COMMIT_PREFIX, '');
|
|
6
|
+
if (withoutHash !== line)
|
|
7
|
+
return withoutHash;
|
|
8
|
+
return line.replace(/^[-*]\s+/, '');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* De-indent an entry's continuation lines. Changesets indents them by two
|
|
12
|
+
* spaces to keep them inside the list item; leaving that in would render them
|
|
13
|
+
* as code blocks.
|
|
14
|
+
*/
|
|
15
|
+
function dedent(lines) {
|
|
16
|
+
return lines
|
|
17
|
+
.map((line) => (line.startsWith(' ') ? line.slice(2) : line))
|
|
18
|
+
.join('\n')
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
21
|
+
export function parseChangelog(markdown) {
|
|
22
|
+
const lines = markdown.split(/\r?\n/);
|
|
23
|
+
const releases = [];
|
|
24
|
+
let release;
|
|
25
|
+
let group;
|
|
26
|
+
let entry;
|
|
27
|
+
const flushEntry = () => {
|
|
28
|
+
if (entry && group) {
|
|
29
|
+
const text = dedent(entry);
|
|
30
|
+
if (text)
|
|
31
|
+
group.items.push(text);
|
|
32
|
+
}
|
|
33
|
+
entry = undefined;
|
|
34
|
+
};
|
|
35
|
+
const flushGroup = () => {
|
|
36
|
+
flushEntry();
|
|
37
|
+
if (group && release && group.items.length > 0)
|
|
38
|
+
release.groups.push(group);
|
|
39
|
+
group = undefined;
|
|
40
|
+
};
|
|
41
|
+
const flushRelease = () => {
|
|
42
|
+
flushGroup();
|
|
43
|
+
if (release)
|
|
44
|
+
releases.push(release);
|
|
45
|
+
release = undefined;
|
|
46
|
+
};
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
const version = /^##\s+(?!#)(.+?)\s*$/.exec(line);
|
|
49
|
+
if (version) {
|
|
50
|
+
flushRelease();
|
|
51
|
+
release = { version: version[1].trim(), groups: [] };
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const heading = /^###\s+(.+?)\s*$/.exec(line);
|
|
55
|
+
if (heading && release) {
|
|
56
|
+
flushGroup();
|
|
57
|
+
group = { kind: heading[1].trim(), items: [] };
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!group)
|
|
61
|
+
continue;
|
|
62
|
+
// A new bullet at column zero starts an entry; anything else continues the
|
|
63
|
+
// current one, which is how multi-paragraph changesets survive intact.
|
|
64
|
+
if (/^[-*]\s+/.test(line)) {
|
|
65
|
+
flushEntry();
|
|
66
|
+
entry = [stripBullet(line)];
|
|
67
|
+
}
|
|
68
|
+
else if (entry) {
|
|
69
|
+
entry.push(line);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
flushRelease();
|
|
73
|
+
return releases;
|
|
74
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse the metadata that follows a code fence's language:
|
|
3
|
+
*
|
|
4
|
+
* ```ts title="vite.config.ts" showLineNumbers startLine=12
|
|
5
|
+
*
|
|
6
|
+
* mdsvex hands this string to the highlighter as its third argument, so no
|
|
7
|
+
* extra plumbing is needed to read it. Unknown entries are ignored rather than
|
|
8
|
+
* rejected, so a fence never fails a build over an unrecognised flag.
|
|
9
|
+
*/
|
|
10
|
+
export type FenceMeta = {
|
|
11
|
+
/** Filename or caption shown above the block. */
|
|
12
|
+
title?: string;
|
|
13
|
+
/** Whether to number the lines. `undefined` means "use the global default". */
|
|
14
|
+
lineNumbers?: boolean;
|
|
15
|
+
/** First line's number, for a snippet lifted out of a larger file. */
|
|
16
|
+
startLine?: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function parseFenceMeta(meta?: string | null): FenceMeta;
|
|
19
|
+
/** Whether a fence opts into Twoslash: ```ts twoslash */
|
|
20
|
+
export declare function hasTwoslash(meta?: string | null): boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse the metadata that follows a code fence's language:
|
|
3
|
+
*
|
|
4
|
+
* ```ts title="vite.config.ts" showLineNumbers startLine=12
|
|
5
|
+
*
|
|
6
|
+
* mdsvex hands this string to the highlighter as its third argument, so no
|
|
7
|
+
* extra plumbing is needed to read it. Unknown entries are ignored rather than
|
|
8
|
+
* rejected, so a fence never fails a build over an unrecognised flag.
|
|
9
|
+
*/
|
|
10
|
+
/** `title="a b"`, `title='a b'`, or `title=a` (unquoted values stop at a space). */
|
|
11
|
+
const KEY_VALUE = /(\w+)=(?:"([^"]*)"|'([^']*)'|(\S+))/g;
|
|
12
|
+
export function parseFenceMeta(meta) {
|
|
13
|
+
const result = {};
|
|
14
|
+
if (!meta)
|
|
15
|
+
return result;
|
|
16
|
+
for (const match of meta.matchAll(KEY_VALUE)) {
|
|
17
|
+
const key = match[1];
|
|
18
|
+
const value = match[2] ?? match[3] ?? match[4] ?? '';
|
|
19
|
+
if (key === 'title')
|
|
20
|
+
result.title = value;
|
|
21
|
+
else if (key === 'startLine') {
|
|
22
|
+
const parsed = Number.parseInt(value, 10);
|
|
23
|
+
// A non-numeric or nonsensical startLine is ignored: numbering from an
|
|
24
|
+
// accidental NaN is worse than numbering from 1.
|
|
25
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
26
|
+
result.startLine = parsed;
|
|
27
|
+
}
|
|
28
|
+
else if (key === 'showLineNumbers' || key === 'lineNumbers') {
|
|
29
|
+
result.lineNumbers = value !== 'false';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Bare flags, e.g. ```ts showLineNumbers
|
|
33
|
+
const bare = meta.replace(KEY_VALUE, ' ').split(/\s+/).filter(Boolean);
|
|
34
|
+
if (bare.includes('showLineNumbers') || bare.includes('lineNumbers'))
|
|
35
|
+
result.lineNumbers = true;
|
|
36
|
+
if (bare.includes('noLineNumbers'))
|
|
37
|
+
result.lineNumbers = false;
|
|
38
|
+
// Numbering from a given line is meaningless without a gutter to show it in.
|
|
39
|
+
if (result.startLine !== undefined && result.lineNumbers === undefined)
|
|
40
|
+
result.lineNumbers = true;
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
/** Whether a fence opts into Twoslash: ```ts twoslash */
|
|
44
|
+
export function hasTwoslash(meta) {
|
|
45
|
+
if (!meta)
|
|
46
|
+
return false;
|
|
47
|
+
return meta.replace(KEY_VALUE, ' ').split(/\s+/).includes('twoslash');
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structure of a markdown page, as every build-time pass over one needs to
|
|
3
|
+
* see it: where the frontmatter block ends, and which lines are prose rather
|
|
4
|
+
* than fenced code.
|
|
5
|
+
*
|
|
6
|
+
* Both rules used to be respelled in each pass, and the copies drifted. A fence
|
|
7
|
+
* scanner that toggles on the marker's first character alone closes a ````
|
|
8
|
+
* block on the ```svelte nested inside it, which pulls sample code into the
|
|
9
|
+
* search index and sample headings into the table of contents. Keeping the
|
|
10
|
+
* rules here means the search index, the table of contents, the LLM output and
|
|
11
|
+
* the archive rewriter all agree on what a page's prose is.
|
|
12
|
+
*/
|
|
13
|
+
/** A page split at its frontmatter delimiters. */
|
|
14
|
+
export type MarkdownSource = {
|
|
15
|
+
/** The YAML between the `---` lines, or undefined when the page has none. */
|
|
16
|
+
frontmatter: string | undefined;
|
|
17
|
+
/** Everything after the frontmatter block: prose, headings, fenced code, tags. */
|
|
18
|
+
body: string;
|
|
19
|
+
};
|
|
20
|
+
/** Split a page into its frontmatter and its body. */
|
|
21
|
+
export declare function splitFrontmatter(source: string): MarkdownSource;
|
|
22
|
+
/**
|
|
23
|
+
* Rewrite a page's frontmatter YAML in place, leaving the delimiters and the
|
|
24
|
+
* body byte-identical — including their line endings, which a split-and-rejoin
|
|
25
|
+
* would normalise. A page without frontmatter is returned untouched.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withFrontmatter(source: string, transform: (frontmatter: string) => string): string;
|
|
28
|
+
/** Apply `transform` to a page's prose lines, leaving fenced code untouched. */
|
|
29
|
+
export declare function outsideCodeFences(text: string, transform: (line: string) => string): string;
|
|
30
|
+
/** A page's prose lines, in order, with fenced code and its markers dropped. */
|
|
31
|
+
export declare function proseLines(text: string): Generator<string>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structure of a markdown page, as every build-time pass over one needs to
|
|
3
|
+
* see it: where the frontmatter block ends, and which lines are prose rather
|
|
4
|
+
* than fenced code.
|
|
5
|
+
*
|
|
6
|
+
* Both rules used to be respelled in each pass, and the copies drifted. A fence
|
|
7
|
+
* scanner that toggles on the marker's first character alone closes a ````
|
|
8
|
+
* block on the ```svelte nested inside it, which pulls sample code into the
|
|
9
|
+
* search index and sample headings into the table of contents. Keeping the
|
|
10
|
+
* rules here means the search index, the table of contents, the LLM output and
|
|
11
|
+
* the archive rewriter all agree on what a page's prose is.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* A page's frontmatter block: `---` on the first line, through the next line
|
|
15
|
+
* that starts with `---`. Group 1 is the YAML; the whole match spans the block
|
|
16
|
+
* including the line break that ends it, so the body starts where it stops.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately as loose about the closing delimiter as remark-frontmatter,
|
|
19
|
+
* which is what mdsvex strips at render time. A stricter rule here would read
|
|
20
|
+
* a page differently from the renderer: a page mdsvex renders body-only would
|
|
21
|
+
* keep its frontmatter as prose, or lose the title that puts it in the nav.
|
|
22
|
+
*/
|
|
23
|
+
const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
24
|
+
/** An opening or closing code fence, indented anywhere on its line. */
|
|
25
|
+
const FENCE = /^\s*(`{3,}|~{3,})/;
|
|
26
|
+
/** Split a page into its frontmatter and its body. */
|
|
27
|
+
export function splitFrontmatter(source) {
|
|
28
|
+
const match = FRONTMATTER.exec(source);
|
|
29
|
+
if (!match)
|
|
30
|
+
return { frontmatter: undefined, body: source };
|
|
31
|
+
return { frontmatter: match[1], body: source.slice(match[0].length) };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Rewrite a page's frontmatter YAML in place, leaving the delimiters and the
|
|
35
|
+
* body byte-identical — including their line endings, which a split-and-rejoin
|
|
36
|
+
* would normalise. A page without frontmatter is returned untouched.
|
|
37
|
+
*/
|
|
38
|
+
export function withFrontmatter(source, transform) {
|
|
39
|
+
const match = FRONTMATTER.exec(source);
|
|
40
|
+
if (!match)
|
|
41
|
+
return source;
|
|
42
|
+
const start = match[0].indexOf('\n') + 1;
|
|
43
|
+
return source.slice(0, start) + transform(match[1]) + source.slice(start + match[1].length);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A line-by-line scanner for fence state, rather than one regex pairing fences
|
|
47
|
+
* up, so an indented fence (inside a list item or a component) and a `~~~`
|
|
48
|
+
* fence are both recognised. Per CommonMark a fence closes only on the same
|
|
49
|
+
* character, at least as long as the opener, with no info string — so the
|
|
50
|
+
* ```svelte inside a ```` block opens nothing and closes nothing.
|
|
51
|
+
*
|
|
52
|
+
* Returns a predicate that consumes the page's lines in order and answers
|
|
53
|
+
* whether each one is prose. Fence markers themselves are not.
|
|
54
|
+
*/
|
|
55
|
+
function fenceScanner() {
|
|
56
|
+
let open;
|
|
57
|
+
return (line) => {
|
|
58
|
+
const match = FENCE.exec(line);
|
|
59
|
+
if (open) {
|
|
60
|
+
const closes = match &&
|
|
61
|
+
match[1][0] === open[0] &&
|
|
62
|
+
match[1].length >= open.length &&
|
|
63
|
+
!line.slice(match[0].length).trim();
|
|
64
|
+
if (closes)
|
|
65
|
+
open = undefined;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (match) {
|
|
69
|
+
open = match[1];
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Apply `transform` to a page's prose lines, leaving fenced code untouched. */
|
|
76
|
+
export function outsideCodeFences(text, transform) {
|
|
77
|
+
const isProse = fenceScanner();
|
|
78
|
+
return text
|
|
79
|
+
.split('\n')
|
|
80
|
+
.map((line) => (isProse(line) ? transform(line) : line))
|
|
81
|
+
.join('\n');
|
|
82
|
+
}
|
|
83
|
+
/** A page's prose lines, in order, with fenced code and its markers dropped. */
|
|
84
|
+
export function* proseLines(text) {
|
|
85
|
+
const isProse = fenceScanner();
|
|
86
|
+
for (const line of text.split('\n')) {
|
|
87
|
+
if (isProse(line))
|
|
88
|
+
yield line;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -22,6 +22,19 @@ export interface DocsmithPreprocessOptions {
|
|
|
22
22
|
remarkPlugins?: MdsvexOptions['remarkPlugins'];
|
|
23
23
|
/** Extra rehype plugins, appended after docsmith's own (slug, sectionize). */
|
|
24
24
|
rehypePlugins?: MdsvexOptions['rehypePlugins'];
|
|
25
|
+
/**
|
|
26
|
+
* Number every code block by default. Per-fence `showLineNumbers` /
|
|
27
|
+
* `noLineNumbers` override it either way. Default: `false`.
|
|
28
|
+
*/
|
|
29
|
+
lineNumbers?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Enable Twoslash on fences that ask for it with ```ts twoslash, adding real
|
|
32
|
+
* hover types from the TypeScript compiler. Requires the optional peer
|
|
33
|
+
* dependencies `@shikijs/twoslash`, `twoslash-svelte`, and `typescript`.
|
|
34
|
+
* A snippet that fails to typecheck falls back to a plain highlight with a
|
|
35
|
+
* build warning rather than failing the build. Default: `false`.
|
|
36
|
+
*/
|
|
37
|
+
twoslash?: boolean;
|
|
25
38
|
}
|
|
26
39
|
/**
|
|
27
40
|
* Build the svelte-docsmith preprocessor: mdsvex with Shiki highlighting (via
|