Skip to content

Commit 9975bb3

Browse files
committed
feat(metadata): support advanced generics using recursion
1 parent 3d00fa8 commit 9975bb3

3 files changed

Lines changed: 121 additions & 79 deletions

File tree

src/generators/metadata/constants.mjs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,5 @@ export const DOC_API_HEADING_TYPES = [
5656
},
5757
];
5858

59-
// This regex is used to match basic TypeScript generic types (e.g., Promise<string>)
60-
export const TYPE_GENERIC_REGEX = /^([^<]+)<([^>]+)>$/;
61-
6259
// This is the base URL of the Man7 documentation
6360
export const DOC_MAN_BASE_URL = 'http://man7.org/linux/man-pages/man';

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,27 @@ describe('transformTypeToReferenceLink', () => {
7575
'[`<Map>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type), [`<number>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type)&gt; & [`<Array>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)&gt;'
7676
);
7777
});
78+
79+
it('should transform a function returning a Generic type', () => {
80+
strictEqual(
81+
transformTypeToReferenceLink('(err: Error) => Promise<boolean>', {}),
82+
'(err: Error) =&gt; [`<Promise>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)&lt;[`<boolean>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)&gt;'
83+
);
84+
});
85+
86+
it('should respect precedence: Unions (|) are weaker than Intersections (&)', () => {
87+
strictEqual(
88+
transformTypeToReferenceLink('string | number & boolean', {}),
89+
'[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type) | [`<number>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type) & [`<boolean>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)'
90+
);
91+
});
92+
93+
it('should handle extreme nested combinations of functions, generics, unions, and intersections', () => {
94+
const input =
95+
'(str: MyType) => Promise<Map<string, number & string>, Map<string | number>>';
96+
const expected =
97+
'(str: MyType) =&gt; [`<Promise>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)&lt;[`<Map>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type), [`<number>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type) & [`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)&gt;, [`<Map>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type) | [`<number>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type)&gt;&gt;';
98+
99+
strictEqual(transformTypeToReferenceLink(input, {}), expected);
100+
});
78101
});

src/generators/metadata/utils/transformers.mjs

