rspress-plugin-api-extractor 0.1.2 → 0.2.0
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/README.md +2 -2
- package/api-extracted-package.js +426 -0
- package/build-program.js +120 -0
- package/build-stages.js +664 -0
- package/category-resolver.js +50 -0
- package/code-post-processor.js +38 -0
- package/config-helpers.js +106 -0
- package/config-utils.js +258 -0
- package/content-hash.js +79 -0
- package/errors.js +29 -0
- package/formatter.js +69 -0
- package/hide-cut-transformer.js +96 -0
- package/index.d.ts +901 -831
- package/index.js +4 -6128
- package/layers/ConfigServiceLive.js +377 -0
- package/layers/ObservabilityLive.js +136 -0
- package/layers/PathDerivationServiceLive.js +16 -0
- package/layers/SnapshotServiceLive.js +94 -0
- package/layers/TypeRegistryServiceLive.js +46 -0
- package/llms-processing.js +270 -0
- package/llms-program.js +262 -0
- package/loader.js +186 -0
- package/markdown/cross-linker.js +156 -0
- package/markdown/helpers.js +364 -0
- package/markdown/index.js +11 -0
- package/markdown/page-generators/class-page.js +357 -0
- package/markdown/page-generators/enum-page.js +152 -0
- package/markdown/page-generators/function-page.js +127 -0
- package/markdown/page-generators/index-pages.js +27 -0
- package/markdown/page-generators/interface-page.js +307 -0
- package/markdown/page-generators/namespace-page.js +280 -0
- package/markdown/page-generators/type-alias-page.js +110 -0
- package/markdown/page-generators/variable-page.js +110 -0
- package/markdown/shiki-utils.js +48 -0
- package/migrations/001_create_snapshots.js +25 -0
- package/model-loader.js +95 -0
- package/multi-entry-resolver.js +70 -0
- package/og-resolver.js +271 -0
- package/package.json +63 -73
- package/path-derivation.js +48 -0
- package/plugin.js +218 -0
- package/prettier-formatter.js +73 -0
- package/public/tsconfig/rspress.json +44 -0
- package/remark-api-codeblocks.js +130 -0
- package/remark-with-api.js +172 -0
- package/route-collisions.js +52 -0
- package/runtime/components/ApiExample/index.js +29 -25
- package/runtime/components/ApiLlmsPackageActions/index.js +252 -323
- package/runtime/components/ApiLlmsViewOptions/index.js +269 -340
- package/runtime/components/ApiMember/index.js +49 -47
- package/runtime/components/ApiSignature/index.js +32 -28
- package/runtime/components/EnumMembersTable/{index_module.css → index.css} +18 -19
- package/runtime/components/EnumMembersTable/index.js +36 -67
- package/runtime/components/EnumMembersTable/index.module.js +9 -6
- package/runtime/components/ExampleBlock/{index_module.css → index.css} +2 -3
- package/runtime/components/ExampleBlock/index.js +23 -28
- package/runtime/components/ExampleBlock/index.module.js +8 -5
- package/runtime/components/MarkdownContent/index.js +26 -18
- package/runtime/components/MarkdownText/index.js +28 -22
- package/runtime/components/MemberSignature/{index_module.css → index.css} +5 -6
- package/runtime/components/MemberSignature/index.js +46 -46
- package/runtime/components/MemberSignature/index.module.js +9 -6
- package/runtime/components/ParametersTable/{index_module.css → index.css} +19 -20
- package/runtime/components/ParametersTable/index.js +36 -67
- package/runtime/components/ParametersTable/index.module.js +9 -6
- package/runtime/components/SignatureBlock/{index_module.css → index.css} +5 -6
- package/runtime/components/SignatureBlock/index.js +30 -29
- package/runtime/components/SignatureBlock/index.module.js +9 -6
- package/runtime/components/SignatureCode/{index_module.css → index.css} +9 -10
- package/runtime/components/SignatureCode/index.js +40 -32
- package/runtime/components/SignatureCode/index.module.js +9 -6
- package/runtime/components/SignatureToolbar/{index_module.css → index.css} +18 -20
- package/runtime/components/SignatureToolbar/index.js +48 -51
- package/runtime/components/SignatureToolbar/index.module.js +13 -10
- package/runtime/components/buttons/ButtonGroup.js +13 -6
- package/runtime/components/buttons/CopyCodeButton.js +40 -38
- package/runtime/components/buttons/WrapSignatureButton.js +20 -16
- package/runtime/components/buttons/{index_module.css → index.css} +5 -6
- package/runtime/components/buttons/index.module.js +8 -5
- package/runtime/components/icons/CheckIcon/index.js +20 -17
- package/runtime/components/icons/CopyIcon/index.js +20 -17
- package/runtime/components/icons/UnwrapIcon/index.js +22 -18
- package/runtime/components/icons/WrapIcon/index.js +20 -17
- package/runtime/components/shared/_twoslash.css +3 -10
- package/runtime/components/shared/variables.css +0 -3
- package/runtime/hooks/useWrapToggle.js +32 -9
- package/runtime/index.d.ts +513 -173
- package/runtime/index.js +11 -9
- package/runtime/utils/decode-hast.js +31 -16
- package/runtime/utils/hast-renderer.js +21 -7
- package/schemas/config.js +199 -0
- package/schemas/index.js +5 -0
- package/schemas/opengraph.js +26 -0
- package/schemas/performance.js +19 -0
- package/serve.js +133 -0
- package/services/ConfigService.js +7 -0
- package/services/PathDerivationService.js +7 -0
- package/services/SnapshotService.js +7 -0
- package/services/TypeRegistryService.js +7 -0
- package/shiki-transformer.js +758 -0
- package/tsconfig-parser.js +127 -0
- package/tsdoc-metadata.json +11 -11
- package/twoslash-patterns.js +87 -0
- package/twoslash-transformer.js +316 -0
- package/type-reference-extractor.js +201 -0
- package/typescript-config.js +168 -0
- package/vfs-registry.js +121 -0
- package/0~llms-program.js +0 -344
- package/runtime/components/ApiLlmsPackageActions/index.module.js +0 -2
package/loader.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { ApiDocumentedItem, ApiItemKind } from "@microsoft/api-extractor-model";
|
|
2
|
+
import { extractPlainText, getDeprecation, getExamples, getParams, getReleaseTag, getReturns, getSummary, hasModifierTag } from "api-extractor-llms";
|
|
3
|
+
|
|
4
|
+
//#region src/loader.ts
|
|
5
|
+
/**
|
|
6
|
+
* Parser for extracting and analyzing information from API Extractor models and TSDoc comments
|
|
7
|
+
*/
|
|
8
|
+
var ApiParser = class ApiParser {
|
|
9
|
+
/**
|
|
10
|
+
* Private constructor to prevent instantiation
|
|
11
|
+
*/
|
|
12
|
+
constructor() {}
|
|
13
|
+
/**
|
|
14
|
+
* Check if an API item has a custom modifier tag
|
|
15
|
+
*/
|
|
16
|
+
static hasModifierTag(item, tagName) {
|
|
17
|
+
return hasModifierTag(item, tagName);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Extract all API items from a package (or resolved entry items) and categorize them based on configuration.
|
|
21
|
+
*
|
|
22
|
+
* When passed a `ResolvedEntryItem[]`, uses the items directly (multi-entry support).
|
|
23
|
+
* When passed an `ApiPackage`, reads from `entryPoints[0]` (legacy single-entry behavior).
|
|
24
|
+
*/
|
|
25
|
+
static categorizeApiItems(source, categories) {
|
|
26
|
+
const items = {};
|
|
27
|
+
for (const categoryKey of Object.keys(categories)) items[categoryKey] = [];
|
|
28
|
+
let members;
|
|
29
|
+
if (Array.isArray(source)) members = source.map((r) => r.item);
|
|
30
|
+
else {
|
|
31
|
+
const entryPoint = source.entryPoints[0];
|
|
32
|
+
if (!entryPoint) return items;
|
|
33
|
+
members = entryPoint.members;
|
|
34
|
+
}
|
|
35
|
+
const sortedCategories = Object.entries(categories).sort((a, b) => {
|
|
36
|
+
const [, configA] = a;
|
|
37
|
+
const [, configB] = b;
|
|
38
|
+
if (configA.tsdocModifier && !configB.tsdocModifier) return -1;
|
|
39
|
+
if (!configA.tsdocModifier && configB.tsdocModifier) return 1;
|
|
40
|
+
return 0;
|
|
41
|
+
});
|
|
42
|
+
for (const member of members) {
|
|
43
|
+
let categorized = false;
|
|
44
|
+
for (const [categoryKey, config] of sortedCategories) {
|
|
45
|
+
if (config.tsdocModifier && ApiParser.hasModifierTag(member, config.tsdocModifier)) {
|
|
46
|
+
items[categoryKey].push(member);
|
|
47
|
+
categorized = true;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
if (config.itemKinds?.includes(member.kind)) {
|
|
51
|
+
items[categoryKey].push(member);
|
|
52
|
+
categorized = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!categorized && typeof process !== "undefined" && !process.env.VITEST) console.warn(`⚠️ API item "${member.displayName}" (kind: ${member.kind}) not categorized`);
|
|
57
|
+
}
|
|
58
|
+
return items;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Extract all members from namespaces in a package (or resolved entry items).
|
|
62
|
+
* Returns a flat list of namespace members with their qualified names.
|
|
63
|
+
*
|
|
64
|
+
* When passed a `ResolvedEntryItem[]`, scans those items for namespaces (multi-entry support).
|
|
65
|
+
* When passed an `ApiPackage`, reads from `entryPoints[0]` (legacy single-entry behavior).
|
|
66
|
+
*
|
|
67
|
+
* @param source - The API package or resolved entry items to extract from
|
|
68
|
+
* @returns Array of namespace members with qualified names
|
|
69
|
+
*/
|
|
70
|
+
static extractNamespaceMembers(source) {
|
|
71
|
+
const members = [];
|
|
72
|
+
let topLevelItems;
|
|
73
|
+
if (Array.isArray(source)) topLevelItems = source.map((r) => r.item);
|
|
74
|
+
else {
|
|
75
|
+
const entryPoint = source.entryPoints[0];
|
|
76
|
+
if (!entryPoint) return members;
|
|
77
|
+
topLevelItems = entryPoint.members;
|
|
78
|
+
}
|
|
79
|
+
for (const item of topLevelItems) if (item.kind === ApiItemKind.Namespace) {
|
|
80
|
+
const namespace = item;
|
|
81
|
+
for (const member of namespace.members) members.push({
|
|
82
|
+
item: member,
|
|
83
|
+
namespace,
|
|
84
|
+
qualifiedName: `${namespace.displayName}.${member.displayName}`
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return members;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Extract plain text from a TSDoc DocNode tree (prose form).
|
|
91
|
+
*
|
|
92
|
+
* Delegates to api-extractor-llms `extractPlainText`. Used internally for
|
|
93
|
+
* `@see` reference text, where `{@link}` targets are flattened to display text.
|
|
94
|
+
*/
|
|
95
|
+
static extractPlainText(node) {
|
|
96
|
+
return extractPlainText(node);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get the summary text from an API item's TSDoc comment
|
|
100
|
+
*/
|
|
101
|
+
static getSummary(item) {
|
|
102
|
+
return getSummary(item);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get the release tag (public, beta, alpha, internal) from an API item
|
|
106
|
+
*/
|
|
107
|
+
static getReleaseTag(item) {
|
|
108
|
+
return getReleaseTag(item);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get parameter documentation from an API item's TSDoc comment
|
|
112
|
+
*/
|
|
113
|
+
static getParams(item) {
|
|
114
|
+
return getParams(item);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get return value documentation from an API item's TSDoc comment
|
|
118
|
+
*/
|
|
119
|
+
static getReturns(item) {
|
|
120
|
+
return getReturns(item);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get code examples from an API item's TSDoc comment
|
|
124
|
+
*/
|
|
125
|
+
static getExamples(item) {
|
|
126
|
+
return getExamples(item);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get deprecation message from an API item's TSDoc comment
|
|
130
|
+
*/
|
|
131
|
+
static getDeprecation(item) {
|
|
132
|
+
return getDeprecation(item);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get inheritance information from a class or interface
|
|
136
|
+
*/
|
|
137
|
+
static getInheritance(item) {
|
|
138
|
+
const result = {};
|
|
139
|
+
if (item.kind === ApiItemKind.Class) {
|
|
140
|
+
const apiClass = item;
|
|
141
|
+
if (apiClass.extendsType) result.extends = [apiClass.extendsType.excerpt.text];
|
|
142
|
+
const implementsTypes = apiClass.implementsTypes || [];
|
|
143
|
+
if (implementsTypes.length > 0) result.implements = implementsTypes.map((type) => type.excerpt.text);
|
|
144
|
+
} else if (item.kind === ApiItemKind.Interface) {
|
|
145
|
+
const extendsTypes = item.extendsTypes || [];
|
|
146
|
+
if (extendsTypes.length > 0) result.extends = extendsTypes.map((type) => type.excerpt.text);
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get see also references from an API item's TSDoc comment
|
|
152
|
+
*/
|
|
153
|
+
static getSeeReferences(item) {
|
|
154
|
+
if (item instanceof ApiDocumentedItem) {
|
|
155
|
+
const tsdoc = item.tsdocComment;
|
|
156
|
+
const references = [];
|
|
157
|
+
for (const seeBlock of tsdoc?.seeBlocks || []) {
|
|
158
|
+
const content = seeBlock.content;
|
|
159
|
+
const text = ApiParser.extractPlainText(content);
|
|
160
|
+
if (text.trim()) references.push({ text: text.replace(/\s+/g, " ").trim() });
|
|
161
|
+
}
|
|
162
|
+
return references;
|
|
163
|
+
}
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get source code link for an API item
|
|
168
|
+
* @param item - The API item
|
|
169
|
+
* @param sourceConfig - Source configuration with repository URL and ref
|
|
170
|
+
* @returns Source code URL with line number, or null if not available
|
|
171
|
+
*/
|
|
172
|
+
static getSourceLink(item, sourceConfig) {
|
|
173
|
+
if (!sourceConfig) return null;
|
|
174
|
+
const itemAny = item;
|
|
175
|
+
const filePath = itemAny.fileUrlPath || itemAny.filePath;
|
|
176
|
+
if (!filePath) return null;
|
|
177
|
+
const lineNumber = itemAny.fileLineNumber || itemAny.line;
|
|
178
|
+
const ref = sourceConfig.ref || "blob/main";
|
|
179
|
+
const baseUrl = `${sourceConfig.url}/${ref}`;
|
|
180
|
+
if (lineNumber) return `${baseUrl}/${filePath}#L${lineNumber}`;
|
|
181
|
+
return `${baseUrl}/${filePath}`;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
//#endregion
|
|
186
|
+
export { ApiParser };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { CrossLinker } from "api-extractor-llms";
|
|
2
|
+
|
|
3
|
+
//#region src/markdown/cross-linker.ts
|
|
4
|
+
/**
|
|
5
|
+
* A cross-linking utility for markdown API documentation.
|
|
6
|
+
*
|
|
7
|
+
* This class maintains a mapping of API item names to their documentation routes,
|
|
8
|
+
* enabling automatic cross-linking of type references in markdown content. It supports
|
|
9
|
+
* both top-level exports and class/interface members.
|
|
10
|
+
*
|
|
11
|
+
* **How it works:**
|
|
12
|
+
* 1. During initialization, it builds a route map from all API items in a package
|
|
13
|
+
* 2. For classes and interfaces, it also maps their members (e.g., `ClassName.methodName`)
|
|
14
|
+
* 3. When processing text, it replaces type names with markdown or HTML links
|
|
15
|
+
*
|
|
16
|
+
* **Relationships:**
|
|
17
|
+
* - Initialized by {@link ApiExtractorPlugin} with categorized API items
|
|
18
|
+
* - Used by page generators to add cross-links in documentation text
|
|
19
|
+
* - Provides route/kind data to {@link ShikiCrossLinker} for code block linking
|
|
20
|
+
*
|
|
21
|
+
* **Link Formats:**
|
|
22
|
+
* - Markdown: `[TypeName](/path/to/type)` - for use in .mdx content
|
|
23
|
+
* - HTML: anchor tags with href - for use in JSX/components
|
|
24
|
+
*
|
|
25
|
+
* @example Initialization
|
|
26
|
+
* ```ts
|
|
27
|
+
* const crossLinker = new MarkdownCrossLinker();
|
|
28
|
+
* const { routes, kinds } = crossLinker.initialize(
|
|
29
|
+
* categorizedItems,
|
|
30
|
+
* "/api/my-package",
|
|
31
|
+
* categories
|
|
32
|
+
* );
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @example Adding cross-links
|
|
36
|
+
* ```ts
|
|
37
|
+
* // Markdown format
|
|
38
|
+
* const text = crossLinker.addCrossLinks("Returns a MyClass instance");
|
|
39
|
+
* // Result: "Returns a [MyClass](/api/my-package/class/myclass) instance"
|
|
40
|
+
*
|
|
41
|
+
* // HTML format (for JSX)
|
|
42
|
+
* const html = crossLinker.addCrossLinksHtml("Returns a MyClass instance");
|
|
43
|
+
* // Result: "Returns a anchor-linked MyClass instance"
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @see {@link ShikiCrossLinker} for code block cross-linking
|
|
47
|
+
*/
|
|
48
|
+
var MarkdownCrossLinker = class {
|
|
49
|
+
/**
|
|
50
|
+
* Map of API item names to their route paths for cross-linking
|
|
51
|
+
*/
|
|
52
|
+
apiItemRoutes = /* @__PURE__ */ new Map();
|
|
53
|
+
/**
|
|
54
|
+
* Clear all accumulated routes. Call at the start of each build.
|
|
55
|
+
*/
|
|
56
|
+
clear() {
|
|
57
|
+
this.apiItemRoutes.clear();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Add routes for API items. Accumulates across multiple calls.
|
|
61
|
+
* Call clear() first if starting a fresh build.
|
|
62
|
+
* @returns Object with routes map and kinds map for semantic highlighting
|
|
63
|
+
*/
|
|
64
|
+
addRoutes(items, baseRoute, categories) {
|
|
65
|
+
const apiItemKinds = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const [categoryKey, categoryConfig] of Object.entries(categories)) {
|
|
67
|
+
const categoryItems = items[categoryKey] || [];
|
|
68
|
+
for (const item of categoryItems) {
|
|
69
|
+
const itemRoute = `${baseRoute}/${categoryConfig.folderName}/${item.displayName.toLowerCase()}`;
|
|
70
|
+
this.apiItemRoutes.set(item.displayName, itemRoute);
|
|
71
|
+
apiItemKinds.set(item.displayName, item.kind);
|
|
72
|
+
if ((item.kind === "Class" || item.kind === "Interface") && item.members) for (const member of item.members) {
|
|
73
|
+
const memberName = member.displayName;
|
|
74
|
+
const memberId = this.sanitizeId(memberName);
|
|
75
|
+
const fullMemberName = `${item.displayName}.${memberName}`;
|
|
76
|
+
const memberRoute = `${itemRoute}#${memberId}`;
|
|
77
|
+
this.apiItemRoutes.set(fullMemberName, memberRoute);
|
|
78
|
+
apiItemKinds.set(fullMemberName, member.kind);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
routes: this.apiItemRoutes,
|
|
84
|
+
kinds: apiItemKinds
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Initialize the cross-link map with all API items.
|
|
89
|
+
* @deprecated Use clear() + addRoutes() instead.
|
|
90
|
+
* @returns Object with routes map and kinds map for semantic highlighting
|
|
91
|
+
*/
|
|
92
|
+
initialize(items, baseRoute, categories) {
|
|
93
|
+
this.clear();
|
|
94
|
+
return this.addRoutes(items, baseRoute, categories);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Set routes directly from pre-built route maps (e.g., from prepareWorkItems).
|
|
98
|
+
* Replaces all existing routes.
|
|
99
|
+
*/
|
|
100
|
+
setRoutes(routes) {
|
|
101
|
+
this.apiItemRoutes.clear();
|
|
102
|
+
for (const [name, route] of routes) this.apiItemRoutes.set(name, route);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Add cross-links to type references in code (markdown format).
|
|
106
|
+
*
|
|
107
|
+
* Skips matches inside backtick code spans and existing markdown links.
|
|
108
|
+
*/
|
|
109
|
+
addCrossLinks(text) {
|
|
110
|
+
if (this.apiItemRoutes.size === 0) return text;
|
|
111
|
+
return new CrossLinker(Array.from(this.apiItemRoutes.keys()).map((name) => ({
|
|
112
|
+
name,
|
|
113
|
+
kind: "type",
|
|
114
|
+
slug: name.toLowerCase()
|
|
115
|
+
})), (ref) => this.apiItemRoutes.get(ref.name) ?? "").addLinks(text);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Add cross-links to type references in code (HTML format)
|
|
119
|
+
* Use this when the text will be rendered as HTML (e.g., in React components)
|
|
120
|
+
*
|
|
121
|
+
* Note: intentionally hand-rolled and test-only — the upstream library's
|
|
122
|
+
* CrossLinker emits markdown links only, so the HTML path has no library
|
|
123
|
+
* equivalent.
|
|
124
|
+
*/
|
|
125
|
+
addCrossLinksHtml(text) {
|
|
126
|
+
let result = text;
|
|
127
|
+
const sortedNames = Array.from(this.apiItemRoutes.keys()).sort((a, b) => b.length - a.length);
|
|
128
|
+
for (const name of sortedNames) {
|
|
129
|
+
const route = this.apiItemRoutes.get(name);
|
|
130
|
+
if (!route) continue;
|
|
131
|
+
const regex = new RegExp(`\\b${name}\\b(?![a-zA-Z])`, "g");
|
|
132
|
+
result = result.replace(regex, (match, offset) => {
|
|
133
|
+
const beforeMatch = result.substring(0, offset);
|
|
134
|
+
if (beforeMatch.includes("<a") && !beforeMatch.includes("</a>")) return match;
|
|
135
|
+
return `<a href="${route}">${match}</a>`;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Sanitize a display name to create a valid HTML ID
|
|
142
|
+
* Converts to lowercase, replaces spaces/special chars with hyphens
|
|
143
|
+
*/
|
|
144
|
+
sanitizeId(displayName, prefix = "") {
|
|
145
|
+
const sanitized = displayName.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
|
|
146
|
+
return prefix ? `${prefix}-${sanitized}` : sanitized;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Module-level instance used by internal generator functions.
|
|
151
|
+
* External callers should create their own instance or use this one.
|
|
152
|
+
*/
|
|
153
|
+
const markdownCrossLinker = new MarkdownCrossLinker();
|
|
154
|
+
|
|
155
|
+
//#endregion
|
|
156
|
+
export { MarkdownCrossLinker, markdownCrossLinker };
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { classifyCutDirective, isTwoslashDirective } from "../twoslash-patterns.js";
|
|
2
|
+
import { formatCode } from "../prettier-formatter.js";
|
|
3
|
+
import { TypeReferenceExtractor } from "../type-reference-extractor.js";
|
|
4
|
+
|
|
5
|
+
//#region src/markdown/helpers.ts
|
|
6
|
+
/* v8 ignore start -- markdown generation helpers, tested via page generator integration tests */
|
|
7
|
+
/**
|
|
8
|
+
* Helper utilities for generating markdown API documentation.
|
|
9
|
+
*
|
|
10
|
+
* This module provides shared utility functions used by the page generators
|
|
11
|
+
* for common tasks like preparing Twoslash examples, generating frontmatter,
|
|
12
|
+
* escaping special characters, and sanitizing IDs.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Generate an "Available from" line for items exported from multiple entry points.
|
|
18
|
+
* Returns empty string if only one entry point or none provided.
|
|
19
|
+
*/
|
|
20
|
+
function generateAvailableFrom(packageName, availableFrom) {
|
|
21
|
+
if (!availableFrom || availableFrom.length <= 1) return "";
|
|
22
|
+
return `Available from: ${availableFrom.map((ep) => ep === "default" ? `\`${packageName}\`` : `\`${packageName}/${ep}\``).join(", ")}\n\n`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Prepare example code for Twoslash rendering.
|
|
26
|
+
*
|
|
27
|
+
* Prepares the code with imports and error directives but does NOT render HTML.
|
|
28
|
+
* Use this for raw markdown output or as input to pre-rendering.
|
|
29
|
+
*
|
|
30
|
+
* @param example - The example with language and code
|
|
31
|
+
* @param apiItemName - The name of the API item being documented
|
|
32
|
+
* @param packageName - The package name for imports
|
|
33
|
+
* @param suppressErrors - Whether to suppress all TypeScript errors (default: true)
|
|
34
|
+
* @returns Object with prepared code and whether it's TypeScript
|
|
35
|
+
*/
|
|
36
|
+
function prepareExampleCode(example, apiItemName, packageName, suppressErrors = true) {
|
|
37
|
+
const { language, code } = example;
|
|
38
|
+
if (!(language === "typescript" || language === "ts" || language === "javascript" || language === "js")) return {
|
|
39
|
+
code,
|
|
40
|
+
isTypeScript: false,
|
|
41
|
+
language
|
|
42
|
+
};
|
|
43
|
+
const importLine = `import { ${apiItemName} } from "${packageName}";`;
|
|
44
|
+
const finalCode = code.includes(`from "${packageName}"`) || code.includes(`from '${packageName}'`) ? code : `${importLine}\n${code}`;
|
|
45
|
+
return {
|
|
46
|
+
code: `${suppressErrors ? "// @noErrors\n" : ""}${finalCode}`,
|
|
47
|
+
isTypeScript: true,
|
|
48
|
+
language: "typescript"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Sanitize a display name to create a URL-safe HTML ID.
|
|
53
|
+
*
|
|
54
|
+
* Converts a display name (e.g., method or property name) into a valid
|
|
55
|
+
* HTML ID suitable for anchor links. Handles special characters, quotes,
|
|
56
|
+
* and optionally adds a prefix for disambiguation.
|
|
57
|
+
*
|
|
58
|
+
* @param displayName - The original display name
|
|
59
|
+
* @param prefix - Optional prefix to add (e.g., "static-property")
|
|
60
|
+
* @returns URL-safe ID string
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* sanitizeId("myMethod"); // "mymethod"
|
|
65
|
+
* sanitizeId("get value"); // "get-value"
|
|
66
|
+
* sanitizeId("run", "static"); // "static-run"
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
function sanitizeId(displayName, prefix = "") {
|
|
70
|
+
const baseName = displayName.replace(/["']/g, "").replace(/[^\w-]/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
71
|
+
return prefix ? `${prefix}-${baseName}` : baseName;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Escape a YAML string value by handling special characters.
|
|
75
|
+
*
|
|
76
|
+
* Normalizes whitespace and wraps strings in double quotes if they contain
|
|
77
|
+
* characters that could break YAML parsing (colons, quotes, hashes, pipes,
|
|
78
|
+
* brackets, braces, Unicode characters, etc.).
|
|
79
|
+
*
|
|
80
|
+
* @param value - The string value to escape
|
|
81
|
+
* @returns YAML-safe string
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* escapeYamlString("Hello World"); // "Hello World"
|
|
86
|
+
* escapeYamlString("Type: string"); // "\"Type: string\""
|
|
87
|
+
* escapeYamlString("He said \"hello\""); // "\"He said \\\"hello\\\"\""
|
|
88
|
+
* escapeYamlString("@pkg/name。:"); // "\"@pkg/name。:\""
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
function escapeYamlString(value) {
|
|
92
|
+
const cleaned = value.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
|
|
93
|
+
if (/["':#|>&*!%@`[\]{},?-]/.test(cleaned) || /[\u0080-\uFFFF]/.test(cleaned) || /^(true|false|null|~|yes|no|on|off)$/i.test(cleaned) || /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(cleaned)) return `"${cleaned.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
94
|
+
return cleaned;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Escape generic type parameters in MDX by wrapping them in backticks.
|
|
98
|
+
*
|
|
99
|
+
* Prevents MDX from interpreting `<T>`, `<TEnv>`, etc. as JSX tags by
|
|
100
|
+
* wrapping them in inline code backticks.
|
|
101
|
+
*
|
|
102
|
+
* @param text - The text containing generic type parameters
|
|
103
|
+
* @returns Text with generics wrapped in backticks
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* escapeMdxGenerics("Returns Promise<T>"); // "Returns Promise`<T>`"
|
|
108
|
+
* escapeMdxGenerics("Map<K, V> extends..."); // "Map`<K, V>` extends..."
|
|
109
|
+
* escapeMdxGenerics("`Pipeline<I, O>`"); // "`Pipeline<I, O>`" (unchanged)
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
function escapeMdxGenerics(text) {
|
|
113
|
+
return text.split(/(`[^`]+`)/g).map((part) => {
|
|
114
|
+
if (part.startsWith("`") && part.endsWith("`")) return part;
|
|
115
|
+
return part.replace(/<([A-Z][A-Za-z0-9_]*(?:\s+extends\s+[^>]+)?(?:,\s*[A-Z][A-Za-z0-9_]*(?:\s+extends\s+[^>]+)?)*)>/g, "`<$1>`");
|
|
116
|
+
}).join("");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build a structured page title for API documentation.
|
|
120
|
+
*
|
|
121
|
+
* Creates a title in the format: `{entityName} | {singularName} | API | {apiName}`
|
|
122
|
+
*
|
|
123
|
+
* @param entityName - The specific entity name (e.g., "MyClass")
|
|
124
|
+
* @param singularName - The category singular name (e.g., "Class")
|
|
125
|
+
* @param apiName - Optional API/package display name
|
|
126
|
+
* @returns Formatted page title
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* buildPageTitle("MyClass", "Class", "My Package");
|
|
131
|
+
* // Returns: "MyClass | Class | API | My Package"
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
function buildPageTitle(entityName, singularName, apiName) {
|
|
135
|
+
const parts = [
|
|
136
|
+
entityName,
|
|
137
|
+
singularName,
|
|
138
|
+
"API"
|
|
139
|
+
];
|
|
140
|
+
if (apiName) parts.push(apiName);
|
|
141
|
+
return parts.join(" | ");
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Generate markdown frontmatter with optional Open Graph metadata.
|
|
145
|
+
*
|
|
146
|
+
* Creates YAML frontmatter for MDX files including title, description,
|
|
147
|
+
* and comprehensive Open Graph meta tags for social sharing.
|
|
148
|
+
*
|
|
149
|
+
* @param entityName - The specific entity name (e.g., "MyClass")
|
|
150
|
+
* @param description - Page description for SEO
|
|
151
|
+
* @param singularName - The category singular name (e.g., "Class")
|
|
152
|
+
* @param apiName - Optional API/package display name
|
|
153
|
+
* @param ogMetadata - Optional Open Graph metadata for social sharing
|
|
154
|
+
* @returns YAML frontmatter string
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* const frontmatter = generateFrontmatter(
|
|
159
|
+
* "MyClass",
|
|
160
|
+
* "A utility class for...",
|
|
161
|
+
* "Class",
|
|
162
|
+
* "My Package"
|
|
163
|
+
* );
|
|
164
|
+
* // Returns:
|
|
165
|
+
* // ---
|
|
166
|
+
* // title: MyClass | Class | API | My Package
|
|
167
|
+
* // description: A utility class for...
|
|
168
|
+
* // ---
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
function generateFrontmatter(entityName, description, singularName, apiName, ogMetadata) {
|
|
172
|
+
const title = buildPageTitle(entityName, singularName, apiName);
|
|
173
|
+
const headEntries = [];
|
|
174
|
+
if (ogMetadata) {
|
|
175
|
+
headEntries.push(["meta", {
|
|
176
|
+
property: "og:url",
|
|
177
|
+
content: `${ogMetadata.siteUrl}${ogMetadata.pageRoute}`
|
|
178
|
+
}]);
|
|
179
|
+
headEntries.push(["meta", {
|
|
180
|
+
property: "og:type",
|
|
181
|
+
content: ogMetadata.ogType
|
|
182
|
+
}]);
|
|
183
|
+
headEntries.push(["meta", {
|
|
184
|
+
property: "og:description",
|
|
185
|
+
content: ogMetadata.description
|
|
186
|
+
}]);
|
|
187
|
+
if (ogMetadata.ogImage) {
|
|
188
|
+
headEntries.push(["meta", {
|
|
189
|
+
property: "og:image",
|
|
190
|
+
content: ogMetadata.ogImage.url
|
|
191
|
+
}]);
|
|
192
|
+
if (ogMetadata.ogImage.secureUrl) headEntries.push(["meta", {
|
|
193
|
+
property: "og:image:secure_url",
|
|
194
|
+
content: ogMetadata.ogImage.secureUrl
|
|
195
|
+
}]);
|
|
196
|
+
if (ogMetadata.ogImage.type) headEntries.push(["meta", {
|
|
197
|
+
property: "og:image:type",
|
|
198
|
+
content: ogMetadata.ogImage.type
|
|
199
|
+
}]);
|
|
200
|
+
if (ogMetadata.ogImage.width) headEntries.push(["meta", {
|
|
201
|
+
property: "og:image:width",
|
|
202
|
+
content: String(ogMetadata.ogImage.width)
|
|
203
|
+
}]);
|
|
204
|
+
if (ogMetadata.ogImage.height) headEntries.push(["meta", {
|
|
205
|
+
property: "og:image:height",
|
|
206
|
+
content: String(ogMetadata.ogImage.height)
|
|
207
|
+
}]);
|
|
208
|
+
if (ogMetadata.ogImage.alt) headEntries.push(["meta", {
|
|
209
|
+
property: "og:image:alt",
|
|
210
|
+
content: ogMetadata.ogImage.alt
|
|
211
|
+
}]);
|
|
212
|
+
}
|
|
213
|
+
headEntries.push(["meta", {
|
|
214
|
+
property: "article:published_time",
|
|
215
|
+
content: ogMetadata.publishedTime
|
|
216
|
+
}]);
|
|
217
|
+
headEntries.push(["meta", {
|
|
218
|
+
property: "article:modified_time",
|
|
219
|
+
content: ogMetadata.modifiedTime
|
|
220
|
+
}]);
|
|
221
|
+
headEntries.push(["meta", {
|
|
222
|
+
property: "article:section",
|
|
223
|
+
content: ogMetadata.section
|
|
224
|
+
}]);
|
|
225
|
+
for (const tag of ogMetadata.tags) headEntries.push(["meta", {
|
|
226
|
+
property: "article:tag",
|
|
227
|
+
content: tag
|
|
228
|
+
}]);
|
|
229
|
+
}
|
|
230
|
+
let frontmatter = `---\ntitle: ${escapeYamlString(title)}\n`;
|
|
231
|
+
frontmatter += `description: ${escapeYamlString(description)}\n`;
|
|
232
|
+
if (headEntries.length > 0) {
|
|
233
|
+
frontmatter += "head:\n";
|
|
234
|
+
for (const [tag, attrs] of headEntries) {
|
|
235
|
+
frontmatter += ` - - ${tag}\n`;
|
|
236
|
+
frontmatter += " - ";
|
|
237
|
+
const attrPairs = Object.entries(attrs).map(([key, value]) => `${key}: ${escapeYamlString(value)}`);
|
|
238
|
+
frontmatter += attrPairs.join("\n ");
|
|
239
|
+
frontmatter += "\n";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
frontmatter += "---\n\n";
|
|
243
|
+
return frontmatter;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Strip Twoslash directives from code for display purposes.
|
|
247
|
+
*
|
|
248
|
+
* Removes Twoslash directive comments like `// @noErrors`, `// @errors: 2304`,
|
|
249
|
+
* `// @filename: ...`, etc. from code so users see clean output and don't
|
|
250
|
+
* copy directives when using the copy button.
|
|
251
|
+
*
|
|
252
|
+
* Also handles cut directives:
|
|
253
|
+
* - `// ---cut---` - Removes this line and all lines before it
|
|
254
|
+
* - `// ---cut-before---` - Same as ---cut---
|
|
255
|
+
* - `// ---cut-after---` - Removes this line and all lines after it
|
|
256
|
+
*
|
|
257
|
+
* @param code - The code containing Twoslash directives
|
|
258
|
+
* @returns Code with Twoslash directives removed
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```ts
|
|
262
|
+
* const display = stripTwoslashDirectives("// @noErrors\nconst x = 1;");
|
|
263
|
+
* // Returns: "const x = 1;"
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
function stripTwoslashDirectives(code) {
|
|
267
|
+
const lines = code.split("\n");
|
|
268
|
+
let cutBeforeIndex = -1;
|
|
269
|
+
let cutAfterIndex = -1;
|
|
270
|
+
const cutRanges = [];
|
|
271
|
+
const cutStartStack = [];
|
|
272
|
+
for (let i = 0; i < lines.length; i++) {
|
|
273
|
+
const cutType = classifyCutDirective(lines[i].trim());
|
|
274
|
+
if (cutType === "cut-before") cutBeforeIndex = i;
|
|
275
|
+
else if (cutType === "cut-after") cutAfterIndex = i;
|
|
276
|
+
else if (cutType === "cut-start") cutStartStack.push(i);
|
|
277
|
+
else if (cutType === "cut-end") {
|
|
278
|
+
const startIdx = cutStartStack.pop();
|
|
279
|
+
if (startIdx !== void 0) cutRanges.push([startIdx, i]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
let filteredLines = lines;
|
|
283
|
+
if (cutBeforeIndex >= 0) {
|
|
284
|
+
filteredLines = filteredLines.slice(cutBeforeIndex + 1);
|
|
285
|
+
if (cutAfterIndex >= 0) cutAfterIndex = cutAfterIndex - cutBeforeIndex - 1;
|
|
286
|
+
for (const range of cutRanges) {
|
|
287
|
+
range[0] -= cutBeforeIndex + 1;
|
|
288
|
+
range[1] -= cutBeforeIndex + 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (cutAfterIndex >= 0) filteredLines = filteredLines.slice(0, cutAfterIndex);
|
|
292
|
+
const excludedLines = /* @__PURE__ */ new Set();
|
|
293
|
+
for (const [start, end] of cutRanges) for (let i = start; i <= end; i++) if (i >= 0 && i < filteredLines.length) excludedLines.add(i);
|
|
294
|
+
return filteredLines.filter((line, i) => {
|
|
295
|
+
if (excludedLines.has(i)) return false;
|
|
296
|
+
if (isTwoslashDirective(line.trim())) return false;
|
|
297
|
+
return true;
|
|
298
|
+
}).join("\n").trim();
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Format import statements with cut directive for hidden imports.
|
|
302
|
+
*
|
|
303
|
+
* Prepends import statements followed by `// ---cut---` so Twoslash can
|
|
304
|
+
* resolve the types but the imports are hidden from rendered output.
|
|
305
|
+
*
|
|
306
|
+
* @param imports - Import statements to format
|
|
307
|
+
* @returns Formatted import block with cut directive, or empty string if no imports
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* ```ts
|
|
311
|
+
* const imports = [{ packageName: "zod", symbols: new Set(["ZodType"]), typeOnly: true }];
|
|
312
|
+
* const block = formatImportsWithCut(imports);
|
|
313
|
+
* // Returns:
|
|
314
|
+
* // import type { ZodType } from "zod";
|
|
315
|
+
* // // ---cut---
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
function formatImportsWithCut(imports) {
|
|
319
|
+
if (imports.length === 0) return "";
|
|
320
|
+
return `${TypeReferenceExtractor.formatImports(imports).join("\n")}\n// ---cut---\n`;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Prepend hidden imports to code using the Twoslash cut directive.
|
|
324
|
+
*
|
|
325
|
+
* This enables type resolution for external types while hiding the import
|
|
326
|
+
* statements from rendered output. The existing `stripTwoslashDirectives()`
|
|
327
|
+
* function handles removing the cut block for clipboard copying.
|
|
328
|
+
*
|
|
329
|
+
* @param code - The code to prepend imports to
|
|
330
|
+
* @param imports - Import statements to add
|
|
331
|
+
* @returns Code with imports prepended (if any), or original code if no imports
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```ts
|
|
335
|
+
* const code = "function foo(): RsbuildPlugin";
|
|
336
|
+
* const imports = [{ packageName: "@rsbuild/core", symbols: new Set(["RsbuildPlugin"]), typeOnly: true }];
|
|
337
|
+
* const result = prependHiddenImports(code, imports);
|
|
338
|
+
* // Returns:
|
|
339
|
+
* // import type { RsbuildPlugin } from "@rsbuild/core";
|
|
340
|
+
* // // ---cut---
|
|
341
|
+
* // function foo(): RsbuildPlugin
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
function prependHiddenImports(code, imports) {
|
|
345
|
+
const importBlock = formatImportsWithCut(imports);
|
|
346
|
+
return importBlock ? importBlock + code : code;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Format example code using Prettier for consistent styling.
|
|
350
|
+
*
|
|
351
|
+
* Wraps the Prettier formatter with error handling and context tracking.
|
|
352
|
+
* If formatting fails, returns the original code (fallthrough behavior).
|
|
353
|
+
*
|
|
354
|
+
* @param code - The code to format
|
|
355
|
+
* @param language - The code fence language (e.g., "typescript", "ts")
|
|
356
|
+
* @param _context - Optional context (reserved for future use)
|
|
357
|
+
* @returns The formatted code (or original if formatting fails)
|
|
358
|
+
*/
|
|
359
|
+
async function formatExampleCode(code, language, _context) {
|
|
360
|
+
return (await formatCode(code, language)).code;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
//#endregion
|
|
364
|
+
export { escapeMdxGenerics, escapeYamlString, formatExampleCode, generateAvailableFrom, generateFrontmatter, prepareExampleCode, prependHiddenImports, sanitizeId, stripTwoslashDirectives };
|