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