@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
356 lines (325 loc) • 11.5 kB
JavaScript
/**
* Type highlighting utilities for converting plain text types to syntax-highlighted HAST.
*
* These functions are used in the loadServerTypes pipeline after highlightTypes()
* to convert plain text type strings into syntax-highlighted HAST with proper
* formatting for display in documentation.
*/
import { unified } from 'unified';
import transformHtmlCodeInline from "../transformHtmlCodeInline/index.mjs";
import { starryNightGutter } from "../parseSource/addLineGutters.mjs";
import { getHastTextContent } from "./hastTypeUtils.mjs";
/**
* Options for formatting inline types as HAST.
*/
/** Default width for splitting union types across multiple lines */
export const DEFAULT_UNION_PRINT_WIDTH = 40;
/** Default width for Prettier formatting of type definitions */
export const DEFAULT_TYPE_PRINT_WIDTH = 60;
/**
* Splits union types across multiple lines.
*
* This function processes HAST nodes containing syntax-highlighted union types and
* reformats them with each union member on a separate line, prefixed with a pipe character.
* Only top-level pipes are split (not those inside parentheses or braces).
*
* Matches the behavior of TableCode.tsx in base-ui docs:
* - Groups content by top-level pipe separators
* - Adds a leading `| ` before the first group
* - Adds `<br>` + `| ` before subsequent groups
* - Removes original pipe nodes (they're replaced by the new styled pipes)
*
* @param hast - The HAST root containing syntax-highlighted type nodes
* @returns A new HAST root with multiline formatting applied
*/
export function formatMultilineUnionHast(hast) {
// Get the code element
const codeElement = hast.children[0];
if (!codeElement || codeElement.type !== 'element') {
return hast;
}
const children = codeElement.children || [];
// Group children by top-level pipes (matching TableCode.tsx behavior)
const unionGroups = [[]];
let parenDepth = 0;
let braceDepth = 0;
let angleDepth = 0;
let groupIndex = 0;
children.forEach((child, index) => {
const nodeText = getHastTextContent(child);
// Track depth changes
for (const char of nodeText) {
if (char === '(') {
parenDepth += 1;
} else if (char === ')') {
parenDepth -= 1;
} else if (char === '{') {
braceDepth += 1;
} else if (char === '}') {
braceDepth -= 1;
} else if (char === '<') {
angleDepth += 1;
} else if (char === '>') {
angleDepth -= 1;
}
}
// Check if this node contains only a pipe at top level
const trimmedText = nodeText.trim();
const isTopLevelPipe = trimmedText === '|' && parenDepth <= 0 && braceDepth <= 0 && angleDepth <= 0 && index !== 0;
if (isTopLevelPipe) {
// Skip the pipe node and start a new group (matching TableCode behavior)
unionGroups.push([]);
groupIndex += 1;
return;
}
unionGroups[groupIndex].push(child);
});
// If we only have one group, no splitting needed
if (unionGroups.length <= 1) {
return hast;
}
// Build enhanced children with pipes and line breaks (matching TableCode.tsx)
const enhancedChildren = [];
const pipeSpan = {
type: 'element',
tagName: 'span',
properties: {
className: ['pl-k']
},
children: [{
type: 'text',
value: '| '
}]
};
unionGroups.forEach((group, idx) => {
if (idx === 0) {
// Leading pipe for first group
enhancedChildren.push({
...pipeSpan
});
} else {
// Newline plus pipe for subsequent groups
enhancedChildren.push({
type: 'element',
tagName: 'br',
properties: {},
children: []
});
enhancedChildren.push({
...pipeSpan
});
}
enhancedChildren.push(...group);
});
// Reconstruct the HAST with new children
return {
type: 'root',
children: [{
...codeElement,
children: enhancedChildren
}]
};
}
/**
* Formats an inline type string with syntax highlighting.
*
* This function transforms type strings (like `string`, `number | null`, etc.) into
* syntax-highlighted HAST nodes. It ensures proper TypeScript context by prefixing
* the type with `type _ =` before highlighting, then removes the prefix from the result.
*
* @param typeText - The type string to format (e.g., "string | number")
* @param unionPrintWidth - Optional width threshold for multiline union formatting.
* When set, unions exceeding this width are split across lines.
* @returns A promise that resolves to a HAST root containing highlighted nodes
*
* @example
* ```ts
* await formatInlineTypeAsHast('string | number')
* // Returns HAST nodes with syntax highlighting for "string | number"
*
* await formatInlineTypeAsHast('"a" | "b" | "c" | "d" | "e"', 20)
* // Returns HAST nodes with multiline formatting for long unions
* ```
*/
export async function formatInlineTypeAsHast(typeText, unionPrintWidth) {
// Construct HAST with a code element
// Add dataHighlightingPrefix so the plugin can temporarily wrap the type in valid syntax
const hast = {
type: 'root',
children: [{
type: 'element',
tagName: 'code',
properties: {
className: ['language-ts'],
dataHighlightingPrefix: 'type _ = '
},
children: [{
type: 'text',
value: typeText
}]
}]
};
// Apply inline syntax highlighting
const processor = unified().use(transformHtmlCodeInline).freeze();
let result = await processor.run(hast);
// Apply multiline union formatting if threshold is exceeded
// Check against original text to avoid extracting text from HAST
if (unionPrintWidth !== undefined && typeText.includes('|') && typeText.length > unionPrintWidth) {
result = formatMultilineUnionHast(result);
}
return result;
}
/**
* Wraps a HAST produced by formatInlineTypeAsHast in a <pre> element.
* Converts root > code > [spans] into root > pre > code > [line-wrapped spans].
*
* This also:
* - Removes the `data-inline` attribute that was added during inline highlighting
* (since the code is no longer inline once wrapped in <pre>)
* - Wraps each line of content in <span class="line"> elements so that
* CSS rules like `.Code .line { white-space: pre }` apply correctly
*/
export function wrapInlineTypeInPre(hast) {
const codeElement = hast.children[0];
if (!codeElement || codeElement.type !== 'element' || codeElement.tagName !== 'code') {
return hast;
}
// Remove data-inline since this code is now inside a <pre>
if (codeElement.properties) {
delete codeElement.properties.dataInline;
}
// Wrap children in .line spans using starryNightGutter (same as formatDetailedTypeAsHast)
const tempRoot = {
type: 'root',
children: codeElement.children
};
starryNightGutter(tempRoot);
codeElement.children = tempRoot.children;
return {
type: 'root',
children: [{
type: 'element',
tagName: 'pre',
properties: {},
children: [codeElement]
}]
};
}
/**
* Formats TypeScript type text as HAST with full syntax highlighting in a code block.
* This is used for detailed/expanded type displays (equivalent to triple backticks in MDX).
* Unlike formatInlineTypeAsHast which uses <code>, this creates a <pre><code> structure
* and also includes line numbers via starryNightGutter.
*/
export async function formatDetailedTypeAsHast(typeText) {
// Construct HAST with a pre > code structure for block-level display
const hast = {
type: 'root',
children: [{
type: 'element',
tagName: 'pre',
properties: {},
children: [{
type: 'element',
tagName: 'code',
properties: {
className: ['language-ts'],
dataHighlightingPrefix: 'type _ = '
},
children: [{
type: 'text',
value: typeText
}]
}]
}]
};
// Apply inline syntax highlighting (with includePreElements to process pre>code)
const processor = unified().use(transformHtmlCodeInline, {
includePreElements: true
}).freeze();
const result = await processor.run(hast);
// Add line gutters to the highlighted code
const preElement = result.children[0];
if (preElement && preElement.type === 'element' && preElement.tagName === 'pre') {
const codeElement = preElement.children[0];
if (codeElement && codeElement.type === 'element' && codeElement.tagName === 'code') {
// Create a temporary root with the code element's children for starryNightGutter
const tempRoot = {
type: 'root',
children: codeElement.children
};
// Apply line gutters (mutates tempRoot in place)
starryNightGutter(tempRoot);
// Put the guttered children back into the code element
codeElement.children = tempRoot.children;
}
}
return result;
}
/**
* Determines whether a property should display its full type definition or a simplified version.
*
* Properties with complex types (unions, callbacks, etc.) benefit from expandable detailed views,
* while simple types (string, number, boolean) can be shown inline without expansion.
*
* @param name - The property name (used for special cases like className, render, event handlers)
* @param type - The plain text type string to analyze
* @returns true if the property should have an expandable detailed type view
*/
export function shouldShowDetailedType(name, type) {
// Event handlers and getters typically have complex function signatures
if (/^(on|get)[A-Z].*/.test(name)) {
return true;
}
if (type === undefined || type === null) {
return false;
}
// className can be string or function, show details
if (name === 'className') {
return true;
}
// render prop can be ReactElement or function, show details
if (name === 'render') {
return true;
}
// Simple types and short unions don't need expansion
if (name.endsWith('Ref') || name === 'children' || type === 'boolean' || type === 'string' || type === 'number' || type.indexOf(' | ') === -1 || type.split('|').length < 3 && type.length < 30) {
return false;
}
// Complex unions benefit from detailed expansion
return true;
}
/**
* Gets the short representation of a type for display in tables.
*
* Returns a simplified type string for complex types (e.g., "Union", "function").
* Simple types like `string`, `number`, `boolean` return undefined (no shortening needed).
*
* @param name - The property name (used for special cases like className, style, render, event handlers)
* @param typeText - The plain text type string to analyze
* @returns A short type string, or undefined if no shortening is needed
*/
export function getShortTypeString(name, typeText) {
// Event handlers and getters show as "function"
if (/^(on|get)[A-Z].*/.test(name)) {
return 'function';
}
// className can be string or function
if (name === 'className') {
return 'string | function';
}
// style can be React.CSSProperties or function
if (name === 'style') {
return 'React.CSSProperties | function';
}
// render can be ReactElement or function
if (name === 'render') {
return 'ReactElement | function';
}
// Complex unions show as "Union"
if (shouldShowDetailedType(name, typeText)) {
return 'Union';
}
// Simple types don't need a short version
return undefined;
}