@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
330 lines (310 loc) • 9.96 kB
JavaScript
import { toKebabCase } from "../loaderUtils/toKebabCase.mjs";
/**
* A proven binding in a scope — links a variable name to its type origin.
* Discriminated on `refKind` so that each variant carries exactly the
* metadata needed to produce the correct link element.
*/
/**
* Entry in the moduleLinkMap configuration.
*/
/**
* Resolved import collected during the scan.
* Used to build the `data-imports` attribute on the `<code>` element.
*/
/**
* Resolved export collected during the scan.
* Used to build the `data-exports` attribute on the `<code>` element.
*/
/**
* A single lexical scope in the scope stack.
* - `'function'`: function body scope (holds `var` bindings and params)
* - `'block'`: block scope (holds `let`/`const` bindings)
*/
/**
* Owner context for property linking.
* Tracks which type/function/component owns the current block of properties.
*/
/**
* Mutable state threaded through the single-pass traversal.
*/
/**
* Creates a fresh ScanState.
*/
export function createScanState() {
return {
ownerStack: [],
sawTypeKeyword: false,
pendingTypeDefName: null,
expectingTypeDefBrace: false,
pendingAnnotationType: null,
expectingAnnotationBrace: false,
lastEntityName: null,
sawJsxOpen: false,
jsxComponentName: null,
lastLinkedProp: null,
pendingFuncCall: null,
typeDefPersist: null,
typeDefParenDepth: 0,
pendingCssProperty: null,
sawFunctionKeyword: false,
scopeStack: [],
expectingFunctionBody: false,
sawArrowForBody: false,
expressionArrowBody: false,
pendingFunctionBindings: null,
lastDeclaredVarName: null,
lastVarKeyword: null,
funcParamContext: null,
pendingValueVar: null,
pendingLiteralCandidate: null,
expressionNewlineReady: false,
pendingExpression: null,
lastFlushedExpression: null,
pendingObjectValue: null,
pendingArrayValue: null,
sawJsImportKeyword: false,
pendingImportNames: [],
pendingDefaultImport: null,
pendingNamespaceImport: null,
importSawAs: false,
sawFromKeyword: false,
inImportBraces: false,
importSawStar: false,
dynamicImportDepth: 0,
pendingDynamicImportLink: null,
pendingDynamicImportAnnotation: null,
dynamicImportIsComputed: false,
sawCssImportKeyword: false,
resolvedImports: new Map(),
unresolvedImports: new Set(),
sawExportKeyword: false,
sawExportDefaultKeyword: false,
pendingExportKind: null,
pendingExportKeywordNode: null,
pendingExportNames: [],
inExportBraces: false,
exportSawAs: false,
resolvedExports: [],
cssModuleExports: new Map(),
pendingExportTypeIndex: null,
pendingExportKindIndex: null,
exportKindParenDepth: 0,
pendingMultiDeclKind: null,
multiDeclNestingDepth: 0,
pendingReExportEntries: [],
pendingStarReExport: false
};
}
/**
* Looks up an owner name in the linkMap.
*/
export function lookupOwner(name, linkMap) {
if (name in linkMap) {
return {
ownerName: name,
anchorHref: linkMap[name]
};
}
return null;
}
/**
* Returns the current active owner context, or null if none.
*/
export function currentOwner(state) {
return state.ownerStack.length > 0 ? state.ownerStack[state.ownerStack.length - 1] : null;
}
/**
* Builds the property href for the given owner and property path.
* - If a named param anchor exists (e.g., `makeItem[0]` → `#make-item:props`):
* uses `paramAnchorHref:propPath` (e.g., `#make-item:props:label`)
* - For type-def/type-annotation: `#anchor:prop-path`
* - For func-call/jsx param 0: `#anchor::prop-path` (zero omitted)
* - For func-call param N: `#anchor:N:prop-path`
*/
export function buildPropHref(owner, propPathStr) {
if (owner.paramAnchorHref) {
return `${owner.paramAnchorHref}:${propPathStr}`;
}
if (owner.kind === 'func-call' || owner.kind === 'jsx') {
if (owner.paramIndex === 0) {
return `${owner.anchorHref}::${propPathStr}`;
}
return `${owner.anchorHref}:${owner.paramIndex}:${propPathStr}`;
}
return `${owner.anchorHref}:${propPathStr}`;
}
/**
* Builds the anchor map lookup key for a deep callback property.
* For JSX/func-call owners with paramAnchorHref, uses the resolved owner key path.
* For type-def/type-annotation, uses `OwnerName:prop.path`.
*/
export function buildParamOwnerKey(owner, propPath) {
const propPathStr = propPath.map(toKebabCase).join('.');
if (owner.paramAnchorHref) {
// The paramAnchorHref is pre-resolved (e.g., Test[0] → #test:props).
// Build the key as Owner:props:propPath to match the linkMap convention.
return `${owner.name}:${propPathStr}`;
}
if (owner.kind === 'func-call' || owner.kind === 'jsx') {
if (owner.paramIndex === 0) {
return `${owner.name}:${propPathStr}`;
}
return `${owner.name}:${owner.paramIndex}:${propPathStr}`;
}
return `${owner.name}:${propPathStr}`;
}
/**
* Builds the anchor for a function parameter.
*
* At **definition sites** (type defs), checks `linkMap["Owner[N]"]` for a named
* anchor, then falls back to the positional format `#anchor[N]`.
*
* At **reference sites**, resolves through `linkMap["OwnerKey[N]"]` named anchors,
* or falls back to `#anchor[N]`.
*/
export function buildParamHref(ctx, paramName, linkMap) {
const basePath = ctx.basePropPath.length > 0 ? ctx.basePropPath.map(toKebabCase).join('.') : null;
if (ctx.isDefinition) {
// Definition site: check for a named anchor in the linkMap first,
// then fall back to positional format so reference sites can link without a map entry
const paramKey = basePath ? `${ctx.ownerName}:${basePath}[${ctx.paramIndex}]` : `${ctx.ownerName}[${ctx.paramIndex}]`;
const namedAnchor = linkMap[paramKey];
if (namedAnchor) {
return namedAnchor.startsWith('#') ? namedAnchor.slice(1) : namedAnchor;
}
if (basePath) {
return `${ctx.anchorHref}:${basePath}[${ctx.paramIndex}]`;
}
return `${ctx.anchorHref}[${ctx.paramIndex}]`;
}
// Reference site: look up named anchor first
const paramKey = basePath ? `${ctx.ownerName}:${basePath}[${ctx.paramIndex}]` : `${ctx.ownerName}[${ctx.paramIndex}]`;
const namedAnchor = linkMap[paramKey];
if (namedAnchor) {
return namedAnchor;
}
// Fallback to positional
if (basePath) {
return `${ctx.anchorHref}:${basePath}[${ctx.paramIndex}]`;
}
return `${ctx.anchorHref}[${ctx.paramIndex}]`;
}
/**
* Records a value-object binding for the current pendingObjectValue in the scope stack.
* Clears pendingObjectValue after flushing.
*/
export function recordObjectValueBinding(state) {
if (!state.pendingObjectValue) {
return;
}
const {
varName,
properties
} = state.pendingObjectValue;
// Only record when at least one key: value pair was tracked.
// Shorthand properties ({ a }) don't produce key: value entries,
// so an empty map means the shape is uncertain — skip the binding.
if (properties.size > 0 && !state.pendingObjectValue.hasUnresolvedKeys) {
const binding = {
refKind: 'value-object',
properties,
varName,
declKind: 'const'
};
const current = state.scopeStack[state.scopeStack.length - 1];
if (current) {
current.bindings.set(varName, binding);
}
}
state.pendingObjectValue = null;
}
/**
* Records a value binding for the current pendingArrayValue in the scope stack.
* Formats the elements as `[elem1, elem2, ...]` and clears pendingArrayValue.
*/
/**
* Clears all JS import-related parsing state.
* Used after an import statement is fully consumed, or when we discover the
* `import` keyword was not actually an import statement (e.g. `import.meta`).
*/
export function resetImportState(state) {
state.sawJsImportKeyword = false;
state.pendingImportNames = [];
state.pendingDefaultImport = null;
state.pendingNamespaceImport = null;
state.importSawAs = false;
state.sawFromKeyword = false;
state.inImportBraces = false;
state.importSawStar = false;
}
/**
* Clears all JS export-related parsing state.
* Used after an export statement is fully consumed.
*/
export function resetExportState(state) {
state.sawExportKeyword = false;
state.sawExportDefaultKeyword = false;
state.pendingExportKind = null;
state.pendingExportKeywordNode = null;
state.pendingExportNames = [];
state.inExportBraces = false;
state.exportSawAs = false;
state.pendingMultiDeclKind = null;
}
/**
* Finalizes an in-progress `export default ...` statement that has reached a
* statement boundary before a named declaration identifier was seen.
*/
export function finalizePendingDefaultExport(state) {
if (!state.sawExportKeyword || !state.sawExportDefaultKeyword) {
return false;
}
recordExport(state, 'default', state.pendingExportKind ?? 'unknown');
resetExportState(state);
return true;
}
/**
* Records a resolved export and sets the `id` attribute on the export keyword node.
*/
export function recordExport(state, name, kind) {
const index = state.resolvedExports.length;
state.resolvedExports.push({
name,
kind
});
if (state.pendingExportKeywordNode) {
state.pendingExportKeywordNode.properties.id = name;
}
return index;
}
export function getResolvedValueExportAt(state, index) {
if (index === null) {
return null;
}
const entry = state.resolvedExports[index];
if (!entry || entry.kind === 'object') {
return null;
}
return entry;
}
export function recordArrayValueBinding(state) {
if (!state.pendingArrayValue) {
return;
}
const {
varName,
elements
} = state.pendingArrayValue;
const value = `[${elements.join(', ')}]`;
const binding = {
refKind: 'value',
value,
varName,
declKind: 'const'
};
const current = state.scopeStack[state.scopeStack.length - 1];
if (current) {
current.bindings.set(varName, binding);
}
state.pendingArrayValue = null;
}