UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

330 lines (310 loc) 9.96 kB
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; }