@mintlify/scraping 4.0.906 → 4.0.908
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__test__/sdk-docfx.test.ts +204 -0
- package/__test__/sdk-javadoc.test.ts +150 -0
- package/__test__/sdk-phpdoc.test.ts +134 -0
- package/__test__/sdk-sphinx.test.ts +84 -0
- package/__test__/sdk-typedoc.test.ts +138 -0
- package/bin/index.d.ts +3 -0
- package/bin/index.js +2 -0
- package/bin/index.js.map +1 -1
- package/bin/sdk/convert.d.ts +6 -0
- package/bin/sdk/convert.js +90 -0
- package/bin/sdk/convert.js.map +1 -0
- package/bin/sdk/converters/docfx.d.ts +2 -0
- package/bin/sdk/converters/docfx.js +402 -0
- package/bin/sdk/converters/docfx.js.map +1 -0
- package/bin/sdk/converters/javadoc.d.ts +2 -0
- package/bin/sdk/converters/javadoc.js +318 -0
- package/bin/sdk/converters/javadoc.js.map +1 -0
- package/bin/sdk/converters/phpdoc.d.ts +2 -0
- package/bin/sdk/converters/phpdoc.js +351 -0
- package/bin/sdk/converters/phpdoc.js.map +1 -0
- package/bin/sdk/converters/sphinx.d.ts +2 -0
- package/bin/sdk/converters/sphinx.js +267 -0
- package/bin/sdk/converters/sphinx.js.map +1 -0
- package/bin/sdk/converters/typedoc.d.ts +2 -0
- package/bin/sdk/converters/typedoc.js +318 -0
- package/bin/sdk/converters/typedoc.js.map +1 -0
- package/bin/sdk/generateSdkReference.d.ts +2 -0
- package/bin/sdk/generateSdkReference.js +20 -0
- package/bin/sdk/generateSdkReference.js.map +1 -0
- package/bin/sdk/types.d.ts +19 -0
- package/bin/sdk/types.js +2 -0
- package/bin/sdk/types.js.map +1 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/src/index.ts +3 -0
- package/src/sdk/convert.ts +101 -0
- package/src/sdk/converters/docfx.ts +491 -0
- package/src/sdk/converters/javadoc.ts +330 -0
- package/src/sdk/converters/phpdoc.ts +446 -0
- package/src/sdk/converters/sphinx.ts +277 -0
- package/src/sdk/converters/typedoc.ts +423 -0
- package/src/sdk/generateSdkReference.ts +25 -0
- package/src/sdk/types.ts +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mintlify/scraping",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.908",
|
|
4
4
|
"description": "Scrape documentation frameworks to Mintlify docs",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -43,8 +43,9 @@
|
|
|
43
43
|
"format:check": "oxfmt --check"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@mintlify/common": "1.0.
|
|
46
|
+
"@mintlify/common": "1.0.1043",
|
|
47
47
|
"@mintlify/openapi-parser": "0.0.8",
|
|
48
|
+
"fast-xml-parser": "5.7.3",
|
|
48
49
|
"fs-extra": "11.1.1",
|
|
49
50
|
"graphql": "16.11.0",
|
|
50
51
|
"hast-util-to-mdast": "10.1.0",
|
|
@@ -65,7 +66,7 @@
|
|
|
65
66
|
"devDependencies": {
|
|
66
67
|
"@mintlify/models": "0.0.343",
|
|
67
68
|
"@mintlify/ts-config": "2.0.2",
|
|
68
|
-
"@mintlify/validation": "0.1.
|
|
69
|
+
"@mintlify/validation": "0.1.799",
|
|
69
70
|
"@tsconfig/recommended": "1.0.2",
|
|
70
71
|
"@types/hast": "3.0.4",
|
|
71
72
|
"@types/mdast": "4.0.4",
|
|
@@ -79,5 +80,5 @@
|
|
|
79
80
|
"typescript": "5.5.3",
|
|
80
81
|
"vitest": "2.1.9"
|
|
81
82
|
},
|
|
82
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "8d31ec350b96cab3712a337498e450d5fb2e30f8"
|
|
83
84
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,3 +13,6 @@ export { getErrorMessage } from './utils/errors.js';
|
|
|
13
13
|
export { checkUrl } from './utils/url.js';
|
|
14
14
|
export { FINAL_SUCCESS_MESSAGE } from './constants.js';
|
|
15
15
|
export { parseGraphqlSdl } from './graphql/parseGraphqlSdl.js';
|
|
16
|
+
export { generateSdkReference } from './sdk/generateSdkReference.js';
|
|
17
|
+
export { htmlToMdx, hastToMdx, markdownToMdx } from './sdk/convert.js';
|
|
18
|
+
export type { SdkFormat, SdkPage, SdkNavGroup, SdkReference, SdkConverter } from './sdk/types.js';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Root as HastRoot, Element } from 'hast';
|
|
2
|
+
import type { Root as MdastRoot } from 'mdast';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import remarkMdx from 'remark-mdx';
|
|
5
|
+
import remarkParse from 'remark-parse';
|
|
6
|
+
import remarkStringify from 'remark-stringify';
|
|
7
|
+
import { unified, type Plugin } from 'unified';
|
|
8
|
+
import { visit } from 'unist-util-visit';
|
|
9
|
+
|
|
10
|
+
import { convertHeaderLinksToText } from '../components/link.js';
|
|
11
|
+
import { createCallout, createCodeGroup, createTabs } from '../customComponents/create.js';
|
|
12
|
+
import { rehypeToRemarkCustomComponents } from '../customComponents/plugin.js';
|
|
13
|
+
import { selectiveRehypeRemark } from '../customComponents/selective.js';
|
|
14
|
+
import { htmlToHast } from '../pipeline/root.js';
|
|
15
|
+
import { unifiedRemoveBreaks } from '../utils/breaks.js';
|
|
16
|
+
import { unifiedRemoveClassNames } from '../utils/className.js';
|
|
17
|
+
import { unifiedRemoveCopyButtons } from '../utils/copyButton.js';
|
|
18
|
+
import { framework } from '../utils/detectFramework.js';
|
|
19
|
+
import { remarkRemoveEmptyEmphases } from '../utils/emptyEmphasis.js';
|
|
20
|
+
import { unifiedRemoveEmptyParagraphs } from '../utils/emptyParagraphs.js';
|
|
21
|
+
import { remarkProperlyFormatEmphasis } from '../utils/formatEmphasis.js';
|
|
22
|
+
import { removeHastComments } from '../utils/hastComments.js';
|
|
23
|
+
import { remarkSpaceListsOut } from '../utils/lists.js';
|
|
24
|
+
import { unifiedRemoveNestedRoots } from '../utils/nestedRoots.js';
|
|
25
|
+
import { unifiedRemovePositions } from '../utils/position.js';
|
|
26
|
+
|
|
27
|
+
export type RewriteLink = (href: string) => string | undefined;
|
|
28
|
+
|
|
29
|
+
const rehypeRewriteLinks =
|
|
30
|
+
(rewriteLink: RewriteLink): Plugin<[], HastRoot> =>
|
|
31
|
+
() =>
|
|
32
|
+
(tree: HastRoot) => {
|
|
33
|
+
visit(tree, 'element', (node: Element) => {
|
|
34
|
+
if (node.tagName !== 'a' || typeof node.properties?.href !== 'string') return;
|
|
35
|
+
const rewritten = rewriteLink(node.properties.href);
|
|
36
|
+
if (rewritten === undefined) {
|
|
37
|
+
delete node.properties.href;
|
|
38
|
+
} else {
|
|
39
|
+
node.properties.href = rewritten;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function hastToMdx(fragment: HastRoot, rewriteLink?: RewriteLink): string {
|
|
45
|
+
const originalVendor = framework.vendor;
|
|
46
|
+
framework.vendor ??= 'gitbook';
|
|
47
|
+
try {
|
|
48
|
+
return runHastToMdx(fragment, rewriteLink);
|
|
49
|
+
} finally {
|
|
50
|
+
framework.vendor = originalVendor;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runHastToMdx(fragment: HastRoot, rewriteLink?: RewriteLink): string {
|
|
55
|
+
const processor = unified().use(unifiedRemoveBreaks).use(unifiedRemoveCopyButtons);
|
|
56
|
+
if (rewriteLink) processor.use(rehypeRewriteLinks(rewriteLink));
|
|
57
|
+
const mdastTree = processor
|
|
58
|
+
.use(createCallout)
|
|
59
|
+
.use(createCodeGroup)
|
|
60
|
+
.use(createTabs)
|
|
61
|
+
.use(unifiedRemoveClassNames)
|
|
62
|
+
.use(unifiedRemoveEmptyParagraphs)
|
|
63
|
+
.use(unifiedRemovePositions)
|
|
64
|
+
.use(selectiveRehypeRemark)
|
|
65
|
+
.use(rehypeToRemarkCustomComponents)
|
|
66
|
+
.use(convertHeaderLinksToText)
|
|
67
|
+
.use(unifiedRemoveNestedRoots)
|
|
68
|
+
.use(remarkSpaceListsOut)
|
|
69
|
+
.use(remarkRemoveEmptyEmphases)
|
|
70
|
+
.use(remarkProperlyFormatEmphasis)
|
|
71
|
+
// @ts-expect-error hast root is transformed to mdast by the plugin chain
|
|
72
|
+
.runSync(fragment) as MdastRoot;
|
|
73
|
+
|
|
74
|
+
return stringifyMdast(mdastTree);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function htmlToMdx(html: string, rewriteLink?: RewriteLink): string {
|
|
78
|
+
const hast = htmlToHast(html);
|
|
79
|
+
removeHastComments(hast);
|
|
80
|
+
return hastToMdx(hast, rewriteLink);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function markdownToMdx(markdown: string): string {
|
|
84
|
+
const mdastTree = unified().use(remarkParse).use(remarkGfm).parse(markdown) as MdastRoot;
|
|
85
|
+
visit(mdastTree, 'html', (node: { type: string; value: string }) => {
|
|
86
|
+
node.type = 'text';
|
|
87
|
+
node.value = node.value.replace(/<[^>]*>/g, '');
|
|
88
|
+
});
|
|
89
|
+
return stringifyMdast(mdastTree);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function stringifyMdast(tree: MdastRoot): string {
|
|
93
|
+
const result = unified().use(remarkMdx).use(remarkGfm).use(remarkStringify).stringify(tree);
|
|
94
|
+
return String(result)
|
|
95
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
96
|
+
.trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function escapeMdxText(text: string): string {
|
|
100
|
+
return markdownToMdx(text.replace(/\r\n/g, '\n'));
|
|
101
|
+
}
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import fse from 'fs-extra';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { htmlToMdx } from '../convert.js';
|
|
6
|
+
import type { SdkNavGroup, SdkPage, SdkReference } from '../types.js';
|
|
7
|
+
|
|
8
|
+
type DocfxParameter = { id?: string; type?: string; description?: string };
|
|
9
|
+
type DocfxSyntax = {
|
|
10
|
+
content?: string;
|
|
11
|
+
parameters?: DocfxParameter[];
|
|
12
|
+
typeParameters?: DocfxParameter[];
|
|
13
|
+
return?: { type?: string; description?: string };
|
|
14
|
+
};
|
|
15
|
+
type DocfxItem = {
|
|
16
|
+
uid: string;
|
|
17
|
+
parent?: string;
|
|
18
|
+
children?: string[];
|
|
19
|
+
name?: string;
|
|
20
|
+
type?: string;
|
|
21
|
+
namespace?: string;
|
|
22
|
+
summary?: string;
|
|
23
|
+
remarks?: string;
|
|
24
|
+
example?: string[];
|
|
25
|
+
syntax?: DocfxSyntax;
|
|
26
|
+
};
|
|
27
|
+
type DocfxFile = { items?: DocfxItem[]; references?: { uid?: string; name?: string }[] };
|
|
28
|
+
type TocNode = { uid?: string; type?: string; items?: TocNode[] };
|
|
29
|
+
|
|
30
|
+
const PAGE_TAGS: Record<string, string> = {
|
|
31
|
+
Class: 'CLASS',
|
|
32
|
+
Interface: 'INTERFACE',
|
|
33
|
+
Enum: 'ENUM',
|
|
34
|
+
Struct: 'STRUCT',
|
|
35
|
+
Delegate: 'DELEGATE',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export async function convertDocfx(sourcePath: string): Promise<SdkReference> {
|
|
39
|
+
const files = await loadManagedReferenceFiles(sourcePath);
|
|
40
|
+
const itemsByUid = new Map<string, DocfxItem>();
|
|
41
|
+
const namesByUid = new Map<string, string>();
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
for (const item of file.items ?? []) {
|
|
44
|
+
if (!item.uid) continue;
|
|
45
|
+
itemsByUid.set(item.uid, item);
|
|
46
|
+
if (item.name) namesByUid.set(item.uid, item.name);
|
|
47
|
+
}
|
|
48
|
+
for (const reference of file.references ?? []) {
|
|
49
|
+
if (reference.uid && reference.name && !namesByUid.has(reference.uid)) {
|
|
50
|
+
namesByUid.set(reference.uid, reference.name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const types = [...itemsByUid.values()]
|
|
56
|
+
.filter((item) => item.type !== undefined && item.type in PAGE_TAGS)
|
|
57
|
+
.sort((left, right) => (left.name ?? '').localeCompare(right.name ?? ''));
|
|
58
|
+
const namespaceSlug = namespaceSlugs(
|
|
59
|
+
[...new Set(types.map((type) => type.namespace ?? ''))].sort()
|
|
60
|
+
);
|
|
61
|
+
const slugByUid = new Map<string, string>();
|
|
62
|
+
for (const type of types) {
|
|
63
|
+
slugByUid.set(
|
|
64
|
+
type.uid,
|
|
65
|
+
path.posix.join(namespaceSlug.get(type.namespace ?? '') ?? '', slugify(type.name ?? type.uid))
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const renderer = new DocfxRenderer(slugByUid, namesByUid, itemsByUid);
|
|
70
|
+
const { tocNamespaces, tocOrder } = await loadToc(sourcePath);
|
|
71
|
+
|
|
72
|
+
const typesByNamespace = new Map<string, DocfxItem[]>();
|
|
73
|
+
for (const type of types) {
|
|
74
|
+
const namespace = type.namespace ?? '';
|
|
75
|
+
typesByNamespace.set(namespace, [...(typesByNamespace.get(namespace) ?? []), type]);
|
|
76
|
+
}
|
|
77
|
+
const namespaceOrder = [
|
|
78
|
+
...tocNamespaces.filter((namespace) => typesByNamespace.has(namespace)),
|
|
79
|
+
...[...typesByNamespace.keys()]
|
|
80
|
+
.filter((namespace) => !tocNamespaces.includes(namespace))
|
|
81
|
+
.sort(),
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const pages: SdkPage[] = [];
|
|
85
|
+
const groups: SdkNavGroup[] = [];
|
|
86
|
+
for (const namespace of namespaceOrder) {
|
|
87
|
+
const ordered = orderByToc(
|
|
88
|
+
typesByNamespace.get(namespace) ?? [],
|
|
89
|
+
tocOrder.get(namespace) ?? []
|
|
90
|
+
);
|
|
91
|
+
const namespacePages = ordered.map((type) => renderer.renderPage(type));
|
|
92
|
+
pages.push(...namespacePages);
|
|
93
|
+
groups.push({
|
|
94
|
+
group: namespace || 'Reference',
|
|
95
|
+
pages: namespacePages.map((page) => page.slug),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { pages, groups };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function loadManagedReferenceFiles(sourcePath: string): Promise<DocfxFile[]> {
|
|
103
|
+
const entries = await fse.readdir(sourcePath);
|
|
104
|
+
const files: DocfxFile[] = [];
|
|
105
|
+
for (const entry of entries.filter((name) => /\.ya?ml$/.test(name)).sort()) {
|
|
106
|
+
const raw = await fse.readFile(path.join(sourcePath, entry), 'utf8');
|
|
107
|
+
if (!raw.startsWith('### YamlMime:ManagedReference')) continue;
|
|
108
|
+
const parsed = yaml.load(raw);
|
|
109
|
+
if (parsed && typeof parsed === 'object') files.push(parsed as DocfxFile);
|
|
110
|
+
}
|
|
111
|
+
return files;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function loadToc(
|
|
115
|
+
sourcePath: string
|
|
116
|
+
): Promise<{ tocNamespaces: string[]; tocOrder: Map<string, string[]> }> {
|
|
117
|
+
const tocNamespaces: string[] = [];
|
|
118
|
+
const tocOrder = new Map<string, string[]>();
|
|
119
|
+
const tocPath = path.join(sourcePath, 'toc.yml');
|
|
120
|
+
if (!(await fse.pathExists(tocPath))) return { tocNamespaces, tocOrder };
|
|
121
|
+
const parsed = yaml.load(await fse.readFile(tocPath, 'utf8')) as
|
|
122
|
+
| { items?: TocNode[] }
|
|
123
|
+
| undefined;
|
|
124
|
+
const collect = (nodes: TocNode[]) => {
|
|
125
|
+
for (const node of nodes) {
|
|
126
|
+
if (node.type === 'Namespace' && node.uid !== undefined) {
|
|
127
|
+
tocNamespaces.push(node.uid);
|
|
128
|
+
tocOrder.set(
|
|
129
|
+
node.uid,
|
|
130
|
+
(node.items ?? [])
|
|
131
|
+
.map((child) => child.uid)
|
|
132
|
+
.filter((uid): uid is string => typeof uid === 'string')
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (node.items) collect(node.items);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
collect(parsed?.items ?? []);
|
|
139
|
+
return { tocNamespaces, tocOrder };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function orderByToc(types: DocfxItem[], order: string[]): DocfxItem[] {
|
|
143
|
+
const rank = new Map(order.map((uid, index) => [uid, index]));
|
|
144
|
+
return [...types].sort((left, right) => {
|
|
145
|
+
const leftRank = rank.get(left.uid);
|
|
146
|
+
const rightRank = rank.get(right.uid);
|
|
147
|
+
if (leftRank !== undefined && rightRank !== undefined) return leftRank - rightRank;
|
|
148
|
+
if (leftRank !== undefined) return -1;
|
|
149
|
+
if (rightRank !== undefined) return 1;
|
|
150
|
+
return (left.name ?? '').localeCompare(right.name ?? '');
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function namespaceSlugs(namespaces: string[]): Map<string, string> {
|
|
155
|
+
const parts = namespaces.map((namespace) => namespace.split('.'));
|
|
156
|
+
let common = parts[0] ?? [];
|
|
157
|
+
for (const segments of parts.slice(1)) {
|
|
158
|
+
let index = 0;
|
|
159
|
+
while (index < common.length && common[index] === segments[index]) index += 1;
|
|
160
|
+
common = common.slice(0, index);
|
|
161
|
+
}
|
|
162
|
+
const slugs = new Map<string, string>();
|
|
163
|
+
const used = new Set<string>();
|
|
164
|
+
for (const [index, namespace] of namespaces.entries()) {
|
|
165
|
+
const segments = parts[index] ?? [];
|
|
166
|
+
const rest = segments.slice(common.length);
|
|
167
|
+
const base = slugify((rest.length ? rest : segments.slice(-1)).join('-'));
|
|
168
|
+
let slug = used.has(base) ? slugify(segments.join('-')) : base;
|
|
169
|
+
for (let suffix = 2; used.has(slug); suffix += 1) {
|
|
170
|
+
slug = `${slugify(segments.join('-'))}-${suffix}`;
|
|
171
|
+
}
|
|
172
|
+
used.add(slug);
|
|
173
|
+
slugs.set(namespace, slug);
|
|
174
|
+
}
|
|
175
|
+
return slugs;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function slugify(name: string): string {
|
|
179
|
+
return (
|
|
180
|
+
name
|
|
181
|
+
.toLowerCase()
|
|
182
|
+
.replace(/`+\d*/g, '')
|
|
183
|
+
.replace(/[^a-z0-9-_.]+/g, '-')
|
|
184
|
+
.replace(/^-+|-+$/g, '') || 'item'
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
class DocfxRenderer {
|
|
189
|
+
constructor(
|
|
190
|
+
private slugByUid: Map<string, string>,
|
|
191
|
+
private namesByUid: Map<string, string>,
|
|
192
|
+
private itemsByUid: Map<string, DocfxItem>
|
|
193
|
+
) {}
|
|
194
|
+
|
|
195
|
+
renderPage(item: DocfxItem): SdkPage {
|
|
196
|
+
const lines: string[] = [this.prose(item.summary)];
|
|
197
|
+
if (item.syntax?.content) lines.push(this.codeBlock(item.syntax.content));
|
|
198
|
+
lines.push(this.prose(item.remarks));
|
|
199
|
+
lines.push(...(item.example ?? []).map((example) => this.renderExample(example)));
|
|
200
|
+
|
|
201
|
+
switch (item.type) {
|
|
202
|
+
case 'Enum':
|
|
203
|
+
lines.push(...this.renderEnum(item));
|
|
204
|
+
break;
|
|
205
|
+
case 'Delegate':
|
|
206
|
+
lines.push(...this.renderSignatureDetails(item.syntax, 2));
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
lines.push(...this.renderTypeMembers(item));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
slug: this.slugByUid.get(item.uid) ?? slugify(item.name ?? item.uid),
|
|
214
|
+
title: item.name ?? item.uid,
|
|
215
|
+
description: this.firstSentence(item.summary),
|
|
216
|
+
tag: PAGE_TAGS[item.type ?? ''],
|
|
217
|
+
content: lines.filter(Boolean).join('\n\n'),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private members(item: DocfxItem): DocfxItem[] {
|
|
222
|
+
return (item.children ?? [])
|
|
223
|
+
.map((uid) => this.itemsByUid.get(uid))
|
|
224
|
+
.filter((member): member is DocfxItem => member !== undefined);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private renderTypeMembers(item: DocfxItem): string[] {
|
|
228
|
+
const members = this.members(item);
|
|
229
|
+
const lines: string[] = [];
|
|
230
|
+
const constructors = members.filter((member) => member.type === 'Constructor');
|
|
231
|
+
const fields = members.filter((member) => member.type === 'Field').sort(byName);
|
|
232
|
+
const properties = members
|
|
233
|
+
.filter((member) => member.type === 'Property' || member.type === 'Event')
|
|
234
|
+
.sort(byName);
|
|
235
|
+
const methods = members.filter(
|
|
236
|
+
(member) => member.type === 'Method' || member.type === 'Operator'
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (constructors.length) {
|
|
240
|
+
lines.push('## Constructors');
|
|
241
|
+
for (const ctor of constructors) lines.push(...this.renderMember(ctor, 3));
|
|
242
|
+
}
|
|
243
|
+
if (fields.length) {
|
|
244
|
+
lines.push('## Fields');
|
|
245
|
+
for (const field of fields) lines.push(this.memberField(field));
|
|
246
|
+
}
|
|
247
|
+
if (properties.length) {
|
|
248
|
+
lines.push('## Properties');
|
|
249
|
+
for (const property of properties) lines.push(this.memberField(property));
|
|
250
|
+
}
|
|
251
|
+
if (methods.length) {
|
|
252
|
+
lines.push('## Methods');
|
|
253
|
+
const grouped = new Map<string, DocfxItem[]>();
|
|
254
|
+
for (const method of methods) {
|
|
255
|
+
const base = baseName(method.name ?? method.uid);
|
|
256
|
+
grouped.set(base, [...(grouped.get(base) ?? []), method]);
|
|
257
|
+
}
|
|
258
|
+
for (const [name, overloads] of [...grouped.entries()].sort(([left], [right]) =>
|
|
259
|
+
left.localeCompare(right)
|
|
260
|
+
)) {
|
|
261
|
+
lines.push(`### ${name}()`);
|
|
262
|
+
for (const overload of overloads) lines.push(...this.renderMember(overload, 4));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return lines;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private renderEnum(item: DocfxItem): string[] {
|
|
269
|
+
const lines: string[] = ['## Members'];
|
|
270
|
+
for (const member of this.members(item).filter((child) => child.type === 'Field')) {
|
|
271
|
+
const value = member.syntax?.content?.split('=')[1]?.trim() ?? '';
|
|
272
|
+
lines.push(
|
|
273
|
+
this.responseField(member.name ?? member.uid, value, false, this.prose(member.summary))
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return lines;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private renderMember(member: DocfxItem, depth: number): string[] {
|
|
280
|
+
const lines: string[] = [];
|
|
281
|
+
if (member.syntax?.content) lines.push(this.codeBlock(member.syntax.content));
|
|
282
|
+
lines.push(this.prose(member.summary), this.prose(member.remarks));
|
|
283
|
+
lines.push(...(member.example ?? []).map((example) => this.renderExample(example)));
|
|
284
|
+
lines.push(...this.renderSignatureDetails(member.syntax, depth));
|
|
285
|
+
return lines.filter(Boolean);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private renderSignatureDetails(syntax: DocfxSyntax | undefined, depth: number): string[] {
|
|
289
|
+
if (!syntax) return [];
|
|
290
|
+
const lines: string[] = [];
|
|
291
|
+
const heading = '#'.repeat(depth);
|
|
292
|
+
if (syntax.typeParameters?.length) {
|
|
293
|
+
lines.push(`${heading} Type Parameters`);
|
|
294
|
+
for (const param of syntax.typeParameters) {
|
|
295
|
+
lines.push(this.responseField(param.id ?? '', '', false, this.prose(param.description)));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (syntax.parameters?.length) {
|
|
299
|
+
lines.push(`${heading} Parameters`);
|
|
300
|
+
for (const param of syntax.parameters) {
|
|
301
|
+
lines.push(
|
|
302
|
+
this.responseField(
|
|
303
|
+
param.id ?? '',
|
|
304
|
+
this.typeName(param.type),
|
|
305
|
+
isOptionalParameter(param.id, syntax.content),
|
|
306
|
+
this.prose(param.description)
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const returnType = this.typeName(syntax.return?.type);
|
|
312
|
+
if (returnType && returnType.toLowerCase() !== 'void') {
|
|
313
|
+
lines.push(`${heading} Returns`);
|
|
314
|
+
const description = this.prose(syntax.return?.description);
|
|
315
|
+
lines.push(`\`${escapeInlineCode(returnType)}\`${description ? `\n\n${description}` : ''}`);
|
|
316
|
+
}
|
|
317
|
+
return lines;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private memberField(member: DocfxItem): string {
|
|
321
|
+
const body = [
|
|
322
|
+
this.prose(member.summary),
|
|
323
|
+
this.prose(member.remarks),
|
|
324
|
+
...(member.example ?? []).map((example) => this.renderExample(example)),
|
|
325
|
+
]
|
|
326
|
+
.filter(Boolean)
|
|
327
|
+
.join('\n\n');
|
|
328
|
+
return this.responseField(
|
|
329
|
+
member.name ?? member.uid,
|
|
330
|
+
this.typeName(member.syntax?.return?.type),
|
|
331
|
+
isNullable(member.syntax?.content),
|
|
332
|
+
body
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private responseField(name: string, type: string, optional: boolean, body?: string): string {
|
|
337
|
+
const typeAttr = type ? ` type=${this.jsxString(truncate(type, 80))}` : '';
|
|
338
|
+
const requiredAttr = optional ? '' : ' required';
|
|
339
|
+
return `<ResponseField name=${this.jsxString(name)}${typeAttr}${requiredAttr}>\n${indent(body || '')}\n</ResponseField>`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private jsxString(value: string): string {
|
|
343
|
+
return `{${JSON.stringify(value)}}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private codeBlock(code: string, lang = 'csharp'): string {
|
|
347
|
+
return `\`\`\`${lang}\n${code}\n\`\`\``;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private renderExample(example: string): string {
|
|
351
|
+
if (/<[a-z][^>]*>/i.test(example)) return this.prose(example);
|
|
352
|
+
return this.codeBlock(decodeEntities(example));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private prose(html?: string): string {
|
|
356
|
+
if (!html) return '';
|
|
357
|
+
const resolved = this.inlineCodeSpans(this.resolveXrefs(html));
|
|
358
|
+
return resolved
|
|
359
|
+
.split(/(<pre[^>]*>[\s\S]*?<\/pre>)/g)
|
|
360
|
+
.map((segment, index) =>
|
|
361
|
+
index % 2 === 1 ? this.preToCodeBlock(segment) : htmlToMdx(segment)
|
|
362
|
+
)
|
|
363
|
+
.filter(Boolean)
|
|
364
|
+
.join('\n\n');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private preToCodeBlock(segment: string): string {
|
|
368
|
+
const lang = /class="lang(?:uage)?-([\w-]+)"/.exec(segment)?.[1] ?? 'csharp';
|
|
369
|
+
const code = decodeEntities(segment.replace(/<[^>]+>/g, '')).replace(/^\n+|\n+$/g, '');
|
|
370
|
+
return this.codeBlock(code, lang);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private resolveXrefs(html: string): string {
|
|
374
|
+
return html.replace(
|
|
375
|
+
/<xref\s[^>]*?(?:href|uid)="([^"]+)"[^>]*>([\s\S]*?)<\/xref>/g,
|
|
376
|
+
(_, href: string, text: string) => {
|
|
377
|
+
const uid = decodeUid(href);
|
|
378
|
+
const label = text.trim() || escapeHtml(this.shortName(uid));
|
|
379
|
+
const slug = this.slugForUid(uid);
|
|
380
|
+
return slug ? `<a href="/${slug}">${label}</a>` : `<code>${label}</code>`;
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private inlineCodeSpans(html: string): string {
|
|
386
|
+
return html
|
|
387
|
+
.split(/(<pre[\s\S]*?<\/pre>|<code[\s\S]*?<\/code>)/g)
|
|
388
|
+
.map((segment, index) =>
|
|
389
|
+
index % 2 === 1
|
|
390
|
+
? segment
|
|
391
|
+
: segment.replace(/`([^`\n]+)`/g, (_, code: string) => `<code>${escapeHtml(code)}</code>`)
|
|
392
|
+
)
|
|
393
|
+
.join('');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private slugForUid(uid: string): string | undefined {
|
|
397
|
+
const direct = this.slugByUid.get(uid);
|
|
398
|
+
if (direct) return direct;
|
|
399
|
+
const parent =
|
|
400
|
+
this.itemsByUid.get(uid)?.parent ??
|
|
401
|
+
uid
|
|
402
|
+
.replace(/\(.*\)$/, '')
|
|
403
|
+
.replace(/\*$/, '')
|
|
404
|
+
.replace(/\.[^.]*$/, '');
|
|
405
|
+
return parent ? this.slugByUid.get(parent) : undefined;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private shortName(uid: string): string {
|
|
409
|
+
return this.namesByUid.get(uid) ?? shortNameFromUid(uid);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private typeName(uid?: string): string {
|
|
413
|
+
return uid ? this.shortName(uid) : '';
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private firstSentence(summary?: string): string | undefined {
|
|
417
|
+
if (!summary) return undefined;
|
|
418
|
+
const text = decodeEntities(summary.replace(/<[^>]*>/g, ' '))
|
|
419
|
+
.replace(/\s+/g, ' ')
|
|
420
|
+
.trim();
|
|
421
|
+
if (!text) return undefined;
|
|
422
|
+
const sentence = text.split(/(?<=\.)\s/)[0] ?? text;
|
|
423
|
+
return truncate(sentence, 160);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function byName(left: DocfxItem, right: DocfxItem): number {
|
|
428
|
+
return (left.name ?? '').localeCompare(right.name ?? '');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function baseName(name: string): string {
|
|
432
|
+
return name.split(/[(<]/)[0]?.trim() || name;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function shortNameFromUid(uid: string): string {
|
|
436
|
+
return uid
|
|
437
|
+
.replace(/`+\d+/g, '')
|
|
438
|
+
.replace(/\{/g, '<')
|
|
439
|
+
.replace(/\}/g, '>')
|
|
440
|
+
.replace(/[A-Za-z_][A-Za-z0-9_.]*/g, (dotted) => dotted.split('.').at(-1) ?? dotted);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function decodeUid(href: string): string {
|
|
444
|
+
try {
|
|
445
|
+
return decodeEntities(decodeURIComponent(href));
|
|
446
|
+
} catch {
|
|
447
|
+
return decodeEntities(href);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function decodeEntities(text: string): string {
|
|
452
|
+
return text
|
|
453
|
+
.replace(/</g, '<')
|
|
454
|
+
.replace(/>/g, '>')
|
|
455
|
+
.replace(/"/g, '"')
|
|
456
|
+
.replace(/'/g, "'")
|
|
457
|
+
.replace(/ /g, ' ')
|
|
458
|
+
.replace(/&/g, '&');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function escapeHtml(text: string): string {
|
|
462
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function isNullable(content?: string): boolean {
|
|
466
|
+
return content !== undefined && /\?\s+\w+\s*[{;=]/.test(content);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function isOptionalParameter(id?: string, content?: string): boolean {
|
|
470
|
+
if (!id || !content) return false;
|
|
471
|
+
return new RegExp(`\\b${escapeRegExp(id)}\\s*=`).test(content);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function escapeRegExp(text: string): string {
|
|
475
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function indent(text: string): string {
|
|
479
|
+
return text
|
|
480
|
+
.split('\n')
|
|
481
|
+
.map((line) => (line ? ` ${line}` : line))
|
|
482
|
+
.join('\n');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function truncate(text: string, max: number): string {
|
|
486
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function escapeInlineCode(text: string): string {
|
|
490
|
+
return text.replace(/`/g, '');
|
|
491
|
+
}
|