@mintlify/scraping 4.0.906 → 4.0.908

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 (43) hide show
  1. package/__test__/sdk-docfx.test.ts +204 -0
  2. package/__test__/sdk-javadoc.test.ts +150 -0
  3. package/__test__/sdk-phpdoc.test.ts +134 -0
  4. package/__test__/sdk-sphinx.test.ts +84 -0
  5. package/__test__/sdk-typedoc.test.ts +138 -0
  6. package/bin/index.d.ts +3 -0
  7. package/bin/index.js +2 -0
  8. package/bin/index.js.map +1 -1
  9. package/bin/sdk/convert.d.ts +6 -0
  10. package/bin/sdk/convert.js +90 -0
  11. package/bin/sdk/convert.js.map +1 -0
  12. package/bin/sdk/converters/docfx.d.ts +2 -0
  13. package/bin/sdk/converters/docfx.js +402 -0
  14. package/bin/sdk/converters/docfx.js.map +1 -0
  15. package/bin/sdk/converters/javadoc.d.ts +2 -0
  16. package/bin/sdk/converters/javadoc.js +318 -0
  17. package/bin/sdk/converters/javadoc.js.map +1 -0
  18. package/bin/sdk/converters/phpdoc.d.ts +2 -0
  19. package/bin/sdk/converters/phpdoc.js +351 -0
  20. package/bin/sdk/converters/phpdoc.js.map +1 -0
  21. package/bin/sdk/converters/sphinx.d.ts +2 -0
  22. package/bin/sdk/converters/sphinx.js +267 -0
  23. package/bin/sdk/converters/sphinx.js.map +1 -0
  24. package/bin/sdk/converters/typedoc.d.ts +2 -0
  25. package/bin/sdk/converters/typedoc.js +318 -0
  26. package/bin/sdk/converters/typedoc.js.map +1 -0
  27. package/bin/sdk/generateSdkReference.d.ts +2 -0
  28. package/bin/sdk/generateSdkReference.js +20 -0
  29. package/bin/sdk/generateSdkReference.js.map +1 -0
  30. package/bin/sdk/types.d.ts +19 -0
  31. package/bin/sdk/types.js +2 -0
  32. package/bin/sdk/types.js.map +1 -0
  33. package/bin/tsconfig.build.tsbuildinfo +1 -1
  34. package/package.json +5 -4
  35. package/src/index.ts +3 -0
  36. package/src/sdk/convert.ts +101 -0
  37. package/src/sdk/converters/docfx.ts +491 -0
  38. package/src/sdk/converters/javadoc.ts +330 -0
  39. package/src/sdk/converters/phpdoc.ts +446 -0
  40. package/src/sdk/converters/sphinx.ts +277 -0
  41. package/src/sdk/converters/typedoc.ts +423 -0
  42. package/src/sdk/generateSdkReference.ts +25 -0
  43. package/src/sdk/types.ts +24 -0
