@tsdoctor/pages 0.1.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/Examples.js ADDED
@@ -0,0 +1,237 @@
1
+ import { CodeText, Example } from "./Blocks.js";
2
+ import { classifyCutDirective, isTwoslashDirective } from "./TwoslashDirectives.js";
3
+ import { Effect, Schema } from "effect";
4
+ import { TypeReferenceExtractor } from "@tsdoctor/model";
5
+ import { format } from "prettier";
6
+
7
+ //#region src/Examples.ts
8
+ /**
9
+ * Prettier could not format an example.
10
+ *
11
+ * @remarks
12
+ * Malformed example code is author input, so this is a typed failure rather
13
+ * than a defect. The original Prettier error rides in `cause` rather than
14
+ * being flattened to a string; an adapter that reports it (the RSPress
15
+ * `PrettierError` event) reads the message from there and falls back to the
16
+ * unformatted code.
17
+ *
18
+ * @public
19
+ */
20
+ var ExampleFormatError = class extends Schema.TaggedError("ExampleFormatError")("ExampleFormatError", {
21
+ /** The fence language the parser was chosen for. */
22
+ language: Schema.String,
23
+ /** The original Prettier error. */
24
+ cause: Schema.Defect()
25
+ }) {
26
+ get message() {
27
+ return `Prettier could not format a ${this.language} example`;
28
+ }
29
+ };
30
+ const TYPESCRIPT_LANGUAGES = /* @__PURE__ */ new Set([
31
+ "typescript",
32
+ "ts",
33
+ "javascript",
34
+ "js"
35
+ ]);
36
+ /**
37
+ * Prepare an example for Twoslash: prepend `import { name } from "pkg"`
38
+ * unless the example already imports the package, and prepend `// @noErrors`
39
+ * when errors are suppressed. Non-TypeScript examples pass through untouched.
40
+ *
41
+ * @param example - The example with language and code
42
+ * @param apiItemName - The documented item, imported at the top of the example
43
+ * @param packageName - The package to import it from
44
+ * @param suppressErrors - Whether to suppress TypeScript errors (default `true`)
45
+ * @public
46
+ */
47
+ function prepareExampleCode(example, apiItemName, packageName, suppressErrors = true) {
48
+ const { language, code } = example;
49
+ if (!TYPESCRIPT_LANGUAGES.has(language)) return {
50
+ code,
51
+ isTypeScript: false,
52
+ language
53
+ };
54
+ const importLine = `import { ${apiItemName} } from "${packageName}";`;
55
+ const finalCode = code.includes(`from "${packageName}"`) || code.includes(`from '${packageName}'`) ? code : `${importLine}\n${code}`;
56
+ return {
57
+ code: `${suppressErrors ? "// @noErrors\n" : ""}${finalCode}`,
58
+ isTypeScript: true,
59
+ language: "typescript"
60
+ };
61
+ }
62
+ /**
63
+ * Strip Twoslash directives from code for display: config directives
64
+ * (`// @noErrors`, `// @errors: 2304`, `// @filename: …`), annotation markers
65
+ * (`// ^?`) and the cut directives — `// ---cut---` removes itself and every
66
+ * line before it, `// ---cut-after---` itself and every line after, and a
67
+ * `// ---cut-start---` / `// ---cut-end---` pair removes the range between.
68
+ *
69
+ * @param code - The code containing Twoslash directives
70
+ * @returns The code a reader sees and copies
71
+ * @public
72
+ */
73
+ function stripTwoslashDirectives(code) {
74
+ const lines = code.split("\n");
75
+ let cutBeforeIndex = -1;
76
+ let cutAfterIndex = -1;
77
+ const cutRanges = [];
78
+ const cutStartStack = [];
79
+ for (let i = 0; i < lines.length; i++) {
80
+ const cutType = classifyCutDirective(lines[i].trim());
81
+ if (cutType === "cut-before") cutBeforeIndex = i;
82
+ else if (cutType === "cut-after") cutAfterIndex = i;
83
+ else if (cutType === "cut-start") cutStartStack.push(i);
84
+ else if (cutType === "cut-end") {
85
+ const startIdx = cutStartStack.pop();
86
+ if (startIdx !== void 0) cutRanges.push([startIdx, i]);
87
+ }
88
+ }
89
+ let filteredLines = lines;
90
+ if (cutBeforeIndex >= 0) {
91
+ filteredLines = filteredLines.slice(cutBeforeIndex + 1);
92
+ if (cutAfterIndex >= 0) cutAfterIndex = cutAfterIndex - cutBeforeIndex - 1;
93
+ for (const range of cutRanges) {
94
+ range[0] -= cutBeforeIndex + 1;
95
+ range[1] -= cutBeforeIndex + 1;
96
+ }
97
+ }
98
+ if (cutAfterIndex >= 0) filteredLines = filteredLines.slice(0, cutAfterIndex);
99
+ const excludedLines = /* @__PURE__ */ new Set();
100
+ for (const [start, end] of cutRanges) for (let i = start; i <= end; i++) if (i >= 0 && i < filteredLines.length) excludedLines.add(i);
101
+ return filteredLines.filter((line, i) => !excludedLines.has(i) && !isTwoslashDirective(line.trim())).join("\n").trim();
102
+ }
103
+ /**
104
+ * Prepend hidden imports to code using the Twoslash cut directive, so the
105
+ * type-checker resolves external types while the reader never sees the
106
+ * import lines. Returns the code unchanged when there is nothing to import.
107
+ *
108
+ * @param code - The code to prepend imports to
109
+ * @param imports - The import statements to add
110
+ * @public
111
+ */
112
+ function prependHiddenImports(code, imports) {
113
+ if (imports.length === 0) return code;
114
+ return `${TypeReferenceExtractor.formatImports([...imports]).join("\n")}\n// ---cut---\n${code}`;
115
+ }
116
+ /**
117
+ * Build both spellings of a code block from its type-check text: the
118
+ * `source` as given, the `display` with every directive stripped.
119
+ *
120
+ * @param source - The type-check text — hidden imports, cut marker, directives intact
121
+ * @public
122
+ */
123
+ function codeText(source) {
124
+ return CodeText.make({
125
+ display: stripTwoslashDirectives(source),
126
+ source
127
+ });
128
+ }
129
+ /**
130
+ * Add logical blank lines between code sections for visual clarity: after an
131
+ * import block, before a section comment and before a `return`.
132
+ *
133
+ * @remarks
134
+ * Runs after Prettier, which does not insert breathing room of its own. Lines
135
+ * inside a multi-line import and Twoslash directive lines never trigger a
136
+ * rule, so a directive stays attached to the line it annotates.
137
+ *
138
+ * @param code - Prettier-formatted code
139
+ * @public
140
+ */
141
+ function addLogicalBlankLines(code) {
142
+ const lines = code.split("\n");
143
+ const result = [];
144
+ let inMultiLineImport = false;
145
+ for (const line of lines) {
146
+ const trimmed = line.trim();
147
+ const wasInMultiLineImport = inMultiLineImport;
148
+ if (!inMultiLineImport && trimmed.startsWith("import ") && !trimmed.endsWith(";")) inMultiLineImport = true;
149
+ else if (inMultiLineImport && trimmed.endsWith(";")) inMultiLineImport = false;
150
+ const isCurrentImport = trimmed.startsWith("import ") || wasInMultiLineImport;
151
+ if (result.length > 0 && !isCurrentImport) {
152
+ const prevTrimmed = result[result.length - 1].trim();
153
+ if (prevTrimmed !== "") {
154
+ const isDirective = isTwoslashDirective(trimmed);
155
+ const prevIsImportEnd = prevTrimmed.startsWith("import ") && prevTrimmed.endsWith(";") || /}\s*from\s+/.test(prevTrimmed) && prevTrimmed.endsWith(";");
156
+ if (prevIsImportEnd && trimmed !== "" && !isDirective) result.push("");
157
+ if (trimmed.startsWith("//") && !isDirective && !prevTrimmed.startsWith("//") && !prevIsImportEnd) result.push("");
158
+ if (/^return[\s;(]/.test(trimmed) && !prevTrimmed.startsWith("//")) result.push("");
159
+ }
160
+ }
161
+ result.push(line);
162
+ }
163
+ return result.join("\n");
164
+ }
165
+ /** Fence languages Prettier can format, mapped to its parser name. */
166
+ const LANGUAGE_TO_PARSER = {
167
+ typescript: "typescript",
168
+ ts: "typescript",
169
+ tsx: "typescript",
170
+ javascript: "babel",
171
+ js: "babel",
172
+ jsx: "babel",
173
+ node: "babel"
174
+ };
175
+ /** The one Prettier configuration every adapter formats examples with. */
176
+ const PRETTIER_OPTIONS = {
177
+ printWidth: 80,
178
+ tabWidth: 2,
179
+ useTabs: false,
180
+ semi: true,
181
+ singleQuote: false,
182
+ trailingComma: "es5",
183
+ bracketSpacing: true,
184
+ arrowParens: "always"
185
+ };
186
+ /**
187
+ * Format example code with Prettier, then add logical blank lines. A
188
+ * language Prettier has no parser for is returned unchanged.
189
+ *
190
+ * @param code - The code to format
191
+ * @param language - The fence language (`typescript`, `ts`, `js`, …)
192
+ * @public
193
+ */
194
+ const formatExampleCode = Effect.fn("Examples.formatExampleCode")(function* (code, language) {
195
+ const parser = LANGUAGE_TO_PARSER[language.toLowerCase()];
196
+ if (!parser) return code;
197
+ return addLogicalBlankLines((yield* Effect.tryPromise({
198
+ try: () => format(code, {
199
+ ...PRETTIER_OPTIONS,
200
+ parser
201
+ }),
202
+ catch: (cause) => new ExampleFormatError({
203
+ language,
204
+ cause
205
+ })
206
+ })).trim());
207
+ });
208
+ /**
209
+ * Build an {@link Example} block item from a raw TSDoc example: prepare it
210
+ * for Twoslash, format it, and produce both code spellings once.
211
+ *
212
+ * @remarks
213
+ * A non-TypeScript example is not type-checked, so its `display` and
214
+ * `source` are the same formatted text and emitters render it in a plain
215
+ * fence.
216
+ *
217
+ * @param example - The example with language and code
218
+ * @param apiItemName - The documented item, imported at the top of the example
219
+ * @param packageName - The package to import it from
220
+ * @param suppressErrors - Whether to suppress TypeScript errors (default `true`)
221
+ * @public
222
+ */
223
+ const buildExample = Effect.fn("Examples.buildExample")(function* (example, apiItemName, packageName, suppressErrors = true) {
224
+ const prepared = prepareExampleCode(example, apiItemName, packageName, suppressErrors);
225
+ const formatted = yield* formatExampleCode(prepared.code, prepared.language);
226
+ return Example.make({
227
+ language: prepared.language,
228
+ code: prepared.isTypeScript ? codeText(formatted) : CodeText.make({
229
+ display: formatted,
230
+ source: formatted
231
+ }),
232
+ typeChecked: prepared.isTypeScript
233
+ });
234
+ });
235
+
236
+ //#endregion
237
+ export { ExampleFormatError, addLogicalBlankLines, buildExample, codeText, formatExampleCode, prepareExampleCode, prependHiddenImports, stripTwoslashDirectives };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C. Spencer Beggs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Llms.js ADDED
@@ -0,0 +1,282 @@
1
+ //#region src/Llms.ts
2
+ /** Pre-compiled regex for parsing llms.txt link lines. */
3
+ const LLMS_TXT_LINE_RE = /^-\s+\[([^\]]+)\]\(([^)]+)\)(?::\s*(.+))?$/;
4
+ /**
5
+ * Parse a single line from llms.txt format.
6
+ *
7
+ * Recognizes the pattern: `- [title](url): description`
8
+ * The description portion (`: description`) is optional.
9
+ *
10
+ * @param line - A single line from an llms.txt file
11
+ * @returns Parsed entry or null for non-link lines (headers, empty lines, plain text)
12
+ *
13
+ * @public
14
+ */
15
+ function parseLlmsTxtLine(line) {
16
+ const trimmed = line.trim();
17
+ if (trimmed === "") return null;
18
+ const match = LLMS_TXT_LINE_RE.exec(trimmed);
19
+ if (!match) return null;
20
+ const title = match[1];
21
+ const url = match[2];
22
+ const rawDescription = match[3];
23
+ return {
24
+ title,
25
+ url,
26
+ description: rawDescription ? rawDescription.trim() : void 0
27
+ };
28
+ }
29
+ /**
30
+ * Filter API page entries from global llms.txt content.
31
+ *
32
+ * Removes lines whose parsed URL is in the `apiRoutes` set.
33
+ * Appends pointer lines for per-package llms files when `pointers` is non-empty.
34
+ *
35
+ * @param content - Full llms.txt content string
36
+ * @param apiRoutes - Set of API route paths to remove
37
+ * @param pointers - Per-package pointer entries to append
38
+ * @returns Filtered llms.txt content
39
+ *
40
+ * @public
41
+ */
42
+ function filterLlmsTxt(content, apiRoutes, pointers) {
43
+ const lines = content.split("\n");
44
+ const filtered = [];
45
+ for (const line of lines) {
46
+ const entry = parseLlmsTxtLine(line);
47
+ if (entry && apiRoutes.has(entry.url)) continue;
48
+ filtered.push(line);
49
+ }
50
+ let result = filtered.join("\n");
51
+ if (pointers.length > 0) {
52
+ result += "\n\n";
53
+ for (const pointer of pointers) result += `- For ${pointer.name} API docs, see [${pointer.name} llms.txt](${pointer.llmsTxtUrl})\n`;
54
+ }
55
+ return result;
56
+ }
57
+ /**
58
+ * Generate a structured global llms.txt that groups pages by package scope.
59
+ *
60
+ * Output format:
61
+ * ```
62
+ * # {site title}
63
+ *
64
+ * ## Others
65
+ * - [Blog Post](/blog/post.md)
66
+ *
67
+ * ## Packages
68
+ *
69
+ * ### {name} {version}
70
+ * {description}
71
+ * - [Guide Page](/pkg/guides/guide.md)
72
+ * - [API Reference](/pkg/llms-api.txt)
73
+ * ```
74
+ *
75
+ * @param content - Original RSPress-generated llms.txt content
76
+ * @param apiRoutes - Set of API route paths to exclude as individual entries
77
+ * @param packages - Package scope metadata
78
+ * @returns Restructured llms.txt content
79
+ *
80
+ * @public
81
+ */
82
+ function generateStructuredLlmsTxt(content, apiRoutes, packages) {
83
+ const lines = content.split("\n");
84
+ let title = "";
85
+ for (const line of lines) if (line.startsWith("# ")) {
86
+ title = line;
87
+ break;
88
+ }
89
+ const allEntries = [];
90
+ for (const line of lines) {
91
+ const entry = parseLlmsTxtLine(line);
92
+ if (entry && !apiRoutes.has(entry.url)) allEntries.push(entry);
93
+ }
94
+ const packageEntries = /* @__PURE__ */ new Map();
95
+ const others = [];
96
+ for (const entry of allEntries) {
97
+ let matched = false;
98
+ for (const pkg of packages) {
99
+ const base = pkg.packageRoute.endsWith("/") ? pkg.packageRoute : `${pkg.packageRoute}/`;
100
+ if (entry.url.startsWith(base) || entry.url === pkg.packageRoute) {
101
+ const existing = packageEntries.get(pkg.packageName) ?? [];
102
+ existing.push(entry);
103
+ packageEntries.set(pkg.packageName, existing);
104
+ matched = true;
105
+ break;
106
+ }
107
+ }
108
+ if (!matched) others.push(entry);
109
+ }
110
+ const output = [];
111
+ if (title) {
112
+ output.push(title);
113
+ output.push("");
114
+ }
115
+ if (others.length > 0) {
116
+ output.push("## Others");
117
+ output.push("");
118
+ for (const entry of others) output.push(formatEntry(entry));
119
+ output.push("");
120
+ }
121
+ const packagesWithEntries = packages;
122
+ if (packagesWithEntries.length > 0) {
123
+ output.push("## Packages");
124
+ output.push("");
125
+ for (const pkg of packagesWithEntries) {
126
+ const versionSuffix = pkg.version ? ` ${pkg.version}` : "";
127
+ output.push(`### ${pkg.name}${versionSuffix}`);
128
+ output.push("");
129
+ if (pkg.description) {
130
+ output.push(pkg.description);
131
+ output.push("");
132
+ }
133
+ const entries = packageEntries.get(pkg.packageName) ?? [];
134
+ for (const entry of entries) output.push(formatEntry(entry));
135
+ output.push(`- [API Reference](${pkg.llmsApiTxtUrl})`);
136
+ output.push("");
137
+ }
138
+ }
139
+ return output.join("\n");
140
+ }
141
+ /**
142
+ * Parse llms-full.txt content into sections delimited by frontmatter blocks.
143
+ *
144
+ * Each section has the format:
145
+ * ```
146
+ * ---
147
+ * url: /path/to/page
148
+ * ---
149
+ *
150
+ * Content here...
151
+ * ```
152
+ */
153
+ function parseSections(content) {
154
+ if (content.trim() === "") return [];
155
+ const sections = [];
156
+ const frontmatterPattern = /^---\nurl:\s*(.+)\n---$/gm;
157
+ let match = frontmatterPattern.exec(content);
158
+ const boundaries = [];
159
+ while (match !== null) {
160
+ boundaries.push({
161
+ url: match[1].trim(),
162
+ start: match.index,
163
+ fmEnd: match.index + match[0].length
164
+ });
165
+ match = frontmatterPattern.exec(content);
166
+ }
167
+ for (let i = 0; i < boundaries.length; i++) {
168
+ const boundary = boundaries[i];
169
+ const nextStart = i + 1 < boundaries.length ? boundaries[i + 1].start : content.length;
170
+ const sectionContent = content.slice(boundary.start, nextStart);
171
+ sections.push({
172
+ url: boundary.url,
173
+ raw: sectionContent.trimEnd()
174
+ });
175
+ }
176
+ return sections;
177
+ }
178
+ /**
179
+ * Filter API page content sections from global llms-full.txt.
180
+ *
181
+ * Sections are delimited by `---\nurl: {path}\n---` frontmatter blocks.
182
+ * Removes entire sections whose URL matches a known API route.
183
+ *
184
+ * @param content - Full llms-full.txt content string
185
+ * @param apiRoutes - Set of API route paths to remove
186
+ * @returns Filtered llms-full.txt content
187
+ *
188
+ * @public
189
+ */
190
+ function filterLlmsFullTxt(content, apiRoutes) {
191
+ if (content.trim() === "") return "";
192
+ const kept = parseSections(content).filter((section) => !apiRoutes.has(section.url));
193
+ if (kept.length === 0) return "";
194
+ return kept.map((section) => section.raw).join("\n\n\n");
195
+ }
196
+ /**
197
+ * Format a single llms.txt link entry.
198
+ */
199
+ function formatEntry(entry) {
200
+ if (entry.description) return `- [${entry.title}](${entry.url}): ${entry.description}`;
201
+ return `- [${entry.title}](${entry.url})`;
202
+ }
203
+ /**
204
+ * Generate a per-package llms.txt index.
205
+ *
206
+ * Output format:
207
+ * ```
208
+ * # {name}
209
+ *
210
+ * ## Guides
211
+ *
212
+ * - [Guide Title](/path): Description
213
+ *
214
+ * ## API Reference
215
+ *
216
+ * - [ApiItem](/path): Description
217
+ * ```
218
+ *
219
+ * Sections with no entries are omitted.
220
+ *
221
+ * @param input - Package name, guide pages, and API pages
222
+ * @returns Generated llms.txt content
223
+ *
224
+ * @public
225
+ */
226
+ function generatePackageLlmsTxt(input) {
227
+ const parts = [
228
+ `# ${input.name}`,
229
+ "",
230
+ `> API documentation for the ${input.packageName} package`
231
+ ];
232
+ if (input.guidePages.length > 0) {
233
+ parts.push("");
234
+ parts.push("## Guides");
235
+ parts.push("");
236
+ for (const page of input.guidePages) parts.push(formatEntry(page));
237
+ }
238
+ if (input.apiPages.length > 0) {
239
+ parts.push("");
240
+ parts.push("## API Reference");
241
+ parts.push("");
242
+ for (const page of input.apiPages) parts.push(formatEntry(page));
243
+ }
244
+ parts.push("");
245
+ return parts.join("\n");
246
+ }
247
+ /**
248
+ * Concatenate page contents with frontmatter delimiters.
249
+ *
250
+ * Used for llms-full.txt, llms-docs.txt, and llms-api.txt generation
251
+ * (pass different page sets for each).
252
+ *
253
+ * Output format:
254
+ * ```
255
+ * ---
256
+ * url: /path/to/page
257
+ * ---
258
+ *
259
+ * Content here...
260
+ *
261
+ *
262
+ * ---
263
+ * url: /path/to/next
264
+ * ---
265
+ *
266
+ * More content...
267
+ * ```
268
+ *
269
+ * @param pages - Array of page URLs and their markdown content
270
+ * @returns Concatenated content with frontmatter delimiters
271
+ *
272
+ * @public
273
+ */
274
+ function generatePackageLlmsFullTxt(pages) {
275
+ if (pages.length === 0) return "";
276
+ const sections = [];
277
+ for (const page of pages) sections.push(`---\nurl: ${page.url}\n---\n\n${page.content}`);
278
+ return sections.join("\n\n\n");
279
+ }
280
+
281
+ //#endregion
282
+ export { filterLlmsFullTxt, filterLlmsTxt, generatePackageLlmsFullTxt, generatePackageLlmsTxt, generateStructuredLlmsTxt, parseLlmsTxtLine };
package/Markdown.js ADDED
@@ -0,0 +1,155 @@
1
+ import { Blockquote, Code, Heading, InlineCode, Link, List, ListItem, Markdown, Paragraph, Root, Strong, Table, TableCell, TableRow, Text } from "@effected/markdown";
2
+ import { Effect, Result } from "effect";
3
+
4
+ //#region src/Markdown.ts
5
+ const text = (value) => Text.make({ value });
6
+ const heading = (depth, value) => Heading.make({
7
+ depth,
8
+ children: [text(value)]
9
+ });
10
+ const paragraph = (children) => Paragraph.make({ children: [...children] });
11
+ const fence = (value, lang) => Code.make({
12
+ value,
13
+ lang
14
+ });
15
+ const cell = (children) => TableCell.make({ children: [...children] });
16
+ const row = (cells) => TableRow.make({ children: [...cells] });
17
+ const parametersTable = (rows) => Table.make({ children: [row([
18
+ cell([text("Name")]),
19
+ cell([text("Type")]),
20
+ cell([text("Description")])
21
+ ]), ...rows.map((r) => row([
22
+ cell([InlineCode.make({ value: r.name })]),
23
+ cell(r.type === void 0 ? [] : [InlineCode.make({ value: r.type })]),
24
+ cell(r.description)
25
+ ]))] });
26
+ const enumMembersTable = (rows) => Table.make({ children: [row([
27
+ cell([text("Name")]),
28
+ cell([text("Value")]),
29
+ cell([text("Description")])
30
+ ]), ...rows.map((r) => row([
31
+ cell([InlineCode.make({ value: r.name })]),
32
+ cell(r.value === void 0 ? [] : [InlineCode.make({ value: r.value })]),
33
+ cell(r.description)
34
+ ]))] });
35
+ const memberNodes = (member) => {
36
+ const nodes = [heading(3, member.name), fence(member.code.display, "ts")];
37
+ if (member.summary && member.summary.length > 0) nodes.push(paragraph(member.summary));
38
+ if (member.parameters && member.parameters.length > 0) nodes.push(parametersTable(member.parameters));
39
+ if (member.returns && member.returns.length > 0) nodes.push(paragraph([
40
+ Strong.make({ children: [text("Returns:")] }),
41
+ text(" "),
42
+ ...member.returns
43
+ ]));
44
+ return nodes;
45
+ };
46
+ /**
47
+ * Render one block to flow nodes.
48
+ *
49
+ * @public
50
+ */
51
+ function markdownBlockTree(block) {
52
+ switch (block.kind) {
53
+ case "title": {
54
+ const nodes = [heading(1, block.name)];
55
+ if (block.deprecation && block.deprecation.length > 0) nodes.push(Blockquote.make({ children: [paragraph([
56
+ Strong.make({ children: [text("Deprecated:")] }),
57
+ text(" "),
58
+ ...block.deprecation
59
+ ])] }));
60
+ if (block.releaseTag !== "Public") nodes.push(paragraph([InlineCode.make({ value: block.releaseTag })]));
61
+ return nodes;
62
+ }
63
+ case "available-from": {
64
+ const children = [text("Available from: ")];
65
+ block.entryPoints.forEach((entryPoint, index) => {
66
+ if (index > 0) children.push(text(", "));
67
+ const spec = entryPoint === "default" ? block.packageName : `${block.packageName}/${entryPoint}`;
68
+ children.push(InlineCode.make({ value: spec }));
69
+ });
70
+ return [paragraph(children)];
71
+ }
72
+ case "prose": return block.role === "summary" ? block.content : [heading(2, block.role === "remarks" ? "Remarks" : "Returns"), ...block.content];
73
+ case "source-link": return [paragraph([Link.make({
74
+ url: block.href,
75
+ children: [text("Source")]
76
+ })])];
77
+ case "signature": return [fence(block.code.display, "ts")];
78
+ case "base-class": return [
79
+ heading(2, "Base Class"),
80
+ paragraph([
81
+ InlineCode.make({ value: block.className }),
82
+ text(" extends "),
83
+ InlineCode.make({ value: block.baseName }),
84
+ text(", a compiler-generated declaration that is not exported from "),
85
+ InlineCode.make({ value: block.packageName }),
86
+ text(".")
87
+ ]),
88
+ fence(block.code.display, "ts")
89
+ ];
90
+ case "member-group": return [heading(2, block.title), ...block.members.flatMap(memberNodes)];
91
+ case "parameters": return [parametersTable(block.rows)];
92
+ case "enum-members": return [enumMembersTable(block.rows)];
93
+ case "examples": return [heading(2, "Examples"), ...block.items.map((item) => fence(item.code.display, item.language))];
94
+ case "see-also": return [heading(2, "See Also"), List.make({
95
+ ordered: false,
96
+ spread: false,
97
+ children: block.references.map((reference) => ListItem.make({
98
+ spread: false,
99
+ children: [paragraph(reference)]
100
+ }))
101
+ })];
102
+ case "member-index": return [heading(2, block.title), List.make({
103
+ ordered: false,
104
+ spread: false,
105
+ children: block.entries.map((entry) => {
106
+ const children = [Link.make({
107
+ url: entry.route,
108
+ children: [text(entry.name)]
109
+ })];
110
+ if (entry.summary && entry.summary.length > 0) children.push(text(" - "), ...entry.summary);
111
+ return ListItem.make({
112
+ spread: false,
113
+ children: [paragraph(children)]
114
+ });
115
+ })
116
+ })];
117
+ }
118
+ }
119
+ /**
120
+ * Render a page's body to flow nodes — the pre-serialization form of
121
+ * {@link renderMarkdownResult}.
122
+ *
123
+ * @public
124
+ */
125
+ function markdownTree(page) {
126
+ return page.blocks.flatMap(markdownBlockTree);
127
+ }
128
+ /**
129
+ * Render a page's body to a markdown string. No frontmatter: that is the
130
+ * adapter's, built from the page facts.
131
+ *
132
+ * @remarks
133
+ * A stringify failure on a tree this module built itself is surfaced rather
134
+ * than thrown, because the prose inside a block arrived from a builder and
135
+ * may carry any node the kit admits; the kit's own error names what it could
136
+ * not serialize.
137
+ *
138
+ * @public
139
+ */
140
+ function renderMarkdownResult(page) {
141
+ const root = Root.make({ children: [...markdownTree(page)] });
142
+ return Result.map(Markdown.stringifyResult(root), (markdown) => {
143
+ const trimmed = markdown.trim();
144
+ return trimmed ? `${trimmed}\n` : "\n";
145
+ });
146
+ }
147
+ /**
148
+ * The Effect form of {@link renderMarkdownResult}.
149
+ *
150
+ * @public
151
+ */
152
+ const renderMarkdown = Effect.fn("Markdown.renderMarkdown")((page) => Effect.fromResult(renderMarkdownResult(page)));
153
+
154
+ //#endregion
155
+ export { markdownBlockTree, markdownTree, renderMarkdown, renderMarkdownResult };