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.
Files changed (36) hide show
  1. package/README.md +47 -14
  2. package/dist/buildtime/markdown-source.d.ts +11 -0
  3. package/dist/buildtime/markdown-source.js +48 -1
  4. package/dist/buildtime/props/extract.d.ts +6 -0
  5. package/dist/buildtime/props/extract.js +20 -9
  6. package/dist/buildtime/vite/agent-markdown.d.ts +43 -0
  7. package/dist/buildtime/vite/agent-markdown.js +204 -0
  8. package/dist/buildtime/vite/collect.d.ts +4 -1
  9. package/dist/buildtime/vite/collect.js +5 -2
  10. package/dist/buildtime/vite/extract.js +8 -3
  11. package/dist/buildtime/vite/index.js +79 -30
  12. package/dist/components/chrome/copy-button.svelte +4 -1
  13. package/dist/components/chrome/copy-button.svelte.d.ts +2 -0
  14. package/dist/components/docs/props-table.svelte +2 -4
  15. package/dist/components/docs/props-table.svelte.d.ts +1 -1
  16. package/dist/components/layouts/copy-page-menu.svelte +110 -17
  17. package/dist/components/layouts/copy-page-menu.svelte.d.ts +4 -0
  18. package/dist/components/layouts/docs-shell.svelte +7 -1
  19. package/dist/components/layouts/docs-shell.svelte.d.ts +1 -0
  20. package/dist/core/config.d.ts +9 -0
  21. package/dist/core/config.js +2 -1
  22. package/dist/core/extracted-props.d.ts +5 -0
  23. package/dist/core/extracted-props.js +8 -1
  24. package/dist/core/version.d.ts +4 -2
  25. package/dist/core/version.js +1 -1
  26. package/dist/generate/llms.d.ts +15 -0
  27. package/dist/generate/llms.js +6 -2
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +5 -0
  30. package/dist/mcp/connect.d.ts +17 -0
  31. package/dist/mcp/connect.js +40 -0
  32. package/dist/mcp/handler.d.ts +57 -0
  33. package/dist/mcp/handler.js +391 -0
  34. package/dist/mcp/tools.d.ts +81 -0
  35. package/dist/mcp/tools.js +196 -0
  36. 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. They may still change in a
28
- > minor until authors have used them for real.
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` transform (Node, build) |
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 LLM content index (for `llms.txt` / `llms-full.txt` routes) |
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/theme.css` | the base style contract |
228
- | `svelte-docsmith/themes/*.css` | the eleven theme presets |
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 { props: extracted, warnings, watchFiles: [file] };
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 { props: extracted, warnings, watchFiles: [file] };
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 === undefined ? undefined : renderMarkdown(prop.deprecated),
122
- description: prop.description ? renderMarkdown(prop.description) : undefined
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
- /** Drop a page's `<script>`/`<style>` blocks: component setup, not content. */
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) {