Lines changed: 98 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
DOC_MAN_BASE_URL,
3-
DOC_API_HEADING_TYPES,
4-
TYPE_GENERIC_REGEX,
5-
} from '../constants.mjs';
1+
import { DOC_MAN_BASE_URL, DOC_API_HEADING_TYPES } from '../constants.mjs';
62
import { slug } from './slugger.mjs';
73
import { transformNodesToString } from '../../../utils/unist.mjs';
84
import BUILTIN_TYPE_MAP from '../maps/builtin.json' with { type: 'json' };
@@ -22,84 +18,134 @@ export const transformUnixManualToLink = (
2218
) => {
2319
return `[\`${text}\`](${DOC_MAN_BASE_URL}${sectionNumber}/${command}.${sectionNumber}${sectionLetter}.html)`;
2420
};
21+
22+
/**
23+
* Safely splits a string by a given set of separators at depth 0 (ignoring those inside < > or ( )).
24+
*
25+
* @param {string} str The string to split
26+
* @param {string} separator The separator to split by (e.g., '|', '&', ',', '=>')
27+
* @returns {string[]} The split pieces
28+
*/
29+
2530
/**
26-
* Safely splits the string by `|` or `&` at the top level (ignoring those
27-
* inside `< >`), and returns both the pieces and the separator used.
2831
*
29-
* @param {string} str The type string to split
30-
* @returns {{ pieces: string[], separator: string }} The split pieces and the separator string used to join them (` | ` or ` & `)
3132
*/
32-
const splitByOuterSeparator = str => {
33+
const splitByOuterSeparator = (str, separator) => {
3334
const pieces = [];
3435
let current = '';
3536
let depth = 0;
36-
let separator;
3737

38-
for (const char of str) {
39-
if (char === '<') {
38+
for (let i = 0; i < str.length; i++) {
39+
const char = str[i];
40+
41+
// Track depth using brackets and parentheses
42+
if (char === '<' || char === '(') {
4043
depth++;
41-
} else if (char === '>') {
44+
} else if (char === '>' || char === ')') {
4245
depth--;
43-
} else if ((char === '|' || char === '&') && depth === 0) {
44-
pieces.push(current);
46+
}
47+
48+
// Check for multi-character separators like '=>'
49+
const isArrow = separator === '=>' && char === '=' && str[i + 1] === '>';
50+
// Check for single-character separators
51+
const isCharSeparator = separator === char;
52+
53+
if (depth === 0 && (isCharSeparator || isArrow)) {
54+
pieces.push(current.trim());
4555
current = '';
46-
separator ??= ` ${char} `;
56+
if (isArrow) {
57+
i++;
58+
} // skip the '>' part of '=>'
4759
continue;
4860
}
61+
4962
current += char;
5063
}
5164

52-
pieces.push(current);
53-
return { pieces, separator };
65+
pieces.push(current.trim());
66+
return pieces;
5467
};
5568

5669
/**
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
70+
* Recursively parses advanced TypeScript types, including Unions, Intersections, Functions, and Nested Generics.
71+
* * @param {string} typeString The plain type string to evaluate
6172
* @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
73+
* @returns {string|null} The formatted Markdown link(s), or null if the base type doesn't map
6374
*/
64-
const formatBasicGeneric = (typePiece, transformType) => {
65-
const genericMatch = typePiece.match(TYPE_GENERIC_REGEX);
75+
const parseAdvancedType = (typeString, transformType) => {
76+
const trimmed = typeString.trim();
77+
if (!trimmed) {
78+
return null;
79+
}
80+
81+
// Handle Unions (|)
82+
if (trimmed.includes('|')) {
83+
const parts = splitByOuterSeparator(trimmed, '|');
84+
if (parts.length > 1) {
85+
// Re-evaluate each part recursively and join with ' | '
86+
const resolvedParts = parts.map(
87+
p => parseAdvancedType(p, transformType) || `\`<${p}>\``
88+
);
89+
return resolvedParts.join(' | ');
90+
}
91+
}
92+
93+
// Handle Intersections (&)
94+
if (trimmed.includes('&')) {
95+
const parts = splitByOuterSeparator(trimmed, '&');
96+
if (parts.length > 1) {
97+
// Re-evaluate each part recursively and join with ' & '
98+
const resolvedParts = parts.map(
99+
p => parseAdvancedType(p, transformType) || `\`<${p}>\``
100+
);
101+
return resolvedParts.join(' & ');
102+
}
103+
}
66104

67-
if (genericMatch) {
68-
const baseType = genericMatch[1].trim();
69-
const innerType = genericMatch[2].trim();
105+
// Handle Functions (=>)
106+
if (trimmed.includes('=>')) {
107+
const parts = splitByOuterSeparator(trimmed, '=>');
108+
if (parts.length === 2) {
109+
const params = parts[0];
110+
const returnType = parts[1];
111+
112+
// Preserve the function signature, just link the return type for now
113+
// (Mapping param types inside the signature string is complex and often unnecessary for simple docs)
114+
const parsedReturn =
115+
parseAdvancedType(returnType, transformType) || `\`<${returnType}>\``;
116+
return `${params} =&gt; ${parsedReturn}`;
117+
}
118+
}
119+
120+
// 3. Handle Generics (Base<Inner, Inner>)
121+
if (trimmed.includes('<') && trimmed.endsWith('>')) {
122+
const firstBracketIndex = trimmed.indexOf('<');
123+
const baseType = trimmed.slice(0, firstBracketIndex).trim();
124+
const innerType = trimmed.slice(firstBracketIndex + 1, -1).trim();
70125

71126
const baseResult = transformType(baseType.replace(/\[\]$/, ''));
72127
const baseFormatted = baseResult
73128
? `[\`<${baseType}>\`](${baseResult})`
74129
: `\`<${baseType}>\``;
75130

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('');
131+
// Split arguments safely by comma
132+
const innerArgs = splitByOuterSeparator(innerType, ',');
133+
const innerFormatted = innerArgs
134+
.map(arg => parseAdvancedType(arg, transformType) || `\`<${arg}>\``)
135+
.join(', ');
97136

98137
return `${baseFormatted}&lt;${innerFormatted}&gt;`;
99138
}
100139

140+
// Base Case: Plain Type (e.g., string, Buffer, Function)
141+
const result = transformType(trimmed.replace(/\[\]$/, ''));
142+
if (trimmed.length && result) {
143+
return `[\`<${trimmed}>\`](${result})`;
144+
}
145+
101146
return null;
102147
};
148+
103149
/**
104150
* This method replaces plain text Types within the Markdown content into Markdown links
105151
* that link to the actual relevant reference for such type (either internal or external link)
@@ -150,32 +196,8 @@ export const transformTypeToReferenceLink = (type, record) => {
150196
return '';
151197
};
152198

153-
const { pieces: outerPieces, separator } = splitByOuterSeparator(typeInput);
154-
155-
const typePieces = outerPieces.map(piece => {
156-
// This is the content to render as the text of the Markdown link
157-
const trimmedPiece = piece.trim();
158-
159-
// 1. Attempt to format as a basic Generic type first
160-
const genericMarkdown = formatBasicGeneric(trimmedPiece, transformType);
161-
if (genericMarkdown) {
162-
return genericMarkdown;
163-
}
164-
165-
// 2. Fallback to the logic for plain types
166-
// This is what we will compare against the API types mappings
167-
// The ReGeX below is used to remove `[]` from the end of the type
168-
const result = transformType(trimmedPiece.replace(/\[\]$/, ''));
169-
170-
// If we have a valid result and the piece is not empty, we return the Markdown link
171-
if (trimmedPiece.length && result.length) {
172-
return `[\`<${trimmedPiece}>\`](${result})`;
173-
}
174-
});
175-
176-
// Filter out pieces that we failed to map and then join the valid ones
177-
// using the same separator that appeared in the original type string
178-
const markdownLinks = typePieces.filter(Boolean).join(separator);
199+
// Kick off the recursive parser on the cleaned input
200+
const markdownLinks = parseAdvancedType(typeInput, transformType);
179201

180202
// Return the replaced links or the original content if they all failed to be replaced
181203
// Note that if some failed to get replaced, only the valid ones will be returned

0 commit comments

Comments
 (0)