@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
213 lines (197 loc) • 8.82 kB
JavaScript
// ============================================================================
// Type Rewriting Utilities
// ============================================================================
/**
* A map from original type names to their canonical export names.
* For example: "DialogTrigger" -> "AlertDialog.Trigger"
*/
/**
* Builds a map from original type names to their canonical export names.
*
* This map is built from two sources:
* 1. `reexportedFrom` - when an export is re-exported with a different name
* e.g., `export { DialogTrigger as Trigger }` -> "DialogTrigger" -> "AlertDialog.Trigger"
* 2. `extendsTypes` - when an interface extends another type
* e.g., `interface AlertDialogRootProps extends Dialog.Props` -> "Dialog.Props" -> "AlertDialog.Root.Props"
*
* The map allows type references in the original namespace to be rewritten
* to the canonical namespace in the documentation.
*/
export function buildTypeCompatibilityMap(allExports, exportNames) {
const map = new Map();
const exportNameSet = new Set(exportNames);
for (const exp of allExports) {
const exportName = exp.name;
const exportIsDotted = exportName.includes('.');
// Handle reexportedFrom: maps the original component name to the new export name
// e.g., AlertDialog.Trigger with reexportedFrom: "DialogTrigger"
// means AlertDialog.Trigger is a re-export of DialogTrigger
// -> "DialogTrigger" -> "AlertDialog.Trigger"
// Child type mappings (e.g., DialogTrigger.State -> AlertDialog.Trigger.State)
// are handled by extendsTypes on those child exports.
const reexportedFrom = exp.reexportedFrom;
if (reexportedFrom && reexportedFrom !== exportName) {
const existingMapping = map.get(reexportedFrom);
// Prefer dotted names over flat names for canonical mappings
// e.g., prefer "Toolbar.Separator" over "ToolbarSeparator"
if (!existingMapping || exportIsDotted && !existingMapping.includes('.')) {
map.set(reexportedFrom, exportName);
}
}
// Handle extendsTypes: maps the extended type names to this export
// e.g., AlertDialogRootProps with extendsTypes: [{ name: "Dialog.Props", resolvedName: "DialogProps" }]
// -> "Dialog.Props" -> "AlertDialog.Root.Props"
// -> "DialogProps" -> "AlertDialog.Root.Props"
const extendsTypes = exp.extendsTypes;
if (extendsTypes) {
for (const extendedType of extendsTypes) {
// Skip if the extended type is itself a public export.
// e.g., ToastManagerAddOptions extends Omit<ToastObject, ...> should NOT
// rewrite ToastObject -> ToastManagerAddOptions because ToastObject is its
// own public type.
if (exportNameSet.has(extendedType.name)) {
continue;
}
// Map the written name: "Dialog.Props" -> "AlertDialog.Root.Props"
const existingWrittenMapping = map.get(extendedType.name);
// Prefer dotted names over flat names for canonical mappings
if (!existingWrittenMapping || exportIsDotted && !existingWrittenMapping.includes('.')) {
map.set(extendedType.name, exportName);
}
// Map the resolved name if different: "DialogProps" -> "AlertDialog.Root.Props"
if (extendedType.resolvedName && extendedType.resolvedName !== extendedType.name) {
// Also skip if the resolved name is a public export
if (exportNameSet.has(extendedType.resolvedName)) {
continue;
}
const existingResolvedMapping = map.get(extendedType.resolvedName);
// Prefer dotted names over flat names for canonical mappings
if (!existingResolvedMapping || exportIsDotted && !existingResolvedMapping.includes('.')) {
map.set(extendedType.resolvedName, exportName);
}
}
}
}
}
return map;
}
/**
* Context for type string rewriting operations.
*/
/**
* Checks if a character is an identifier character (letter, digit, or underscore).
*/
function isIdentifierChar(char) {
if (!char) {
return false;
}
const code = char.charCodeAt(0);
// a-z, A-Z, 0-9, _
return code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 95;
}
/**
* Replaces all occurrences of `search` with `replacement` in `text`,
* but only when `search` appears at word boundaries (not preceded or followed by an identifier char).
* This prevents "Dialog" from matching within "AlertDialog" and "DialogTrigger" from matching
* within "DialogTriggerState".
*/
function replaceAtWordBoundary(text, search, replacement) {
let result = '';
let i = 0;
while (i < text.length) {
const matchIndex = text.indexOf(search, i);
if (matchIndex === -1) {
result += text.slice(i);
break;
}
// Check if preceded by an identifier character
const charBefore = matchIndex > 0 ? text[matchIndex - 1] : undefined;
// Check if followed by an identifier character
const charAfter = text[matchIndex + search.length];
if (isIdentifierChar(charBefore) || isIdentifierChar(charAfter)) {
// Not at word boundary, skip this occurrence
result += text.slice(i, matchIndex + 1);
i = matchIndex + 1;
} else {
// At word boundary, perform replacement
result += text.slice(i, matchIndex) + replacement;
i = matchIndex + search.length;
}
}
return result;
}
/**
* Rewrites a single type string value based on the rewrite context.
* Handles internal suffix normalization and inherited namespace transformations.
*/
function rewriteTypeValue(value, context) {
const {
typeCompatibilityMap,
exportNames,
typeNameMap
} = context;
let next = value.replaceAll('.RootInternal', '.Root');
// Apply type compatibility mappings
// Sort by key length (longest first) to avoid partial matches
// e.g., "DialogTriggerState" should match before "DialogTrigger"
const sortedEntries = Array.from(typeCompatibilityMap.entries()).sort((a, b) => b[0].length - a[0].length);
for (const [originalName, canonicalName] of sortedEntries) {
// Only apply if the canonical name exists in our exports or is a valid canonical name from the map
// The canonical names come from extendsTypes mappings, so they are known-valid namespace paths
// like "AlertDialog.Trigger.State" even if not directly in exportNames
const canonicalNameExists = exportNames.includes(canonicalName) || typeCompatibilityMap.has(originalName);
// Get the final dotted name that this canonical name would transform to
// e.g., if canonicalName is "ToolbarSeparatorState" and typeNameMap has
// "ToolbarSeparatorState" -> "Toolbar.Separator.State", use the dotted version
const finalDottedName = typeNameMap?.[canonicalName] || canonicalName;
// Skip if the text already contains the final dotted name with a namespace prefix
// e.g., if text has "Toolbar.Separator.State", don't replace "Separator.State"
// because it would create "Toolbar.ToolbarSeparatorState" which then becomes
// "Toolbar.Toolbar.Separator.State"
const alreadyHasDottedName = finalDottedName.includes('.') && next.includes(finalDottedName);
if (canonicalNameExists && next.includes(originalName) && !alreadyHasDottedName) {
next = replaceAtWordBoundary(next, originalName, canonicalName);
}
}
// After applying compatibility mappings, convert flat type names to dotted names
// e.g., "AlertDialogTriggerState" -> "AlertDialog.Trigger.State"
if (typeNameMap) {
// Sort by key length (longest first) to avoid partial matches
const sortedTypeNameEntries = Object.entries(typeNameMap).sort((a, b) => b[0].length - a[0].length);
for (const [flatName, dottedName] of sortedTypeNameEntries) {
if (next.includes(flatName)) {
next = replaceAtWordBoundary(next, flatName, dottedName);
}
}
}
return next;
}
/**
* Recursively rewrites type strings in a data structure.
*
* This function traverses objects, arrays, and primitive values, applying type name
* transformations to all string values. Used to normalize type references after
* formatting.
*
* The function preserves the structure of the input, only transforming string values,
* so the output type matches the input type.
*/
export function rewriteTypeStringsDeep(node, context) {
if (node == null) {
return node;
}
if (typeof node === 'string') {
return rewriteTypeValue(node, context);
}
if (Array.isArray(node)) {
return node.map(item => rewriteTypeStringsDeep(item, context));
}
if (typeof node === 'object') {
const result = {};
for (const [key, value] of Object.entries(node)) {
result[key] = rewriteTypeStringsDeep(value, context);
}
return result;
}
return node;
}