svelte-docsmith 0.15.0 → 0.16.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/README.md +47 -14
- package/dist/buildtime/markdown-source.d.ts +11 -0
- package/dist/buildtime/markdown-source.js +48 -1
- package/dist/buildtime/props/extract.d.ts +6 -0
- package/dist/buildtime/props/extract.js +20 -9
- package/dist/buildtime/vite/agent-markdown.d.ts +43 -0
- package/dist/buildtime/vite/agent-markdown.js +204 -0
- package/dist/buildtime/vite/collect.d.ts +4 -1
- package/dist/buildtime/vite/collect.js +5 -2
- package/dist/buildtime/vite/extract.js +8 -3
- package/dist/buildtime/vite/index.js +79 -30
- package/dist/components/chrome/copy-button.svelte +4 -1
- package/dist/components/chrome/copy-button.svelte.d.ts +2 -0
- package/dist/components/docs/props-table.svelte +2 -4
- package/dist/components/docs/props-table.svelte.d.ts +1 -1
- package/dist/components/layouts/copy-page-menu.svelte +110 -17
- package/dist/components/layouts/copy-page-menu.svelte.d.ts +4 -0
- package/dist/components/layouts/docs-shell.svelte +7 -1
- package/dist/components/layouts/docs-shell.svelte.d.ts +1 -0
- package/dist/core/config.d.ts +9 -0
- package/dist/core/config.js +2 -1
- package/dist/core/extracted-props.d.ts +5 -0
- package/dist/core/extracted-props.js +8 -1
- package/dist/core/version.d.ts +4 -2
- package/dist/core/version.js +1 -1
- package/dist/generate/llms.d.ts +15 -0
- package/dist/generate/llms.js +6 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/mcp/connect.d.ts +17 -0
- package/dist/mcp/connect.js +40 -0
- package/dist/mcp/handler.d.ts +57 -0
- package/dist/mcp/handler.js +391 -0
- package/dist/mcp/tools.d.ts +81 -0
- package/dist/mcp/tools.js +196 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,8 +24,11 @@ live table of contents. No per-page wiring, no content collection to configure.
|
|
|
24
24
|
> breaking changes ride a major.
|
|
25
25
|
>
|
|
26
26
|
> **What stays experimental.** Versioning: `DocsVersions`, `currentOnly`, the
|
|
27
|
-
> archive marker, and the `archive-version` command.
|
|
28
|
-
>
|
|
27
|
+
> archive marker, and the `archive-version` command. Generated props tables:
|
|
28
|
+
> `?props` imports and `PropsTable`'s `of`. The MCP server: `mcpHandler` and the
|
|
29
|
+
> `mcp` config field. Agent markdown: the props tables and example code written
|
|
30
|
+
> into `llms-full.txt` and `.md` pages. They may still change in a minor until
|
|
31
|
+
> authors have used them for real.
|
|
29
32
|
|
|
30
33
|
## Install
|
|
31
34
|
|
|
@@ -170,6 +173,35 @@ plugin you already added:
|
|
|
170
173
|
</LiveExample>
|
|
171
174
|
```
|
|
172
175
|
|
|
176
|
+
To type the `?source` import, reference the package's client types once in
|
|
177
|
+
`src/app.d.ts`:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
/// <reference types="svelte-docsmith/client" />
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Props tables from your types
|
|
184
|
+
|
|
185
|
+
`PropsTable` can read a component's API straight from its typed `$props()`, so
|
|
186
|
+
the reference can't drift from the code either. Import the component with
|
|
187
|
+
`?props` and pass it to `of`:
|
|
188
|
+
|
|
189
|
+
```svelte
|
|
190
|
+
<script>
|
|
191
|
+
import { PropsTable } from 'svelte-docsmith';
|
|
192
|
+
import buttonProps from '#lib/button.svelte?props';
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
<PropsTable of={buttonProps} />
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Types come from the TypeScript checker (an imported `ButtonVariant` alias shows
|
|
199
|
+
its values), descriptions from JSDoc, defaults from the destructuring. Snippets,
|
|
200
|
+
callbacks and `$bindable()` props all get rows, and inherited HTML attributes
|
|
201
|
+
collapse to one line. It needs the optional peers `typescript` and `svelte2tsx`
|
|
202
|
+
(`npm i -D typescript svelte2tsx`). Generated props tables are **experimental**:
|
|
203
|
+
outside the 1.0 promise for now.
|
|
204
|
+
|
|
173
205
|
## Batteries included
|
|
174
206
|
|
|
175
207
|
- **Search.** A ⌘K / Ctrl-K command palette over a build-time full-text index, no
|
|
@@ -214,18 +246,19 @@ override the tokens yourself.
|
|
|
214
246
|
|
|
215
247
|
## What's exported
|
|
216
248
|
|
|
217
|
-
| Entry point | What it is
|
|
218
|
-
| ------------------------------ |
|
|
219
|
-
| `svelte-docsmith` | Components (`DocsShell`, `Changelog`, landing sections, `LiveExample`, `Callout`, …), `defineConfig`, `createSearchEngine`, `generateSitemap` / `generateLlmsTxt` / `generateLlmsFullTxt` / `generateFeed`, `currentOnly`, and the types |
|
|
220
|
-
| `svelte-docsmith/preprocess` | `docsmith()`, the mdsvex/Shiki pipeline (Node, config time)
|
|
221
|
-
| `svelte-docsmith/vite` | `docsmith()`, the content / search / llms / changelog indexes and `?source`
|
|
222
|
-
| `svelte-docsmith/content` | `docs` and `versions`, the generated sidebar content index and version manifest
|
|
223
|
-
| `svelte-docsmith/search` | `docs`, the generated full-text search index (lazy-load it)
|
|
224
|
-
| `svelte-docsmith/llms` | `docs`, the generated
|
|
225
|
-
| `svelte-docsmith/changelog` | `releases`, the generated changelog index (parsed from `CHANGELOG.md`)
|
|
226
|
-
| `svelte-docsmith/mermaid` | the Mermaid diagram component (lazy-loaded by the preprocessor when a mermaid fence is present)
|
|
227
|
-
| `svelte-docsmith/
|
|
228
|
-
| `svelte-docsmith/
|
|
249
|
+
| Entry point | What it is |
|
|
250
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
251
|
+
| `svelte-docsmith` | Components (`DocsShell`, `Changelog`, landing sections, `LiveExample`, `Callout`, …), `defineConfig`, `createSearchEngine`, `generateSitemap` / `generateLlmsTxt` / `generateLlmsFullTxt` / `generateFeed`, `mcpHandler`, `currentOnly`, and the types |
|
|
252
|
+
| `svelte-docsmith/preprocess` | `docsmith()`, the mdsvex/Shiki pipeline (Node, config time) |
|
|
253
|
+
| `svelte-docsmith/vite` | `docsmith()`, the content / search / llms / changelog indexes and `?source` / `?props` transforms (Node, build) |
|
|
254
|
+
| `svelte-docsmith/content` | `docs` and `versions`, the generated sidebar content index and version manifest |
|
|
255
|
+
| `svelte-docsmith/search` | `docs`, the generated full-text search index (lazy-load it) |
|
|
256
|
+
| `svelte-docsmith/llms` | `docs`, the generated agent markdown index (for `llms.txt` / `llms-full.txt` routes) |
|
|
257
|
+
| `svelte-docsmith/changelog` | `releases`, the generated changelog index (parsed from `CHANGELOG.md`) |
|
|
258
|
+
| `svelte-docsmith/mermaid` | the Mermaid diagram component (lazy-loaded by the preprocessor when a mermaid fence is present) |
|
|
259
|
+
| `svelte-docsmith/client` | ambient types for `?source` and `?props` imports (reference it from `src/app.d.ts`) |
|
|
260
|
+
| `svelte-docsmith/theme.css` | the base style contract |
|
|
261
|
+
| `svelte-docsmith/themes/*.css` | the eleven theme presets |
|
|
229
262
|
|
|
230
263
|
The package also ships a CLI binary: `npx svelte-docsmith archive-version <id>`
|
|
231
264
|
freezes the current docs into a versioned archive.
|
|
@@ -27,6 +27,17 @@ export declare function splitFrontmatter(source: string): MarkdownSource;
|
|
|
27
27
|
export declare function withFrontmatter(source: string, transform: (frontmatter: string) => string): string;
|
|
28
28
|
/** Apply `transform` to a page's prose lines, leaving fenced code untouched. */
|
|
29
29
|
export declare function outsideCodeFences(text: string, transform: (line: string) => string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Apply `transform` to each run of consecutive prose lines, leaving fenced code
|
|
32
|
+
* and its markers untouched. For rewrites that span lines, such as a component
|
|
33
|
+
* tag with children, which {@link outsideCodeFences} would see a line at a time.
|
|
34
|
+
*
|
|
35
|
+
* With `setupBlocks`, each top-level `<script>`/`<style>` block stays whole
|
|
36
|
+
* inside one run, even when its code has a fence-like line.
|
|
37
|
+
*/
|
|
38
|
+
export declare function outsideCodeFenceRuns(text: string, transform: (prose: string) => string, options?: {
|
|
39
|
+
setupBlocks?: boolean;
|
|
40
|
+
}): string;
|
|
30
41
|
/** A page's prose lines, in order, with fenced code and its markers dropped. */
|
|
31
42
|
export declare function proseLines(text: string): Generator<string>;
|
|
32
43
|
/**
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
24
24
|
/** An opening or closing code fence, indented anywhere on its line. */
|
|
25
25
|
const FENCE = /^\s*(`{3,}|~{3,})/;
|
|
26
|
+
/** The open tag of a `<script>` or `<style>` block, starting a line as HTML may. */
|
|
27
|
+
const SETUP_BLOCK_OPEN = /^ {0,3}<(script|style)(?=[\s>]|$)/i;
|
|
26
28
|
/** Split a page into its frontmatter and its body. */
|
|
27
29
|
export function splitFrontmatter(source) {
|
|
28
30
|
const match = FRONTMATTER.exec(source);
|
|
@@ -51,10 +53,20 @@ export function withFrontmatter(source, transform) {
|
|
|
51
53
|
*
|
|
52
54
|
* Returns a predicate that consumes the page's lines in order and answers
|
|
53
55
|
* whether each one is prose. Fence markers themselves are not.
|
|
56
|
+
*
|
|
57
|
+
* With `setupBlocks`, a `<script>` or `<style>` block starting a prose line is
|
|
58
|
+
* prose through its closing tag, as CommonMark reads such an HTML block, so a
|
|
59
|
+
* fence-like line in its code (a ``` in a template literal) opens nothing.
|
|
54
60
|
*/
|
|
55
|
-
function fenceScanner() {
|
|
61
|
+
function fenceScanner({ setupBlocks = false } = {}) {
|
|
56
62
|
let open;
|
|
63
|
+
let setupClose;
|
|
57
64
|
return (line) => {
|
|
65
|
+
if (setupClose) {
|
|
66
|
+
if (setupClose.test(line))
|
|
67
|
+
setupClose = undefined;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
58
70
|
const match = FENCE.exec(line);
|
|
59
71
|
if (open) {
|
|
60
72
|
const closes = match &&
|
|
@@ -69,6 +81,12 @@ function fenceScanner() {
|
|
|
69
81
|
open = match[1];
|
|
70
82
|
return false;
|
|
71
83
|
}
|
|
84
|
+
const setup = setupBlocks ? SETUP_BLOCK_OPEN.exec(line) : null;
|
|
85
|
+
if (setup) {
|
|
86
|
+
const close = new RegExp(`</${setup[1]}>`, 'i');
|
|
87
|
+
if (!close.test(line.slice(setup[0].length)))
|
|
88
|
+
setupClose = close;
|
|
89
|
+
}
|
|
72
90
|
return true;
|
|
73
91
|
};
|
|
74
92
|
}
|
|
@@ -80,6 +98,35 @@ export function outsideCodeFences(text, transform) {
|
|
|
80
98
|
.map((line) => (isProse(line) ? transform(line) : line))
|
|
81
99
|
.join('\n');
|
|
82
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Apply `transform` to each run of consecutive prose lines, leaving fenced code
|
|
103
|
+
* and its markers untouched. For rewrites that span lines, such as a component
|
|
104
|
+
* tag with children, which {@link outsideCodeFences} would see a line at a time.
|
|
105
|
+
*
|
|
106
|
+
* With `setupBlocks`, each top-level `<script>`/`<style>` block stays whole
|
|
107
|
+
* inside one run, even when its code has a fence-like line.
|
|
108
|
+
*/
|
|
109
|
+
export function outsideCodeFenceRuns(text, transform, options = {}) {
|
|
110
|
+
const isProse = fenceScanner(options);
|
|
111
|
+
const out = [];
|
|
112
|
+
let run = [];
|
|
113
|
+
const flush = () => {
|
|
114
|
+
if (run.length)
|
|
115
|
+
out.push(transform(run.join('\n')));
|
|
116
|
+
run = [];
|
|
117
|
+
};
|
|
118
|
+
for (const line of text.split('\n')) {
|
|
119
|
+
if (isProse(line)) {
|
|
120
|
+
run.push(line);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
flush();
|
|
124
|
+
out.push(line);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
flush();
|
|
128
|
+
return out.join('\n');
|
|
129
|
+
}
|
|
83
130
|
/** A page's prose lines, in order, with fenced code and its markers dropped. */
|
|
84
131
|
export function* proseLines(text) {
|
|
85
132
|
const isProse = fenceScanner();
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { ExtractedProps } from '../../core/extracted-props.js';
|
|
2
2
|
export interface ExtractResult {
|
|
3
|
+
/** What `<PropsTable of={…}>` renders: descriptions as inline HTML. */
|
|
3
4
|
props: ExtractedProps;
|
|
5
|
+
/**
|
|
6
|
+
* The same props with descriptions and deprecation notes as the JSDoc's own
|
|
7
|
+
* markdown, for agent markdown.
|
|
8
|
+
*/
|
|
9
|
+
markdown: ExtractedProps;
|
|
4
10
|
warnings: string[];
|
|
5
11
|
/** Files whose changes should re-extract this component. */
|
|
6
12
|
watchFiles: string[];
|
|
@@ -78,18 +78,33 @@ export function createPropsExtractor(options) {
|
|
|
78
78
|
warn('reading a published package file. Published components usually have their ' +
|
|
79
79
|
'types stripped, so point the props import at the source instead.');
|
|
80
80
|
}
|
|
81
|
+
// Rows are built once, with JSDoc as markdown; the rendered table's copy
|
|
82
|
+
// differs only in turning that markdown into HTML.
|
|
83
|
+
const done = (watchFiles) => ({
|
|
84
|
+
props: {
|
|
85
|
+
...extracted,
|
|
86
|
+
props: extracted.props.map((prop) => clean({
|
|
87
|
+
...prop,
|
|
88
|
+
deprecated: prop.deprecated === undefined ? undefined : renderMarkdown(prop.deprecated),
|
|
89
|
+
description: prop.description ? renderMarkdown(prop.description) : undefined
|
|
90
|
+
}))
|
|
91
|
+
},
|
|
92
|
+
markdown: extracted,
|
|
93
|
+
warnings,
|
|
94
|
+
watchFiles
|
|
95
|
+
});
|
|
81
96
|
let parsed;
|
|
82
97
|
try {
|
|
83
98
|
parsed = parseComponent(fs.readFileSync(file, 'utf-8'), file);
|
|
84
99
|
}
|
|
85
100
|
catch (error) {
|
|
86
101
|
warn(`could not parse the component (${message(error)}).`);
|
|
87
|
-
return
|
|
102
|
+
return done([file]);
|
|
88
103
|
}
|
|
89
104
|
extracted.typeParameters = parsed.typeParameters;
|
|
90
105
|
if (!parsed.hasProps) {
|
|
91
106
|
warn('no `$props()` found, so the props table is empty.');
|
|
92
|
-
return
|
|
107
|
+
return done([file]);
|
|
93
108
|
}
|
|
94
109
|
let typed;
|
|
95
110
|
if (parsed.typed) {
|
|
@@ -118,8 +133,8 @@ export function createPropsExtractor(options) {
|
|
|
118
133
|
default: fallback,
|
|
119
134
|
required: !prop.optional && fallback === undefined,
|
|
120
135
|
bindable: destructured?.bindable ?? false,
|
|
121
|
-
deprecated: prop.deprecated
|
|
122
|
-
description: prop.description
|
|
136
|
+
deprecated: prop.deprecated,
|
|
137
|
+
description: prop.description
|
|
123
138
|
}));
|
|
124
139
|
}
|
|
125
140
|
extracted.inherited = typed.inherited;
|
|
@@ -129,11 +144,7 @@ export function createPropsExtractor(options) {
|
|
|
129
144
|
extracted.props.push(clean({ name: d.name, default: d.default, required: false, bindable: d.bindable }));
|
|
130
145
|
}
|
|
131
146
|
}
|
|
132
|
-
return
|
|
133
|
-
props: extracted,
|
|
134
|
-
warnings,
|
|
135
|
-
watchFiles: [...new Set([file, ...(typed?.files ?? [])])]
|
|
136
|
-
};
|
|
147
|
+
return done([...new Set([file, ...(typed?.files ?? [])])]);
|
|
137
148
|
}
|
|
138
149
|
}
|
|
139
150
|
/** `file-tree-item.svelte` → `FileTreeItem`. */
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ExtractedProps } from '../../core/extracted-props.js';
|
|
2
|
+
import type { SourcePage } from './pages.js';
|
|
3
|
+
/** Which build-time transform a page import goes through. */
|
|
4
|
+
export type DocImportKind = 'source' | 'props';
|
|
5
|
+
/** One `*.svelte?source` or `*.svelte?props` import in a page's `<script>`. */
|
|
6
|
+
export type DocImport = {
|
|
7
|
+
kind: DocImportKind;
|
|
8
|
+
/** The specifier as written, query included, for messages. */
|
|
9
|
+
specifier: string;
|
|
10
|
+
/** The component file it resolves to, or `undefined` when it did not resolve. */
|
|
11
|
+
file: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a specifier (query stripped) from a page, the way Vite resolves the
|
|
15
|
+
* page's own imports: the app's aliases (`$lib`, `#lib`) included. Returns the
|
|
16
|
+
* resolved file, or `undefined` when nothing resolves it.
|
|
17
|
+
*/
|
|
18
|
+
export type ResolveImport = (specifier: string, importer: string) => Promise<string | undefined>;
|
|
19
|
+
/** What building agent markdown needs from the plugin. */
|
|
20
|
+
export type AgentMarkdownHost = {
|
|
21
|
+
resolve: ResolveImport;
|
|
22
|
+
/** Record a file the agent markdown was read from, so editing it rebuilds the index. */
|
|
23
|
+
watch(file: string): void;
|
|
24
|
+
warn(message: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* A component's props as the `?props` transform extracts them, with
|
|
27
|
+
* descriptions as markdown. The host watches the files they were read from
|
|
28
|
+
* and passes on extraction warnings.
|
|
29
|
+
*/
|
|
30
|
+
props(file: string): Promise<ExtractedProps>;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* A page's `?source` and `?props` imports, keyed by the local name each binds,
|
|
34
|
+
* with every specifier resolved from the page. Imports inside fenced code
|
|
35
|
+
* samples are not the page's, and are skipped.
|
|
36
|
+
*/
|
|
37
|
+
export declare function docImports(page: Pick<SourcePage, 'file' | 'source'>, resolve: ResolveImport): Promise<Map<string, DocImport>>;
|
|
38
|
+
/**
|
|
39
|
+
* Build the agent markdown rewrite for one page: a function from the page's
|
|
40
|
+
* markdown, setup blocks stripped, to its agent markdown. `undefined` when the page imports nothing
|
|
41
|
+
* agent markdown expands, so most pages cost one scan of their script.
|
|
42
|
+
*/
|
|
43
|
+
export declare function agentMarkdownFor(page: SourcePage, host: AgentMarkdownHost): Promise<((content: string) => string) | undefined>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent markdown: a page's markdown as an agent reads it (llms-full.txt, the
|
|
3
|
+
* `.md` twins, the MCP server). The page as written keeps `<LiveExample
|
|
4
|
+
* source={…}>` and `<PropsTable of={…}>` as bare tags and their imports are
|
|
5
|
+
* stripped with the `<script>` block, so an agent would never see the example
|
|
6
|
+
* code or the component's props. Here each `LiveExample` whose source is a
|
|
7
|
+
* `?source` import becomes a fenced `svelte` block of the example file's raw
|
|
8
|
+
* source, and each `PropsTable` whose `of` is a props import becomes a heading
|
|
9
|
+
* and a markdown table of the same rows the page renders.
|
|
10
|
+
*
|
|
11
|
+
* Unlike `./collect.js` this reads the filesystem and resolves imports, so it
|
|
12
|
+
* runs in the plugin, which hands it Vite's resolver and the props extractor.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import { propsCaption } from '../../core/extracted-props.js';
|
|
16
|
+
import { outsideCodeFenceRuns, scriptBlocks } from '../markdown-source.js';
|
|
17
|
+
import { relativeToCwd } from '../paths.js';
|
|
18
|
+
/** A default import of a `.svelte` file through `?source` or `?props`. */
|
|
19
|
+
const DOC_IMPORT = /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s*(['"])([^'"\n]+?\.svelte)\?(source|props)\2/g;
|
|
20
|
+
/**
|
|
21
|
+
* A page's `?source` and `?props` imports, keyed by the local name each binds,
|
|
22
|
+
* with every specifier resolved from the page. Imports inside fenced code
|
|
23
|
+
* samples are not the page's, and are skipped.
|
|
24
|
+
*/
|
|
25
|
+
export async function docImports(page, resolve) {
|
|
26
|
+
const imports = new Map();
|
|
27
|
+
for (const block of scriptBlocks(page.source)) {
|
|
28
|
+
for (const [, local, , base, kind] of block.matchAll(DOC_IMPORT)) {
|
|
29
|
+
const file = await resolve(base, page.file);
|
|
30
|
+
imports.set(local, { kind: kind, specifier: `${base}?${kind}`, file });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return imports;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A `<LiveExample …>…</LiveExample>` (or self-closing) tag starting a line.
|
|
37
|
+
* Group 1 is its indentation, group 2 its attributes; an attribute value in
|
|
38
|
+
* braces may contain `>`.
|
|
39
|
+
*/
|
|
40
|
+
const LIVE_EXAMPLE = /^([ \t]*)<LiveExample\b((?:[^>{}]|\{[^{}]*\})*?)(?:\/>|>[\s\S]*?<\/LiveExample\s*>)/gm;
|
|
41
|
+
/** `source={name}`, or the `{source}` shorthand. */
|
|
42
|
+
const SOURCE_ATTRIBUTE = /(?:\bsource\s*=\s*\{\s*([A-Za-z_$][\w$]*)\s*\}|\{\s*(source)\s*\})/;
|
|
43
|
+
/** A backtick fence one longer than the longest backtick run in `text`, and at least `min`. */
|
|
44
|
+
function fenceFor(text, min) {
|
|
45
|
+
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map(([run]) => run.length));
|
|
46
|
+
return '`'.repeat(Math.max(min, longest + 1));
|
|
47
|
+
}
|
|
48
|
+
/** `text` with `indent` before each non-empty line. */
|
|
49
|
+
function indented(text, indent) {
|
|
50
|
+
return text
|
|
51
|
+
.split('\n')
|
|
52
|
+
.map((line) => (line ? indent + line : line))
|
|
53
|
+
.join('\n');
|
|
54
|
+
}
|
|
55
|
+
/** A fenced block of `code`, fenced longer than any backtick run inside it. */
|
|
56
|
+
function fenced(code, lang, indent) {
|
|
57
|
+
const fence = fenceFor(code, 3);
|
|
58
|
+
return indented([fence + lang, code, fence].join('\n'), indent);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build the agent markdown rewrite for one page: a function from the page's
|
|
62
|
+
* markdown, setup blocks stripped, to its agent markdown. `undefined` when the page imports nothing
|
|
63
|
+
* agent markdown expands, so most pages cost one scan of their script.
|
|
64
|
+
*/
|
|
65
|
+
export async function agentMarkdownFor(page, host) {
|
|
66
|
+
const imports = await docImports(page, host.resolve);
|
|
67
|
+
if (imports.size === 0)
|
|
68
|
+
return undefined;
|
|
69
|
+
// Read up front, so the rewrite itself is synchronous. Props extractions all
|
|
70
|
+
// start together, so the extractor batches them into one TypeScript update.
|
|
71
|
+
const sources = new Map();
|
|
72
|
+
const tables = new Map();
|
|
73
|
+
await Promise.all([...imports].map(async ([local, entry]) => {
|
|
74
|
+
// Watched even when it can't be read, so creating or fixing it rebuilds.
|
|
75
|
+
if (entry.file !== undefined)
|
|
76
|
+
host.watch(entry.file);
|
|
77
|
+
if (entry.kind === 'source') {
|
|
78
|
+
sources.set(local, entry.file === undefined ? undefined : readExample(entry.file));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
tables.set(local, entry.file === undefined ? undefined : await host.props(entry.file));
|
|
82
|
+
}
|
|
83
|
+
}));
|
|
84
|
+
const keepTag = (tag, local, attribute) => {
|
|
85
|
+
host.warn(`${relativeToCwd(page.file)}: could not read \`${imports.get(local)?.specifier}\` ` +
|
|
86
|
+
`for <${tag} ${attribute}={${local}}>, so agent markdown keeps the tag.`);
|
|
87
|
+
};
|
|
88
|
+
return (content) => {
|
|
89
|
+
// The level of the heading a table sits under, carried across code fences.
|
|
90
|
+
let level = 1;
|
|
91
|
+
return outsideCodeFenceRuns(content, (prose) => {
|
|
92
|
+
const levelAt = (offset) => lastHeadingLevel(prose.slice(0, offset)) ?? level;
|
|
93
|
+
const withTables = prose.replace(PROPS_TABLE, (tag, indent, attributes, offset) => {
|
|
94
|
+
const match = OF_ATTRIBUTE.exec(attributes);
|
|
95
|
+
const local = match?.[1] ?? match?.[2];
|
|
96
|
+
if (local === undefined || !tables.has(local))
|
|
97
|
+
return tag;
|
|
98
|
+
const props = tables.get(local);
|
|
99
|
+
if (props === undefined) {
|
|
100
|
+
keepTag('PropsTable', local, 'of');
|
|
101
|
+
return tag;
|
|
102
|
+
}
|
|
103
|
+
const title = TITLE_ATTRIBUTE.exec(attributes);
|
|
104
|
+
const caption = title ? title.slice(1).find((value) => value !== undefined) : undefined;
|
|
105
|
+
return propsTable(props, caption, Math.min(6, levelAt(offset) + 1), indent);
|
|
106
|
+
});
|
|
107
|
+
level = levelAt(prose.length);
|
|
108
|
+
return withTables.replace(LIVE_EXAMPLE, (tag, indent, attributes) => {
|
|
109
|
+
const match = SOURCE_ATTRIBUTE.exec(attributes);
|
|
110
|
+
const local = match?.[1] ?? match?.[2];
|
|
111
|
+
if (local === undefined || !sources.has(local))
|
|
112
|
+
return tag;
|
|
113
|
+
const code = sources.get(local);
|
|
114
|
+
if (code === undefined) {
|
|
115
|
+
keepTag('LiveExample', local, 'source');
|
|
116
|
+
return tag;
|
|
117
|
+
}
|
|
118
|
+
return fenced(code, 'svelte', indent);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A `<PropsTable …/>` or `<PropsTable …>…</PropsTable>` tag starting a line,
|
|
125
|
+
* grouped like {@link LIVE_EXAMPLE}.
|
|
126
|
+
*/
|
|
127
|
+
const PROPS_TABLE = /^([ \t]*)<PropsTable\b((?:[^>{}]|\{[^{}]*\})*?)(?:\/>|>[\s\S]*?<\/PropsTable\s*>)/gm;
|
|
128
|
+
/** `of={name}`, or the `{of}` shorthand. */
|
|
129
|
+
const OF_ATTRIBUTE = /(?:(?:^|\s)of\s*=\s*\{\s*([A-Za-z_$][\w$]*)\s*\}|\{\s*(of)\s*\})/;
|
|
130
|
+
/** A `title` given as a string literal, quoted or in braces. */
|
|
131
|
+
const TITLE_ATTRIBUTE = /(?:^|\s)title\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*(?:"([^"]*)"|'([^']*)')\s*\})/;
|
|
132
|
+
/** An ATX heading's marker. */
|
|
133
|
+
const HEADING = /^ {0,3}(#{1,6})(?:[ \t]|$)/gm;
|
|
134
|
+
/** The level of the last heading in `prose`, if it has one. */
|
|
135
|
+
function lastHeadingLevel(prose) {
|
|
136
|
+
return [...prose.matchAll(HEADING)].at(-1)?.[1].length;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A generated props table as agent markdown: the caption as a heading, then a
|
|
140
|
+
* `Prop | Type | Default | Description` table with the rows the page renders,
|
|
141
|
+
* then the inherited attributes summary.
|
|
142
|
+
*/
|
|
143
|
+
function propsTable(props, title, level, indent) {
|
|
144
|
+
const blocks = [`${'#'.repeat(level)} ${code(title ?? propsCaption(props))}`];
|
|
145
|
+
if (props.props.length) {
|
|
146
|
+
blocks.push([
|
|
147
|
+
'| Prop | Type | Default | Description |',
|
|
148
|
+
'| --- | --- | --- | --- |',
|
|
149
|
+
...props.props.map((prop) => `| ${propCells(prop).join(' | ')} |`)
|
|
150
|
+
].join('\n'));
|
|
151
|
+
}
|
|
152
|
+
for (const group of props.inherited) {
|
|
153
|
+
blocks.push(group.kind === 'element'
|
|
154
|
+
? `Also accepts every ${code(`<${group.label}>`)} attribute.`
|
|
155
|
+
: `Also accepts every prop of ${code(group.label)}.`);
|
|
156
|
+
}
|
|
157
|
+
if (props.props.length === 0 && props.inherited.length === 0)
|
|
158
|
+
blocks.push('No props.');
|
|
159
|
+
return indented(blocks.join('\n\n'), indent);
|
|
160
|
+
}
|
|
161
|
+
/** One row's cells, marked as the rendered row is and escaped for a table. */
|
|
162
|
+
function propCells(prop) {
|
|
163
|
+
let name = code(prop.required ? prop.name : `${prop.name}?`);
|
|
164
|
+
if (prop.deprecated !== undefined)
|
|
165
|
+
name = `~~${name}~~`;
|
|
166
|
+
const marks = [
|
|
167
|
+
prop.bindable && '*(bindable)*',
|
|
168
|
+
prop.deprecated !== undefined && '*(deprecated)*'
|
|
169
|
+
];
|
|
170
|
+
const description = [prop.deprecated && `Deprecated: ${prop.deprecated}`, prop.description]
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.map((text) => inline(text))
|
|
173
|
+
.join('<br>');
|
|
174
|
+
return [
|
|
175
|
+
[name, ...marks.filter(Boolean)].join(' '),
|
|
176
|
+
prop.type === undefined ? '' : code(prop.type),
|
|
177
|
+
prop.default === undefined ? '' : code(prop.default),
|
|
178
|
+
description
|
|
179
|
+
].map((cell) => cell.replaceAll('|', '\\|'));
|
|
180
|
+
}
|
|
181
|
+
/** `text` as a code span on one line, fenced longer than any backtick run in it. */
|
|
182
|
+
function code(text) {
|
|
183
|
+
const flat = text.replace(/\s*\n\s*/g, ' ');
|
|
184
|
+
const fence = fenceFor(flat, 1);
|
|
185
|
+
const pad = flat.startsWith('`') || flat.endsWith('`') ? ' ' : '';
|
|
186
|
+
return fence + pad + flat + pad + fence;
|
|
187
|
+
}
|
|
188
|
+
/** Markdown on one line: paragraphs joined with `<br><br>`, soft breaks as spaces. */
|
|
189
|
+
function inline(markdown) {
|
|
190
|
+
return markdown
|
|
191
|
+
.trim()
|
|
192
|
+
.split(/\n[ \t]*\n\s*/)
|
|
193
|
+
.map((paragraph) => paragraph.replace(/\s*\n\s*/g, ' '))
|
|
194
|
+
.join('<br><br>');
|
|
195
|
+
}
|
|
196
|
+
/** An example file's raw source, as the `?source` transform reads it. */
|
|
197
|
+
function readExample(file) {
|
|
198
|
+
try {
|
|
199
|
+
return fs.readFileSync(file, 'utf-8').trimEnd();
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -29,5 +29,8 @@ export declare function searchIndex(pages: SourcePage[], options: IndexOptions):
|
|
|
29
29
|
* The LLM index: title, section, description, and the full markdown content.
|
|
30
30
|
* Served as the `svelte-docsmith/llms` virtual module and consumed server-side
|
|
31
31
|
* by `llms.txt` / `llms-full.txt` routes, so it never ships to the client.
|
|
32
|
+
*
|
|
33
|
+
* `rewrite` turns a page's markdown into agent markdown (see
|
|
34
|
+
* `./agent-markdown.js`); without it, the content is the page as written.
|
|
32
35
|
*/
|
|
33
|
-
export declare function llmsIndex(pages: SourcePage[], options: IndexOptions): LlmsDoc[];
|
|
36
|
+
export declare function llmsIndex(pages: SourcePage[], options: IndexOptions, rewrite?: (content: string, page: SourcePage) => string): LlmsDoc[];
|
|
@@ -108,15 +108,18 @@ export function searchIndex(pages, options) {
|
|
|
108
108
|
* The LLM index: title, section, description, and the full markdown content.
|
|
109
109
|
* Served as the `svelte-docsmith/llms` virtual module and consumed server-side
|
|
110
110
|
* by `llms.txt` / `llms-full.txt` routes, so it never ships to the client.
|
|
111
|
+
*
|
|
112
|
+
* `rewrite` turns a page's markdown into agent markdown (see
|
|
113
|
+
* `./agent-markdown.js`); without it, the content is the page as written.
|
|
111
114
|
*/
|
|
112
|
-
export function llmsIndex(pages, options) {
|
|
115
|
+
export function llmsIndex(pages, options, rewrite = (content) => content) {
|
|
113
116
|
return indexedPages(pages, options).map((page) => ({
|
|
114
117
|
path: page.url,
|
|
115
118
|
title: page.title,
|
|
116
119
|
section: sectionKey(page.frontmatter),
|
|
117
120
|
order: readField(page.frontmatter, 'order'),
|
|
118
121
|
description: readField(page.frontmatter, 'description'),
|
|
119
|
-
content: extractLlmsContent(page.source, page.title),
|
|
122
|
+
content: rewrite(extractLlmsContent(page.source, page.title), page),
|
|
120
123
|
version: page.version
|
|
121
124
|
}));
|
|
122
125
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import GithubSlugger from 'github-slugger';
|
|
2
|
-
import { proseLines, splitFrontmatter } from '../markdown-source.js';
|
|
3
|
-
/**
|
|
2
|
+
import { outsideCodeFenceRuns, proseLines, splitFrontmatter } from '../markdown-source.js';
|
|
3
|
+
/**
|
|
4
|
+
* Drop a page's `<script>`/`<style>` blocks: component setup, not content. One
|
|
5
|
+
* inside a fenced code sample is part of the sample, and stays. A top-level one
|
|
6
|
+
* is read whole, so a fence-like line in its code neither splits it nor opens a
|
|
7
|
+
* fence.
|
|
8
|
+
*/
|
|
4
9
|
function withoutSetupBlocks(body) {
|
|
5
|
-
return body.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '');
|
|
10
|
+
return outsideCodeFenceRuns(body, (prose) => prose.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, ''), { setupBlocks: true });
|
|
6
11
|
}
|
|
7
12
|
/** Strip inline markdown so a heading's TOC label is plain text. */
|
|
8
13
|
function stripInlineMarkdown(text) {
|