UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

995 lines (930 loc) 31.9 kB
import { unified } from 'unified'; import remarkParse from 'remark-parse'; import remarkGfm from 'remark-gfm'; import remarkTypography from 'remark-typography'; import remarkRehype from 'remark-rehype'; import transformMarkdownCode from "../transformMarkdownCode/index.mjs"; import { organizeTypesByExport } from "./organizeTypesByExport.mjs"; /** * Variant data structure for a single variant. * Contains the types and optional typeNameMap specific to that variant. */ /** * Decode HTML entities commonly found in markdown. * `&amp;` is unescaped LAST to avoid double-unescaping inputs like `&amp;lt;`, * which should decode to `&lt;` rather than `<`. */ function decodeHtmlEntities(text) { return text.replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&#39;/g, "'").replace(/&apos;/g, "'").replace(/&amp;/g, '&').replace(/“/g, '"') // Left double quote .replace(/”/g, '"'); // Right double quote } /** * Extract text content from markdown AST nodes recursively. */ function extractText(node) { if (Array.isArray(node)) { return node.map(extractText).join(''); } if ('value' in node) { return node.value; } if ('children' in node) { return node.children.map(extractText).join(''); } return ''; } /** * Extract text content while preserving markdown link syntax. */ function extractTextWithLinks(node) { if (Array.isArray(node)) { return node.map(extractTextWithLinks).join(''); } if (node.type === 'link') { const linkText = node.children.map(extractTextWithLinks).join(''); return `[${linkText}](${node.url})`; } if ('value' in node) { return node.value; } if ('children' in node) { return node.children.map(extractTextWithLinks).join(''); } return ''; } /** * Extract inline code content from a node, or return the text if not inline code. */ function extractInlineCodeOrText(node) { if (Array.isArray(node)) { return node.map(extractInlineCodeOrText).join(''); } if (node.type === 'inlineCode') { return decodeHtmlEntities(node.value); } if ('value' in node) { return decodeHtmlEntities(node.value); } if ('children' in node) { return node.children.map(extractInlineCodeOrText).join(''); } return ''; } /** * A target for batch HAST conversion: a setter to assign the result and the mdast nodes to convert. */ /** * Append reference items from a markdown list node to a prop/parameter's see fields. * Extracts each list item preserving link syntax and pushes a HAST conversion target. */ function appendListReferences(entry, listNode, targets) { if (!('children' in listNode)) { return; } const items = listNode.children.map(li => { if (li.type === 'listItem' && 'children' in li && li.children) { return extractTextWithLinks(li.children); } return ''; }).filter(Boolean).map(item => `- ${item}`); const listText = items.join('\n'); const existing = entry.seeText; entry.seeText = existing ? `${existing}\n${listText}` : listText; targets.push([hast => { entry.see = hast; }, [listNode]]); } /** * Append a text reference (from a paragraph node) to a prop/parameter's see fields. */ function appendTextReference(entry, text, node, targets) { const existing = entry.seeText; entry.seeText = existing ? `${existing}\n\n${text}` : text; targets.push([hast => { entry.see = hast; }, [node]]); } /** * Strip positional data from a HAST tree. * Positions reference source locations in the original markdown file * and are not used downstream, so removing them reduces noise and data size. */ function stripPositions(node) { delete node.position; if ('children' in node) { for (const child of node.children) { stripPositions(child); } } return node; } /** * Convert all collected mdast description nodes to HAST in parallel. * Runs only the transform plugins (transformMarkdownCode → remarkTypography → remarkRehype) * on mdast nodes that were already parsed, avoiding a redundant text → mdast re-parse. */ async function convertDescriptions(targets) { const processor = unified().use(transformMarkdownCode).use(remarkTypography, []).use(remarkRehype); await Promise.all(targets.map(async ([setter, children]) => { const root = { type: 'root', children }; setter(stripPositions(await processor.run(root))); })); } /** * Check if a node is a bold/strong heading pattern like "**Name Props:**" */ function isBoldHeading(node) { if (node.type !== 'paragraph') { return false; } const children = node.children; if (children.length === 0) { return false; } const first = children[0]; return first.type === 'strong'; } /** * Extract the heading type from a bold heading like "**Root Props:**" or "**`children` Prop Example:**" */ function parseBoldHeading(node) { const strong = node.children[0]; if (strong.type !== 'strong') { return null; } const text = extractText(strong.children); // Check for prop/param example patterns: "`propName` Prop Example:" or "`paramName` Parameter Example:" if (text.endsWith(' Prop Example:')) { // Extract prop name from the inline code const strongChildren = strong.children; if (strongChildren.length >= 1 && strongChildren[0].type === 'inlineCode') { return { name: strongChildren[0].value, type: 'prop-example' }; } } if (text.endsWith(' Parameter Example:')) { const strongChildren = strong.children; if (strongChildren.length >= 1 && strongChildren[0].type === 'inlineCode') { return { name: strongChildren[0].value, type: 'param-example' }; } } // Check for prop/param reference patterns: "`propName` Prop References:" or "`paramName` Parameter References:" if (text.endsWith(' Prop References:')) { const strongChildren = strong.children; if (strongChildren.length >= 1 && strongChildren[0].type === 'inlineCode') { return { name: strongChildren[0].value, type: 'prop-references' }; } } if (text.endsWith(' Parameter References:')) { const strongChildren = strong.children; if (strongChildren.length >= 1 && strongChildren[0].type === 'inlineCode') { return { name: strongChildren[0].value, type: 'param-references' }; } } // Standard patterns if (text.endsWith(' Props:')) { return { name: text.slice(0, -' Props:'.length), type: 'props' }; } if (text.endsWith(' Data Attributes:')) { return { name: text.slice(0, -' Data Attributes:'.length), type: 'data-attributes' }; } if (text.endsWith(' CSS Variables:')) { return { name: text.slice(0, -' CSS Variables:'.length), type: 'css-variables' }; } if (text.endsWith(' Parameters:') || text === 'Parameters:') { const name = text === 'Parameters:' ? '' : text.slice(0, -' Parameters:'.length); return { name, type: 'parameters' }; } if (text.endsWith(' Properties:') || text === 'Properties:') { const name = text === 'Properties:' ? '' : text.slice(0, -' Properties:'.length); return { name, type: 'properties' }; } if (text.endsWith(' Return Value:') || text === 'Return Value:') { const name = text === 'Return Value:' ? '' : text.slice(0, -' Return Value:'.length); return { name, type: 'return-value' }; } return null; } /** * Parse a props/parameters table into FormattedProperty records. */ function parsePropsTable(table, targets) { const props = {}; const rows = table.children; // Skip header row for (let i = 1; i < rows.length; i += 1) { const row = rows[i]; if (row.type !== 'tableRow') { continue; } const cells = row.children; if (cells.length < 4) { continue; } // Prop | Type | Default | Description let propName = extractText(cells[0].children); const typeText = extractInlineCodeOrText(cells[1].children); const defaultText = extractInlineCodeOrText(cells[2].children); const descriptionText = extractText(cells[3].children); // Handle required marker (*) const required = propName.endsWith('*'); if (required) { propName = propName.slice(0, -1); } // Handle optional marker (?) const optional = propName.endsWith('?'); if (optional) { propName = propName.slice(0, -1); } const prop = { typeText: typeText === '-' ? '' : typeText }; if (required) { prop.required = true; } if (defaultText && defaultText !== '-') { prop.defaultText = defaultText; } if (descriptionText && descriptionText !== '-') { prop.descriptionText = descriptionText; targets.push([hast => { prop.description = hast; }, [{ type: 'paragraph', children: cells[3].children }]]); } props[propName] = prop; } return props; } /** * Parse a parameters table into FormattedParameter records. */ function parseParametersTable(table, targets) { const params = {}; const rows = table.children; // Skip header row for (let i = 1; i < rows.length; i += 1) { const row = rows[i]; if (row.type !== 'tableRow') { continue; } const cells = row.children; if (cells.length < 4) { continue; } // Parameter | Type | Default | Description let paramName = extractText(cells[0].children); const typeText = extractInlineCodeOrText(cells[1].children); const defaultText = extractInlineCodeOrText(cells[2].children); const descriptionText = extractText(cells[3].children); // Handle required marker (*) const required = paramName.endsWith('*'); if (required) { paramName = paramName.slice(0, -1); } // Handle optional marker (?) const optional = paramName.endsWith('?'); if (optional) { paramName = paramName.slice(0, -1); } const param = { name: paramName, typeText: typeText === '-' ? '' : typeText }; if (optional) { param.optional = true; } if (defaultText && defaultText !== '-') { param.defaultText = defaultText; } if (descriptionText && descriptionText !== '-') { param.descriptionText = descriptionText; targets.push([hast => { param.description = hast; }, [{ type: 'paragraph', children: cells[3].children }]]); } params[paramName] = param; } return params; } /** * Parse a data attributes table into FormattedEnumMember records. */ function parseDataAttributesTable(table, targets) { const attrs = {}; const rows = table.children; // Skip header row for (let i = 1; i < rows.length; i += 1) { const row = rows[i]; if (row.type !== 'tableRow') { continue; } const cells = row.children; if (cells.length < 3) { continue; } // Attribute | Type | Description const attrName = extractText(cells[0].children); const type = extractInlineCodeOrText(cells[1].children); const descriptionText = extractText(cells[2].children); const attr = {}; if (type && type !== '-') { attr.type = type; } if (descriptionText && descriptionText !== '-') { attr.descriptionText = descriptionText; targets.push([hast => { attr.description = hast; }, [{ type: 'paragraph', children: cells[2].children }]]); } attrs[attrName] = attr; } return attrs; } /** * Parse a CSS variables table into FormattedEnumMember records. */ function parseCssVariablesTable(table, targets) { const vars = {}; const rows = table.children; // Skip header row for (let i = 1; i < rows.length; i += 1) { const row = rows[i]; if (row.type !== 'tableRow') { continue; } const cells = row.children; if (cells.length < 3) { continue; } // Variable | Type | Description const varName = extractInlineCodeOrText(cells[0].children); const type = extractInlineCodeOrText(cells[1].children); const descriptionText = extractText(cells[2].children); const cssVar = {}; if (type && type !== '-') { cssVar.type = type; } if (descriptionText && descriptionText !== '-') { cssVar.descriptionText = descriptionText; targets.push([hast => { cssVar.description = hast; }, [{ type: 'paragraph', children: cells[2].children }]]); } vars[varName] = cssVar; } return vars; } /** * Parse a return value table into FormattedProperty records. */ function parseReturnValueTable(table, targets) { const props = {}; const rows = table.children; // Skip header row for (let i = 1; i < rows.length; i += 1) { const row = rows[i]; if (row.type !== 'tableRow') { continue; } const cells = row.children; if (cells.length < 3) { continue; } // Property | Type | Description const propName = extractInlineCodeOrText(cells[0].children); const typeText = extractInlineCodeOrText(cells[1].children); const descriptionText = extractText(cells[2].children); const prop = { typeText: typeText === '-' ? '' : typeText }; if (descriptionText && descriptionText !== '-') { prop.descriptionText = descriptionText; targets.push([hast => { prop.description = hast; }, [{ type: 'paragraph', children: cells[2].children }]]); } props[propName] = prop; } return props; } /** * Determine the type of a section based on its content. * Returns 'component', 'hook', 'function', or 'raw'. */ function determineTypeKind(name, hasProps, hasDataAttrs, hasCssVars, hasParams, hasReturnValue, hasCodeBlock, isReExport) { // Re-exports are raw types if (isReExport) { return 'raw'; } // Hooks start with 'use' if (name.startsWith('use')) { return 'hook'; } // Components have props, data attributes, or CSS variables if (hasProps || hasDataAttrs || hasCssVars) { return 'component'; } // Functions have parameters if (hasParams || hasReturnValue) { return 'function'; } // Raw types are code blocks or re-exports if (hasCodeBlock) { return 'raw'; } // Default to component if nothing else matches return 'component'; } /** * Parse types.md content into structured type metadata. */ export async function parseTypesMarkdown(content, ordering) { // Parse markdown into AST const processor = unified().use(remarkParse).use(remarkGfm); const ast = processor.parse(content); const allTypes = []; const descriptionTargets = []; const externalTypes = {}; // Track current context let currentH3Name = null; let currentDescription = []; let currentDescriptionNodes = []; let currentProps = {}; let currentDataAttrs = {}; let currentCssVars = {}; let currentParams = {}; let currentProperties = {}; let currentReturnValue = null; let currentReturnValueDescription = null; let currentReturnValueDescriptionNode = null; let currentCodeBlock = null; let isReExport = false; let lastBoldHeadingType = null; let lastPropExampleName = null; let lastParamExampleName = null; let lastPropReferencesName = null; let lastParamReferencesName = null; // Helper to flush the current section const flushSection = () => { if (!currentH3Name) { return; } const kind = determineTypeKind(currentH3Name, Object.keys(currentProps).length > 0, Object.keys(currentDataAttrs).length > 0, Object.keys(currentCssVars).length > 0, Object.keys(currentParams).length > 0 || Object.keys(currentProperties).length > 0, currentReturnValue !== null, currentCodeBlock !== null, isReExport); const descriptionText = currentDescription.length > 0 ? currentDescription.join('\n\n') : undefined; if (kind === 'component') { const componentMeta = { name: currentH3Name, props: currentProps, dataAttributes: currentDataAttrs, cssVariables: currentCssVars }; if (descriptionText) { componentMeta.descriptionText = descriptionText; descriptionTargets.push([hast => { componentMeta.description = hast; }, currentDescriptionNodes]); } allTypes.push({ type: 'component', name: currentH3Name, data: componentMeta }); } else if (kind === 'hook') { const hookMeta = { name: currentH3Name, ...(Object.keys(currentProperties).length > 0 ? { expandedProperties: currentProperties } : { parameters: Object.values(currentParams) }), returnValue: currentReturnValue || {} }; if (descriptionText) { hookMeta.descriptionText = descriptionText; descriptionTargets.push([hast => { hookMeta.description = hast; }, currentDescriptionNodes]); } if (currentReturnValueDescription) { hookMeta.returnValueDescriptionText = currentReturnValueDescription; if (currentReturnValueDescriptionNode) { descriptionTargets.push([hast => { hookMeta.returnValueDescription = hast; }, [currentReturnValueDescriptionNode]]); } } allTypes.push({ type: 'hook', name: currentH3Name, data: hookMeta }); } else if (kind === 'function') { const funcMeta = { name: currentH3Name, ...(Object.keys(currentProperties).length > 0 ? { expandedProperties: currentProperties } : { parameters: Object.values(currentParams) }), returnValue: currentReturnValue || {} }; if (descriptionText) { funcMeta.descriptionText = descriptionText; descriptionTargets.push([hast => { funcMeta.description = hast; }, currentDescriptionNodes]); } if (currentReturnValueDescription) { funcMeta.returnValueDescriptionText = currentReturnValueDescription; if (currentReturnValueDescriptionNode) { descriptionTargets.push([hast => { funcMeta.returnValueDescription = hast; }, [currentReturnValueDescriptionNode]]); } } allTypes.push({ type: 'function', name: currentH3Name, data: funcMeta }); } else if (kind === 'raw') { const rawMeta = { name: currentH3Name, formattedCode: currentCodeBlock || '' }; if (descriptionText) { rawMeta.descriptionText = descriptionText; descriptionTargets.push([hast => { rawMeta.description = hast; }, currentDescriptionNodes]); } if (isReExport) { // Parse re-export info from description like "Re-export of [Root](#root) props." const reExportMatch = descriptionText?.match(/Re-export of \[([^\]]+)\]\(#([^)]+)\)/); if (reExportMatch) { rawMeta.reExportOf = { name: reExportMatch[1], slug: reExportMatch[2], suffix: 'props' }; } } allTypes.push({ type: 'raw', name: currentH3Name, data: rawMeta }); } // Reset state currentH3Name = null; currentDescription = []; currentDescriptionNodes = []; currentProps = {}; currentDataAttrs = {}; currentCssVars = {}; currentParams = {}; currentProperties = {}; currentReturnValue = null; currentReturnValueDescription = null; currentReturnValueDescriptionNode = null; currentCodeBlock = null; isReExport = false; lastBoldHeadingType = null; lastPropExampleName = null; lastParamExampleName = null; lastPropReferencesName = null; lastParamReferencesName = null; }; // Track if we're in the External Types section let inExternalTypes = false; let currentExternalTypeName = null; // Process nodes for (const node of ast.children) { // Handle ## headings if (node.type === 'heading' && node.depth === 2) { flushSection(); const text = extractText(node.children); if (text === 'External Types') { inExternalTypes = true; } else if (text === 'Additional Types') { inExternalTypes = false; } else if (text === 'Export Groups' || text === 'Canonical Types') { // Skip metadata sections - they are parsed separately inExternalTypes = false; } continue; } // Handle ### headings (component/hook/type names) if (node.type === 'heading' && node.depth === 3) { flushSection(); inExternalTypes = false; currentH3Name = extractText(node.children); continue; } // Handle #### headings in External Types section if (node.type === 'heading' && node.depth === 4 && inExternalTypes) { currentExternalTypeName = extractText(node.children); continue; } // Handle code blocks if (node.type === 'code') { if (inExternalTypes && currentExternalTypeName) { // External type definition externalTypes[currentExternalTypeName] = node.value; currentExternalTypeName = null; } else if (currentH3Name) { if (lastBoldHeadingType === 'return-value') { // Return value code block currentReturnValue = node.value; } else if (lastPropExampleName) { // Prop example code block if (currentProps[lastPropExampleName]) { currentProps[lastPropExampleName].exampleText = `\`\`\`${node.lang || 'tsx'}\n${node.value}\n\`\`\``; } lastPropExampleName = null; } else if (lastParamExampleName) { // Parameter example code block if (currentParams[lastParamExampleName]) { currentParams[lastParamExampleName].exampleText = `\`\`\`${node.lang || 'tsx'}\n${node.value}\n\`\`\``; } lastParamExampleName = null; } else { // Raw type code block currentCodeBlock = node.value; } } continue; } // Handle bold headings and tables within a section if (currentH3Name) { // Check for bold heading patterns if (isBoldHeading(node)) { const parsed = parseBoldHeading(node); if (parsed) { lastBoldHeadingType = parsed.type; lastPropReferencesName = null; lastParamReferencesName = null; if (parsed.type === 'prop-example') { lastPropExampleName = parsed.name; } else if (parsed.type === 'param-example') { lastParamExampleName = parsed.name; } else if (parsed.type === 'prop-references') { lastPropReferencesName = parsed.name; } else if (parsed.type === 'param-references') { lastParamReferencesName = parsed.name; } } continue; } // Handle tables if (node.type === 'table') { if (lastBoldHeadingType === 'props') { currentProps = parsePropsTable(node, descriptionTargets); } else if (lastBoldHeadingType === 'parameters') { currentParams = parseParametersTable(node, descriptionTargets); } else if (lastBoldHeadingType === 'properties') { // Properties are expanded parameters — parse as props table currentProperties = parsePropsTable(node, descriptionTargets); } else if (lastBoldHeadingType === 'data-attributes') { currentDataAttrs = parseDataAttributesTable(node, descriptionTargets); } else if (lastBoldHeadingType === 'css-variables') { currentCssVars = parseCssVariablesTable(node, descriptionTargets); } else if (lastBoldHeadingType === 'return-value') { currentReturnValue = parseReturnValueTable(node, descriptionTargets); } continue; } // Handle list nodes for references (@see tags produce bullet lists) if (node.type === 'list' && currentH3Name) { if (lastPropReferencesName && currentProps[lastPropReferencesName]) { appendListReferences(currentProps[lastPropReferencesName], node, descriptionTargets); continue; } if (lastParamReferencesName && currentParams[lastParamReferencesName]) { appendListReferences(currentParams[lastParamReferencesName], node, descriptionTargets); continue; } } // Handle paragraphs as description (including non-bold paragraphs) // Note: isBoldHeading already checked paragraphs and returned early if true if ('children' in node && node.type !== 'blockquote') { const text = extractTextWithLinks(node.children); // Check for re-export pattern if (text.startsWith('Re-export of ')) { isReExport = true; currentDescription.push(text); currentDescriptionNodes.push(node); } else if (lastPropReferencesName && currentProps[lastPropReferencesName]) { appendTextReference(currentProps[lastPropReferencesName], text, node, descriptionTargets); } else if (lastParamReferencesName && currentParams[lastParamReferencesName]) { appendTextReference(currentParams[lastParamReferencesName], text, node, descriptionTargets); } else if (!lastBoldHeadingType) { // Only add to description if we haven't seen a bold heading yet currentDescription.push(text); currentDescriptionNodes.push(node); } else if (lastBoldHeadingType === 'return-value' && !currentReturnValue) { // Return value description before the code block/table currentReturnValueDescription = text; currentReturnValueDescriptionNode = node; } } } } // Flush the last section flushSection(); // Parse metadata from human-readable sections instead of JSON comments // The new format uses "## Export Groups" and "## Canonical Types" sections // Parse Export Groups section // Format: - `VariantName`: `Type1`, `Type2` (or just `- VariantName` if key equals single value) let variantTypes = null; const exportGroupsMatch = content.match(/## Export Groups\n([\s\S]*?)(?=\n## |$)/); if (exportGroupsMatch) { variantTypes = {}; const lines = exportGroupsMatch[1].trim().split('\n'); for (const line of lines) { // Match: - `VariantName`: `Type1`, `Type2` OR - `VariantName` const withValuesMatch = line.match(/^- `([^`]+)`:\s*(.+)$/); const singleMatch = line.match(/^- `([^`]+)`$/); if (withValuesMatch) { const variantName = withValuesMatch[1]; // Extract all backtick-wrapped values const values = withValuesMatch[2].match(/`([^`]+)`/g); variantTypes[variantName] = values ? values.map(v => v.slice(1, -1)) : []; } else if (singleMatch) { // Key equals value - the variant name IS the single type const variantName = singleMatch[1]; variantTypes[variantName] = [variantName]; } } // Only keep variantTypes if we found entries if (Object.keys(variantTypes).length === 0) { variantTypes = null; } } // Parse Canonical Types section // Format: - `CanonicalName` (`Variant1`, `Variant2`): `FlatName1`, `FlatName2` // OR: - `CanonicalName`: `FlatName1`, `FlatName2` (no variant annotation) const typeNameMap = {}; let variantTypeNameMapKeys = null; const canonicalTypesMatch = content.match(/## Canonical Types\n([\s\S]*?)(?=\n## |$)/); if (canonicalTypesMatch) { variantTypeNameMapKeys = {}; const lines = canonicalTypesMatch[1].trim().split('\n'); for (const line of lines) { // Match: - `CanonicalName` (`Variant1`, `Variant2`): `FlatName1`, `FlatName2` // OR: - `CanonicalName`: `FlatName1`, `FlatName2` const withVariantMatch = line.match(/^- `([^`]+)`\s+\(([^)]+)\):\s*(.+)$/); const withoutVariantMatch = line.match(/^- `([^`]+)`:\s*(.+)$/); let canonicalName; let flatNamesStr; let variants = []; if (withVariantMatch) { canonicalName = withVariantMatch[1]; // Extract variants from parentheses const variantStr = withVariantMatch[2]; const variantMatches = variantStr.match(/`([^`]+)`/g); variants = variantMatches ? variantMatches.map(v => v.slice(1, -1)) : []; flatNamesStr = withVariantMatch[3]; } else if (withoutVariantMatch) { canonicalName = withoutVariantMatch[1]; flatNamesStr = withoutVariantMatch[2]; // No variant annotation - applies to all variants } else { continue; } // Extract all flat names const flatNameMatches = flatNamesStr.match(/`([^`]+)`/g); const flatNames = flatNameMatches ? flatNameMatches.map(v => v.slice(1, -1)) : []; // Build typeNameMap (flat name -> canonical name) for (const flatName of flatNames) { typeNameMap[flatName] = canonicalName; } // Build variantTypeNameMapKeys (variant -> list of keys) // If no variants specified, we don't track variant-specific keys if (variants.length > 0) { for (const variant of variants) { if (!variantTypeNameMapKeys[variant]) { variantTypeNameMapKeys[variant] = []; } for (const flatName of flatNames) { variantTypeNameMapKeys[variant].push(flatName); } } } } // Only keep variantTypeNameMapKeys if we found entries if (Object.keys(variantTypeNameMapKeys).length === 0) { variantTypeNameMapKeys = null; } } // Convert all description mdast nodes to HAST in parallel await convertDescriptions(descriptionTargets); // Build a map from type name to TypesMeta for quick lookup const typesByName = new Map(); for (const typeMeta of allTypes) { typesByName.set(typeMeta.name, typeMeta); } // Reconstruct variantData from the parsed metadata let variantData; if (variantTypes) { // Reconstruct variantData from the embedded metadata variantData = {}; for (const [variantName, typeNames] of Object.entries(variantTypes)) { const types = typeNames.map(name => { // Try exact match first const exact = typesByName.get(name); if (exact) { return exact; } // If the name is dotted (e.g. "Component.Root"), try the last part ("Root") // since H3 headings in the markdown may use the short name const dotIndex = name.lastIndexOf('.'); if (dotIndex > 0) { return typesByName.get(name.slice(dotIndex + 1)); } return undefined; }).filter(t => t !== undefined); // Derive per-variant typeNameMap by filtering the merged typeNameMap using the keys let variantTypeNameMap; const keys = variantTypeNameMapKeys?.[variantName]; if (keys && keys.length > 0) { variantTypeNameMap = {}; for (const key of keys) { if (key in typeNameMap) { variantTypeNameMap[key] = typeNameMap[key]; } } } variantData[variantName] = { types, typeNameMap: variantTypeNameMap }; } } else { // No variant metadata - create a single "Default" variant with all types variantData = { Default: { types: allTypes, typeNameMap: Object.keys(typeNameMap).length > 0 ? typeNameMap : undefined } }; } // Organize types into exports structure for UI consumption const organized = organizeTypesByExport(variantData, typeNameMap, ordering); return { exports: organized.exports, additionalTypes: organized.additionalTypes, variantOnlyAdditionalTypes: organized.variantOnlyAdditionalTypes, externalTypes, typeNameMap, variantTypeNameMaps: organized.variantTypeNameMaps, variantTypeNames: organized.variantTypeNames }; }