@mintlify/scraping 4.0.1191 → 4.0.1193

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,236 @@
1
+ import type { Element, ElementContent, Root } from 'hast';
2
+ import path from 'node:path';
3
+
4
+ import { htmlToHast } from '../pipeline/root.js';
5
+ import type { SdkNavGroup } from './types.js';
6
+
7
+ export type SphinxNavigationDoc = {
8
+ slug: string;
9
+ title: string;
10
+ body: string;
11
+ tag?: string;
12
+ toc?: string;
13
+ parents?: { link: string }[];
14
+ };
15
+
16
+ export function buildSphinxNavigation(docs: SphinxNavigationDoc[]): SdkNavGroup[] {
17
+ const bySlug = new Map(docs.map((doc) => [doc.slug, doc]));
18
+ const children = new Map<string, string[]>();
19
+ const parents = new Map<string, string[]>();
20
+ const guideContent = new Set<string>();
21
+
22
+ function addChild(parent: string, child: string): void {
23
+ if (parent === child) return;
24
+ const siblings = children.get(parent) ?? [];
25
+ if (!siblings.includes(child)) siblings.push(child);
26
+ children.set(parent, siblings);
27
+ const ancestors = parents.get(child) ?? [];
28
+ if (!ancestors.includes(parent)) ancestors.push(parent);
29
+ parents.set(child, ancestors);
30
+ }
31
+
32
+ for (const doc of docs) {
33
+ const body = htmlToHast(doc.body);
34
+ if (hasGuideContent(body)) guideContent.add(doc.slug);
35
+ const wrappers = toctreeWrappers(body);
36
+ const sources: (Root | Element)[] = wrappers;
37
+ if (!sources.length && doc.toc) sources.push(htmlToHast(doc.toc));
38
+
39
+ function readTree(node: Root | Element, parentSlug: string): void {
40
+ let nextParent = parentSlug;
41
+ if (node.type === 'element' && node.tagName === 'li') {
42
+ const href = firstPageHref(node);
43
+ const child = href ? resolvePageHref(href, doc.slug, bySlug) : undefined;
44
+ if (child) {
45
+ addChild(parentSlug, child);
46
+ nextParent = child;
47
+ }
48
+ }
49
+ for (const child of node.children) {
50
+ if (child.type === 'element') readTree(child, nextParent);
51
+ }
52
+ }
53
+
54
+ for (const source of sources) readTree(source, doc.slug);
55
+ }
56
+
57
+ for (const doc of docs) {
58
+ if (parents.has(doc.slug)) continue;
59
+ const ancestor = doc.parents
60
+ ?.map((parent) => resolvePageHref(parent.link, doc.slug, bySlug))
61
+ .filter((slug): slug is string => slug !== undefined)
62
+ .pop();
63
+ if (ancestor) addChild(ancestor, doc.slug);
64
+ }
65
+
66
+ const prefixCounts = new Map<string, number>();
67
+ for (const doc of docs) {
68
+ if (parents.has(doc.slug)) continue;
69
+ for (const prefix of ancestorPrefixes(doc.slug)) {
70
+ prefixCounts.set(prefix, (prefixCounts.get(prefix) ?? 0) + 1);
71
+ }
72
+ }
73
+ const virtual = new Set<string>();
74
+ for (const [prefix, count] of prefixCounts) {
75
+ if (count < 2 || bySlug.has(prefix)) continue;
76
+ virtual.add(prefix);
77
+ bySlug.set(prefix, { slug: prefix, title: path.posix.basename(prefix), body: '' });
78
+ }
79
+ const navigationDocs = [...bySlug.values()].sort((left, right) =>
80
+ left.slug.localeCompare(right.slug)
81
+ );
82
+ for (const doc of navigationDocs) {
83
+ if (parents.has(doc.slug)) continue;
84
+ const ancestor = ancestorPrefixes(doc.slug)
85
+ .reverse()
86
+ .find((prefix) => bySlug.has(prefix));
87
+ if (ancestor) addChild(ancestor, doc.slug);
88
+ }
89
+
90
+ const reference = new Set(docs.filter((doc) => doc.tag).map((doc) => doc.slug));
91
+ let changed = true;
92
+ while (changed) {
93
+ changed = false;
94
+ for (const doc of navigationDocs) {
95
+ const descendants = children.get(doc.slug) ?? [];
96
+ if (
97
+ !reference.has(doc.slug) &&
98
+ descendants.length &&
99
+ descendants.every((slug) => reference.has(slug)) &&
100
+ !guideContent.has(doc.slug)
101
+ ) {
102
+ reference.add(doc.slug);
103
+ changed = true;
104
+ }
105
+ }
106
+ }
107
+
108
+ const orderedDocs = [...navigationDocs].sort((left, right) => {
109
+ if (left.slug === 'index') return -1;
110
+ if (right.slug === 'index') return 1;
111
+ return left.slug.localeCompare(right.slug);
112
+ });
113
+ function entries(
114
+ slug: string,
115
+ category: boolean,
116
+ emitted: Set<string>
117
+ ): (string | SdkNavGroup)[] {
118
+ if (emitted.has(slug) || (!virtual.has(slug) && reference.has(slug) !== category)) return [];
119
+ const doc = bySlug.get(slug);
120
+ if (!doc) return [];
121
+ emitted.add(slug);
122
+ const nested = (children.get(slug) ?? []).flatMap((child) => entries(child, category, emitted));
123
+ if (virtual.has(slug)) {
124
+ if (nested.length < 2) return nested;
125
+ return [{ group: doc.title, pages: nested }];
126
+ }
127
+ if (!nested.length) return [slug];
128
+ return [{ group: doc.title, pages: [slug, ...nested] }];
129
+ }
130
+
131
+ const groups: SdkNavGroup[] = [];
132
+ for (const category of [false, true]) {
133
+ const emitted = new Set<string>();
134
+ const pages: (string | SdkNavGroup)[] = [];
135
+ const categoryDocs = orderedDocs.filter(
136
+ (doc) => virtual.has(doc.slug) || reference.has(doc.slug) === category
137
+ );
138
+ const roots = categoryDocs.filter(
139
+ (doc) =>
140
+ !(parents.get(doc.slug) ?? []).some(
141
+ (slug) => virtual.has(slug) || reference.has(slug) === category
142
+ )
143
+ );
144
+ for (const doc of [...roots, ...categoryDocs])
145
+ pages.push(...entries(doc.slug, category, emitted));
146
+ if (pages.length) {
147
+ groups.push({ group: category ? 'API Reference' : 'Getting Started', pages });
148
+ }
149
+ }
150
+ return groups;
151
+ }
152
+
153
+ function ancestorPrefixes(slug: string): string[] {
154
+ return [...slug.matchAll(/[/.]/g)].map((match) => slug.slice(0, match.index));
155
+ }
156
+
157
+ function toctreeWrappers(node: Root | Element): Element[] {
158
+ const wrappers: Element[] = [];
159
+ for (const child of node.children) {
160
+ if (child.type !== 'element') continue;
161
+ if (classes(child).includes('toctree-wrapper')) wrappers.push(child);
162
+ else wrappers.push(...toctreeWrappers(child));
163
+ }
164
+ return wrappers;
165
+ }
166
+
167
+ function firstPageHref(node: Element): string | undefined {
168
+ for (const child of node.children) {
169
+ if (child.type !== 'element' || ['ul', 'ol'].includes(child.tagName)) continue;
170
+ if (child.tagName === 'a' && typeof child.properties.href === 'string') {
171
+ return child.properties.href;
172
+ }
173
+ const href = firstPageHref(child);
174
+ if (href) return href;
175
+ }
176
+ return undefined;
177
+ }
178
+
179
+ function resolvePageHref(
180
+ href: string,
181
+ fromSlug: string,
182
+ docs: Map<string, SphinxNavigationDoc>
183
+ ): string | undefined {
184
+ if (!href || href.includes('#') || /^(?:[a-z][\w+.-]*:|\/\/)/i.test(href)) return;
185
+ let target = href.split('?')[0] ?? '';
186
+ try {
187
+ target = decodeURIComponent(target);
188
+ } catch {
189
+ return;
190
+ }
191
+ let candidates: string[];
192
+ if (target.startsWith('/')) candidates = [target.slice(1)];
193
+ else {
194
+ const fileRelative = path.posix.join(path.posix.dirname(fromSlug), target);
195
+ const directoryRelative = path.posix.join(fromSlug, target);
196
+ if (target.endsWith('/') && fromSlug !== 'index' && !fromSlug.endsWith('/index')) {
197
+ candidates = [directoryRelative, fileRelative];
198
+ } else {
199
+ candidates = [fileRelative, directoryRelative];
200
+ }
201
+ }
202
+ for (const candidate of candidates) {
203
+ let slug = path.posix
204
+ .normalize(candidate)
205
+ .replace(/\.(?:html?|fjson)$/, '')
206
+ .replace(/\/$/, '');
207
+ if (!slug || slug === '.') slug = 'index';
208
+ if (docs.has(slug)) return slug;
209
+ const index = path.posix.join(slug, 'index');
210
+ if (docs.has(index)) return index;
211
+ }
212
+ return;
213
+ }
214
+
215
+ function hasGuideContent(node: Root | Element): boolean {
216
+ if (node.type === 'element') {
217
+ if (classes(node).includes('toctree-wrapper')) return false;
218
+ if (['p', 'pre', 'table', 'dl'].includes(node.tagName)) {
219
+ return node.children.some((child) => textContent(child).trim());
220
+ }
221
+ }
222
+ return node.children.some((child) => child.type === 'element' && hasGuideContent(child));
223
+ }
224
+
225
+ function textContent(node: ElementContent): string {
226
+ if (node.type === 'text') return node.value;
227
+ if (node.type !== 'element') return '';
228
+ return node.children.map(textContent).join('');
229
+ }
230
+
231
+ function classes(node: Element): string[] {
232
+ const value = node.properties.className;
233
+ if (Array.isArray(value)) return value.map(String);
234
+ if (typeof value === 'string') return value.split(/\s+/);
235
+ return [];
236
+ }
@@ -0,0 +1,233 @@
1
+ import { hastToText } from '@mintlify/common';
2
+ import type { Element, ElementContent } from 'hast';
3
+
4
+ import { inlineCode, linkedCode, responseField, safeHref, type LinkedToken } from './mdx.js';
5
+
6
+ type DocumentedTypes = {
7
+ parameters: Map<string, LinkedToken[][]>;
8
+ returns: LinkedToken[][];
9
+ };
10
+
11
+ export function renderSphinxLinkedCode(element: Element): string | undefined {
12
+ return linkedCode(trimTokens(linkedTokens(element.children)));
13
+ }
14
+
15
+ export function renderSphinxSignature(signature: Element, description?: Element): string {
16
+ const code = hastToText(signature).replace(/\s+/g, ' ').trim();
17
+ const fence = '`'.repeat(Math.max(3, ...(code.match(/`+/g) ?? []).map((run) => run.length + 1)));
18
+ const sections = [`${fence}python\n${code}\n${fence}`];
19
+ const documented = documentedTypes(description);
20
+ const parameters = findElements(signature, 'sig-param')
21
+ .map((parameter) => renderParameter(parameter, documented))
22
+ .filter((field) => field !== undefined);
23
+ if (parameters.length) sections.push('**Parameters**', ...parameters);
24
+ const returns = findElements(signature, 'sig-return-typehint')[0];
25
+ if (returns) {
26
+ const tokens = trimTokens(linkedTokens(returns.children));
27
+ const text = tokens.map((token) => token.text).join('');
28
+ if (text && !documented.returns.some((type) => sameLinkedType(tokens, type))) {
29
+ sections.push(`**Returns:** ${linkedCode(tokens) ?? inlineCode(text)}`);
30
+ }
31
+ }
32
+ return sections.join('\n\n');
33
+ }
34
+
35
+ function renderParameter(parameter: Element, documented: DocumentedTypes): string | undefined {
36
+ const tokens = linkedTokens(parameter.children);
37
+ const text = tokens.map((token) => token.text).join('');
38
+ const colon = topLevelDelimiter(text, ':');
39
+ if (colon === -1) return undefined;
40
+ const name = text.slice(0, colon).trim();
41
+ if (!name) return undefined;
42
+ const defaultStart = topLevelDelimiter(text, '=');
43
+ const end = defaultStart === -1 ? text.length : defaultStart;
44
+ const typeTokens = trimTokens(sliceTokens(tokens, colon + 1, end));
45
+ const type = typeTokens.map((token) => token.text).join('');
46
+ if (!type) return undefined;
47
+ if (documented.parameters.get(name)?.some((type) => sameLinkedType(typeTokens, type))) {
48
+ return undefined;
49
+ }
50
+ const only = typeTokens[0];
51
+ const typeHref = typeTokens.length === 1 ? only?.href : undefined;
52
+ let body: string | undefined;
53
+ if (!typeHref || type.length > 80) {
54
+ const linked = linkedCode(typeTokens);
55
+ if (linked) body = `**Type:** ${linked}`;
56
+ else if (type.length > 80) body = `**Type:** ${inlineCode(type)}`;
57
+ }
58
+ return responseField(name, type, defaultStart !== -1 || name.startsWith('*'), body, typeHref);
59
+ }
60
+
61
+ function documentedTypes(description: Element | undefined): DocumentedTypes {
62
+ const documented: DocumentedTypes = { parameters: new Map(), returns: [] };
63
+ if (!description) return documented;
64
+ for (const fieldList of ownFieldLists(description)) {
65
+ let label = '';
66
+ for (const child of fieldList.children) {
67
+ if (child.type !== 'element') continue;
68
+ if (child.tagName === 'dt') {
69
+ label = hastToText(child).trim().replace(/:\s*$/, '').replace(/\s+/g, ' ').toLowerCase();
70
+ } else if (child.tagName === 'dd') {
71
+ if (label === 'return type' || label === 'rtype') {
72
+ documented.returns.push(trimTokens(linkedTokens(child.children)));
73
+ } else if (label === 'parameter' || label === 'parameters') {
74
+ addDocumentedParameters(child, documented.parameters);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ return documented;
80
+ }
81
+
82
+ function ownFieldLists(description: Element): Element[] {
83
+ const lists: Element[] = [];
84
+ for (const child of description.children) {
85
+ if (child.type !== 'element') continue;
86
+ const classes = child.properties.className;
87
+ const names = Array.isArray(classes) ? classes.map(String) : String(classes ?? '').split(' ');
88
+ if (child.tagName === 'dl' && names.includes('py')) continue;
89
+ if (child.tagName === 'dl' && names.includes('field-list')) lists.push(child);
90
+ else lists.push(...ownFieldLists(child));
91
+ }
92
+ return lists;
93
+ }
94
+
95
+ function addDocumentedParameters(element: Element, parameters: Map<string, LinkedToken[][]>): void {
96
+ for (const child of element.children) {
97
+ if (child.type !== 'element') continue;
98
+ if (child.tagName !== 'p') {
99
+ addDocumentedParameters(child, parameters);
100
+ continue;
101
+ }
102
+ const first = child.children.find((node) => node.type !== 'text' || node.value.trim() !== '');
103
+ if (first?.type !== 'element' || first.tagName !== 'strong') continue;
104
+ const name = hastToText(first).trim();
105
+ const tokens = linkedTokens(child.children);
106
+ const text = tokens.map((token) => token.text).join('');
107
+ const nameEnd = text.indexOf(name) + name.length;
108
+ const start = text.indexOf('(', nameEnd);
109
+ if (start === -1 || text.slice(nameEnd, start).trim() !== '') continue;
110
+ const end = closingParenthesis(text, start);
111
+ if (end === -1) continue;
112
+ const types = parameters.get(name) ?? [];
113
+ types.push(trimTokens(sliceTokens(tokens, start + 1, end)));
114
+ parameters.set(name, types);
115
+ }
116
+ }
117
+
118
+ function closingParenthesis(text: string, start: number): number {
119
+ let depth = 0;
120
+ let quote: string | undefined;
121
+ let escaped = false;
122
+ for (let index = start; index < text.length; index += 1) {
123
+ const character = text[index];
124
+ if (escaped) escaped = false;
125
+ else if (quote) {
126
+ if (character === '\\') escaped = true;
127
+ else if (character === quote) quote = undefined;
128
+ } else if (character === '"' || character === "'") quote = character;
129
+ else if (character === '(') depth += 1;
130
+ else if (character === ')') {
131
+ depth -= 1;
132
+ if (depth === 0) return index;
133
+ }
134
+ }
135
+ return -1;
136
+ }
137
+
138
+ function sameLinkedType(left: LinkedToken[], right: LinkedToken[]): boolean {
139
+ const key = (tokens: LinkedToken[]) =>
140
+ JSON.stringify(
141
+ tokens
142
+ .map((token) =>
143
+ token.href ? ['href', token.href] : ['text', token.text.replace(/\s+/g, '')]
144
+ )
145
+ .filter(([, value]) => value)
146
+ );
147
+ return key(left) === key(right);
148
+ }
149
+
150
+ function linkedTokens(children: ElementContent[], href?: string): LinkedToken[] {
151
+ const tokens: LinkedToken[] = [];
152
+ for (const child of children) {
153
+ if (child.type === 'text') {
154
+ tokens.push({ text: child.value.replace(/\s+/g, ' '), href });
155
+ } else if (child.type === 'element') {
156
+ let nextHref = href;
157
+ if (child.tagName === 'a' && typeof child.properties.href === 'string') {
158
+ nextHref = safeHref(child.properties.href)?.replace(
159
+ /[\u0000-\u0020\u007f"'()[\]<>`{}\\]/g,
160
+ (character) => `%${character.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')}`
161
+ );
162
+ }
163
+ tokens.push(...linkedTokens(child.children, nextHref));
164
+ }
165
+ }
166
+ const merged: LinkedToken[] = [];
167
+ for (const token of tokens) {
168
+ const previous = merged.at(-1);
169
+ if (previous && previous.href === token.href) previous.text += token.text;
170
+ else merged.push({ ...token });
171
+ }
172
+ return merged;
173
+ }
174
+
175
+ function trimTokens(tokens: LinkedToken[]): LinkedToken[] {
176
+ const text = tokens.map((token) => token.text).join('');
177
+ return sliceTokens(tokens, text.length - text.trimStart().length, text.trimEnd().length);
178
+ }
179
+
180
+ function sliceTokens(tokens: LinkedToken[], start: number, end: number): LinkedToken[] {
181
+ const sliced: LinkedToken[] = [];
182
+ let offset = 0;
183
+ for (const token of tokens) {
184
+ const tokenEnd = offset + token.text.length;
185
+ if (tokenEnd > start && offset < end) {
186
+ sliced.push({
187
+ text: token.text.slice(Math.max(0, start - offset), end - offset),
188
+ href: token.href,
189
+ });
190
+ }
191
+ offset = tokenEnd;
192
+ }
193
+ return sliced;
194
+ }
195
+
196
+ function topLevelDelimiter(text: string, delimiter: string): number {
197
+ let depth = 0;
198
+ let quote: string | undefined;
199
+ let escaped = false;
200
+ for (let index = 0; index < text.length; index += 1) {
201
+ const character = text[index];
202
+ if (escaped) {
203
+ escaped = false;
204
+ } else if (quote) {
205
+ if (character === '\\') escaped = true;
206
+ else if (character === quote) quote = undefined;
207
+ } else if (character === '"' || character === "'") {
208
+ quote = character;
209
+ } else if (character === '(' || character === '[' || character === '{') {
210
+ depth += 1;
211
+ } else if (character === ')' || character === ']' || character === '}') {
212
+ depth = Math.max(0, depth - 1);
213
+ } else if (character === delimiter && depth === 0) {
214
+ return index;
215
+ }
216
+ }
217
+ return -1;
218
+ }
219
+
220
+ function findElements(node: Element, className: string): Element[] {
221
+ const elements: Element[] = [];
222
+ for (const child of node.children) {
223
+ if (child.type !== 'element') continue;
224
+ const classes = child.properties.className;
225
+ if (Array.isArray(classes) && classes.includes(className)) elements.push(child);
226
+ else if (typeof classes === 'string' && classes.split(' ').includes(className)) {
227
+ elements.push(child);
228
+ } else {
229
+ elements.push(...findElements(child, className));
230
+ }
231
+ }
232
+ return elements;
233
+ }