@mintlify/scraping 4.0.1161 → 4.0.1163

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.
@@ -0,0 +1,925 @@
1
+ import fse from 'fs-extra';
2
+ import type { Heading, ImageReference, LinkReference, Paragraph, Root as MdastRoot } from 'mdast';
3
+ import path from 'node:path';
4
+ import { visit } from 'unist-util-visit';
5
+ import { z } from 'zod';
6
+
7
+ import {
8
+ isHtmlParagraph,
9
+ isProseParagraph,
10
+ parseMarkdown,
11
+ plainText,
12
+ rewriteMdastLinks,
13
+ stringifyMdast,
14
+ } from '../convert.js';
15
+ import {
16
+ encodeHref,
17
+ indent,
18
+ inlineCode,
19
+ linkedCode,
20
+ responseField,
21
+ safeHref,
22
+ truncate,
23
+ type LinkedToken,
24
+ } from '../mdx.js';
25
+ import type { SdkNavGroup, SdkPage, SdkPageTarget, SdkReference } from '../types.js';
26
+
27
+ const SUPPORTED_SCHEMA_VERSION = 1;
28
+ const PACKAGE_PAGE = 'package-overview';
29
+ const MEMBER_HEADING_DEPTH = 3;
30
+ const MIN_COMMENT_HEADING_DEPTH = 4;
31
+ const MAX_TYPE_ATTR = 80;
32
+ const MIN_README_HEADING_DEPTH = 2;
33
+ const GITHUB_MODULE = /^github\.com\/([^/]+)\/([^/]+)((?:\/[^/]+)*?)(?:\/v(?:[2-9]|[1-9]\d+))?$/;
34
+
35
+ const MAX_TEXT = 1 << 20;
36
+ const MAX_CODE = 1 << 18;
37
+ const identifierSchema = z
38
+ .string()
39
+ .max(512)
40
+ .regex(/^[\p{L}_][\p{L}\p{N}_]*$/u, 'expected a Go identifier');
41
+ const importPathSchema = z
42
+ .string()
43
+ .max(512)
44
+ .regex(/^[\p{L}\p{N}._~-]+(?:\/[\p{L}\p{N}._~-]+)*$/u, 'expected a Go import path');
45
+ const DIR_SEGMENT = /^[\p{L}\p{N}._~-]+$/u;
46
+ const dirSchema = z
47
+ .string()
48
+ .max(512)
49
+ .refine(
50
+ (dir) =>
51
+ dir === '.' ||
52
+ dir.split('/').every((segment) => DIR_SEGMENT.test(segment) && !/^\.+$/.test(segment)),
53
+ 'expected "." or a relative directory of import-path segments, none made only of dots'
54
+ );
55
+ const textSchema = z.string().max(MAX_TEXT);
56
+ const codeTextSchema = z.string().max(MAX_CODE);
57
+ const refSchema = z.object({
58
+ pkg: importPathSchema.optional(),
59
+ name: identifierSchema,
60
+ private: z.boolean().optional(),
61
+ });
62
+ const codeSchema = z.array(z.object({ t: codeTextSchema, ref: refSchema.optional() }));
63
+ const exampleSchema = z.object({
64
+ suffix: identifierSchema.optional(),
65
+ doc: textSchema.optional(),
66
+ code: codeTextSchema,
67
+ output: codeTextSchema.optional(),
68
+ });
69
+ const typeParamSchema = z.object({ name: identifierSchema, constraint: codeSchema });
70
+ const paramSchema = z.object({ name: identifierSchema.optional(), type: codeSchema });
71
+ const funcSchema = z.object({
72
+ name: identifierSchema,
73
+ recv: codeTextSchema.optional(),
74
+ decl: codeSchema,
75
+ doc: textSchema.optional(),
76
+ deprecated: textSchema.optional(),
77
+ typeParams: z.array(typeParamSchema).optional(),
78
+ params: z.array(paramSchema).optional(),
79
+ results: z.array(paramSchema).optional(),
80
+ examples: z.array(exampleSchema).optional(),
81
+ });
82
+ const valueSchema = z.object({
83
+ names: z.array(identifierSchema),
84
+ decl: codeSchema,
85
+ doc: textSchema.optional(),
86
+ deprecated: textSchema.optional(),
87
+ });
88
+ const fieldSchema = z.object({
89
+ name: identifierSchema,
90
+ type: codeSchema,
91
+ doc: textSchema.optional(),
92
+ deprecated: textSchema.optional(),
93
+ embedded: z.boolean().optional(),
94
+ tag: codeTextSchema.optional(),
95
+ });
96
+ const typeSchema = z.object({
97
+ name: identifierSchema,
98
+ kind: z.enum(['struct', 'interface', 'defined', 'alias']),
99
+ decl: codeSchema,
100
+ doc: textSchema.optional(),
101
+ deprecated: textSchema.optional(),
102
+ typeParams: z.array(typeParamSchema).optional(),
103
+ fields: z.array(fieldSchema).optional(),
104
+ methods: z.array(funcSchema).optional(),
105
+ funcs: z.array(funcSchema).optional(),
106
+ consts: z.array(valueSchema).optional(),
107
+ vars: z.array(valueSchema).optional(),
108
+ examples: z.array(exampleSchema).optional(),
109
+ });
110
+ const packageSchema = z.object({
111
+ importPath: importPathSchema,
112
+ name: identifierSchema,
113
+ dir: dirSchema,
114
+ synopsis: textSchema.optional(),
115
+ doc: textSchema.optional(),
116
+ readme: textSchema.optional(),
117
+ deprecated: textSchema.optional(),
118
+ consts: z.array(valueSchema).optional(),
119
+ vars: z.array(valueSchema).optional(),
120
+ funcs: z.array(funcSchema).optional(),
121
+ types: z.array(typeSchema).optional(),
122
+ examples: z.array(exampleSchema).optional(),
123
+ });
124
+ const goArtifactSchema = z.object({
125
+ schemaVersion: z.literal(SUPPORTED_SCHEMA_VERSION),
126
+ generator: z.object({
127
+ name: z.string().max(512),
128
+ version: z.string().max(512),
129
+ go: z.string().max(512),
130
+ }),
131
+ module: importPathSchema,
132
+ goos: z.string(),
133
+ goarch: z.string(),
134
+ commentSyntax: z.enum(['markdown', 'godoc']),
135
+ packages: z.array(packageSchema).superRefine((packages, ctx) => {
136
+ const importPaths = new Set<string>();
137
+ const dirs = new Set<string>();
138
+ const namespaces = new Map<string, string>();
139
+ const root = packages.find((pkg) => pkg.dir === '.');
140
+ packages.forEach((pkg, index) => {
141
+ if (importPaths.has(pkg.importPath)) {
142
+ ctx.addIssue({
143
+ code: z.ZodIssueCode.custom,
144
+ path: [index, 'importPath'],
145
+ message: `duplicate package ${pkg.importPath}`,
146
+ });
147
+ }
148
+ importPaths.add(pkg.importPath);
149
+ if (dirs.has(pkg.dir)) {
150
+ ctx.addIssue({
151
+ code: z.ZodIssueCode.custom,
152
+ path: [index, 'dir'],
153
+ message: `duplicate package directory ${pkg.dir}`,
154
+ });
155
+ }
156
+ dirs.add(pkg.dir);
157
+ const namespace = packageNamespace(pkg, root);
158
+ const other = namespaces.get(namespace);
159
+ if (other !== undefined) {
160
+ ctx.addIssue({
161
+ code: z.ZodIssueCode.custom,
162
+ path: [index, 'dir'],
163
+ message: `packages ${other} and ${pkg.dir} would both be addressed as ${namespace}`,
164
+ });
165
+ }
166
+ namespaces.set(namespace, pkg.dir);
167
+ });
168
+ }),
169
+ });
170
+
171
+ type GoArtifact = z.infer<typeof goArtifactSchema>;
172
+ type GoPackage = z.infer<typeof packageSchema>;
173
+ type GoType = z.infer<typeof typeSchema>;
174
+ type GoFunc = z.infer<typeof funcSchema>;
175
+ type GoField = z.infer<typeof fieldSchema>;
176
+ type GoValue = z.infer<typeof valueSchema>;
177
+ type GoExample = z.infer<typeof exampleSchema>;
178
+ type GoParam = z.infer<typeof paramSchema>;
179
+ type GoCode = z.infer<typeof codeSchema>;
180
+ type GoRef = z.infer<typeof refSchema>;
181
+ type Location = { slug: string; anchor?: string; page?: string };
182
+ type GoDoc = { tree: MdastRoot };
183
+
184
+ const TYPE_TAGS: Record<GoType['kind'], string> = {
185
+ struct: 'STRUCT',
186
+ interface: 'INTERFACE',
187
+ defined: 'TYPE',
188
+ alias: 'TYPE',
189
+ };
190
+
191
+ export async function convertGo(sourcePath: string): Promise<SdkReference> {
192
+ const artifact = parseArtifact(await fse.readJson(sourcePath), sourcePath);
193
+ const index = new GoIndex(artifact);
194
+ const renderers = artifact.packages.map((pkg) => new GoRenderer(index, pkg));
195
+
196
+ const pages: SdkPage[] = [];
197
+ const groups: SdkNavGroup[] = [];
198
+ for (const renderer of renderers) {
199
+ const packagePages = renderer.packagePages();
200
+ pages.push(...packagePages, ...renderer.memberPages());
201
+ groups.push({ group: renderer.group, pages: packagePages.map((page) => page.slug) });
202
+ }
203
+
204
+ return { pages, groups };
205
+ }
206
+
207
+ function parseArtifact(raw: unknown, sourcePath: string): GoArtifact {
208
+ const version =
209
+ typeof raw === 'object' && raw !== null && 'schemaVersion' in raw
210
+ ? raw.schemaVersion
211
+ : undefined;
212
+ if (typeof version === 'number' && version !== SUPPORTED_SCHEMA_VERSION) {
213
+ throw new Error(
214
+ `Go SDK artifact ${sourcePath} uses schema version ${version}, but Mintlify reads schema version ${SUPPORTED_SCHEMA_VERSION}. Regenerate it with a compatible mintlify-go-extractor release.`
215
+ );
216
+ }
217
+ const result = goArtifactSchema.safeParse(raw);
218
+ if (!result.success) {
219
+ const issue = result.error.issues[0];
220
+ const detail = issue ? `${issue.path.join('.') || 'artifact'}: ${issue.message}` : 'invalid';
221
+ throw new Error(`Invalid Go SDK artifact ${sourcePath}: ${detail}`);
222
+ }
223
+ return result.data;
224
+ }
225
+
226
+ function slugify(name: string): string {
227
+ return name.replace(/[^\p{L}\p{N}\-_.]+/gu, '-').replace(/^-+|-+$/g, '') || 'item';
228
+ }
229
+
230
+ function packageBase(pkg: GoPackage): string {
231
+ return pkg.dir === '.' ? '' : pkg.dir.split('/').map(slugify).join('/');
232
+ }
233
+
234
+ // Packages are addressed as a tree headed by the root package's name and
235
+ // continued by directory path, dotted: the root of openai-go is `package
236
+ // openai` and its symbols `type openai.Client`; audio/client is `package
237
+ // openai.audio.client` (or any trailing part, `package client`) and its
238
+ // symbols `type openai.audio.client.Foo`. api/v1 (package api) is `package
239
+ // api.v1`, not `package api`. The root prefix keeps a root package and a
240
+ // top-level directory of the same name apart; a module without a root package
241
+ // has nothing to keep apart and uses the directory path alone.
242
+ function packageNamespace(pkg: Pick<GoPackage, 'dir' | 'name'>, root?: { name: string }): string {
243
+ if (pkg.dir === '.') return pkg.name;
244
+ const dotted = pkg.dir.replaceAll('/', '.');
245
+ return root ? `${root.name}.${dotted}` : dotted;
246
+ }
247
+
248
+ function packageTargetName(pkg: GoPackage): string {
249
+ return pkg.dir === '.' ? pkg.name : path.posix.basename(pkg.dir);
250
+ }
251
+
252
+ function packageParent(pkg: GoPackage, root?: GoPackage): string | undefined {
253
+ if (pkg.dir === '.') return undefined;
254
+ const parent = path.posix.dirname(pkg.dir);
255
+ const dotted = parent === '.' ? undefined : parent.replaceAll('/', '.');
256
+ if (!root) return dotted;
257
+ return dotted ? `${root.name}.${dotted}` : root.name;
258
+ }
259
+
260
+ function memberHeading(name: string): string {
261
+ return `${name}()`;
262
+ }
263
+
264
+ // Method headings carry an explicit id, pkg.go.dev style, so links to them
265
+ // do not depend on how many same-named headings precede them on the page.
266
+ function methodAnchor(type: GoType, method: GoFunc): string {
267
+ return `${type.name}.${method.name}`;
268
+ }
269
+
270
+ function packageFuncs(pkg: GoPackage): GoFunc[] {
271
+ return [...(pkg.funcs ?? []), ...(pkg.types ?? []).flatMap((type) => type.funcs ?? [])].sort(
272
+ (left, right) => left.name.localeCompare(right.name)
273
+ );
274
+ }
275
+
276
+ class GoIndex {
277
+ readonly module: string;
278
+ readonly root: GoPackage | undefined;
279
+ private readonly packages = new Map<string, string>();
280
+ private readonly symbols = new Map<string, Location>();
281
+ private readonly routes = new Map<string, number>();
282
+
283
+ constructor(artifact: GoArtifact) {
284
+ this.module = artifact.module;
285
+ this.root = artifact.packages.find((pkg) => pkg.dir === '.');
286
+ for (const pkg of artifact.packages) {
287
+ const base = packageBase(pkg);
288
+ const overview = { slug: this.route(base, PACKAGE_PAGE) };
289
+ this.packages.set(pkg.importPath, overview.slug);
290
+ for (const value of [...(pkg.consts ?? []), ...(pkg.vars ?? [])]) {
291
+ for (const name of value.names) this.add(pkg, name, overview);
292
+ }
293
+ for (const fn of packageFuncs(pkg)) {
294
+ this.add(pkg, fn.name, { slug: this.route(base, fn.name) });
295
+ }
296
+ for (const type of pkg.types ?? []) {
297
+ const slug = this.route(base, type.name);
298
+ this.add(pkg, type.name, { slug });
299
+ for (const value of [...(type.consts ?? []), ...(type.vars ?? [])]) {
300
+ for (const name of value.names) this.add(pkg, name, { slug });
301
+ }
302
+ }
303
+ }
304
+ for (const pkg of artifact.packages) {
305
+ for (const type of pkg.types ?? []) {
306
+ const slug = this.slugOf(pkg, type.name);
307
+ for (const method of type.methods ?? []) {
308
+ this.add(pkg, `${type.name}.${method.name}`, {
309
+ slug,
310
+ anchor: methodAnchor(type, method),
311
+ page: this.route(slug, method.name),
312
+ });
313
+ }
314
+ for (const field of type.fields ?? []) {
315
+ this.add(pkg, `${type.name}.${field.name}`, {
316
+ slug,
317
+ anchor: field.name,
318
+ page: this.route(slug, field.name),
319
+ });
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+ private add(pkg: GoPackage, name: string, location: Location): void {
326
+ this.symbols.set(`${pkg.importPath}#${name}`, location);
327
+ }
328
+
329
+ private slugOf(pkg: GoPackage, name: string): string {
330
+ return this.symbols.get(`${pkg.importPath}#${name}`)?.slug ?? packageBase(pkg);
331
+ }
332
+
333
+ private route(base: string, name: string): string {
334
+ const slug = path.posix.join(base, slugify(name));
335
+ // Same key prebuild uses for route collisions (getGeneratedRouteKey).
336
+ const key = slug.normalize('NFC').toLowerCase();
337
+ const taken = this.routes.get(key);
338
+ if (taken === undefined) {
339
+ this.routes.set(key, 1);
340
+ return slug;
341
+ }
342
+ // A Go identifier cannot contain "-", so `${key}-${attempt}` is only ever a
343
+ // route this function reserved itself and the next number is free.
344
+ const attempt = taken + 1;
345
+ this.routes.set(key, attempt);
346
+ this.routes.set(`${key}-${attempt}`, 1);
347
+ return `${slug}-${attempt}`;
348
+ }
349
+
350
+ packageSlug(importPath: string): string | undefined {
351
+ return this.packages.get(importPath);
352
+ }
353
+
354
+ symbolSlug(importPath: string, name: string): string | undefined {
355
+ return this.symbols.get(`${importPath}#${name}`)?.slug;
356
+ }
357
+
358
+ memberPageSlug(importPath: string, name: string): string | undefined {
359
+ return this.symbols.get(`${importPath}#${name}`)?.page;
360
+ }
361
+
362
+ href(importPath: string, name: string | undefined, isPrivate: boolean): string | undefined {
363
+ if (name) {
364
+ const dot = name.indexOf('.');
365
+ const location =
366
+ this.symbols.get(`${importPath}#${name}`) ??
367
+ (dot > 0 ? this.symbols.get(`${importPath}#${name.slice(0, dot)}`) : undefined);
368
+ if (location) return `/${location.slug}${location.anchor ? `#${location.anchor}` : ''}`;
369
+ }
370
+ const packageSlug = this.packages.get(importPath);
371
+ if (packageSlug) return `/${packageSlug}`;
372
+ if (isPrivate) return undefined;
373
+ return `https://pkg.go.dev/${encodeHref(importPath)}${name ? `#${encodeHref(name)}` : ''}`;
374
+ }
375
+ }
376
+
377
+ class GoRenderer {
378
+ readonly group: string;
379
+ private readonly base: string;
380
+ private readonly funcs: GoFunc[];
381
+ private readonly rendered = new Map<string, GoDoc>();
382
+
383
+ constructor(
384
+ private readonly index: GoIndex,
385
+ private readonly pkg: GoPackage
386
+ ) {
387
+ this.group = pkg.dir === '.' ? pkg.name : pkg.dir;
388
+ this.base = packageBase(pkg);
389
+ this.funcs = packageFuncs(pkg);
390
+ }
391
+
392
+ packagePages(): SdkPage[] {
393
+ return [
394
+ this.packagePage(),
395
+ ...(this.pkg.types ?? []).map((type) => this.typePage(type)),
396
+ ...this.funcs.map((fn) => this.funcPage(fn)),
397
+ ];
398
+ }
399
+
400
+ memberPages(): SdkPage[] {
401
+ return (this.pkg.types ?? []).flatMap((type) => this.typeMemberPages(type));
402
+ }
403
+
404
+ private packagePage(): SdkPage {
405
+ const slug =
406
+ this.index.packageSlug(this.pkg.importPath) ?? path.posix.join(this.base, PACKAGE_PAGE);
407
+ const lines = [this.prose(this.pkg.doc), this.deprecated(this.pkg.deprecated)];
408
+ const types = this.pkg.types ?? [];
409
+ if (types.length) lines.push('## Types', this.indexList(types.map((type) => type.name)));
410
+ if (this.funcs.length) {
411
+ lines.push('## Functions', this.indexList(this.funcs.map((fn) => fn.name)));
412
+ }
413
+ lines.push(...this.valueSection('Constants', this.pkg.consts, 2));
414
+ lines.push(...this.valueSection('Variables', this.pkg.vars, 2));
415
+ lines.push(...this.exampleSection(this.pkg.examples, 2));
416
+ const parent = packageParent(this.pkg, this.index.root);
417
+ // The synopsis is the package doc's first sentence, so it is read from the
418
+ // doc's parse, which rendering the page needs anyway.
419
+ const synopsis = this.description(this.pkg.doc);
420
+ const readme = this.pkg.readme ? this.readme(this.pkg.readme, synopsis) : undefined;
421
+ const description = synopsis ?? readme?.description;
422
+ return {
423
+ slug,
424
+ title: `package ${this.pkg.name}`,
425
+ ...(description ? { description } : {}),
426
+ tag: 'PACKAGE',
427
+ ...(readme?.intro ? { intro: readme.intro } : {}),
428
+ content: join(lines),
429
+ target: {
430
+ kind: 'package',
431
+ name: packageTargetName(this.pkg),
432
+ ...(parent ? { namespace: parent } : {}),
433
+ },
434
+ };
435
+ }
436
+
437
+ // Renders a README the way pkg.go.dev shows it, minus the sentence the page
438
+ // shows as its description (see takeLeadingSentence).
439
+ private readme(markdown: string, synopsis?: string): { intro: string; description?: string } {
440
+ if (unparseable(markdown)) return { intro: codeBlock(markdown, 'text') };
441
+ const tree = parseMarkdown(markdown);
442
+ const first = tree.children[0];
443
+ if (first?.type === 'heading' && first.depth === 1) tree.children.shift();
444
+ visit(tree, 'heading', (node: Heading) => {
445
+ node.depth = Math.max(node.depth, MIN_README_HEADING_DEPTH) as Heading['depth'];
446
+ });
447
+ const imageDefinitions = new Set<string>();
448
+ const linkDefinitions = new Set<string>();
449
+ visit(tree, 'imageReference', (node: ImageReference) => {
450
+ imageDefinitions.add(node.identifier);
451
+ });
452
+ visit(tree, 'linkReference', (node: LinkReference) => {
453
+ linkDefinitions.add(node.identifier);
454
+ });
455
+ rewriteMdastLinks(tree, (node) =>
456
+ this.repoUrl(
457
+ node.url,
458
+ node.type === 'image' ||
459
+ (node.type === 'definition' &&
460
+ imageDefinitions.has(node.identifier) &&
461
+ !linkDefinitions.has(node.identifier))
462
+ )
463
+ );
464
+ const description = takeLeadingSentence(tree, synopsis);
465
+ return { intro: stringifyMdast(tree), ...(description ? { description } : {}) };
466
+ }
467
+
468
+ // The first sentence of a doc's opening paragraph, read from the parsed tree
469
+ // rather than from the Markdown source, so link syntax and escapes are
470
+ // already gone and the cost is linear in the doc's length.
471
+ private describe(markdown?: string): { description?: string } {
472
+ const description = this.description(markdown);
473
+ return description ? { description } : {};
474
+ }
475
+
476
+ private description(markdown?: string): string | undefined {
477
+ if (!markdown) return undefined;
478
+ const paragraph = this.doc(markdown).tree.children.find(
479
+ (node): node is Paragraph => node.type === 'paragraph'
480
+ );
481
+ if (!paragraph) return undefined;
482
+ return firstSentence(plainText(paragraph).replace(/\s+/g, ' ').trim());
483
+ }
484
+
485
+ // Relative README links resolve against the package directory in the
486
+ // repository; a leading slash means the repository root, as on GitHub.
487
+ private repoUrl(url: string, raw: boolean): string | undefined {
488
+ const href = safeHref(url);
489
+ if (href === undefined || !/^[^#]/.test(href) || /^[a-z][a-z0-9+.-]*:/i.test(href)) {
490
+ return href;
491
+ }
492
+ const match = GITHUB_MODULE.exec(this.index.module);
493
+ if (!match) return href;
494
+ const [, owner, repo, moduleDir = ''] = match;
495
+ const [file = '', hash] = href.split('#');
496
+ const resolved = file.startsWith('/')
497
+ ? path.posix.normalize(file.slice(1))
498
+ : path.posix.normalize(
499
+ path.posix.join(moduleDir.slice(1), this.pkg.dir === '.' ? '' : this.pkg.dir, file)
500
+ );
501
+ if (resolved.startsWith('..')) return href;
502
+ const view = raw ? 'raw' : 'blob';
503
+ return `https://github.com/${owner}/${repo}/${view}/HEAD/${resolved}${hash ? `#${hash}` : ''}`;
504
+ }
505
+
506
+ private typePage(type: GoType): SdkPage {
507
+ const lines = [
508
+ this.prose(type.doc),
509
+ this.deprecated(type.deprecated),
510
+ codeBlock(declarationHeader(type)),
511
+ ];
512
+ if (type.typeParams?.length) {
513
+ lines.push(
514
+ '## Type Parameters',
515
+ ...type.typeParams.map((param) => this.field(param.name, param.constraint))
516
+ );
517
+ }
518
+ if (type.fields?.length) {
519
+ lines.push('## Fields', ...type.fields.map((field) => this.fieldEntry(field)));
520
+ }
521
+ lines.push(...this.memberSection('Constructors', type.funcs));
522
+ lines.push(...this.memberSection('Methods', type.methods, type));
523
+ lines.push(...this.valueSection('Constants', type.consts, 2));
524
+ lines.push(...this.valueSection('Variables', type.vars, 2));
525
+ lines.push(...this.exampleSection(type.examples, 2));
526
+ return {
527
+ slug: this.slug(type.name),
528
+ title: type.name,
529
+ ...this.describe(type.doc),
530
+ tag: TYPE_TAGS[type.kind],
531
+ content: join(lines),
532
+ target: this.target(type.kind === 'interface' ? 'interface' : 'type', type.name),
533
+ };
534
+ }
535
+
536
+ private funcPage(fn: GoFunc): SdkPage {
537
+ const lines = [
538
+ this.prose(fn.doc),
539
+ this.deprecated(fn.deprecated),
540
+ codeBlock(codeText(fn.decl)),
541
+ ...this.signatureDetails(fn, 2),
542
+ ];
543
+ return {
544
+ slug: this.slug(fn.name),
545
+ title: fn.name,
546
+ ...this.describe(fn.doc),
547
+ tag: 'FUNCTION',
548
+ content: join(lines),
549
+ target: this.target('function', fn.name),
550
+ };
551
+ }
552
+
553
+ private typeMemberPages(type: GoType): SdkPage[] {
554
+ const methods = (type.methods ?? []).map(
555
+ (method): SdkPage => ({
556
+ slug: this.memberSlug(type, method.name),
557
+ title: `${type.name}.${method.name}`,
558
+ ...this.describe(method.doc),
559
+ tag: 'METHOD',
560
+ content: join(this.funcBody(method, 2)),
561
+ target: this.target('method', method.name, type.name),
562
+ })
563
+ );
564
+ const fields = (type.fields ?? []).map(
565
+ (field): SdkPage => ({
566
+ slug: this.memberSlug(type, field.name),
567
+ title: `${type.name}.${field.name}`,
568
+ ...this.describe(field.doc),
569
+ tag: 'FIELD',
570
+ content: this.fieldEntry(field),
571
+ target: this.target('property', field.name, type.name),
572
+ })
573
+ );
574
+ return [...methods, ...fields];
575
+ }
576
+
577
+ private target(kind: SdkPageTarget['kind'], name: string, parent?: string): SdkPageTarget {
578
+ return {
579
+ kind,
580
+ name,
581
+ ...(parent ? { parent } : {}),
582
+ namespace: packageNamespace(this.pkg, this.index.root),
583
+ };
584
+ }
585
+
586
+ private slug(name: string): string {
587
+ return (
588
+ this.index.symbolSlug(this.pkg.importPath, name) ?? path.posix.join(this.base, slugify(name))
589
+ );
590
+ }
591
+
592
+ private memberSlug(type: GoType, name: string): string {
593
+ return (
594
+ this.index.memberPageSlug(this.pkg.importPath, `${type.name}.${name}`) ??
595
+ path.posix.join(this.slug(type.name), slugify(name))
596
+ );
597
+ }
598
+
599
+ private indexList(names: string[]): string {
600
+ return names.map((name) => `- [${name}](/${this.slug(name)})`).join('\n');
601
+ }
602
+
603
+ private memberSection(heading: string, fns: GoFunc[] | undefined, owner?: GoType): string[] {
604
+ if (!fns?.length) return [];
605
+ const marker = '#'.repeat(MEMBER_HEADING_DEPTH);
606
+ return [
607
+ `## ${heading}`,
608
+ ...fns.flatMap((fn) => [
609
+ `${marker} ${memberHeading(fn.name)}${owner ? ` {#${methodAnchor(owner, fn)}}` : ''}`,
610
+ ...this.funcBody(fn, 4),
611
+ ]),
612
+ ];
613
+ }
614
+
615
+ private funcBody(fn: GoFunc, depth: number): string[] {
616
+ return [
617
+ codeBlock(codeText(fn.decl)),
618
+ this.prose(fn.doc),
619
+ this.deprecated(fn.deprecated),
620
+ ...this.signatureDetails(fn, depth),
621
+ ];
622
+ }
623
+
624
+ private signatureDetails(fn: GoFunc, depth: number): string[] {
625
+ const heading = '#'.repeat(depth);
626
+ const lines: string[] = [];
627
+ if (fn.typeParams?.length) {
628
+ lines.push(
629
+ `${heading} Type Parameters`,
630
+ ...fn.typeParams.map((param) => this.field(param.name, param.constraint))
631
+ );
632
+ }
633
+ const params = fn.params ?? [];
634
+ if (params.length) {
635
+ lines.push(
636
+ `${heading} Parameters`,
637
+ ...(params.every((param): param is GoParam & { name: string } => Boolean(param.name))
638
+ ? params.map((param) => this.field(param.name, param.type))
639
+ : [this.returns(params)])
640
+ );
641
+ }
642
+ if (fn.results?.length) lines.push(`${heading} Returns`, this.returns(fn.results));
643
+ lines.push(...this.exampleSection(fn.examples, depth));
644
+ return lines;
645
+ }
646
+
647
+ private returns(results: GoParam[]): string {
648
+ const tokens: LinkedToken[] = [];
649
+ results.forEach((result, position) => {
650
+ if (position > 0) tokens.push({ text: ', ' });
651
+ tokens.push(...this.tokens(result.type));
652
+ });
653
+ if (results.length > 1) {
654
+ tokens.unshift({ text: '(' });
655
+ tokens.push({ text: ')' });
656
+ }
657
+ return linkedCode(tokens) ?? inlineCode(tokens.map((token) => token.text).join(''));
658
+ }
659
+
660
+ private fieldEntry(field: GoField): string {
661
+ const body = join([
662
+ field.embedded ? 'Embedded field.' : undefined,
663
+ field.tag ? `**Tag:** ${inlineCode(field.tag)}` : undefined,
664
+ this.prose(field.doc),
665
+ this.deprecated(field.deprecated),
666
+ ]);
667
+ return this.field(field.name, field.type, body, field.name);
668
+ }
669
+
670
+ // The type attribute is cut at 80 characters, so a longer type is also
671
+ // written out in full in the body: linked when it has refs, plain otherwise.
672
+ private field(name: string, code: GoCode, body?: string, id?: string): string {
673
+ const typeHref = this.singleRefHref(code);
674
+ const text = codeText(code);
675
+ const full = typeHref
676
+ ? undefined
677
+ : (linkedCode(this.tokens(code)) ??
678
+ (text.length > MAX_TYPE_ATTR ? inlineCode(text) : undefined));
679
+ const content = join([full && `**Type:** ${full}`, body]);
680
+ return responseField(name, text, true, content, typeHref, id);
681
+ }
682
+
683
+ private singleRefHref(code: GoCode): string | undefined {
684
+ const position = code.findIndex((token) => token.ref);
685
+ if (position === -1 || position !== code.length - 1) return undefined;
686
+ const prefix = codeText(code.slice(0, position));
687
+ if (!/^(?:\*|\[\]|\.\.\.)*(?:[\p{L}_][\p{L}\p{N}_]*\.)?$/u.test(prefix)) return undefined;
688
+ const ref = code[position]?.ref;
689
+ const href = ref ? this.refHref(ref) : undefined;
690
+ return href?.startsWith('/') ? href : undefined;
691
+ }
692
+
693
+ private tokens(code: GoCode): LinkedToken[] {
694
+ return code.map((token) => {
695
+ const href = token.ref ? this.refHref(token.ref) : undefined;
696
+ return href ? { text: token.t, href } : { text: token.t };
697
+ });
698
+ }
699
+
700
+ private refHref(ref: GoRef): string | undefined {
701
+ return this.index.href(ref.pkg ?? this.pkg.importPath, ref.name, ref.private ?? false);
702
+ }
703
+
704
+ private valueSection(heading: string, values: GoValue[] | undefined, depth: number): string[] {
705
+ if (!values?.length) return [];
706
+ return [
707
+ `${'#'.repeat(depth)} ${heading}`,
708
+ ...values.flatMap((value) => [
709
+ codeBlock(codeText(value.decl)),
710
+ this.prose(value.doc),
711
+ this.deprecated(value.deprecated),
712
+ ]),
713
+ ];
714
+ }
715
+
716
+ private exampleSection(examples: GoExample[] | undefined, depth: number): string[] {
717
+ if (!examples?.length) return [];
718
+ const heading = '#'.repeat(depth);
719
+ return [
720
+ `${heading} Examples`,
721
+ ...examples.flatMap((example) => [
722
+ `${heading}# ${example.suffix ? `Example (${example.suffix})` : 'Example'}`,
723
+ this.prose(example.doc),
724
+ codeBlock(example.code),
725
+ example.output ? `Output:\n\n${codeBlock(example.output.replace(/\n+$/, ''), 'text')}` : '',
726
+ ]),
727
+ ];
728
+ }
729
+
730
+ private deprecated(text?: string): string {
731
+ if (!text) return '';
732
+ return `<Warning>\n${indent(`Deprecated: ${this.prose(text)}`)}\n</Warning>`;
733
+ }
734
+
735
+ private prose(markdown?: string): string {
736
+ return markdown ? stringifyMdast(this.doc(markdown).tree) : '';
737
+ }
738
+
739
+ private doc(markdown: string): GoDoc {
740
+ let doc = this.rendered.get(markdown);
741
+ if (!doc) {
742
+ doc = this.parseDoc(markdown);
743
+ this.rendered.set(markdown, doc);
744
+ }
745
+ return doc;
746
+ }
747
+
748
+ private parseDoc(markdown: string): GoDoc {
749
+ if (unparseable(markdown)) {
750
+ return {
751
+ tree: { type: 'root', children: [{ type: 'code', lang: 'text', value: markdown }] },
752
+ };
753
+ }
754
+ const tree = parseMarkdown(markdown, MIN_COMMENT_HEADING_DEPTH);
755
+ rewriteMdastLinks(tree, (node) => {
756
+ if (!node.url.startsWith('go:')) return safeHref(node.url);
757
+ return this.resolveDocLink(node.url.slice('go:'.length));
758
+ });
759
+ return { tree };
760
+ }
761
+
762
+ private resolveDocLink(target: string): string | undefined {
763
+ const hash = target.indexOf('#');
764
+ const importPath = hash === -1 ? target : target.slice(0, hash);
765
+ const name = hash === -1 ? undefined : target.slice(hash + 1);
766
+ return this.index.href(importPath, name, false);
767
+ }
768
+ }
769
+
770
+ function codeText(code: GoCode): string {
771
+ return code.map((token) => token.t).join('');
772
+ }
773
+
774
+ function declarationHeader(type: GoType): string {
775
+ const text = codeText(type.decl);
776
+ if (type.kind !== 'struct' && type.kind !== 'interface') return text;
777
+ const open = bodyStart(text, type.kind);
778
+ if (open === -1) return text;
779
+ if (type.kind === 'interface') {
780
+ const body = text.slice(open + type.kind.length + 3, text.lastIndexOf('}'));
781
+ const entries = body
782
+ .split('\n')
783
+ .map((line) => line.trim())
784
+ .filter((line) => line && !line.startsWith('//'));
785
+ const shown = (type.methods?.length ?? 0) + (type.fields?.length ?? 0);
786
+ if (entries.length > shown) return text;
787
+ }
788
+ return text.slice(0, open + type.kind.length + 1);
789
+ }
790
+
791
+ // The declaration's own body opens outside any type-parameter brackets; a
792
+ // constraint written as `interface{ ... }` inside `[...]` comes first in the
793
+ // text but is not it.
794
+ function bodyStart(text: string, kind: 'struct' | 'interface'): number {
795
+ const marker = ` ${kind} {`;
796
+ let depth = 0;
797
+ for (let i = 0; i < text.length; i += 1) {
798
+ const char = text[i];
799
+ if (char === '[') depth += 1;
800
+ else if (char === ']') depth -= 1;
801
+ else if (depth === 0 && text.startsWith(marker, i)) return i;
802
+ }
803
+ return -1;
804
+ }
805
+
806
+ function codeBlock(code: string, lang = 'go'): string {
807
+ const longestRun = Math.max(0, ...(code.match(/`+/g) ?? []).map((run) => run.length));
808
+ const fence = '`'.repeat(Math.max(3, longestRun + 1));
809
+ return `${fence}${lang}\n${code}\n${fence}`;
810
+ }
811
+
812
+ function join(chunks: (string | undefined)[]): string {
813
+ return chunks.filter(Boolean).join('\n\n');
814
+ }
815
+
816
+ // The README's first prose paragraph (badges, images, and HTML blocks such as a
817
+ // centred <h1> are skipped) supplies the description when the page has none,
818
+ // provided it is a single sentence, and is then removed from the intro since
819
+ // the page already shows it. When the package doc supplied the description and
820
+ // the README opens with the same sentence, that sentence is removed instead.
821
+ function takeLeadingSentence(tree: MdastRoot, synopsis?: string): string | undefined {
822
+ const index = tree.children.findIndex(
823
+ (node) => node.type === 'paragraph' && !isHtmlParagraph(node) && isProseParagraph(node)
824
+ );
825
+ const paragraph = tree.children[index] as Paragraph | undefined;
826
+ if (!paragraph) return synopsis;
827
+ const text = plainText(paragraph).replace(/\s+/g, ' ').trim();
828
+ const sentence = leadingSentence(text);
829
+ if (!sentence || (synopsis !== undefined && truncate(sentence, MAX_DESCRIPTION) !== synopsis)) {
830
+ return synopsis;
831
+ }
832
+ if (sentence === text) {
833
+ tree.children.splice(index, 1);
834
+ return synopsis ?? truncate(sentence, MAX_DESCRIPTION);
835
+ }
836
+ if (synopsis !== undefined) dropLeadingSentence(paragraph);
837
+ return synopsis;
838
+ }
839
+
840
+ // Removes a paragraph's first sentence, whatever inline markup it contains,
841
+ // by dropping the children that lie wholly inside it and trimming the text
842
+ // node it ends in. When the sentence ends inside a link or emphasis, nothing
843
+ // is removed rather than splitting that node.
844
+ function dropLeadingSentence(paragraph: Paragraph): void {
845
+ const pieces = paragraph.children.map(plainText);
846
+ const end = /\.(?:\s|$)/.exec(pieces.join(''));
847
+ if (!end) return;
848
+ const stop = end.index + 1;
849
+ let offset = 0;
850
+ for (let i = 0; i < paragraph.children.length; i += 1) {
851
+ const child = paragraph.children[i];
852
+ const length = pieces[i]?.length ?? 0;
853
+ if (offset + length <= stop) {
854
+ offset += length;
855
+ continue;
856
+ }
857
+ if (offset === stop) {
858
+ // The sentence ended with the previous node; drop it and any hard
859
+ // break that separated it from the rest.
860
+ paragraph.children.splice(0, i);
861
+ while (paragraph.children[0]?.type === 'break') paragraph.children.shift();
862
+ const next = paragraph.children[0];
863
+ if (next?.type === 'text') {
864
+ next.value = next.value.trimStart();
865
+ if (next.value === '') paragraph.children.shift();
866
+ }
867
+ return;
868
+ }
869
+ if (child?.type !== 'text') return;
870
+ child.value = child.value.slice(stop - offset).trimStart();
871
+ paragraph.children.splice(0, i);
872
+ if (child.value === '') paragraph.children.shift();
873
+ return;
874
+ }
875
+ paragraph.children = [];
876
+ }
877
+
878
+ // Text past either limit is rendered as a text block instead of Markdown.
879
+ // micromark is quadratic in unclosed brackets (80k `[a](` takes about 45 s)
880
+ // and in balanced link and emphasis markup (40k nested brackets, `[]` pairs,
881
+ // `[a]` references, or `*a` runs take 5 to 36 s), and remark-stringify is
882
+ // quadratic in the characters it escapes (40k `<` or `{` take 0.7 s, minutes
883
+ // at the 1 MiB cap). At 10k either costs well under 2 s. The largest real doc
884
+ // seen (openai-go's README, 60 KB) has about 1,500 such characters and no more
885
+ // than 3 unclosed brackets.
886
+ const MAX_UNCLOSED = 1000;
887
+ const MAX_MARKUP = 10000;
888
+ const MARKUP = new Set(['[', ']', '*', '_', '<', '>', '{', '}', '`', '&']);
889
+
890
+ function unparseable(markdown: string): boolean {
891
+ let square = 0;
892
+ let round = 0;
893
+ let unclosed = 0;
894
+ let markup = 0;
895
+ for (let i = 0; i < markdown.length; i += 1) {
896
+ const char = markdown[i];
897
+ if (char !== undefined && MARKUP.has(char)) markup += 1;
898
+ if (char === '\\') i += 1;
899
+ else if (char === '[') square += 1;
900
+ else if (char === '(') round += 1;
901
+ else if (char === ']') {
902
+ if (square > 0) square -= 1;
903
+ else unclosed += 1;
904
+ } else if (char === ')') {
905
+ if (round > 0) round -= 1;
906
+ else unclosed += 1;
907
+ }
908
+ }
909
+ return square + round + unclosed > MAX_UNCLOSED || markup > MAX_MARKUP;
910
+ }
911
+
912
+ const MAX_DESCRIPTION = 160;
913
+
914
+ // The text up to and including the first full stop that ends a sentence, or
915
+ // all of it; untruncated, so it can be compared with the paragraph it came from.
916
+ function leadingSentence(text: string): string | undefined {
917
+ if (!text) return undefined;
918
+ const end = /\.\s/.exec(text);
919
+ return end ? text.slice(0, end.index + 1) : text;
920
+ }
921
+
922
+ function firstSentence(text: string): string | undefined {
923
+ const sentence = leadingSentence(text);
924
+ return sentence && truncate(sentence, MAX_DESCRIPTION);
925
+ }