-
Notifications
You must be signed in to change notification settings - Fork 45
Expand file tree
/
Copy pathindex.mjs
More file actions
275 lines (238 loc) · 9.07 KB
/
index.mjs
File metadata and controls
275 lines (238 loc) · 9.07 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
'use strict';
import yaml from 'yaml';
import {
DOC_API_HEADING_TYPES,
DOC_MDN_BASE_URL_JS_GLOBALS,
DOC_MDN_BASE_URL_JS_PRIMITIVES,
DOC_TYPES_MAPPING_GLOBALS,
DOC_TYPES_MAPPING_OTHER,
DOC_TYPES_MAPPING_PRIMITIVES,
DOC_MAN_BASE_URL,
TYPE_GENERIC_REGEX,
} from './constants.mjs';
import { slug } from './slugger.mjs';
import createQueries from '../queries/index.mjs';
/**
* Extracts raw YAML content from a node
*
* @param {import('mdast').Node} node A HTML node containing the YAML content
* @returns {string} The extracted raw YAML content
*/
export const extractYamlContent = node => {
return node.value.replace(
createQueries.QUERIES.yamlInnerContent,
// Either capture a YAML multinline block, or a simple single-line YAML block
(_, simple, yaml) => simple || yaml
);
};
/**
* Normalizes YAML syntax by fixing some non-cool formatted properties of the
* docs schema
*
* @param {string} yamlContent The raw YAML content to normalize
* @returns {string} The normalized YAML content
*/
export const normalizeYamlSyntax = yamlContent => {
return yamlContent
.replace('introduced_in=', 'introduced_in: ')
.replace('source_link=', 'source_link: ')
.replace('type=', 'type: ')
.replace('name=', 'name: ')
.replace('llm_description=', 'llm_description: ')
.replace(/^[\r\n]+|[\r\n]+$/g, ''); // Remove initial and final line breaks
};
/**
* @param {string} text The inner text
* @param {string} command The manual page
* @param {string} sectionNumber The manual section
* @param {string} sectionLetter The manual section number
*/
export const transformUnixManualToLink = (
text,
command,
sectionNumber,
sectionLetter = ''
) => {
return `[\`${text}\`](${DOC_MAN_BASE_URL}${sectionNumber}/${command}.${sectionNumber}${sectionLetter}.html)`;
};
/**
* Safely splits the string by `|`, ignoring pipes that are inside `< >`
*
* @param {string} str The type string to split
* @returns {string[]} An array of type pieces
*/
const splitByOuterUnion = str => {
const result = [];
let current = '';
let depth = 0;
for (const char of str) {
if (char === '<') {
depth++;
} else if (char === '>') {
depth--;
} else if (char === '|' && depth === 0) {
result.push(current);
current = '';
continue;
}
current += char;
}
result.push(current);
return result;
};
/**
* Attempts to parse and format a basic Generic type (e.g., Promise<string>).
* It also supports union and multi-parameter types within the generic brackets.
*
* @param {string} typePiece The plain type piece to be evaluated
* @param {Function} transformType The function used to resolve individual types into links
* @returns {string|null} The formatted Markdown link, or null if no match is found
*/
const formatBasicGeneric = (typePiece, transformType) => {
const genericMatch = typePiece.match(TYPE_GENERIC_REGEX);
if (genericMatch) {
const baseType = genericMatch[1].trim();
const innerType = genericMatch[2].trim();
const baseResult = transformType(baseType.replace(/\[\]$/, ''));
const baseFormatted = baseResult
? `[\`<${baseType}>\`](${baseResult})`
: `\`<${baseType}>\``;
// Split while capturing delimiters (| or ,) to preserve original syntax
const parts = innerType.split(/([|,])/);
const innerFormatted = parts
.map(part => {
const trimmed = part.trim();
// If it is a delimiter, return it as is
if (trimmed === '|') {
return ' | ';
}
if (trimmed === ',') {
return ', ';
}
const innerRes = transformType(trimmed.replace(/\[\]$/, ''));
return innerRes
? `[\`<${trimmed}>\`](${innerRes})`
: `\`<${trimmed}>\``;
})
.join('');
return `${baseFormatted}<${innerFormatted}>`;
}
return null;
};
/**
* This method replaces plain text Types within the Markdown content into Markdown links
* that link to the actual relevant reference for such type (either internal or external link)
*
* @param {string} type The plain type to be transformed into a Markdown link
* @param {Record<string, string>} record The mapping of types to links
* @returns {string} The Markdown link as a string (formatted in Markdown)
*/
export const transformTypeToReferenceLink = (type, record) => {
// Removes the wrapping curly braces that wrap the type references
// We keep the angle brackets `<>` intact here to parse Generics later
const typeInput = type.replace(/[{}]/g, '');
/**
* Handles the mapping (if there's a match) of the input text
* into the reference type from the API docs
*
* @param {string} lookupPiece
* @returns {string} The reference URL or empty string if no match
*/
const transformType = lookupPiece => {
// Transform JS primitive type references into Markdown links (MDN)
if (lookupPiece.toLowerCase() in DOC_TYPES_MAPPING_PRIMITIVES) {
const typeValue = DOC_TYPES_MAPPING_PRIMITIVES[lookupPiece.toLowerCase()];
return `${DOC_MDN_BASE_URL_JS_PRIMITIVES}#${typeValue}_type`;
}
// Transforms JS Global type references into Markdown links (MDN)
if (lookupPiece in DOC_TYPES_MAPPING_GLOBALS) {
return `${DOC_MDN_BASE_URL_JS_GLOBALS}${lookupPiece}`;
}
// Transform other external Web/JavaScript type references into Markdown links
// to diverse different external websites. These already are formatted as links
if (lookupPiece in DOC_TYPES_MAPPING_OTHER) {
return DOC_TYPES_MAPPING_OTHER[lookupPiece];
}
// Transform Node.js type/module references into Markdown links
// that refer to other API docs pages within the Node.js API docs
if (record && lookupPiece in record) {
return record[lookupPiece];
}
// Transform Node.js types like 'vm.Something'.
if (lookupPiece.indexOf('.') >= 0) {
const [mod, ...pieces] = lookupPiece.split('.');
const isClass = pieces.at(-1).match(/^[A-Z][a-z]/);
return `${mod}.html#${isClass ? 'class-' : ''}${slug(lookupPiece)}`;
}
return '';
};
const typePieces = splitByOuterUnion(typeInput).map(piece => {
// This is the content to render as the text of the Markdown link
const trimmedPiece = piece.trim();
// 1. Attempt to format as a basic Generic type first
const genericMarkdown = formatBasicGeneric(trimmedPiece, transformType);
if (genericMarkdown) {
return genericMarkdown;
}
// 2. Fallback to the logic for plain types
// This is what we will compare against the API types mappings
// The ReGeX below is used to remove `[]` from the end of the type
const result = transformType(trimmedPiece.replace(/\[\]$/, ''));
// If we have a valid result and the piece is not empty, we return the Markdown link
if (trimmedPiece.length && result.length) {
return `[\`<${trimmedPiece}>\`](${result})`;
}
});
// Filter out pieces that we failed to map and then join the valid ones
// into different links separated by a ` | `
const markdownLinks = typePieces.filter(Boolean).join(' | ');
// Return the replaced links or the original content if they all failed to be replaced
// Note that if some failed to get replaced, only the valid ones will be returned
// If no valid entry exists, we return the original string/type
return markdownLinks || type;
};
/**
* Parses Markdown YAML source into a JavaScript object containing all the metadata
* (this is forwarded to the parser so it knows what to do with said metadata)
*
* @param {string} yamlString The YAML string to be parsed
* @returns {ApiDocRawMetadataEntry} The parsed YAML metadata
*/
export const parseYAMLIntoMetadata = yamlString => {
const normalizedYaml = normalizeYamlSyntax(yamlString);
// Ensures that the parsed YAML is an object, because even if it is not
// i.e. a plain string or an array, it will simply not result into anything
/** @type {ApiDocRawMetadataEntry | string} */
let parsedYaml = yaml.parse(normalizedYaml);
// Ensure that only Objects get parsed on Object.keys(), since some `<!--`
// comments, might be just plain strings and not even a valid YAML metadata
if (typeof parsedYaml === 'string') {
parsedYaml = { tags: [parsedYaml] };
}
return parsedYaml;
};
/**
* Parses a raw Heading string into Heading metadata
*
* @param {string} heading The raw Heading text
* @param {number} depth The depth of the heading
* @returns {HeadingMetadataEntry} Parsed Heading entry
*/
export const parseHeadingIntoMetadata = (heading, depth) => {
for (const { type, regex } of DOC_API_HEADING_TYPES) {
// Attempts to get a match from one of the heading types, if a match is found
// we use that type as the heading type, and extract the regex expression match group
// which should be the inner "plain" heading content (or the title of the heading for navigation)
const [, ...matches] = heading.match(regex) ?? [];
if (matches?.length) {
return {
text: heading,
type,
// The highest match group should be used.
name: matches.filter(Boolean).at(-1),
depth,
};
}
}
return { text: heading, name: heading, depth };
};