Skip to content

Commit c83af18

Browse files
authored
feat(parser): support generics in type parsing (#668)
* feat(parser): support basic TypeScript generics in markdown links * test(parser): add coverage for basic generic types * feat(parser): support inner union types and multi-parameters in generics * style(parser): apply reviewer feedback (move functions, extract regex, cleanup fallback) * fix(parser): use regex for array suffix and fix test spacing * refactor(parser): move generic regex to constants file per reviewer feedback * chore: remove deprecated constants file resurrected during merge
1 parent dce2f3a commit c83af18

File tree

3 files changed

+127
-6
lines changed

3 files changed

+127
-6
lines changed

src/generators/metadata/constants.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export const DOC_API_HEADING_TYPES = [
7373
},
7474
];
7575

76+
// This regex is used to match basic TypeScript generic types (e.g., Promise<string>)
77+
export const TYPE_GENERIC_REGEX = /^([^<]+)<([^>]+)>$/;
78+
7679
// This is a mapping for types within the Markdown content and their respective
7780
// JavaScript primitive types within the MDN JavaScript docs
7881
// @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Data_structures#primitive_values

src/generators/metadata/utils/__tests__/transformers.mjs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,39 @@ describe('transformTypeToReferenceLink', () => {
2626
'[`<SomeOtherType>`](fromTypeMap)'
2727
);
2828
});
29+
30+
it('should transform a basic Generic type into a Markdown link', () => {
31+
strictEqual(
32+
transformTypeToReferenceLink('{Promise<string>}'),
33+
'[`<Promise>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)&lt;[`<string>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type)&gt;'
34+
);
35+
});
36+
37+
it('should partially transform a Generic type if only one part is known', () => {
38+
strictEqual(
39+
transformTypeToReferenceLink('{CustomType<string>}', {}),
40+
'`<CustomType>`&lt;[`<string>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type)&gt;'
41+
);
42+
});
43+
44+
it('should transform a Generic type with an inner union like {Promise<string|boolean>}', () => {
45+
strictEqual(
46+
transformTypeToReferenceLink('{Promise<string|boolean>}', {}),
47+
'[`<Promise>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)&lt;[`<string>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) | [`<boolean>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#boolean_type)&gt;'
48+
);
49+
});
50+
51+
it('should transform multi-parameter generics like {Map<string, number>}', () => {
52+
strictEqual(
53+
transformTypeToReferenceLink('{Map<string, number>}', {}),
54+
'[`<Map>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)&lt;[`<string>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type), [`<number>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type)&gt;'
55+
);
56+
});
57+
58+
it('should handle outer unions with generics like {Promise<string|number> | boolean}', () => {
59+
strictEqual(
60+
transformTypeToReferenceLink('{Promise<string|number> | boolean}', {}),
61+
'[`<Promise>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)&lt;[`<string>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) | [`<number>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type)&gt; | [`<boolean>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#boolean_type)'
62+
);
63+
});
2964
});

