-
Notifications
You must be signed in to change notification settings - Fork 46
Expand file tree
/
Copy pathbuildContent.mjs
More file actions
351 lines (305 loc) · 11 KB
/
buildContent.mjs
File metadata and controls
351 lines (305 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
'use strict';
import { h as createElement } from 'hastscript';
import { slice } from 'mdast-util-slice-markdown';
import { u as createTree } from 'unist-builder';
import { SKIP, visit } from 'unist-util-visit';
import { createJSXElement } from './ast.mjs';
import { buildMetaBarProps } from './buildBarProps.mjs';
import buildStabilityOverview from './buildStabilityOverview.mjs';
import { enforceArray } from '../../../utils/array.mjs';
import { JSX_IMPORTS } from '../../web/constants.mjs';
import {
STABILITY_LEVELS,
LIFECYCLE_LABELS,
INTERNATIONALIZABLE,
STABILITY_PREFIX_LENGTH,
DEPRECATION_TYPE_PATTERNS,
ALERT_LEVELS,
TYPES_WITH_METHOD_SIGNATURES,
TYPE_PREFIX_LENGTH,
} from '../constants.mjs';
import {
insertSignatureCodeBlock,
createSignatureTable,
getFullName,
} from './signature.mjs';
import getConfig from '../../../utils/configuration/index.mjs';
import {
GITHUB_BLOB_URL,
populate,
} from '../../../utils/configuration/templates.mjs';
import { UNIST } from '../../../utils/queries/index.mjs';
/**
* Processes lifecycle and change history data into a sorted array of change entries.
* @param {import('../../metadata/types').MetadataEntry} entry - The metadata entry
* @param {import('unified').Processor} remark - The remark processor
*/
export const gatherChangeEntries = (entry, remark) => {
// Lifecycle changes (e.g., added, deprecated)
const lifecycleChanges = Object.entries(LIFECYCLE_LABELS)
.filter(([field]) => entry[field])
.map(([field, label]) => ({
versions: enforceArray(entry[field]),
label: `${label}: ${enforceArray(entry[field]).join(', ')}`,
}));
// Explicit changes with parsed JSX labels
const explicitChanges = (entry.changes || []).map(change => ({
versions: enforceArray(change.version),
label: remark.runSync(remark.parse(change.description)).body[0].expression,
url: change['pr-url'],
}));
return [...lifecycleChanges, ...explicitChanges];
};
/**
* Creates a JSX ChangeHistory element or returns null if no changes.
* @param {import('../../metadata/types').MetadataEntry} entry - The metadata entry
* @param {import('unified').Processor} remark - The remark processor
*/
export const createChangeElement = (entry, remark) => {
const changes = gatherChangeEntries(entry, remark);
if (!changes.length) {
return null;
}
return createJSXElement(JSX_IMPORTS.ChangeHistory.name, {
changes,
className: 'change-history',
});
};
/**
* Creates a span element with a link to the source code, or null if no source.
* @param {string|undefined} sourceLink - The source link path
*/
export const createSourceLink = sourceLink => {
const config = getConfig('jsx-ast');
return sourceLink
? createElement('span', [
INTERNATIONALIZABLE.sourceCode,
createElement(
'a',
{
href: `${populate(GITHUB_BLOB_URL, config)}${sourceLink}`,
target: '_blank',
},
[
sourceLink,
createJSXElement(JSX_IMPORTS.ArrowUpRightIcon.name, {
inline: true,
className: 'arrow',
}),
]
),
])
: null;
};
/**
* Extracts heading content text with fallback and formats it.
* @param {import('mdast').Node} content - The content node to extract text from
*/
export const extractHeadingContent = content => {
const { text, type } = content.data;
if (!text) {
return content.children;
}
// Try to get full name; fallback slices text after first colon
const fullName = getFullName(content.data, false);
if (fullName) {
return type === 'ctor' ? `${fullName} Constructor` : fullName;
}
return content.children;
};
/**
* Creates a heading wrapper element with anchors, icons, and optional change history.
* @param {import('../../metadata/types').HeadingNode} content - The content node to extract text from
* @param {import('unist').Node|null} changeElement - The change history element, if available
*/
export const createHeadingElement = (content, changeElement) => {
const { type, slug } = content.data;
let headingContent = extractHeadingContent(content);
// Build heading with anchor link
const headingWrapper = createElement('div', [
createElement(
`h${content.depth}`,
{ id: slug },
createElement(
'a',
{ href: `#${slug}`, className: ['anchor'] },
headingContent
)
),
]);
// Prepend type icon if not 'misc' and type exists
if (type && type !== 'misc') {
headingWrapper.children.unshift(
createJSXElement(JSX_IMPORTS.DataTag.name, { kind: type, size: 'sm' })
);
}
// Append change history if available
if (changeElement) {
headingWrapper.children.push(changeElement);
}
return headingWrapper;
};
/**
* Converts a stability note node to an AlertBox JSX element
* @param {import('../../metadata/types').StabilityNode} node - The stability node to transform
* @param {number} index - The index of the node in its parent's children array
* @param {import('unist').Parent} parent - The parent node containing the stability node
*/
export const transformStabilityNode = (node, index, parent) => {
// Calculate slice start to skip the stability prefix + index length
const start = STABILITY_PREFIX_LENGTH + node.data.index.length;
const stabilityLevel = parseInt(node.data.index, 10);
parent.children[index] = createJSXElement(JSX_IMPORTS.AlertBox.name, {
children: slice(node, start, undefined, {
textHandling: { boundaries: 'preserve' },
}).node.children[0].children,
level: STABILITY_LEVELS[stabilityLevel],
title: `Stability: ${node.data.index}`,
});
return [SKIP];
};
/**
* Maps deprecation type text to AlertBox level
*
* @param {string} typeText - The deprecation type text
* @returns {string} The corresponding AlertBox level
*/
const getLevelFromDeprecationType = typeText => {
const match = DEPRECATION_TYPE_PATTERNS.find(p => p.pattern.test(typeText));
return match ? match.level : ALERT_LEVELS.DANGER;
};
/**
* Transforms a heading node by injecting metadata, source links, and signatures.
* @param {import('../../metadata/types').MetadataEntry} entry - The API metadata entry
* @param {import('unified').Processor} remark - The remark processor
* @param {import('../../metadata/types').HeadingNode} node - The heading node to transform
* @param {number} index - The index of the node in its parent's children array
* @param {import('unist').Parent} parent - The parent node containing the heading
*/
export const transformHeadingNode = async (
entry,
remark,
node,
index,
parent
) => {
// Replace heading node with our enhanced heading element
parent.children[index] = createHeadingElement(
node,
createChangeElement(entry, remark)
);
if (entry.api === 'deprecations' && node.depth === 3) {
// On the 'deprecations.md' page, "Type: <XYZ>" turns into an AlertBox
// Extract the nodes representing the type text
const { node } = slice(
parent.children[index + 1],
TYPE_PREFIX_LENGTH,
undefined,
{ textHandling: { boundaries: 'preserve' } }
);
// Then retrieve its children to be the AlertBox content
const { children: sliced } = node;
parent.children[index + 1] = createJSXElement(JSX_IMPORTS.AlertBox.name, {
children: sliced,
// we assume sliced[0] is a text node here that contains the type text
level: getLevelFromDeprecationType(sliced[0].value),
title: 'Type',
});
}
// Add source link element if available, right after heading
const sourceLink = createSourceLink(entry.source_link);
if (sourceLink) {
parent.children.splice(index + 1, 0, sourceLink);
}
// If the heading type supports method signatures, insert signature block
if (TYPES_WITH_METHOD_SIGNATURES.includes(node.data.type)) {
insertSignatureCodeBlock(parent, node, index + 1);
}
return [SKIP];
};
/**
* Processes a single API documentation entry's content
* @param {import('../../metadata/types').MetadataEntry} entry - The API metadata entry to process
* @param {import('unified').Processor} remark - The remark processor
*/
export const processEntry = (entry, remark, stabilityOverviewEntries = []) => {
// Deep copy content to avoid mutations on original
const content = structuredClone(entry.content);
// Visit and transform stability nodes
visit(content, UNIST.isStabilityNode, transformStabilityNode);
// Visit and transform headings with metadata and links
visit(content, UNIST.isHeading, (...args) =>
transformHeadingNode(entry, remark, ...args)
);
// Transform typed lists into property tables
visit(
content,
UNIST.isStronglyTypedList,
(node, idx, parent) =>
(parent.children[idx] = createSignatureTable(node, remark))
);
// Inject the stability overview table where the slot tag is present
if (
stabilityOverviewEntries.length &&
entry.tags?.includes('STABILITY_OVERVIEW_SLOT_BEGIN')
) {
content.children.push(buildStabilityOverview(stabilityOverviewEntries));
}
return content;
};
/**
* Builds the overall document layout tree
* @param {Array<import('../../metadata/types').MetadataEntry>} entries - API documentation metadata entries
* @param {ReturnType<import('./buildBarProps.mjs').buildSideBarProps>} sideBarProps - Props for the sidebar component
* @param {ReturnType<buildMetaBarProps>} metaBarProps - Props for the meta bar component
* @param {import('unified').Processor} remark - The remark processor
*/
export const createDocumentLayout = (
entries,
sideBarProps,
metaBarProps,
remark,
stabilityOverviewEntries = []
) =>
createTree('root', [
createJSXElement(JSX_IMPORTS.Layout.name, {
sideBarProps,
metaBarProps,
children: entries.map(entry =>
processEntry(entry, remark, stabilityOverviewEntries)
),
}),
]);
/**
* @typedef {import('estree').Node & { data: import('../../metadata/types').MetadataEntry }} JSXContent
*
* Transforms API metadata entries into processed MDX content
* @param {Array<import('../../metadata/types').MetadataEntry>} metadataEntries - API documentation metadata entries
* @param {import('../../metadata/types').MetadataEntry} head - Main API metadata entry with version information
* @param {Object} sideBarProps - Props for the sidebar component
* @param {import('unified').Processor} remark - Remark processor instance for markdown processing
* @returns {Promise<JSXContent>}
*/
const buildContent = async (
metadataEntries,
head,
sideBarProps,
remark,
stabilityOverviewEntries = []
) => {
// Build props for the MetaBar from head and entries
const metaBarProps = buildMetaBarProps(head, metadataEntries);
// Create root document AST with all layout components and processed content
const root = createDocumentLayout(
metadataEntries,
sideBarProps,
metaBarProps,
remark,
stabilityOverviewEntries
);
// Run remark processor to transform AST (parse markdown, plugins, etc.)
const ast = await remark.run(root);
// The final MDX content is the expression in the Program's first body node
return { ...ast.body[0].expression, data: head };
};
export default buildContent;