-
Notifications
You must be signed in to change notification settings - Fork 46
feat(metadata): support advanced generics using recursion #763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
moshams272
wants to merge
3
commits into
nodejs:main
Choose a base branch
from
moshams272:feat/advanced-generics
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+117
−79
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,8 +1,4 @@ | ||||||||
| import { | ||||||||
| DOC_MAN_BASE_URL, | ||||||||
| DOC_API_HEADING_TYPES, | ||||||||
| TYPE_GENERIC_REGEX, | ||||||||
| } from '../constants.mjs'; | ||||||||
| import { DOC_MAN_BASE_URL, DOC_API_HEADING_TYPES } from '../constants.mjs'; | ||||||||
| import { slug } from './slugger.mjs'; | ||||||||
| import { transformNodesToString } from '../../../utils/unist.mjs'; | ||||||||
| import BUILTIN_TYPE_MAP from '../maps/builtin.json' with { type: 'json' }; | ||||||||
|
|
@@ -22,84 +18,130 @@ export const transformUnixManualToLink = ( | |||||||
| ) => { | ||||||||
| return `[\`${text}\`](${DOC_MAN_BASE_URL}${sectionNumber}/${command}.${sectionNumber}${sectionLetter}.html)`; | ||||||||
| }; | ||||||||
|
|
||||||||
| /** | ||||||||
| * Safely splits the string by `|` or `&` at the top level (ignoring those | ||||||||
| * inside `< >`), and returns both the pieces and the separator used. | ||||||||
| * Safely splits a string by a given set of separators at depth 0 (ignoring those inside < > or ( )). | ||||||||
| * | ||||||||
| * @param {string} str The type string to split | ||||||||
| * @returns {{ pieces: string[], separator: string }} The split pieces and the separator string used to join them (` | ` or ` & `) | ||||||||
| * @param {string} str The string to split | ||||||||
| * @param {string} separator The separator to split by (e.g., '|', '&', ',', '=>') | ||||||||
| * @returns {string[]} The split pieces | ||||||||
| */ | ||||||||
| const splitByOuterSeparator = str => { | ||||||||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
| const splitByOuterSeparator = (str, separator) => { | ||||||||
| const pieces = []; | ||||||||
| let current = ''; | ||||||||
| let depth = 0; | ||||||||
| let separator; | ||||||||
|
|
||||||||
| for (const char of str) { | ||||||||
| if (char === '<') { | ||||||||
| for (let i = 0; i < str.length; i++) { | ||||||||
| const char = str[i]; | ||||||||
|
|
||||||||
| // Track depth using brackets and parentheses | ||||||||
| if (char === '<' || char === '(') { | ||||||||
| depth++; | ||||||||
| } else if (char === '>') { | ||||||||
| } else if ((char === '>' && str[i - 1] !== '=') || char === ')') { | ||||||||
| depth--; | ||||||||
| } else if ((char === '|' || char === '&') && depth === 0) { | ||||||||
| pieces.push(current); | ||||||||
| } | ||||||||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
|
||||||||
| // Check for multi-character separators like '=>' | ||||||||
| const isArrow = separator === '=>' && char === '=' && str[i + 1] === '>'; | ||||||||
| // Check for single-character separators | ||||||||
| const isCharSeparator = separator === char; | ||||||||
|
|
||||||||
| if (depth === 0 && (isCharSeparator || isArrow)) { | ||||||||
| pieces.push(current.trim()); | ||||||||
| current = ''; | ||||||||
| separator ??= ` ${char} `; | ||||||||
| if (isArrow) { | ||||||||
| i++; | ||||||||
| } // skip the '>' part of '=>' | ||||||||
| continue; | ||||||||
| } | ||||||||
|
|
||||||||
| current += char; | ||||||||
| } | ||||||||
|
|
||||||||
| pieces.push(current); | ||||||||
| return { pieces, separator }; | ||||||||
| pieces.push(current.trim()); | ||||||||
| return pieces; | ||||||||
| }; | ||||||||
|
|
||||||||
| /** | ||||||||
| * 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 | ||||||||
| * Recursively parses advanced TypeScript types, including Unions, Intersections, Functions, and Nested Generics. | ||||||||
| * * @param {string} typeString The plain type string to evaluate | ||||||||
| * @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 | ||||||||
| * @returns {string|null} The formatted Markdown link(s), or null if the base type doesn't map | ||||||||
| */ | ||||||||
| const formatBasicGeneric = (typePiece, transformType) => { | ||||||||
| const genericMatch = typePiece.match(TYPE_GENERIC_REGEX); | ||||||||
| const parseAdvancedType = (typeString, transformType) => { | ||||||||
| const trimmed = typeString.trim(); | ||||||||
| if (!trimmed) { | ||||||||
| return null; | ||||||||
| } | ||||||||
|
|
||||||||
| // Handle Unions (|) | ||||||||
| if (trimmed.includes('|')) { | ||||||||
| const parts = splitByOuterSeparator(trimmed, '|'); | ||||||||
| if (parts.length > 1) { | ||||||||
| // Re-evaluate each part recursively and join with ' | ' | ||||||||
| const resolvedParts = parts.map( | ||||||||
| p => parseAdvancedType(p, transformType) || `\`<${p}>\`` | ||||||||
| ); | ||||||||
| return resolvedParts.join(' | '); | ||||||||
moshams272 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| // Handle Intersections (&) | ||||||||
| if (trimmed.includes('&')) { | ||||||||
| const parts = splitByOuterSeparator(trimmed, '&'); | ||||||||
| if (parts.length > 1) { | ||||||||
| // Re-evaluate each part recursively and join with ' & ' | ||||||||
| const resolvedParts = parts.map( | ||||||||
| p => parseAdvancedType(p, transformType) || `\`<${p}>\`` | ||||||||
| ); | ||||||||
| return resolvedParts.join(' & '); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| // Handle Functions (=>) | ||||||||
| if (trimmed.includes('=>')) { | ||||||||
| const parts = splitByOuterSeparator(trimmed, '=>'); | ||||||||
| if (parts.length === 2) { | ||||||||
| const params = parts[0]; | ||||||||
| const returnType = parts[1]; | ||||||||
|
|
||||||||
| // Preserve the function signature, just link the return type for now | ||||||||
| // (Mapping param types inside the signature string is complex and often unnecessary for simple docs) | ||||||||
| const parsedReturn = | ||||||||
| parseAdvancedType(returnType, transformType) || `\`<${returnType}>\``; | ||||||||
| return `${params} => ${parsedReturn}`; | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| if (genericMatch) { | ||||||||
| const baseType = genericMatch[1].trim(); | ||||||||
| const innerType = genericMatch[2].trim(); | ||||||||
| // 3. Handle Generics (Base<Inner, Inner>) | ||||||||
| if (trimmed.includes('<') && trimmed.endsWith('>')) { | ||||||||
| const firstBracketIndex = trimmed.indexOf('<'); | ||||||||
| const baseType = trimmed.slice(0, firstBracketIndex).trim(); | ||||||||
| const innerType = trimmed.slice(firstBracketIndex + 1, -1).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(''); | ||||||||
| // Split arguments safely by comma | ||||||||
| const innerArgs = splitByOuterSeparator(innerType, ','); | ||||||||
| const innerFormatted = innerArgs | ||||||||
| .map(arg => parseAdvancedType(arg, transformType) || `\`<${arg}>\``) | ||||||||
| .join(', '); | ||||||||
|
|
||||||||
| return `${baseFormatted}<${innerFormatted}>`; | ||||||||
| } | ||||||||
|
|
||||||||
| // Base Case: Plain Type (e.g., string, Buffer, Function) | ||||||||
| const result = transformType(trimmed.replace(/\[\]$/, '')); | ||||||||
| if (trimmed.length && result) { | ||||||||
| return `[\`<${trimmed}>\`](${result})`; | ||||||||
| } | ||||||||
|
|
||||||||
| 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) | ||||||||
|
|
@@ -150,32 +192,8 @@ export const transformTypeToReferenceLink = (type, record) => { | |||||||
| return ''; | ||||||||
| }; | ||||||||
|
|
||||||||
| const { pieces: outerPieces, separator } = splitByOuterSeparator(typeInput); | ||||||||
|
|
||||||||
| const typePieces = outerPieces.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 | ||||||||
| // using the same separator that appeared in the original type string | ||||||||
| const markdownLinks = typePieces.filter(Boolean).join(separator); | ||||||||
| // Kick off the recursive parser on the cleaned input | ||||||||
| const markdownLinks = parseAdvancedType(typeInput, transformType); | ||||||||
|
Comment on lines
+195
to
+196
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Let's call this parseType, and, since it's getting very complex, let's put it in it's own file. |
||||||||
|
|
||||||||
| // 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 | ||||||||
|
|
||||||||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MyTypeshould be handled, no?