src/generators/metadata/utils/transformers.mjs

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { transformNodesToString } from '../../../utils/unist.mjs';
21
import {
32
DOC_MDN_BASE_URL_JS_GLOBALS,
43
DOC_MDN_BASE_URL_JS_PRIMITIVES,
@@ -7,8 +6,10 @@ import {
76
DOC_TYPES_MAPPING_PRIMITIVES,
87
DOC_MAN_BASE_URL,
98
DOC_API_HEADING_TYPES,
9+
TYPE_GENERIC_REGEX,
1010
} from '../constants.mjs';
1111
import { slug } from './slugger.mjs';
12+
import { transformNodesToString } from '../../../utils/unist.mjs';
1213

1314
/**
1415
* @param {string} text The inner text
@@ -24,7 +25,81 @@ export const transformUnixManualToLink = (
2425
) => {
2526
return `[\`${text}\`](${DOC_MAN_BASE_URL}${sectionNumber}/${command}.${sectionNumber}${sectionLetter}.html)`;
2627
};
28+
/**
29+
* Safely splits the string by `|`, ignoring pipes that are inside `< >`
30+
*
31+
* @param {string} str The type string to split
32+
* @returns {string[]} An array of type pieces
33+
*/
34+
const splitByOuterUnion = str => {
35+
const result = [];
36+
let current = '';
37+
let depth = 0;
38+
39+
for (const char of str) {
40+
if (char === '<') {
41+
depth++;
42+
} else if (char === '>') {
43+
depth--;
44+
} else if (char === '|' && depth === 0) {
45+
result.push(current);
46+
current = '';
47+
continue;
48+
}
49+
current += char;
50+
}
51+
52+
result.push(current);
53+
return result;
54+
};
2755

56+
/**
57+
* Attempts to parse and format a basic Generic type (e.g., Promise<string>).
58+
* It also supports union and multi-parameter types within the generic brackets.
59+
*
60+
* @param {string} typePiece The plain type piece to be evaluated
61+
* @param {Function} transformType The function used to resolve individual types into links
62+
* @returns {string|null} The formatted Markdown link, or null if no match is found
63+
*/
64+
const formatBasicGeneric = (typePiece, transformType) => {
65+
const genericMatch = typePiece.match(TYPE_GENERIC_REGEX);
66+
67+
if (genericMatch) {
68+
const baseType = genericMatch[1].trim();
69+
const innerType = genericMatch[2].trim();
70+
71+
const baseResult = transformType(baseType.replace(/\[\]$/, ''));
72+
const baseFormatted = baseResult
73+
? `[\`<${baseType}>\`](${baseResult})`
74+
: `\`<${baseType}>\``;
75+
76+
// Split while capturing delimiters (| or ,) to preserve original syntax
77+
const parts = innerType.split(/([|,])/);
78+
79+
const innerFormatted = parts
80+
.map(part => {
81+
const trimmed = part.trim();
82+
// If it is a delimiter, return it as is
83+
if (trimmed === '|') {
84+
return ' | ';
85+
}
86+
87+
if (trimmed === ',') {
88+
return ', ';
89+
}
90+
91+
const innerRes = transformType(trimmed.replace(/\[\]$/, ''));
92+
return innerRes
93+
? `[\`<${trimmed}>\`](${innerRes})`
94+
: `\`<${trimmed}>\``;
95+
})
96+
.join('');
97+
98+
return `${baseFormatted}&lt;${innerFormatted}&gt;`;
99+
}
100+
101+
return null;
102+
};
28103
/**
29104
* This method replaces plain text Types within the Markdown content into Markdown links
30105
* that link to the actual relevant reference for such type (either internal or external link)
@@ -34,8 +109,9 @@ export const transformUnixManualToLink = (
34109
* @returns {string} The Markdown link as a string (formatted in Markdown)
35110
*/
36111
export const transformTypeToReferenceLink = (type, record) => {
37-
// Removes the wrapping tags that wrap the type references such as `<>` and `{}`
38-
const typeInput = type.replace(/[{}<>]/g, '');
112+
// Removes the wrapping curly braces that wrap the type references
113+
// We keep the angle brackets `<>` intact here to parse Generics later
114+
const typeInput = type.replace(/[{}]/g, '');
39115

40116
/**
41117
* Handles the mapping (if there's a match) of the input text
@@ -65,7 +141,7 @@ export const transformTypeToReferenceLink = (type, record) => {
65141

66142
// Transform Node.js type/module references into Markdown links
67143
// that refer to other API docs pages within the Node.js API docs
68-
if (lookupPiece in record) {
144+
if (record && lookupPiece in record) {
69145
return record[lookupPiece];
70146
}
71147

@@ -80,13 +156,20 @@ export const transformTypeToReferenceLink = (type, record) => {
80156
return '';
81157
};
82158

83-
const typePieces = typeInput.split('|').map(piece => {
159+
const typePieces = splitByOuterUnion(typeInput).map(piece => {
84160
// This is the content to render as the text of the Markdown link
85161
const trimmedPiece = piece.trim();
86162

163+
// 1. Attempt to format as a basic Generic type first
164+
const genericMarkdown = formatBasicGeneric(trimmedPiece, transformType);
165+
if (genericMarkdown) {
166+
return genericMarkdown;
167+
}
168+
169+
// 2. Fallback to the logic for plain types
87170
// This is what we will compare against the API types mappings
88171
// The ReGeX below is used to remove `[]` from the end of the type
89-
const result = transformType(trimmedPiece.replace('[]', ''));
172+
const result = transformType(trimmedPiece.replace(/\[\]$/, ''));
90173

91174
// If we have a valid result and the piece is not empty, we return the Markdown link
92175
if (trimmedPiece.length && result.length) {

0 commit comments

Comments
 (0)