UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

286 lines (271 loc) 12.2 kB
import { unified } from 'unified'; import transformHtmlCodeInline from "../transformHtmlCodeInline/index.mjs"; import { transformHtmlCodeBlock } from "../transformHtmlCodeBlock/transformHtmlCodeBlock.mjs"; import { formatInlineTypeAsHast } from "./typeHighlighting.mjs"; import { resolveSerializer } from "./hastTypeUtils.mjs"; /** * Result of the highlightTypes function. */ /** * Applies syntax highlighting to code blocks in descriptions and examples. * * This function processes all TypesMeta objects and applies transformHtmlCodeBlock * to expand any code blocks in markdown content (descriptions and examples) with precomputed * syntax highlighting. It operates in parallel for maximum performance. * * Note: Type strings (typeText, defaultText) remain as plain text at this stage. * Highlighting of types and generation of shortType/detailedType is deferred to * highlightTypesMeta() which runs after this function. * * The transform is applied to: * - Component and hook descriptions (markdown with code blocks) * - Prop/parameter examples (markdown with code blocks) * - Prop/parameter descriptions (markdown with code blocks) * - Data attribute and CSS variable descriptions (markdown with code blocks) * * Additionally, this function builds a highlightedExports map that maps * export names to their highlighted type definitions, enabling type reference * expansion in highlightTypesMeta(). * * @param types - The types array to process * @param externalTypes - External types discovered during formatting (type name -> definition) * @returns Result object with transformed types and highlightedExports map */ export async function highlightTypes(types, externalTypes = {}, output = 'hast', codeBlockEmphasisOptions) { const processor = unified().use(transformHtmlCodeInline).use(transformHtmlCodeBlock, codeBlockEmphasisOptions); const transformedTypes = await Promise.all(types.map(async typeMeta => { if (typeMeta.type === 'component') { return { ...typeMeta, data: await highlightComponentType(processor, typeMeta.data, output) }; } if (typeMeta.type === 'hook') { return { ...typeMeta, data: await highlightCallableType(processor, typeMeta.data, output) }; } if (typeMeta.type === 'function') { return { ...typeMeta, data: await highlightCallableType(processor, typeMeta.data, output) }; } return typeMeta; })); // Build highlightedExports map from all type metadata and external types // This enables type reference expansion in highlightTypesMeta const highlightedExports = await buildHighlightedExports(transformedTypes, externalTypes); return { types: transformedTypes, highlightedExports }; } /** * Builds a map of export names to their highlighted type definitions. * * This enables type reference expansion in highlightTypesMeta. For each component's * props, dataAttributes, cssVariables, and state types, we create an entry that * can be used to replace type references like "Checkbox.Root.State" with * their actual type definitions. * * External types (like `Orientation`) are also included in this map, enabling * their expansion in detailedType fields. * * @param types - The types array * @param externalTypes - External types discovered during formatting (type name -> definition) * @returns Map of export names (e.g., "Checkbox.Root.Props") to highlighted HAST */ async function buildHighlightedExports(types, externalTypes = {}) { const exports = {}; if (types.length === 0) { // Still add external types even if no types await Promise.all(Object.entries(externalTypes).map(async ([typeName, definition]) => { exports[typeName] = await formatInlineTypeAsHast(definition); })); return exports; } // Collect all types that can be referenced await Promise.all(types.map(async typeMeta => { if (typeMeta.type === 'component') { // Add component's Props type if it has props const propsEntries = Object.entries(typeMeta.data.props); if (propsEntries.length > 0) { // Build Props type string from the component's props const propsType = buildObjectTypeString(propsEntries.map(([name, prop]) => ({ name, type: prop.typeText, optional: !prop.required }))); exports[`${typeMeta.name}.Props`] = await formatInlineTypeAsHast(propsType); } // Add component's DataAttributes type if it has data attributes const dataAttrEntries = Object.entries(typeMeta.data.dataAttributes); if (dataAttrEntries.length > 0) { const dataAttrType = buildObjectTypeString(dataAttrEntries.map(([name, attr]) => ({ name: `'data-${name}'`, type: attr.type || 'string', optional: true }))); exports[`${typeMeta.name}.DataAttributes`] = await formatInlineTypeAsHast(dataAttrType); } // Add component's CssVariables type if it has CSS variables const cssVarEntries = Object.entries(typeMeta.data.cssVariables); if (cssVarEntries.length > 0) { const cssVarType = buildObjectTypeString(cssVarEntries.map(([name, cssVar]) => ({ name: `'${name}'`, type: cssVar.type || 'string', optional: true }))); exports[`${typeMeta.name}.CssVariables`] = await formatInlineTypeAsHast(cssVarType); } } })); // Add external types to the exports map so they can be expanded in detailedType. // These are small type aliases (e.g., Orientation = 'horizontal' | 'vertical') that // appear in the External Types section — expanding them inline is useful for readability. // External types are stored as full declarations (e.g., `type Orientation = 'horizontal' | 'vertical';`) // so we strip the `type NAME = ` prefix and trailing semicolon to get just the definition. await Promise.all(Object.entries(externalTypes).map(async ([typeName, definition]) => { if (!exports[typeName]) { // Strip "type NAME = " prefix and trailing semicolon to get the raw definition let rhs = definition; const prefix = `type ${typeName} = `; if (rhs.startsWith(prefix)) { rhs = rhs.substring(prefix.length); } if (rhs.endsWith(';')) { rhs = rhs.slice(0, -1); } exports[typeName] = await formatInlineTypeAsHast(rhs.trim()); } })); return exports; } /** * Builds an object type string from a list of properties. */ function buildObjectTypeString(props) { if (props.length === 0) { return '{}'; } const members = props.map(p => { const optionalMark = p.optional ? '?' : ''; return `${p.name}${optionalMark}: ${p.type}`; }); return `{ ${members.join('; ')} }`; } /** * Applies syntax highlighting to code blocks in component descriptions and examples. * Type fields (typeText, defaultText) remain as plain text - highlighting is * deferred to highlightTypesMeta() for type/shortType/detailedType generation. */ async function highlightComponentType(processor, data, output) { const s = resolveSerializer(output); // Transform markdown content (descriptions and examples) in parallel // Type fields remain as plain text - highlighting is done in highlightTypesMeta const [description, propsEntries, dataAttributesEntries, cssVariablesEntries] = await Promise.all([ // Transform component description (markdown with code blocks) data.description ? processor.run(data.description).then(s) : Promise.resolve(data.description), // Transform prop descriptions and examples (markdown with code blocks) // Skip typeText/defaultText - highlighting is done in highlightTypesMeta Promise.all(Object.entries(data.props).map(async ([propName, prop]) => { const [propDescription, example] = await Promise.all([prop.description ? processor.run(prop.description).then(s) : Promise.resolve(prop.description), prop.example ? processor.run(prop.example).then(s) : Promise.resolve(prop.example)]); return [propName, { ...prop, description: propDescription, example }]; })), // Transform data attribute descriptions (markdown with code blocks) Promise.all(Object.entries(data.dataAttributes).map(async ([attrName, attr]) => { const attrDescription = attr.description ? s(await processor.run(attr.description)) : attr.description; return [attrName, { ...attr, description: attrDescription }]; })), // Transform CSS variable descriptions (markdown with code blocks) Promise.all(Object.entries(data.cssVariables).map(async ([varName, cssVar]) => { const varDescription = cssVar.description ? s(await processor.run(cssVar.description)) : cssVar.description; return [varName, { ...cssVar, description: varDescription }]; }))]); return { ...data, description, props: Object.fromEntries(propsEntries), dataAttributes: Object.fromEntries(dataAttributesEntries), cssVariables: Object.fromEntries(cssVariablesEntries) }; } /** * Applies syntax highlighting to code blocks in hook/function descriptions and examples. * Type fields (typeText, defaultText) remain as plain text - highlighting is * deferred to highlightTypesMeta() for type/shortType/detailedType generation. * * Used for both hooks and functions since they share the same structure * (description, parameters/properties, returnValue). */ async function highlightCallableType(processor, data, output) { const s = resolveSerializer(output); // Transform markdown content (descriptions and examples) in parallel // Type fields remain as plain text - highlighting is done in highlightTypesMeta const [description, parametersEntries, returnValue] = await Promise.all([ // Transform description (markdown with code blocks) data.description ? processor.run(data.description).then(s) : Promise.resolve(data.description), // Transform parameter/property descriptions and examples (markdown with code blocks) // Skip typeText/defaultText - highlighting is done in highlightTypesMeta (async () => { const paramsOrProps = data.expandedProperties ?? data.parameters ?? []; const entries = Array.isArray(paramsOrProps) ? paramsOrProps.map(p => [p.name, p]) : Object.entries(paramsOrProps); return Promise.all(entries.map(async ([paramName, param]) => { const [paramDescription, example] = await Promise.all([param.description ? processor.run(param.description).then(s) : Promise.resolve(param.description), param.example ? processor.run(param.example).then(s) : Promise.resolve(param.example)]); return [paramName, { ...param, description: paramDescription, example }]; })); })(), // Transform returnValue descriptions and examples (async () => { if (!data.returnValue) { return data.returnValue; } // Check if returnValue is a plain string (single return type) // This will be highlighted in highlightTypesMeta if (typeof data.returnValue === 'string') { return data.returnValue; } // returnValue is an object with FormattedProperty values // Transform descriptions and examples (skip typeText/defaultText - done in highlightTypesMeta) const returnValueEntries = await Promise.all(Object.entries(data.returnValue).map(async ([propName, prop]) => { const [propDescription, example] = await Promise.all([prop.description ? processor.run(prop.description).then(s) : Promise.resolve(prop.description), prop.example ? processor.run(prop.example).then(s) : Promise.resolve(prop.example)]); return [propName, { ...prop, description: propDescription, example }]; })); return Object.fromEntries(returnValueEntries); })()]); const processedParamsOrProps = Object.fromEntries(parametersEntries); const isExpandedProperties = Boolean(data.expandedProperties); return { ...data, description, ...(isExpandedProperties ? { expandedProperties: processedParamsOrProps } : { parameters: parametersEntries.map(([name, param]) => ({ ...param, name })) }), returnValue }; }