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.
Files changed (82) hide show
  1. package/bin/svelte-docsmith.mjs +161 -0
  2. package/dist/buildtime/archive.d.ts +31 -0
  3. package/dist/buildtime/archive.js +50 -0
  4. package/dist/buildtime/changelog.d.ts +20 -0
  5. package/dist/buildtime/changelog.js +74 -0
  6. package/dist/buildtime/fence-meta.d.ts +20 -0
  7. package/dist/buildtime/fence-meta.js +48 -0
  8. package/dist/buildtime/markdown-layout.svelte.d.ts +3 -0
  9. package/dist/buildtime/markdown-source.d.ts +31 -0
  10. package/dist/buildtime/markdown-source.js +90 -0
  11. package/dist/buildtime/preprocess.d.ts +13 -0
  12. package/dist/buildtime/preprocess.js +77 -17
  13. package/dist/buildtime/twoslash.d.ts +26 -0
  14. package/dist/buildtime/twoslash.js +40 -0
  15. package/dist/buildtime/vite/collect.d.ts +4 -3
  16. package/dist/buildtime/vite/collect.js +31 -12
  17. package/dist/buildtime/vite/extract.js +9 -35
  18. package/dist/buildtime/vite/frontmatter.js +4 -3
  19. package/dist/buildtime/vite/git.d.ts +7 -0
  20. package/dist/buildtime/vite/git.js +32 -0
  21. package/dist/buildtime/vite/index.d.ts +22 -0
  22. package/dist/buildtime/vite/index.js +27 -4
  23. package/dist/buildtime/vite/releases.d.ts +12 -0
  24. package/dist/buildtime/vite/releases.js +57 -0
  25. package/dist/components/changelog/changelog-entry.svelte +90 -0
  26. package/dist/components/changelog/changelog-entry.svelte.d.ts +7 -0
  27. package/dist/components/changelog/changelog.svelte +56 -0
  28. package/dist/components/changelog/changelog.svelte.d.ts +14 -0
  29. package/dist/components/chrome/copy-button.svelte +3 -2
  30. package/dist/components/chrome/search.svelte +16 -4
  31. package/dist/components/chrome/search.svelte.d.ts +4 -1
  32. package/dist/components/chrome/version-banner.svelte +82 -0
  33. package/dist/components/chrome/version-banner.svelte.d.ts +10 -0
  34. package/dist/components/chrome/version-switcher.svelte +63 -0
  35. package/dist/components/chrome/version-switcher.svelte.d.ts +14 -0
  36. package/dist/components/docs/mermaid.svelte +213 -0
  37. package/dist/components/docs/mermaid.svelte.d.ts +6 -0
  38. package/dist/components/landing/action.svelte +50 -0
  39. package/dist/components/landing/action.svelte.d.ts +13 -0
  40. package/dist/components/landing/cta.svelte +52 -0
  41. package/dist/components/landing/cta.svelte.d.ts +13 -0
  42. package/dist/components/landing/feature-grid.svelte +51 -0
  43. package/dist/components/landing/feature-grid.svelte.d.ts +19 -0
  44. package/dist/components/landing/feature.svelte +33 -0
  45. package/dist/components/landing/feature.svelte.d.ts +11 -0
  46. package/dist/components/landing/hero.svelte +70 -0
  47. package/dist/components/landing/hero.svelte.d.ts +19 -0
  48. package/dist/components/layouts/docs-header.svelte +61 -5
  49. package/dist/components/layouts/docs-header.svelte.d.ts +9 -1
  50. package/dist/components/layouts/docs-mobile-header.svelte +25 -2
  51. package/dist/components/layouts/docs-mobile-header.svelte.d.ts +9 -1
  52. package/dist/components/layouts/docs-shell.svelte +84 -66
  53. package/dist/components/layouts/docs-shell.svelte.d.ts +11 -2
  54. package/dist/components/layouts/error-page.svelte +23 -3
  55. package/dist/components/layouts/error-page.svelte.d.ts +8 -2
  56. package/dist/components/layouts/seo-head.svelte +8 -1
  57. package/dist/components/layouts/seo-head.svelte.d.ts +2 -0
  58. package/dist/components/markdown/pre.svelte +238 -8
  59. package/dist/components/markdown/pre.svelte.d.ts +6 -0
  60. package/dist/core/changelog.d.ts +31 -0
  61. package/dist/core/changelog.js +7 -0
  62. package/dist/core/content.d.ts +9 -0
  63. package/dist/core/docs-page.d.ts +70 -0
  64. package/dist/core/docs-page.js +55 -0
  65. package/dist/core/index.d.ts +2 -0
  66. package/dist/core/index.js +2 -0
  67. package/dist/core/version.d.ts +80 -0
  68. package/dist/core/version.js +85 -0
  69. package/dist/fallbacks/changelog.d.ts +10 -0
  70. package/dist/fallbacks/changelog.js +3 -0
  71. package/dist/fallbacks/content.d.ts +2 -0
  72. package/dist/fallbacks/content.js +1 -0
  73. package/dist/generate/feed.d.ts +35 -0
  74. package/dist/generate/feed.js +80 -0
  75. package/dist/index.d.ts +9 -0
  76. package/dist/index.js +14 -0
  77. package/dist/search/context.svelte.d.ts +2 -0
  78. package/dist/search/context.svelte.js +2 -0
  79. package/dist/search/create-search.d.ts +8 -2
  80. package/dist/search/create-search.js +11 -3
  81. package/dist/theme.css +53 -0
  82. 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
+ }
@@ -1,4 +1,7 @@
1
1
  export declare const pre: import("svelte").Component<{
2
+ title?: string;
3
+ lineNumbers?: boolean;
4
+ startLine?: number;
2
5
  children: Snippet;
3
6
  }, {}, "">;
4
7
  export declare const code: import("svelte").Component<{
@@ -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