From af619ccb5cf66ff409fb8ca7edb0ed765a77fed8 Mon Sep 17 00:00:00 2001 From: moshams272 Date: Thu, 12 Mar 2026 14:28:24 +0200 Subject: [PATCH 1/6] feat(parser): support basic TypeScript generics in markdown links --- src/utils/parser/index.mjs | 54 ++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/src/utils/parser/index.mjs b/src/utils/parser/index.mjs index 1fdda080..4c244c0f 100644 --- a/src/utils/parser/index.mjs +++ b/src/utils/parser/index.mjs @@ -69,8 +69,9 @@ export const transformUnixManualToLink = ( * @returns {string} The Markdown link as a string (formatted in Markdown) */ export const transformTypeToReferenceLink = (type, record) => { - // Removes the wrapping tags that wrap the type references such as `<>` and `{}` - const typeInput = type.replace(/[{}<>]/g, ''); + // 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 @@ -115,17 +116,60 @@ export const transformTypeToReferenceLink = (type, record) => { return ''; }; + /** + * Attempts to parse and format a basic Generic type (e.g., Promise). + * + * @param {string} typePiece The plain type piece to be evaluated + * @returns {string|null} The formatted Markdown link, or null if no match is found + */ + const formatBasicGeneric = typePiece => { + const genericMatch = typePiece.match(/^([^<]+)<([^>]+)>$/); + + if (genericMatch) { + const baseType = genericMatch[1].trim(); + const innerType = genericMatch[2].trim(); + + const baseResult = transformType(baseType.replace('[]', '')); + const innerResult = transformType(innerType.replace('[]', '')); + + // If at least one part is mapped successfully, format as a Generic Markdown link + if (baseResult || innerResult) { + const baseFormatted = baseResult + ? `[\`<${baseType}>\`](${baseResult})` + : `\`<${baseType}>\``; + + const innerFormatted = innerResult + ? `[\`<${innerType}>\`](${innerResult})` + : `\`<${innerType}>\``; + + return `${baseFormatted}<${innerFormatted}>`; + } + } + + return null; + }; + const typePieces = typeInput.split('|').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); + if (genericMarkdown) { + return genericMarkdown; + } + + // 2. Fallback to the logic for plain types + // Strip angle brackets here to match the behavior for non-generics + const plainPiece = trimmedPiece.replace(/[<>]/g, ''); + // 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('[]', '')); + const result = transformType(plainPiece.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})`; + if (plainPiece.length && result.length) { + return `[\`<${plainPiece}>\`](${result})`; } }); From 0753220c5a4c6b55621602388662c1c6427ad643 Mon Sep 17 00:00:00 2001 From: moshams272 Date: Thu, 12 Mar 2026 14:28:57 +0200 Subject: [PATCH 2/6] test(parser): add coverage for basic generic types --- src/utils/parser/__tests__/index.test.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/utils/parser/__tests__/index.test.mjs b/src/utils/parser/__tests__/index.test.mjs index ae411291..dd2f9b3a 100644 --- a/src/utils/parser/__tests__/index.test.mjs +++ b/src/utils/parser/__tests__/index.test.mjs @@ -31,6 +31,19 @@ describe('transformTypeToReferenceLink', () => { '[``](fromTypeMap)' ); }); + it('should transform a basic Generic type into a Markdown link', () => { + strictEqual( + transformTypeToReferenceLink('{Promise}'), + '[``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type)>' + ); + }); + + it('should partially transform a Generic type if only one part is known', () => { + strictEqual( + transformTypeToReferenceLink('{CustomType}', {}), + '``<[``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type)>' + ); + }); }); describe('normalizeYamlSyntax', () => { From 72e262ff0d370cfbd2803c3632843f43013839b1 Mon Sep 17 00:00:00 2001 From: moshams272 Date: Thu, 12 Mar 2026 16:59:28 +0200 Subject: [PATCH 3/6] feat(parser): support inner union types and multi-parameters in generics --- src/utils/parser/__tests__/index.test.mjs | 21 +++++++ src/utils/parser/index.mjs | 70 ++++++++++++++++++----- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/utils/parser/__tests__/index.test.mjs b/src/utils/parser/__tests__/index.test.mjs index dd2f9b3a..a372e082 100644 --- a/src/utils/parser/__tests__/index.test.mjs +++ b/src/utils/parser/__tests__/index.test.mjs @@ -44,6 +44,27 @@ describe('transformTypeToReferenceLink', () => { '``<[``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type)>' ); }); + + it('should transform a Generic type with an inner union like {Promise}', () => { + strictEqual( + transformTypeToReferenceLink('{Promise}', {}), + '[``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) | [``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#boolean_type)>' + ); + }); + + it('should transform multi-parameter generics like {Map}', () => { + strictEqual( + transformTypeToReferenceLink('{Map}', {}), + '[``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)<[``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type), [``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type)>' + ); + }); + + it('should handle outer unions with generics like {Promise | boolean}', () => { + strictEqual( + transformTypeToReferenceLink('{Promise | boolean}', {}), + '[``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) | [``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type)> | [``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#boolean_type)' + ); + }); }); describe('normalizeYamlSyntax', () => { diff --git a/src/utils/parser/index.mjs b/src/utils/parser/index.mjs index 4c244c0f..d083c296 100644 --- a/src/utils/parser/index.mjs +++ b/src/utils/parser/index.mjs @@ -101,7 +101,7 @@ export const transformTypeToReferenceLink = (type, record) => { // Transform Node.js type/module references into Markdown links // that refer to other API docs pages within the Node.js API docs - if (lookupPiece in record) { + if (record && lookupPiece in record) { return record[lookupPiece]; } @@ -118,6 +118,7 @@ export const transformTypeToReferenceLink = (type, record) => { /** * Attempts to parse and format a basic Generic type (e.g., Promise). + * It also supports union and multi-parameter types within the generic brackets. * * @param {string} typePiece The plain type piece to be evaluated * @returns {string|null} The formatted Markdown link, or null if no match is found @@ -130,26 +131,67 @@ export const transformTypeToReferenceLink = (type, record) => { const innerType = genericMatch[2].trim(); const baseResult = transformType(baseType.replace('[]', '')); - const innerResult = transformType(innerType.replace('[]', '')); - - // If at least one part is mapped successfully, format as a Generic Markdown link - if (baseResult || innerResult) { - const baseFormatted = baseResult - ? `[\`<${baseType}>\`](${baseResult})` - : `\`<${baseType}>\``; + 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}>`; + } - const innerFormatted = innerResult - ? `[\`<${innerType}>\`](${innerResult})` - : `\`<${innerType}>\``; + return null; + }; - return `${baseFormatted}<${innerFormatted}>`; + /** + * 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; } - return null; + result.push(current); + return result; }; - const typePieces = typeInput.split('|').map(piece => { + const typePieces = splitByOuterUnion(typeInput).map(piece => { // This is the content to render as the text of the Markdown link const trimmedPiece = piece.trim(); From 713d0503d5b9baeb667e60e841b0ca8a1f2029ef Mon Sep 17 00:00:00 2001 From: moshams272 Date: Thu, 12 Mar 2026 18:02:51 +0200 Subject: [PATCH 4/6] style(parser): apply reviewer feedback (move functions, extract regex, cleanup fallback) --- src/utils/parser/index.mjs | 162 ++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 82 deletions(-) diff --git a/src/utils/parser/index.mjs b/src/utils/parser/index.mjs index d083c296..64e5ddce 100644 --- a/src/utils/parser/index.mjs +++ b/src/utils/parser/index.mjs @@ -14,6 +14,8 @@ import { import { slug } from './slugger.mjs'; import createQueries from '../queries/index.mjs'; +const BASIC_GENERIC_REGEX = /^([^<]+)<([^>]+)>$/; + /** * Extracts raw YAML content from a node * @@ -59,7 +61,81 @@ export const transformUnixManualToLink = ( ) => { 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). + * 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(BASIC_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) @@ -116,102 +192,24 @@ export const transformTypeToReferenceLink = (type, record) => { return ''; }; - /** - * Attempts to parse and format a basic Generic type (e.g., Promise). - * It also supports union and multi-parameter types within the generic brackets. - * - * @param {string} typePiece The plain type piece to be evaluated - * @returns {string|null} The formatted Markdown link, or null if no match is found - */ - const formatBasicGeneric = typePiece => { - const genericMatch = typePiece.match(/^([^<]+)<([^>]+)>$/); - - 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; - }; - - /** - * 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; - }; - 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); + const genericMarkdown = formatBasicGeneric(trimmedPiece, transformType); if (genericMarkdown) { return genericMarkdown; } // 2. Fallback to the logic for plain types - // Strip angle brackets here to match the behavior for non-generics - const plainPiece = trimmedPiece.replace(/[<>]/g, ''); - // 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(plainPiece.replace('[]', '')); + const result = transformType(trimmedPiece.replace('[]', '')); // If we have a valid result and the piece is not empty, we return the Markdown link - if (plainPiece.length && result.length) { - return `[\`<${plainPiece}>\`](${result})`; + if (trimmedPiece.length && result.length) { + return `[\`<${trimmedPiece}>\`](${result})`; } }); From 602c2b72485d1ed3f9743843b51de070db91402c Mon Sep 17 00:00:00 2001 From: moshams272 Date: Thu, 12 Mar 2026 18:37:15 +0200 Subject: [PATCH 5/6] fix(parser): use regex for array suffix and fix test spacing --- src/utils/parser/__tests__/index.test.mjs | 1 + src/utils/parser/index.mjs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils/parser/__tests__/index.test.mjs b/src/utils/parser/__tests__/index.test.mjs index a372e082..96702b0f 100644 --- a/src/utils/parser/__tests__/index.test.mjs +++ b/src/utils/parser/__tests__/index.test.mjs @@ -31,6 +31,7 @@ describe('transformTypeToReferenceLink', () => { '[``](fromTypeMap)' ); }); + it('should transform a basic Generic type into a Markdown link', () => { strictEqual( transformTypeToReferenceLink('{Promise}'), diff --git a/src/utils/parser/index.mjs b/src/utils/parser/index.mjs index 64e5ddce..077021e9 100644 --- a/src/utils/parser/index.mjs +++ b/src/utils/parser/index.mjs @@ -104,7 +104,7 @@ const formatBasicGeneric = (typePiece, transformType) => { const baseType = genericMatch[1].trim(); const innerType = genericMatch[2].trim(); - const baseResult = transformType(baseType.replace('[]', '')); + const baseResult = transformType(baseType.replace(/\[\]$/, '')); const baseFormatted = baseResult ? `[\`<${baseType}>\`](${baseResult})` : `\`<${baseType}>\``; @@ -124,7 +124,7 @@ const formatBasicGeneric = (typePiece, transformType) => { return ', '; } - const innerRes = transformType(trimmed.replace('[]', '')); + const innerRes = transformType(trimmed.replace(/\[\]$/, '')); return innerRes ? `[\`<${trimmed}>\`](${innerRes})` : `\`<${trimmed}>\``; @@ -205,7 +205,7 @@ export const transformTypeToReferenceLink = (type, record) => { // 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('[]', '')); + 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) { From 1b2b3d33e96a538ffa47b7fae5cbdd2de3905d78 Mon Sep 17 00:00:00 2001 From: moshams272 Date: Thu, 12 Mar 2026 18:46:27 +0200 Subject: [PATCH 6/6] refactor(parser): move generic regex to constants file per reviewer feedback --- src/utils/parser/constants.mjs | 3 +++ src/utils/parser/index.mjs | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/parser/constants.mjs b/src/utils/parser/constants.mjs index 3e54973c..e7965b80 100644 --- a/src/utils/parser/constants.mjs +++ b/src/utils/parser/constants.mjs @@ -69,6 +69,9 @@ export const DOC_API_HEADING_TYPES = [ }, ]; +// This regex is used to match basic TypeScript generic types (e.g., Promise) +export const TYPE_GENERIC_REGEX = /^([^<]+)<([^>]+)>$/; + // This is a mapping for types within the Markdown content and their respective // JavaScript primitive types within the MDN JavaScript docs // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Data_structures#primitive_values diff --git a/src/utils/parser/index.mjs b/src/utils/parser/index.mjs index 077021e9..c1a9a024 100644 --- a/src/utils/parser/index.mjs +++ b/src/utils/parser/index.mjs @@ -10,12 +10,11 @@ import { 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'; -const BASIC_GENERIC_REGEX = /^([^<]+)<([^>]+)>$/; - /** * Extracts raw YAML content from a node * @@ -98,7 +97,7 @@ const splitByOuterUnion = str => { * @returns {string|null} The formatted Markdown link, or null if no match is found */ const formatBasicGeneric = (typePiece, transformType) => { - const genericMatch = typePiece.match(BASIC_GENERIC_REGEX); + const genericMatch = typePiece.match(TYPE_GENERIC_REGEX); if (genericMatch) { const baseType = genericMatch[1].trim();