@@ -0,0 +1,277 @@
1
+ import fse from 'fs-extra';
2
+ import type { Element, ElementContent, Root as HastRoot } from 'hast';
3
+ import path from 'node:path';
4
+ import { visit, SKIP } from 'unist-util-visit';
5
+
6
+ import { htmlToHast } from '../../pipeline/root.js';
7
+ import { turnChildrenIntoMdx } from '../../utils/children.js';
8
+ import { removeHastComments } from '../../utils/hastComments.js';
9
+ import { hastToMdx } from '../convert.js';
10
+ import type { SdkNavGroup, SdkPage, SdkReference } from '../types.js';
11
+
12
+ const SKIP_PAGES = new Set(['genindex', 'search', 'py-modindex']);
13
+ const LANG_MARKER = '@@mintlang:';
14
+ const FENCE_MARKER = /(`{3,})[^\S\n]*\n[> ]*@@mintlang:([\w+-]+)@@[^\S\n]*\n/g;
15
+ const ADMONITION_TAGS: Record<string, string> = {
16
+ note: 'Note',
17
+ hint: 'Tip',
18
+ tip: 'Tip',
19
+ important: 'Info',
20
+ seealso: 'Info',
21
+ warning: 'Warning',
22
+ caution: 'Warning',
23
+ danger: 'Warning',
24
+ error: 'Warning',
25
+ attention: 'Warning',
26
+ };
27
+
28
+ type SphinxDoc = { slug: string; title: string; body: string; tag?: string };
29
+
30
+ export async function convertSphinx(sourcePath: string): Promise<SdkReference> {
31
+ const docs = await loadDocs(sourcePath);
32
+ const slugs = new Set(docs.map((doc) => doc.slug));
33
+
34
+ const guides = docs
35
+ .filter((doc) => !doc.tag)
36
+ .sort((left, right) =>
37
+ left.slug === 'index' ? -1 : right.slug === 'index' ? 1 : left.slug.localeCompare(right.slug)
38
+ );
39
+ const modules = docs
40
+ .filter((doc) => doc.tag)
41
+ .sort((left, right) => left.slug.localeCompare(right.slug));
42
+
43
+ const pages = [...guides, ...modules].map((doc) => renderPage(doc, slugs));
44
+ const groups: SdkNavGroup[] = [];
45
+ if (guides.length) groups.push({ group: 'Getting Started', pages: guides.map((d) => d.slug) });
46
+ if (modules.length) groups.push({ group: 'API Reference', pages: modules.map((d) => d.slug) });
47
+
48
+ return { pages, groups };
49
+ }
50
+
51
+ async function loadDocs(sourcePath: string): Promise<SphinxDoc[]> {
52
+ const docs: SphinxDoc[] = [];
53
+ for (const file of await collectFjsonFiles(sourcePath)) {
54
+ const raw = (await fse.readJson(file)) as {
55
+ body?: string;
56
+ title?: string;
57
+ current_page_name?: string;
58
+ };
59
+ const slug =
60
+ raw.current_page_name ??
61
+ path
62
+ .relative(sourcePath, file)
63
+ .split(path.sep)
64
+ .join('/')
65
+ .replace(/\.fjson$/, '');
66
+ if (SKIP_PAGES.has(slug) || slug.startsWith('_modules')) continue;
67
+ if (!raw.body?.trim()) continue;
68
+ docs.push({
69
+ slug,
70
+ title: cleanTitle(raw.title ?? slug),
71
+ body: raw.body,
72
+ tag: /<dl class="py[ "]/.test(raw.body) ? 'MODULE' : undefined,
73
+ });
74
+ }
75
+ return docs;
76
+ }
77
+
78
+ async function collectFjsonFiles(dir: string): Promise<string[]> {
79
+ const files: string[] = [];
80
+ for (const entry of await fse.readdir(dir, { withFileTypes: true })) {
81
+ if (entry.name.startsWith('_')) continue;
82
+ const fullPath = path.join(dir, entry.name);
83
+ if (entry.isDirectory()) files.push(...(await collectFjsonFiles(fullPath)));
84
+ else if (entry.name.endsWith('.fjson')) files.push(fullPath);
85
+ }
86
+ return files.sort();
87
+ }
88
+
89
+ function renderPage(doc: SphinxDoc, slugs: Set<string>): SdkPage {
90
+ const hast = htmlToHast(doc.body);
91
+ removeHastComments(hast);
92
+ removeSphinxChrome(hast);
93
+ const description = extractDescription(hast);
94
+ rewriteLinks(hast, doc.slug, slugs);
95
+ transformAdmonitions(hast);
96
+ markHighlightLanguages(hast);
97
+ transformPyDls(hast, 0);
98
+ const content = hastToMdx(hast)
99
+ .replace(FENCE_MARKER, '$1$2\n')
100
+ .replace(/^[> ]*@@mintlang:[\w+-]+@@\n?/gm, '');
101
+ return {
102
+ slug: doc.slug,
103
+ title: doc.title,
104
+ description,
105
+ tag: doc.tag,
106
+ content,
107
+ };
108
+ }
109
+
110
+ function removeSphinxChrome(hast: HastRoot): void {
111
+ let removedH1 = false;
112
+ visit(hast, 'element', (node: Element, index, parent) => {
113
+ if (!parent || typeof index !== 'number') return;
114
+ const isHeaderlink = node.tagName === 'a' && classList(node).includes('headerlink');
115
+ const isViewcode =
116
+ node.tagName === 'a' &&
117
+ node.children.some(
118
+ (child) => child.type === 'element' && classList(child).includes('viewcode-link')
119
+ );
120
+ const isFirstH1 = node.tagName === 'h1' && !removedH1;
121
+ if (isFirstH1) removedH1 = true;
122
+ if (isHeaderlink || isViewcode || isFirstH1) {
123
+ parent.children.splice(index, 1);
124
+ return index;
125
+ }
126
+ });
127
+ }
128
+
129
+ function rewriteLinks(hast: HastRoot, fromSlug: string, slugs: Set<string>): void {
130
+ visit(hast, 'element', (node: Element) => {
131
+ if (node.tagName !== 'a' || typeof node.properties.href !== 'string') return;
132
+ const rewritten = rewriteHref(node.properties.href, fromSlug, slugs);
133
+ if (rewritten === undefined) delete node.properties.href;
134
+ else node.properties.href = rewritten;
135
+ });
136
+ }
137
+
138
+ function rewriteHref(href: string, fromSlug: string, slugs: Set<string>): string | undefined {
139
+ if (/^(https?:|mailto:|#)/.test(href)) return href;
140
+ const [target = ''] = href.split('#');
141
+ let resolved = path.posix
142
+ .normalize(path.posix.join(path.posix.dirname(fromSlug), target))
143
+ .replace(/\/$/, '')
144
+ .replace(/\.html?$/, '');
145
+ if (resolved === '.' || resolved === '') resolved = 'index';
146
+ return slugs.has(resolved) ? `/${resolved}` : undefined;
147
+ }
148
+
149
+ function transformAdmonitions(hast: HastRoot): void {
150
+ visit(hast, 'element', (node: Element) => {
151
+ if (node.tagName !== 'div' || !classList(node).includes('admonition')) return;
152
+ const type = classList(node).find((name) => name in ADMONITION_TAGS);
153
+ const children = node.children.filter(
154
+ (child) => !(child.type === 'element' && classList(child).includes('admonition-title'))
155
+ );
156
+ node.tagName = ADMONITION_TAGS[type ?? ''] ?? 'Note';
157
+ node.properties = {};
158
+ node.children = turnChildrenIntoMdx(children) as ElementContent[];
159
+ return SKIP;
160
+ });
161
+ }
162
+
163
+ function markHighlightLanguages(hast: HastRoot): void {
164
+ visit(hast, 'element', (node: Element) => {
165
+ const lang = classList(node)
166
+ .find((name) => name.startsWith('highlight-'))
167
+ ?.slice('highlight-'.length);
168
+ if (!lang || lang === 'default' || lang === 'none') return;
169
+ visit(node, 'element', (inner: Element) => {
170
+ if (inner.tagName !== 'pre') return;
171
+ inner.children.unshift({ type: 'text', value: `${LANG_MARKER}${lang}@@\n` });
172
+ return SKIP;
173
+ });
174
+ return SKIP;
175
+ });
176
+ }
177
+
178
+ function transformPyDls(node: HastRoot | Element, depth: number): void {
179
+ const children: ElementContent[] = [];
180
+ for (const child of node.children as ElementContent[]) {
181
+ if (child.type === 'element' && child.tagName === 'dl' && classList(child).includes('py')) {
182
+ children.push(...explodePyDl(child, depth));
183
+ } else {
184
+ if (child.type === 'element') transformPyDls(child, depth);
185
+ children.push(child);
186
+ }
187
+ }
188
+ node.children = children;
189
+ }
190
+
191
+ function explodePyDl(dl: Element, depth: number): ElementContent[] {
192
+ const callable = classList(dl).some((name) => ['method', 'function'].includes(name));
193
+ const out: ElementContent[] = [];
194
+ for (const child of dl.children) {
195
+ if (child.type !== 'element') continue;
196
+ if (child.tagName === 'dt' && classList(child).includes('sig')) {
197
+ const name = findDescName(child);
198
+ if (name) out.push(heading(Math.min(3 + depth, 6), callable ? `${name}()` : name));
199
+ out.push(pythonCodeBlock(textOf(child).replace(/\s+/g, ' ').trim()));
200
+ } else if (child.tagName === 'dd') {
201
+ transformPyDls(child, depth + 1);
202
+ out.push(...child.children);
203
+ }
204
+ }
205
+ return out;
206
+ }
207
+
208
+ function findDescName(node: Element): string | undefined {
209
+ let name: string | undefined;
210
+ visit(node, 'element', (inner: Element) => {
211
+ if (!classList(inner).includes('descname')) return;
212
+ name = textOf(inner).trim();
213
+ return SKIP;
214
+ });
215
+ return name;
216
+ }
217
+
218
+ function heading(level: number, text: string): Element {
219
+ return {
220
+ type: 'element',
221
+ tagName: `h${level}`,
222
+ properties: {},
223
+ children: [{ type: 'text', value: text }],
224
+ };
225
+ }
226
+
227
+ function pythonCodeBlock(code: string): Element {
228
+ return {
229
+ type: 'element',
230
+ tagName: 'pre',
231
+ properties: {},
232
+ children: [{ type: 'text', value: `${LANG_MARKER}python@@\n${code}` }],
233
+ };
234
+ }
235
+
236
+ function extractDescription(hast: HastRoot): string | undefined {
237
+ let description: string | undefined;
238
+ visit(hast, 'element', (node: Element, _index, parent) => {
239
+ if (description || node.tagName !== 'p') return;
240
+ if (parent && parent.type === 'element' && parent.tagName === 'li') return SKIP;
241
+ const text = textOf(node).replace(/\s+/g, ' ').trim();
242
+ if (!text || text.startsWith('Bases:')) return;
243
+ const sentence = text.split(/(?<=\.)\s/)[0] ?? text;
244
+ description = sentence.length > 160 ? `${sentence.slice(0, 159)}…` : sentence;
245
+ });
246
+ return description;
247
+ }
248
+
249
+ function textOf(node: ElementContent): string {
250
+ if (node.type === 'text') return node.value;
251
+ if ('children' in node) return node.children.map(textOf).join('');
252
+ return '';
253
+ }
254
+
255
+ function classList(node: Element): string[] {
256
+ const value = node.properties.className;
257
+ return Array.isArray(value)
258
+ ? value.map(String)
259
+ : typeof value === 'string'
260
+ ? value.split(' ')
261
+ : [];
262
+ }
263
+
264
+ function cleanTitle(title: string): string {
265
+ return title
266
+ .replace(/<[^>]+>/g, '')
267
+ .replace(/&amp;/g, '&')
268
+ .replace(/&lt;/g, '<')
269
+ .replace(/&gt;/g, '>')
270
+ .replace(/&quot;/g, '"')
271
+ .replace(/&#(\d+);/g, (_, code: string) => {
272
+ const codePoint = Number(code);
273
+ return codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : `&#${code};`;
274
+ })
275
+ .replace(/\s+/g, ' ')
276
+ .trim();
277
+ }
@@ -0,0 +1,423 @@
1
+ import fse from 'fs-extra';
2
+ import path from 'node:path';
3
+
4
+ import { markdownToMdx } from '../convert.js';
5
+ import type { SdkNavGroup, SdkPage, SdkReference } from '../types.js';
6
+
7
+ const KIND = {
8
+ project: 1,
9
+ module: 2,
10
+ namespace: 4,
11
+ enum: 8,
12
+ enumMember: 16,
13
+ variable: 32,
14
+ function: 64,
15
+ class: 128,
16
+ interface: 256,
17
+ constructor: 512,
18
+ property: 1024,
19
+ method: 2048,
20
+ callSignature: 4096,
21
+ indexSignature: 8192,
22
+ constructorSignature: 16384,
23
+ parameter: 32768,
24
+ typeLiteral: 65536,
25
+ accessor: 262144,
26
+ getSignature: 524288,
27
+ setSignature: 1048576,
28
+ typeAlias: 2097152,
29
+ } as const;
30
+
31
+ type CommentPart = { kind: string; text: string; target?: number | string };
32
+ type Comment = { summary?: CommentPart[]; blockTags?: { tag: string; content: CommentPart[] }[] };
33
+ type TypedocType = {
34
+ type?: string;
35
+ name?: string;
36
+ value?: unknown;
37
+ types?: TypedocType[];
38
+ elementType?: TypedocType;
39
+ typeArguments?: TypedocType[];
40
+ target?: number | TypedocType;
41
+ declaration?: TypedocNode;
42
+ elements?: TypedocType[];
43
+ checkType?: TypedocType;
44
+ extendsType?: TypedocType;
45
+ trueType?: TypedocType;
46
+ falseType?: TypedocType;
47
+ operator?: string;
48
+ indexType?: TypedocType;
49
+ objectType?: TypedocType;
50
+ qualifiedName?: string;
51
+ head?: string;
52
+ tail?: [TypedocType, string][];
53
+ };
54
+ type TypedocNode = {
55
+ id: number;
56
+ name: string;
57
+ kind: number;
58
+ comment?: Comment;
59
+ children?: TypedocNode[];
60
+ signatures?: TypedocNode[];
61
+ parameters?: TypedocNode[];
62
+ type?: TypedocType;
63
+ flags?: { isOptional?: boolean; isStatic?: boolean; isReadonly?: boolean; isPrivate?: boolean };
64
+ defaultValue?: string;
65
+ getSignature?: TypedocNode;
66
+ setSignature?: TypedocNode;
67
+ extendedTypes?: TypedocType[];
68
+ implementedTypes?: TypedocType[];
69
+ inheritedFrom?: { name: string };
70
+ };
71
+
72
+ const SECTIONS: { kind: number; group: string; dir: string; tag: string }[] = [
73
+ { kind: KIND.class, group: 'Classes', dir: 'classes', tag: 'CLASS' },
74
+ { kind: KIND.interface, group: 'Interfaces', dir: 'interfaces', tag: 'INTERFACE' },
75
+ { kind: KIND.enum, group: 'Enumerations', dir: 'enums', tag: 'ENUM' },
76
+ { kind: KIND.function, group: 'Functions', dir: 'functions', tag: 'FUNCTION' },
77
+ { kind: KIND.typeAlias, group: 'Type Aliases', dir: 'types', tag: 'TYPE' },
78
+ { kind: KIND.variable, group: 'Variables', dir: 'variables', tag: 'VARIABLE' },
79
+ ];
80
+
81
+ export async function convertTypedoc(sourcePath: string): Promise<SdkReference> {
82
+ const project = (await fse.readJson(sourcePath)) as TypedocNode;
83
+ const modules =
84
+ project.children?.some((child) => child.kind === KIND.module) === true
85
+ ? (project.children?.filter((child) => child.kind === KIND.module) ?? [])
86
+ : [project];
87
+ const multiModule = modules.length > 1;
88
+
89
+ const slugById = new Map<number, string>();
90
+ for (const module of modules) {
91
+ const prefix = multiModule ? slugify(module.name) : '';
92
+ for (const { node, namespacePath } of collectDeclarations(module)) {
93
+ const section = SECTIONS.find((entry) => entry.kind === node.kind);
94
+ if (!section) continue;
95
+ slugById.set(
96
+ node.id,
97
+ path.posix.join(prefix, ...namespacePath.map(slugify), section.dir, slugify(node.name))
98
+ );
99
+ }
100
+ }
101
+
102
+ const renderer = new TypedocRenderer(slugById);
103
+ const pages: SdkPage[] = [];
104
+ const groups: SdkNavGroup[] = [];
105
+
106
+ for (const module of modules) {
107
+ const prefix = multiModule ? `${module.name} ` : '';
108
+ const declarations = collectDeclarations(module);
109
+ for (const section of SECTIONS) {
110
+ const entries = declarations
111
+ .filter(({ node }) => node.kind === section.kind && !node.flags?.isPrivate)
112
+ .sort((left, right) => qualifiedName(left).localeCompare(qualifiedName(right)));
113
+ if (entries.length === 0) continue;
114
+ const sectionPages = entries.map((entry) =>
115
+ renderer.renderPage(entry.node, section, qualifiedName(entry))
116
+ );
117
+ pages.push(...sectionPages);
118
+ groups.push({ group: `${prefix}${section.group}`, pages: sectionPages.map((p) => p.slug) });
119
+ }
120
+ }
121
+
122
+ return { pages, groups };
123
+ }
124
+
125
+ type Declaration = { node: TypedocNode; namespacePath: string[] };
126
+
127
+ function qualifiedName({ node, namespacePath }: Declaration): string {
128
+ return [...namespacePath, node.name].join('.');
129
+ }
130
+
131
+ function collectDeclarations(container: TypedocNode, namespacePath: string[] = []): Declaration[] {
132
+ const declarations: Declaration[] = [];
133
+ for (const child of container.children ?? []) {
134
+ if (child.kind === KIND.namespace) {
135
+ declarations.push(...collectDeclarations(child, [...namespacePath, child.name]));
136
+ } else {
137
+ declarations.push({ node: child, namespacePath });
138
+ }
139
+ }
140
+ return declarations;
141
+ }
142
+
143
+ function slugify(name: string): string {
144
+ return name.replace(/[^a-zA-Z0-9-_.]+/g, '-').replace(/^-+|-+$/g, '') || 'item';
145
+ }
146
+
147
+ class TypedocRenderer {
148
+ constructor(private slugById: Map<number, string>) {}
149
+
150
+ renderPage(node: TypedocNode, section: { dir: string; tag: string }, title?: string): SdkPage {
151
+ const slug = this.slugById.get(node.id) ?? path.posix.join(section.dir, slugify(node.name));
152
+ const lines: string[] = [];
153
+ const summary = this.comment(node.comment);
154
+ if (summary) lines.push(summary);
155
+
156
+ switch (node.kind) {
157
+ case KIND.class:
158
+ case KIND.interface:
159
+ lines.push(...this.renderClassLike(node));
160
+ break;
161
+ case KIND.enum:
162
+ lines.push(...this.renderEnum(node));
163
+ break;
164
+ case KIND.function:
165
+ lines.push(...this.renderSignatures(node, 2));
166
+ break;
167
+ case KIND.typeAlias:
168
+ case KIND.variable:
169
+ lines.push(
170
+ this.codeBlock(
171
+ `${node.kind === KIND.variable ? 'const' : 'type'} ${node.name}${node.kind === KIND.typeAlias ? ' =' : ':'} ${this.type(node.type)}`
172
+ )
173
+ );
174
+ break;
175
+ }
176
+
177
+ return {
178
+ slug,
179
+ title: title ?? node.name,
180
+ description: this.firstSentence(node.comment),
181
+ tag: section.tag,
182
+ content: lines.filter(Boolean).join('\n\n'),
183
+ };
184
+ }
185
+
186
+ private renderClassLike(node: TypedocNode): string[] {
187
+ const lines: string[] = [];
188
+ const heritage = [
189
+ ...(node.extendedTypes?.length
190
+ ? [`extends ${node.extendedTypes.map((t) => this.type(t)).join(', ')}`]
191
+ : []),
192
+ ...(node.implementedTypes?.length
193
+ ? [`implements ${node.implementedTypes.map((t) => this.type(t)).join(', ')}`]
194
+ : []),
195
+ ];
196
+ if (heritage.length) {
197
+ lines.push(
198
+ this.codeBlock(
199
+ `${node.kind === KIND.class ? 'class' : 'interface'} ${node.name} ${heritage.join(' ')}`
200
+ )
201
+ );
202
+ }
203
+
204
+ const members = (node.children ?? []).filter((child) => !child.flags?.isPrivate);
205
+ const constructors = members.filter((child) => child.kind === KIND.constructor);
206
+ const properties = members
207
+ .filter((child) => child.kind === KIND.property || child.kind === KIND.accessor)
208
+ .sort((left, right) => left.name.localeCompare(right.name));
209
+ const methods = members
210
+ .filter((child) => child.kind === KIND.method)
211
+ .sort((left, right) => left.name.localeCompare(right.name));
212
+
213
+ for (const ctor of constructors) {
214
+ lines.push('## Constructor');
215
+ lines.push(...this.renderSignatures(ctor, 3));
216
+ }
217
+
218
+ if (properties.length) {
219
+ lines.push('## Properties');
220
+ for (const property of properties) {
221
+ const type = property.kind === KIND.accessor ? property.getSignature?.type : property.type;
222
+ const optional = property.flags?.isOptional === true;
223
+ lines.push(
224
+ this.responseField(
225
+ property.name,
226
+ this.type(type),
227
+ optional,
228
+ this.comment(property.comment ?? property.getSignature?.comment)
229
+ )
230
+ );
231
+ }
232
+ }
233
+
234
+ if (methods.length) {
235
+ lines.push('## Methods');
236
+ for (const method of methods) {
237
+ lines.push(`### ${method.name}()`);
238
+ lines.push(...this.renderSignatures(method, 4));
239
+ }
240
+ }
241
+
242
+ return lines;
243
+ }
244
+
245
+ private renderEnum(node: TypedocNode): string[] {
246
+ const lines: string[] = ['## Members'];
247
+ for (const member of node.children ?? []) {
248
+ lines.push(
249
+ this.responseField(
250
+ member.name,
251
+ member.type?.type === 'literal' ? JSON.stringify(member.type.value) : '',
252
+ false,
253
+ this.comment(member.comment)
254
+ )
255
+ );
256
+ }
257
+ return lines;
258
+ }
259
+
260
+ private renderSignatures(node: TypedocNode, depth: number): string[] {
261
+ const lines: string[] = [];
262
+ for (const signature of node.signatures ?? []) {
263
+ const params = (signature.parameters ?? [])
264
+ .map(
265
+ (param) =>
266
+ `${param.name}${param.flags?.isOptional || param.defaultValue !== undefined ? '?' : ''}: ${this.type(param.type)}`
267
+ )
268
+ .join(', ');
269
+ lines.push(this.codeBlock(`${signature.name}(${params}): ${this.type(signature.type)}`));
270
+ const comment = this.comment(signature.comment);
271
+ if (comment) lines.push(comment);
272
+ if (signature.parameters?.length) {
273
+ lines.push(`${'#'.repeat(depth)} Parameters`);
274
+ for (const param of signature.parameters) {
275
+ lines.push(
276
+ this.responseField(
277
+ param.name,
278
+ this.type(param.type),
279
+ param.flags?.isOptional === true || param.defaultValue !== undefined,
280
+ this.comment(param.comment)
281
+ )
282
+ );
283
+ }
284
+ }
285
+ const returns = signature.comment?.blockTags?.find((tag) => tag.tag === '@returns');
286
+ const returnType = this.type(signature.type);
287
+ if (returnType && returnType !== 'void') {
288
+ lines.push(`${'#'.repeat(depth)} Returns`);
289
+ lines.push(
290
+ `\`${escapeInlineCode(returnType)}\`${returns ? `\n\n${this.parts(returns.content)}` : ''}`
291
+ );
292
+ }
293
+ }
294
+ return lines;
295
+ }
296
+
297
+ private responseField(name: string, type: string, optional: boolean, body?: string): string {
298
+ const typeAttr = type ? ` type=${this.jsxString(truncate(type, 80))}` : '';
299
+ const requiredAttr = optional ? '' : ' required';
300
+ return `<ResponseField name=${this.jsxString(name)}${typeAttr}${requiredAttr}>\n${indent(body || '')}\n</ResponseField>`;
301
+ }
302
+
303
+ private jsxString(value: string): string {
304
+ return `{${JSON.stringify(value)}}`;
305
+ }
306
+
307
+ private codeBlock(code: string): string {
308
+ return `\`\`\`typescript\n${code}\n\`\`\``;
309
+ }
310
+
311
+ private comment(comment?: Comment): string {
312
+ if (!comment?.summary?.length) return '';
313
+ const markdown = this.parts(comment.summary);
314
+ const deprecated = comment.blockTags?.find((tag) => tag.tag === '@deprecated');
315
+ const example = comment.blockTags?.find((tag) => tag.tag === '@example');
316
+ const chunks = [markdown];
317
+ if (deprecated)
318
+ chunks.push(
319
+ `<Warning>Deprecated${deprecated.content.length ? `: ${this.parts(deprecated.content)}` : ''}</Warning>`
320
+ );
321
+ if (example) chunks.push(this.parts(example.content));
322
+ return chunks.filter(Boolean).join('\n\n');
323
+ }
324
+
325
+ private parts(parts: CommentPart[]): string {
326
+ const markdown = parts
327
+ .map((part) => {
328
+ if (part.kind === 'inline-tag') {
329
+ if (typeof part.target === 'number' && this.slugById.has(part.target)) {
330
+ return `[${part.text}](/${this.slugById.get(part.target)})`;
331
+ }
332
+ if (typeof part.target === 'string') return `[${part.text}](${part.target})`;
333
+ return `\`${part.text}\``;
334
+ }
335
+ return part.text;
336
+ })
337
+ .join('');
338
+ return markdownToMdx(markdown);
339
+ }
340
+
341
+ private firstSentence(comment?: Comment): string | undefined {
342
+ if (!comment?.summary?.length) return undefined;
343
+ const text = comment.summary
344
+ .filter((part) => part.kind === 'text')
345
+ .map((part) => part.text)
346
+ .join('')
347
+ .replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
348
+ .replace(/[`*_]/g, '')
349
+ .replace(/\s+/g, ' ')
350
+ .trim();
351
+ if (!text) return undefined;
352
+ const sentence = text.split(/(?<=\.)\s/)[0] ?? text;
353
+ return truncate(sentence, 160);
354
+ }
355
+
356
+ type(type?: TypedocType): string {
357
+ if (!type) return 'void';
358
+ switch (type.type) {
359
+ case 'intrinsic':
360
+ return type.name ?? 'unknown';
361
+ case 'reference': {
362
+ const args = type.typeArguments?.length
363
+ ? `<${type.typeArguments.map((arg) => this.type(arg)).join(', ')}>`
364
+ : '';
365
+ return `${type.name ?? 'unknown'}${args}`;
366
+ }
367
+ case 'union':
368
+ return (type.types ?? []).map((entry) => this.type(entry)).join(' | ');
369
+ case 'intersection':
370
+ return (type.types ?? []).map((entry) => this.type(entry)).join(' & ');
371
+ case 'array':
372
+ return `${this.type(type.elementType)}[]`;
373
+ case 'literal':
374
+ return JSON.stringify(type.value);
375
+ case 'tuple':
376
+ return `[${(type.elements ?? []).map((entry) => this.type(entry)).join(', ')}]`;
377
+ case 'reflection':
378
+ return this.reflection(type.declaration);
379
+ case 'typeOperator':
380
+ return `${type.operator} ${this.type(type.target as TypedocType)}`;
381
+ case 'indexedAccess':
382
+ return `${this.type(type.objectType)}[${this.type(type.indexType)}]`;
383
+ case 'conditional':
384
+ return `${this.type(type.checkType)} extends ${this.type(type.extendsType)} ? ${this.type(type.trueType)} : ${this.type(type.falseType)}`;
385
+ case 'query':
386
+ return `typeof ${this.type(type.target as TypedocType)}`;
387
+ case 'templateLiteral':
388
+ return 'string';
389
+ default:
390
+ return type.name ?? 'unknown';
391
+ }
392
+ }
393
+
394
+ private reflection(declaration?: TypedocNode): string {
395
+ if (!declaration) return 'object';
396
+ if (declaration.signatures?.length) {
397
+ const signature = declaration.signatures[0]!;
398
+ const params = (signature.parameters ?? [])
399
+ .map((param) => `${param.name}: ${this.type(param.type)}`)
400
+ .join(', ');
401
+ return `(${params}) => ${this.type(signature.type)}`;
402
+ }
403
+ if (declaration.children?.length) {
404
+ return `{ ${declaration.children.map((child) => `${child.name}: ${this.type(child.type)}`).join('; ')} }`;
405
+ }
406
+ return 'object';
407
+ }
408
+ }
409
+
410
+ function indent(text: string): string {
411
+ return text
412
+ .split('\n')
413
+ .map((line) => (line ? ` ${line}` : line))
414
+ .join('\n');
415
+ }
416
+
417
+ function truncate(text: string, max: number): string {
418
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
419
+ }
420
+
421
+ function escapeInlineCode(text: string): string {
422
+ return text.replace(/`/g, '');
423
+ }