@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.
- package/__test__/sdk-sphinx-images.test.ts +79 -0
- package/__test__/sdk-sphinx-modules.test.ts +97 -0
- package/__test__/sdk-sphinx-navigation.test.ts +318 -0
- package/__test__/sdk-sphinx-signatures.test.ts +148 -0
- package/__test__/sdk-sphinx.test.ts +130 -1
- package/bin/sdk/converters/sphinx.js +202 -77
- package/bin/sdk/converters/sphinx.js.map +1 -1
- package/bin/sdk/sphinx-images.d.ts +2 -0
- package/bin/sdk/sphinx-images.js +23 -0
- package/bin/sdk/sphinx-images.js.map +1 -0
- package/bin/sdk/sphinx-modules.d.ts +5 -0
- package/bin/sdk/sphinx-modules.js +121 -0
- package/bin/sdk/sphinx-modules.js.map +1 -0
- package/bin/sdk/sphinx-navigation.d.ts +12 -0
- package/bin/sdk/sphinx-navigation.js +225 -0
- package/bin/sdk/sphinx-navigation.js.map +1 -0
- package/bin/sdk/sphinx-signatures.d.ts +3 -0
- package/bin/sdk/sphinx-signatures.js +244 -0
- package/bin/sdk/sphinx-signatures.js.map +1 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/sdk/converters/sphinx.ts +219 -89
- package/src/sdk/sphinx-images.ts +25 -0
- package/src/sdk/sphinx-modules.ts +124 -0
- package/src/sdk/sphinx-navigation.ts +236 -0
- package/src/sdk/sphinx-signatures.ts +233 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mintlify/scraping",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1193",
|
|
4
4
|
"description": "Scrape documentation frameworks to Mintlify docs",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"format:check": "oxfmt --check"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@mintlify/common": "1.0.
|
|
46
|
+
"@mintlify/common": "1.0.1315",
|
|
47
47
|
"fast-xml-parser": "5.7.3",
|
|
48
48
|
"fs-extra": "11.1.1",
|
|
49
49
|
"graphql": "16.11.0",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"zod": "3.24.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@mintlify/models": "0.0.
|
|
65
|
+
"@mintlify/models": "0.0.380",
|
|
66
66
|
"@mintlify/ts-config": "2.0.2",
|
|
67
|
-
"@mintlify/validation": "0.1.
|
|
67
|
+
"@mintlify/validation": "0.1.946",
|
|
68
68
|
"@tsconfig/recommended": "1.0.2",
|
|
69
69
|
"@types/hast": "3.0.4",
|
|
70
70
|
"@types/mdast": "4.0.4",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"optional": true
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "ab2c294d7086f0c5b03babf75baee85a9d4ab608"
|
|
91
91
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { hastToText } from '@mintlify/common';
|
|
1
|
+
import { hastToText, safeCleanHeadingId } from '@mintlify/common';
|
|
2
2
|
import fse from 'fs-extra';
|
|
3
3
|
import type { Element, ElementContent, Root as HastRoot } from 'hast';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { visit, SKIP } from 'unist-util-visit';
|
|
6
7
|
|
|
@@ -8,7 +9,12 @@ import { htmlToHast } from '../../pipeline/root.js';
|
|
|
8
9
|
import { turnChildrenIntoMdx } from '../../utils/children.js';
|
|
9
10
|
import { removeHastComments } from '../../utils/hastComments.js';
|
|
10
11
|
import { hastToMdx } from '../convert.js';
|
|
11
|
-
import
|
|
12
|
+
import { escapeInline, jsxString, safeHref } from '../mdx.js';
|
|
13
|
+
import { rewriteSphinxImages } from '../sphinx-images.js';
|
|
14
|
+
import { splitSphinxModules, type SphinxModuleDoc } from '../sphinx-modules.js';
|
|
15
|
+
import { buildSphinxNavigation, type SphinxNavigationDoc } from '../sphinx-navigation.js';
|
|
16
|
+
import { renderSphinxLinkedCode, renderSphinxSignature } from '../sphinx-signatures.js';
|
|
17
|
+
import type { SdkPage, SdkReference } from '../types.js';
|
|
12
18
|
|
|
13
19
|
const SKIP_PAGES = new Set(['genindex', 'search', 'py-modindex']);
|
|
14
20
|
const LANG_MARKER = '@@mintlang:';
|
|
@@ -26,51 +32,71 @@ const ADMONITION_TAGS: Record<string, string> = {
|
|
|
26
32
|
attention: 'Warning',
|
|
27
33
|
};
|
|
28
34
|
|
|
29
|
-
type SphinxDoc = { slug: string; title: string; body: string; tag?: string };
|
|
30
|
-
|
|
31
35
|
export async function convertSphinx(sourcePath: string): Promise<SdkReference> {
|
|
32
|
-
const docs = await loadDocs(sourcePath);
|
|
36
|
+
const docs = splitSphinxModules(await loadDocs(sourcePath));
|
|
33
37
|
const slugs = new Set(docs.map((doc) => doc.slug));
|
|
38
|
+
const anchors = collectAnchors(docs);
|
|
39
|
+
return {
|
|
40
|
+
pages: docs.map((doc) => renderPage(doc, slugs, anchors)),
|
|
41
|
+
groups: buildSphinxNavigation(docs),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
.filter((doc) => !doc.tag)
|
|
37
|
-
.sort((left, right) =>
|
|
38
|
-
left.slug === 'index' ? -1 : right.slug === 'index' ? 1 : left.slug.localeCompare(right.slug)
|
|
39
|
-
);
|
|
40
|
-
const modules = docs
|
|
41
|
-
.filter((doc) => doc.tag)
|
|
42
|
-
.sort((left, right) => left.slug.localeCompare(right.slug));
|
|
43
|
-
|
|
44
|
-
const pages = [...guides, ...modules].map((doc) => renderPage(doc, slugs));
|
|
45
|
-
const groups: SdkNavGroup[] = [];
|
|
46
|
-
if (guides.length) groups.push({ group: 'Getting Started', pages: guides.map((d) => d.slug) });
|
|
47
|
-
if (modules.length) groups.push({ group: 'API Reference', pages: modules.map((d) => d.slug) });
|
|
45
|
+
type SphinxAnchors = Map<string, Map<string, string>>;
|
|
48
46
|
|
|
49
|
-
|
|
47
|
+
function collectAnchors(docs: SphinxModuleDoc[]): SphinxAnchors {
|
|
48
|
+
const anchors: SphinxAnchors = new Map();
|
|
49
|
+
const ordered = [
|
|
50
|
+
...docs.filter((doc) => !doc.sourceSlug),
|
|
51
|
+
...docs.filter((doc) => doc.sourceSlug),
|
|
52
|
+
];
|
|
53
|
+
for (const doc of ordered) {
|
|
54
|
+
const source = doc.sourceSlug ?? doc.slug;
|
|
55
|
+
const targets = anchors.get(source) ?? new Map<string, string>();
|
|
56
|
+
visit(htmlToHast(doc.body), 'element', (node: Element) => {
|
|
57
|
+
if (typeof node.properties.id === 'string')
|
|
58
|
+
targets.set(anchorId(node.properties.id), doc.slug);
|
|
59
|
+
});
|
|
60
|
+
anchors.set(source, targets);
|
|
61
|
+
}
|
|
62
|
+
return anchors;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
|
-
async function loadDocs(sourcePath: string): Promise<
|
|
53
|
-
const docs:
|
|
65
|
+
async function loadDocs(sourcePath: string): Promise<SphinxNavigationDoc[]> {
|
|
66
|
+
const docs: SphinxNavigationDoc[] = [];
|
|
54
67
|
for (const file of await collectFjsonFiles(sourcePath)) {
|
|
55
|
-
const raw =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
current_page_name?: string;
|
|
59
|
-
};
|
|
68
|
+
const raw: unknown = await fse.readJson(file);
|
|
69
|
+
if (!raw || typeof raw !== 'object' || !('body' in raw)) continue;
|
|
70
|
+
if (typeof raw.body !== 'string' || !raw.body.trim()) continue;
|
|
60
71
|
const slug =
|
|
61
|
-
raw.current_page_name
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
'current_page_name' in raw && typeof raw.current_page_name === 'string'
|
|
73
|
+
? raw.current_page_name
|
|
74
|
+
: path
|
|
75
|
+
.relative(sourcePath, file)
|
|
76
|
+
.split(path.sep)
|
|
77
|
+
.join('/')
|
|
78
|
+
.replace(/\.fjson$/, '');
|
|
67
79
|
if (SKIP_PAGES.has(slug) || slug.startsWith('_modules')) continue;
|
|
68
|
-
|
|
80
|
+
const parents: { link: string }[] = [];
|
|
81
|
+
if ('parents' in raw && Array.isArray(raw.parents)) {
|
|
82
|
+
for (const parent of raw.parents) {
|
|
83
|
+
if (
|
|
84
|
+
parent &&
|
|
85
|
+
typeof parent === 'object' &&
|
|
86
|
+
'link' in parent &&
|
|
87
|
+
typeof parent.link === 'string'
|
|
88
|
+
) {
|
|
89
|
+
parents.push({ link: parent.link });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
69
93
|
docs.push({
|
|
70
94
|
slug,
|
|
71
|
-
title: cleanTitle(raw.title
|
|
95
|
+
title: cleanTitle('title' in raw && typeof raw.title === 'string' ? raw.title : slug),
|
|
72
96
|
body: raw.body,
|
|
73
97
|
tag: /<dl class="py[ "]/.test(raw.body) ? 'MODULE' : undefined,
|
|
98
|
+
toc: 'toc' in raw && typeof raw.toc === 'string' ? raw.toc : undefined,
|
|
99
|
+
parents,
|
|
74
100
|
});
|
|
75
101
|
}
|
|
76
102
|
return docs;
|
|
@@ -87,18 +113,38 @@ async function collectFjsonFiles(dir: string): Promise<string[]> {
|
|
|
87
113
|
return files.sort();
|
|
88
114
|
}
|
|
89
115
|
|
|
90
|
-
function renderPage(doc:
|
|
116
|
+
function renderPage(doc: SphinxModuleDoc, slugs: Set<string>, anchors: SphinxAnchors): SdkPage {
|
|
91
117
|
const hast = htmlToHast(doc.body);
|
|
118
|
+
const generated = new Map<string, string>();
|
|
119
|
+
const prefix = `MINTSPHINX${randomUUID().replaceAll('-', '')}`;
|
|
120
|
+
function marker(content: string): string {
|
|
121
|
+
const marker = `${prefix}BLOCK${generated.size}END`;
|
|
122
|
+
generated.set(marker, content);
|
|
123
|
+
return marker;
|
|
124
|
+
}
|
|
125
|
+
function block(content: string): Element {
|
|
126
|
+
return {
|
|
127
|
+
type: 'element',
|
|
128
|
+
tagName: 'p',
|
|
129
|
+
properties: {},
|
|
130
|
+
children: [{ type: 'text', value: marker(content) }],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
92
133
|
removeHastComments(hast);
|
|
93
|
-
removeSphinxChrome(hast);
|
|
134
|
+
removeSphinxChrome(hast, doc.title);
|
|
94
135
|
const description = extractDescription(hast);
|
|
95
|
-
rewriteLinks(hast, doc
|
|
96
|
-
|
|
136
|
+
rewriteLinks(hast, doc, slugs, anchors);
|
|
137
|
+
rewriteSphinxImages(hast, doc.sourceSlug ?? doc.slug, doc.slug);
|
|
97
138
|
markHighlightLanguages(hast);
|
|
98
|
-
|
|
99
|
-
|
|
139
|
+
preserveAnchors(hast, block);
|
|
140
|
+
transformPyDls(hast, 0, block);
|
|
141
|
+
preserveLinkedCode(hast, marker);
|
|
142
|
+
transformAdmonitions(hast);
|
|
143
|
+
let content = hastToMdx(hast)
|
|
100
144
|
.replace(FENCE_MARKER, '$1$2\n')
|
|
101
145
|
.replace(/^[> ]*@@mintlang:[\w+-]+@@\n?/gm, '');
|
|
146
|
+
for (const [marker, replacement] of generated)
|
|
147
|
+
content = content.replaceAll(marker, () => replacement);
|
|
102
148
|
return {
|
|
103
149
|
slug: doc.slug,
|
|
104
150
|
title: doc.title,
|
|
@@ -108,8 +154,7 @@ function renderPage(doc: SphinxDoc, slugs: Set<string>): SdkPage {
|
|
|
108
154
|
};
|
|
109
155
|
}
|
|
110
156
|
|
|
111
|
-
function removeSphinxChrome(hast: HastRoot): void {
|
|
112
|
-
let removedH1 = false;
|
|
157
|
+
function removeSphinxChrome(hast: HastRoot, title: string): void {
|
|
113
158
|
visit(hast, 'element', (node: Element, index, parent) => {
|
|
114
159
|
if (!parent || typeof index !== 'number') return;
|
|
115
160
|
const isHeaderlink = node.tagName === 'a' && classList(node).includes('headerlink');
|
|
@@ -118,33 +163,120 @@ function removeSphinxChrome(hast: HastRoot): void {
|
|
|
118
163
|
node.children.some(
|
|
119
164
|
(child) => child.type === 'element' && classList(child).includes('viewcode-link')
|
|
120
165
|
);
|
|
121
|
-
|
|
122
|
-
if (isFirstH1) removedH1 = true;
|
|
123
|
-
if (isHeaderlink || isViewcode || isFirstH1) {
|
|
166
|
+
if (isHeaderlink || isViewcode) {
|
|
124
167
|
parent.children.splice(index, 1);
|
|
125
168
|
return index;
|
|
126
169
|
}
|
|
127
170
|
});
|
|
171
|
+
let removedTitle = false;
|
|
172
|
+
visit(hast, 'element', (node: Element, index, parent) => {
|
|
173
|
+
if (removedTitle || !parent || typeof index !== 'number') return;
|
|
174
|
+
if (
|
|
175
|
+
node.tagName !== 'h1' &&
|
|
176
|
+
!(/^h[1-6]$/.test(node.tagName) && cleanTitle(hastToText(node)) === title)
|
|
177
|
+
)
|
|
178
|
+
return;
|
|
179
|
+
removedTitle = true;
|
|
180
|
+
parent.children.splice(index, 1);
|
|
181
|
+
return index;
|
|
182
|
+
});
|
|
128
183
|
}
|
|
129
184
|
|
|
130
|
-
function rewriteLinks(
|
|
185
|
+
function rewriteLinks(
|
|
186
|
+
hast: HastRoot,
|
|
187
|
+
doc: SphinxModuleDoc,
|
|
188
|
+
slugs: Set<string>,
|
|
189
|
+
anchors: SphinxAnchors
|
|
190
|
+
): void {
|
|
131
191
|
visit(hast, 'element', (node: Element) => {
|
|
132
192
|
if (node.tagName !== 'a' || typeof node.properties.href !== 'string') return;
|
|
133
|
-
const rewritten = rewriteHref(node.properties.href,
|
|
193
|
+
const rewritten = rewriteHref(node.properties.href, doc, slugs, anchors);
|
|
134
194
|
if (rewritten === undefined) delete node.properties.href;
|
|
135
195
|
else node.properties.href = rewritten;
|
|
136
196
|
});
|
|
137
197
|
}
|
|
138
198
|
|
|
139
|
-
function rewriteHref(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
199
|
+
function rewriteHref(
|
|
200
|
+
href: string,
|
|
201
|
+
doc: SphinxModuleDoc,
|
|
202
|
+
slugs: Set<string>,
|
|
203
|
+
anchors: SphinxAnchors
|
|
204
|
+
): string | undefined {
|
|
205
|
+
const fromSlug = doc.sourceSlug ?? doc.slug;
|
|
206
|
+
const safe = safeHref(href);
|
|
207
|
+
if (safe === undefined || /^(https?:|mailto:)/i.test(safe)) return safe;
|
|
208
|
+
const hashIndex = safe.indexOf('#');
|
|
209
|
+
const hash = hashIndex < 0 ? '' : `#${anchorId(safe.slice(hashIndex + 1))}`;
|
|
210
|
+
const beforeHash = hashIndex < 0 ? safe : safe.slice(0, hashIndex);
|
|
211
|
+
const queryIndex = beforeHash.indexOf('?');
|
|
212
|
+
const query = queryIndex < 0 ? '' : beforeHash.slice(queryIndex);
|
|
213
|
+
let target = queryIndex < 0 ? beforeHash : beforeHash.slice(0, queryIndex);
|
|
214
|
+
function destination(slug: string): string {
|
|
215
|
+
let owner = slug;
|
|
216
|
+
if (hash.length > 1) owner = anchors.get(slug)?.get(hash.slice(1)) ?? slug;
|
|
217
|
+
if (!target && owner === doc.slug) return `${query}${hash}`;
|
|
218
|
+
return `/${owner}${query}${hash}`;
|
|
219
|
+
}
|
|
220
|
+
if (!target) return destination(fromSlug);
|
|
221
|
+
try {
|
|
222
|
+
target = decodeURIComponent(target);
|
|
223
|
+
} catch {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const base = path.posix.dirname(fromSlug);
|
|
227
|
+
let candidates = [path.posix.join(base, target), path.posix.join(fromSlug, target)];
|
|
228
|
+
if (target.endsWith('/') && path.posix.basename(fromSlug) !== 'index') candidates.reverse();
|
|
229
|
+
if (target.startsWith('/')) candidates = [target.slice(1)];
|
|
230
|
+
for (const candidate of candidates) {
|
|
231
|
+
let resolved = path.posix
|
|
232
|
+
.normalize(candidate)
|
|
233
|
+
.replace(/\.(?:html?|fjson)$/, '')
|
|
234
|
+
.replace(/\/$/, '');
|
|
235
|
+
if (resolved === '.' || resolved === '') resolved = 'index';
|
|
236
|
+
if (slugs.has(resolved)) return destination(resolved);
|
|
237
|
+
const index = path.posix.join(resolved, 'index');
|
|
238
|
+
if (slugs.has(index)) return destination(index);
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function anchorId(value: string): string {
|
|
244
|
+
return safeCleanHeadingId(value)
|
|
245
|
+
.replace(/[<>"&]/g, '')
|
|
246
|
+
.replace(/\s+/g, '-');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function preserveAnchors(node: HastRoot | Element, block: (content: string) => Element): void {
|
|
250
|
+
const children: ElementContent[] = [];
|
|
251
|
+
for (const child of node.children) {
|
|
252
|
+
if (child.type === 'element') {
|
|
253
|
+
const id = child.properties.id;
|
|
254
|
+
if (typeof id === 'string' && !(child.tagName === 'dt' && classList(child).includes('sig'))) {
|
|
255
|
+
children.push(block(`<span id=${jsxString(anchorId(id))} />`));
|
|
256
|
+
}
|
|
257
|
+
preserveAnchors(child, block);
|
|
258
|
+
}
|
|
259
|
+
if (child.type !== 'doctype' && child.type !== 'mdxjsEsm') children.push(child);
|
|
260
|
+
}
|
|
261
|
+
node.children = children;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function preserveLinkedCode(node: HastRoot | Element, marker: (content: string) => string): void {
|
|
265
|
+
const children: ElementContent[] = [];
|
|
266
|
+
for (const child of node.children) {
|
|
267
|
+
if (child.type === 'element') {
|
|
268
|
+
if (child.tagName === 'code') {
|
|
269
|
+
const content = renderSphinxLinkedCode(child);
|
|
270
|
+
if (content) {
|
|
271
|
+
children.push({ type: 'text', value: marker(content) });
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (child.tagName !== 'pre') preserveLinkedCode(child, marker);
|
|
276
|
+
}
|
|
277
|
+
if (child.type !== 'doctype' && child.type !== 'mdxjsEsm') children.push(child);
|
|
278
|
+
}
|
|
279
|
+
node.children = children;
|
|
148
280
|
}
|
|
149
281
|
|
|
150
282
|
function transformAdmonitions(hast: HastRoot): void {
|
|
@@ -176,30 +308,48 @@ function markHighlightLanguages(hast: HastRoot): void {
|
|
|
176
308
|
});
|
|
177
309
|
}
|
|
178
310
|
|
|
179
|
-
function transformPyDls(
|
|
311
|
+
function transformPyDls(
|
|
312
|
+
node: HastRoot | Element,
|
|
313
|
+
depth: number,
|
|
314
|
+
block: (content: string) => Element
|
|
315
|
+
): void {
|
|
180
316
|
const children: ElementContent[] = [];
|
|
181
|
-
for (const child of node.children
|
|
317
|
+
for (const child of node.children) {
|
|
182
318
|
if (child.type === 'element' && child.tagName === 'dl' && classList(child).includes('py')) {
|
|
183
|
-
children.push(...explodePyDl(child, depth));
|
|
319
|
+
children.push(...explodePyDl(child, depth, block));
|
|
184
320
|
} else {
|
|
185
|
-
if (child.type === 'element') transformPyDls(child, depth);
|
|
186
|
-
children.push(child);
|
|
321
|
+
if (child.type === 'element') transformPyDls(child, depth, block);
|
|
322
|
+
if (child.type !== 'doctype' && child.type !== 'mdxjsEsm') children.push(child);
|
|
187
323
|
}
|
|
188
324
|
}
|
|
189
325
|
node.children = children;
|
|
190
326
|
}
|
|
191
327
|
|
|
192
|
-
function explodePyDl(
|
|
328
|
+
function explodePyDl(
|
|
329
|
+
dl: Element,
|
|
330
|
+
depth: number,
|
|
331
|
+
block: (content: string) => Element
|
|
332
|
+
): ElementContent[] {
|
|
193
333
|
const callable = classList(dl).some((name) => ['method', 'function'].includes(name));
|
|
194
334
|
const out: ElementContent[] = [];
|
|
195
|
-
for (const child of dl.children) {
|
|
335
|
+
for (const [index, child] of dl.children.entries()) {
|
|
196
336
|
if (child.type !== 'element') continue;
|
|
197
337
|
if (child.tagName === 'dt' && classList(child).includes('sig')) {
|
|
198
338
|
const name = findDescName(child);
|
|
199
|
-
if (name)
|
|
200
|
-
|
|
339
|
+
if (name) {
|
|
340
|
+
const title = escapeInline(callable ? `${name}()` : name);
|
|
341
|
+
const id = child.properties.id;
|
|
342
|
+
const suffix = typeof id === 'string' ? ` {#${anchorId(id)}}` : '';
|
|
343
|
+
out.push(block(`${'#'.repeat(Math.min(3 + depth, 6))} ${title}${suffix}`));
|
|
344
|
+
}
|
|
345
|
+
const description = dl.children
|
|
346
|
+
.slice(index + 1)
|
|
347
|
+
.find(
|
|
348
|
+
(sibling): sibling is Element => sibling.type === 'element' && sibling.tagName === 'dd'
|
|
349
|
+
);
|
|
350
|
+
out.push(block(renderSphinxSignature(child, description)));
|
|
201
351
|
} else if (child.tagName === 'dd') {
|
|
202
|
-
transformPyDls(child, depth + 1);
|
|
352
|
+
transformPyDls(child, depth + 1, block);
|
|
203
353
|
out.push(...child.children);
|
|
204
354
|
}
|
|
205
355
|
}
|
|
@@ -216,24 +366,6 @@ function findDescName(node: Element): string | undefined {
|
|
|
216
366
|
return name;
|
|
217
367
|
}
|
|
218
368
|
|
|
219
|
-
function heading(level: number, text: string): Element {
|
|
220
|
-
return {
|
|
221
|
-
type: 'element',
|
|
222
|
-
tagName: `h${level}`,
|
|
223
|
-
properties: {},
|
|
224
|
-
children: [{ type: 'text', value: text }],
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function pythonCodeBlock(code: string): Element {
|
|
229
|
-
return {
|
|
230
|
-
type: 'element',
|
|
231
|
-
tagName: 'pre',
|
|
232
|
-
properties: {},
|
|
233
|
-
children: [{ type: 'text', value: `${LANG_MARKER}python@@\n${code}` }],
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
369
|
function extractDescription(hast: HastRoot): string | undefined {
|
|
238
370
|
let description: string | undefined;
|
|
239
371
|
visit(hast, 'element', (node: Element, _index, parent) => {
|
|
@@ -249,11 +381,9 @@ function extractDescription(hast: HastRoot): string | undefined {
|
|
|
249
381
|
|
|
250
382
|
function classList(node: Element): string[] {
|
|
251
383
|
const value = node.properties.className;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
? value.split(' ')
|
|
256
|
-
: [];
|
|
384
|
+
if (Array.isArray(value)) return value.map(String);
|
|
385
|
+
if (typeof value === 'string') return value.split(' ');
|
|
386
|
+
return [];
|
|
257
387
|
}
|
|
258
388
|
|
|
259
389
|
function cleanTitle(title: string): string {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Element, Root } from 'hast';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { visit } from 'unist-util-visit';
|
|
4
|
+
|
|
5
|
+
import { safeHref } from './mdx.js';
|
|
6
|
+
|
|
7
|
+
export function rewriteSphinxImages(hast: Root, sourceSlug: string, destinationSlug: string): void {
|
|
8
|
+
const base = path.posix.relative(
|
|
9
|
+
path.posix.dirname(destinationSlug),
|
|
10
|
+
path.posix.dirname(sourceSlug)
|
|
11
|
+
);
|
|
12
|
+
if (!base) return;
|
|
13
|
+
visit(hast, 'element', (node: Element) => {
|
|
14
|
+
if (node.tagName !== 'img' || typeof node.properties.src !== 'string') return;
|
|
15
|
+
const href = safeHref(node.properties.src);
|
|
16
|
+
if (!href || href.startsWith('/') || href.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(href))
|
|
17
|
+
return;
|
|
18
|
+
const suffixStart = href.search(/[?#]/);
|
|
19
|
+
const file = suffixStart === -1 ? href : href.slice(0, suffixStart);
|
|
20
|
+
if (!file) return;
|
|
21
|
+
const suffix = suffixStart === -1 ? '' : href.slice(suffixStart);
|
|
22
|
+
const relative = path.posix.join(base, file);
|
|
23
|
+
node.properties.src = `${relative.startsWith('../') ? relative : `./${relative}`}${suffix}`;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Element, ElementContent, Root } from 'hast';
|
|
2
|
+
|
|
3
|
+
import { htmlToHast } from '../pipeline/root.js';
|
|
4
|
+
import type { SphinxNavigationDoc } from './sphinx-navigation.js';
|
|
5
|
+
|
|
6
|
+
export type SphinxModuleDoc = SphinxNavigationDoc & { sourceSlug?: string };
|
|
7
|
+
|
|
8
|
+
type ModuleSection = { node: Element; name: string; start: number; end: number };
|
|
9
|
+
|
|
10
|
+
export function splitSphinxModules(docs: SphinxNavigationDoc[]): SphinxModuleDoc[] {
|
|
11
|
+
const occupied = new Set(docs.map((doc) => doc.slug));
|
|
12
|
+
const result: SphinxModuleDoc[] = [];
|
|
13
|
+
for (const doc of docs) {
|
|
14
|
+
const sections = moduleSections(htmlToHast(doc.body)).sort(
|
|
15
|
+
(left, right) => left.start - right.start
|
|
16
|
+
);
|
|
17
|
+
const slugs = sections.map((section) => `${doc.slug}/${section.name}`);
|
|
18
|
+
if (
|
|
19
|
+
sections.length < 2 ||
|
|
20
|
+
new Set(slugs).size !== slugs.length ||
|
|
21
|
+
slugs.some((slug) => occupied.has(slug))
|
|
22
|
+
) {
|
|
23
|
+
result.push(doc);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let body = '';
|
|
28
|
+
let cursor = 0;
|
|
29
|
+
const aliases = new Set<string>();
|
|
30
|
+
const modules: SphinxModuleDoc[] = [];
|
|
31
|
+
for (const section of sections) {
|
|
32
|
+
const slug = `${doc.slug}/${section.name}`;
|
|
33
|
+
const title = sectionTitle(section.node) || section.name;
|
|
34
|
+
body += doc.body.slice(cursor, section.start);
|
|
35
|
+
for (const id of sectionIds(section.node)) {
|
|
36
|
+
if (aliases.has(id)) continue;
|
|
37
|
+
aliases.add(id);
|
|
38
|
+
body += `<span id="${escapeHtml(id)}"></span>`;
|
|
39
|
+
}
|
|
40
|
+
body += `<div class="toctree-wrapper"><ul><li><a class="reference internal" href="/${escapeHtml(slug)}">${escapeHtml(title)}</a></li></ul></div>`;
|
|
41
|
+
cursor = section.end;
|
|
42
|
+
occupied.add(slug);
|
|
43
|
+
modules.push({
|
|
44
|
+
slug,
|
|
45
|
+
title,
|
|
46
|
+
body: doc.body.slice(section.start, section.end),
|
|
47
|
+
tag: 'MODULE',
|
|
48
|
+
sourceSlug: doc.slug,
|
|
49
|
+
parents: [{ link: `/${doc.slug}` }],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
body += doc.body.slice(cursor);
|
|
53
|
+
result.push({ ...doc, body, tag: 'MODULE' }, ...modules);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function moduleSections(node: Root | Element): ModuleSection[] {
|
|
59
|
+
const peers: ModuleSection[] = [];
|
|
60
|
+
const containers: Element[] = [];
|
|
61
|
+
for (const child of node.children) {
|
|
62
|
+
if (child.type !== 'element') continue;
|
|
63
|
+
const name = moduleName(child);
|
|
64
|
+
if (name) {
|
|
65
|
+
const start = child.position?.start.offset;
|
|
66
|
+
const end = child.position?.end.offset;
|
|
67
|
+
if (typeof start === 'number' && typeof end === 'number') {
|
|
68
|
+
peers.push({ node: child, name, start, end });
|
|
69
|
+
}
|
|
70
|
+
} else containers.push(child);
|
|
71
|
+
}
|
|
72
|
+
const sections = containers.flatMap(moduleSections);
|
|
73
|
+
if (peers.length > 1) return [...peers, ...sections];
|
|
74
|
+
return sections;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function moduleName(node: Element): string | undefined {
|
|
78
|
+
const id = node.properties.id;
|
|
79
|
+
if (typeof id !== 'string' || !id.startsWith('module-')) return;
|
|
80
|
+
if (node.tagName !== 'section' && node.tagName !== 'div') return;
|
|
81
|
+
if (node.tagName === 'div' && !classes(node).includes('section')) return;
|
|
82
|
+
const name = id.slice('module-'.length);
|
|
83
|
+
if (!/^[\p{ID_Start}_][\p{ID_Continue}]*(?:\.[\p{ID_Start}_][\p{ID_Continue}]*)*$/u.test(name))
|
|
84
|
+
return;
|
|
85
|
+
return name;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sectionTitle(node: Element): string | undefined {
|
|
89
|
+
for (const child of node.children) {
|
|
90
|
+
if (child.type !== 'element') continue;
|
|
91
|
+
if (/^h[1-6]$/.test(child.tagName)) return textContent(child).replace(/\s+/g, ' ').trim();
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sectionIds(node: Element): string[] {
|
|
97
|
+
const ids: string[] = [];
|
|
98
|
+
if (typeof node.properties.id === 'string') ids.push(node.properties.id);
|
|
99
|
+
for (const child of node.children) {
|
|
100
|
+
if (child.type === 'element') ids.push(...sectionIds(child));
|
|
101
|
+
}
|
|
102
|
+
return ids;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function textContent(node: ElementContent): string {
|
|
106
|
+
if (node.type === 'text') return node.value;
|
|
107
|
+
if (node.type !== 'element' || classes(node).includes('headerlink')) return '';
|
|
108
|
+
return node.children.map(textContent).join('');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function classes(node: Element): string[] {
|
|
112
|
+
const value = node.properties.className;
|
|
113
|
+
if (Array.isArray(value)) return value.map(String);
|
|
114
|
+
if (typeof value === 'string') return value.split(/\s+/);
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function escapeHtml(value: string): string {
|
|
119
|
+
return value
|
|
120
|
+
.replaceAll('&', '&')
|
|
121
|
+
.replaceAll('<', '<')
|
|
122
|
+
.replaceAll('>', '>')
|
|
123
|
+
.replaceAll('"', '"');
|
|
124
|
+
}
|