UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

1,188 lines (1,121 loc) 41.7 kB
import { currentOwner, lookupOwner, buildPropHref, finalizePendingDefaultExport, getResolvedValueExportAt, recordObjectValueBinding, recordArrayValueBinding, resetImportState, resetExportState, recordExport } from "./scanState.mjs"; import { propPathToString } from "./hastUtils.mjs"; import { createPropRefElement } from "./createElements.mjs"; import { tryStartFuncParamContext, flushUnannotatedParam } from "./tryStartFuncParamContext.mjs"; /** * Process a text node for brace/JSX tracking and plain text property extraction. * Returns an array of ElementContent nodes (possibly splitting the text node). */ export function processTextNode(text, state, linkMap, linkProps, linkParams, linkScope, linkValues, linkArrays, typePropRefComponent, lang, siblings, siblingIndex) { const output = []; let textStart = 0; /** Flush accumulated text from textStart to `end` as a text node. */ function flush(end) { if (end > textStart) { output.push({ type: 'text', value: text.substring(textStart, end) }); } textStart = end; } let i = 0; while (i < text.length) { const ch = text[i]; // Import statement punctuation: track `{`, `}`, `*` within import context. // This must come before all other handlers so import-internal characters // don't trigger unrelated state changes (e.g., brace tracking, JSX). if (state.sawJsImportKeyword) { if (ch === '.') { // `import.meta` — property access, not an import statement. // Clear import state so subsequent strings aren't treated as specifiers. resetImportState(state); break; } if (ch === '(') { // `import(` — dynamic import expression, not a static import statement. // Switch to dynamic import tracking mode. state.sawJsImportKeyword = false; state.dynamicImportDepth = 1; i += 1; continue; } if (ch === '{') { state.inImportBraces = true; i += 1; continue; } if (ch === '}') { state.inImportBraces = false; i += 1; continue; } if (ch === '*') { state.importSawStar = true; i += 1; continue; } // Skip other characters within the import statement // (commas, spaces, etc. are fine to pass through) } // Export statement punctuation: track `{`, `}` within export { ... } context. // This must come before other handlers so export-internal characters // don't trigger unrelated state changes. if (state.sawExportKeyword && (!state.pendingExportKind || state.pendingExportKind === 'type')) { if (ch === '{') { state.inExportBraces = true; // `export type { ... }` — the kind applies to each individual name // inside the braces, so preserve pendingExportKind for later use. i += 1; continue; } if (ch === '}' && state.inExportBraces) { state.inExportBraces = false; // Finalize the `export { ... }` or `export type { ... }` statement. // Clear the keyword node so recordExport doesn't overwrite it with each name. state.pendingExportKeywordNode = null; const listKind = state.pendingExportKind === 'type' ? 'type' : 'unknown'; // Save entries for potential `from 'module'` enrichment via moduleLinkMap. // Must be done before resetExportState clears pendingExportNames. state.pendingReExportEntries = []; for (const { localName, exportedName, node } of state.pendingExportNames) { const idx = recordExport(state, exportedName, listKind); node.properties.id = exportedName; state.pendingReExportEntries.push({ localName, index: idx }); // Enrich with type info from scope when available if (linkScope) { enrichExportFromScope(state, idx, localName, linkMap); } } resetExportState(state); i += 1; continue; } // `export *` — star re-export. Record immediately with name '*'. if (ch === '*' && !state.inExportBraces) { recordExport(state, '*', 'unknown'); state.pendingStarReExport = true; resetExportState(state); i += 1; continue; } } // Dynamic import `import(` tracking: the `import` keyword was handled as // pl-k but in dynamic imports the `(` follows directly in a text node. if (ch === '(' && state.dynamicImportDepth > 0) { state.dynamicImportDepth += 1; i += 1; continue; } if (ch === ')' && state.dynamicImportDepth > 0) { state.dynamicImportDepth -= 1; if (state.dynamicImportDepth === 0) { // Finalization of pendingDynamicImportLink happens in the main loop // after processTextNode returns (it needs access to createLinkElement). state.dynamicImportIsComputed = false; } i += 1; continue; } // Any non-whitespace text inside dynamic import parens marks it as computed, // preventing the deferred string link from being finalized. if (state.dynamicImportDepth > 0 && ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r') { state.dynamicImportIsComputed = true; state.pendingDynamicImportLink = null; state.pendingDynamicImportAnnotation = null; } // NOTE: expectingFunctionBody is NOT cleared in text nodes. // Between ) and {, return-type annotations can contain arbitrary text // (e.g. ): Promise<Result<T[]>> {) including type-name identifiers. // Stale flags are cleared by: `;` handler, element-level guard, and `{` itself. // JSX opening "<" — but not inside funcParamContext where it's a generic angle bracket if (ch === '<' && lang.supportsJsx && !state.funcParamContext) { state.sawJsxOpen = true; i += 1; continue; } // JSX self-closing "/>" if (ch === '/' && text[i + 1] === '>' && lang.supportsJsx && currentOwner(state)?.kind === 'jsx') { flush(i); state.ownerStack.pop(); i += 2; textStart = i; continue; } // JSX closing ">" (only in JSX context; avoid matching ">" in "=>") if (ch === '>' && lang.supportsJsx && currentOwner(state)?.kind === 'jsx') { flush(i); state.ownerStack.pop(); i += 1; textStart = i; continue; } // Arrow "=>" in text — some highlighters emit => as plain text rather than // a pl-k keyword span. Detect it here so expressionArrowBody tracking works. if (ch === '=' && text[i + 1] === '>' && state.expectingFunctionBody && !state.sawArrowForBody) { state.sawArrowForBody = true; i += 2; continue; } // Open parenthesis "(" — start function call tracking, type def paren tracking, // or function parameter context if (ch === '(') { // A `(` while pendingValueVar is active means the initializer is a call // expression or grouping, not a simple literal. Clear to prevent false captures. if (state.pendingValueVar && !state.pendingObjectValue && !state.pendingArrayValue) { state.pendingValueVar = null; } // Also invalidate any deferred literal candidate (e.g., `const x = 42(...)`) state.pendingLiteralCandidate = null; state.pendingExpression = null; state.expressionNewlineReady = false; // Track paren depth for export kind refinement if (state.pendingExportKindIndex != null) { state.exportKindParenDepth += 1; } // An open paren means the initializer is a function call or grouping, // not a simple literal — stop trying to infer a type from the value. if (state.pendingExportTypeIndex !== null) { state.pendingExportTypeIndex = null; } // Track nesting for multi-declarator export comma detection if (state.pendingMultiDeclKind) { state.multiDeclNestingDepth += 1; } // Nested paren inside an existing funcParamContext if (state.funcParamContext) { state.funcParamContext.parenDepth += 1; i += 1; continue; } // Expression-bodied arrow: `=> ({...})` — the `(` after `=>` means the // body is an expression, not a block. Mark so the `{` pushes a block scope. if (state.sawArrowForBody && state.expectingFunctionBody) { state.expressionArrowBody = true; state.sawArrowForBody = false; } // Try to start a funcParamContext for param/scope linking if ((linkParams || linkScope) && lang.semantics === 'js') { const paramCtx = tryStartFuncParamContext(state, linkMap, linkProps, linkScope, siblings, siblingIndex, i + 1 // charIndex after the '(' ); if (paramCtx) { state.funcParamContext = paramCtx; i += 1; continue; } } if (state.typeDefParenDepth > 0) { state.typeDefParenDepth += 1; } else if (state.expectingTypeDefBrace && state.pendingTypeDefName && linkProps) { const lookup = lookupOwner(state.pendingTypeDefName, linkMap); if (lookup) { state.typeDefPersist = { name: lookup.ownerName, anchorHref: lookup.anchorHref }; state.typeDefParenDepth = 1; } state.pendingTypeDefName = null; state.expectingTypeDefBrace = false; } else if (state.pendingFuncCall) { state.pendingFuncCall.parenDepth += 1; } else if (lang.semantics === 'js' && state.lastEntityName && state.lastEntityName in linkMap && (linkProps || linkScope)) { state.pendingFuncCall = { name: state.lastEntityName, anchorHref: linkMap[state.lastEntityName], parenDepth: 1, paramIndex: 0 }; state.lastEntityName = null; } i += 1; continue; } // Close parenthesis ")" — end func param context, function call, or type def paren tracking if (ch === ')') { // Track paren depth for export kind refinement if (state.pendingExportKindIndex != null && state.exportKindParenDepth > 0) { state.exportKindParenDepth -= 1; } // Track nesting for multi-declarator export comma detection if (state.pendingMultiDeclKind && state.multiDeclNestingDepth > 0) { state.multiDeclNestingDepth -= 1; } if (state.funcParamContext) { state.funcParamContext.parenDepth -= 1; if (state.funcParamContext.parenDepth === 0) { // Flush last unannotated param as positional binding before saving if (linkScope) { flushUnannotatedParam(state.funcParamContext, linkMap); } // Save pending scope bindings before clearing the context if (linkScope && !state.funcParamContext.isDefinition) { state.pendingFunctionBindings = state.funcParamContext.pendingScopeBindings; state.expectingFunctionBody = true; } state.funcParamContext = null; } i += 1; continue; } if (state.typeDefParenDepth > 0) { state.typeDefParenDepth -= 1; // Keep typeDefPersist for potential & { continuation after ) } else if (state.pendingFuncCall) { state.pendingFuncCall.parenDepth -= 1; if (state.pendingFuncCall.parenDepth === 0) { state.pendingFuncCall = null; } } i += 1; continue; } // Comma "," — increment parameter index in function calls or func param contexts if (ch === ',') { // Multi-declarator export: comma at nesting depth 0 separates declarators. // Re-arm export state for the next variable name. if (state.pendingMultiDeclKind && state.multiDeclNestingDepth === 0) { state.pendingExportKind = state.pendingMultiDeclKind; state.sawExportKeyword = true; state.pendingExportTypeIndex = null; state.pendingExportKindIndex = null; } if (state.funcParamContext && state.funcParamContext.parenDepth === 1 && state.funcParamContext.nestedBracketDepth === 0 && state.funcParamContext.nestedAngleDepth === 0) { // Flush unannotated param as positional binding before advancing if (linkScope) { flushUnannotatedParam(state.funcParamContext, linkMap); } state.funcParamContext.paramIndex += 1; state.funcParamContext.inDefaultValue = false; } else if (state.pendingFuncCall && state.pendingFuncCall.parenDepth === 1 && !currentOwner(state)) { state.pendingFuncCall.paramIndex += 1; } } // CSS colon ":" — start CSS property owner context if (ch === ':' && lang.semantics === 'css' && state.pendingCssProperty && linkProps) { state.ownerStack.push({ name: state.pendingCssProperty.name, anchorHref: state.pendingCssProperty.anchorHref, kind: 'css-property', braceDepth: 0, propPath: [], propPathDepths: [], paramIndex: 0, paramAnchorHref: null }); state.pendingCssProperty = null; i += 1; continue; } // CSS semicolon ";" — end CSS property owner context if (ch === ';' && currentOwner(state)?.kind === 'css-property') { flush(i); state.ownerStack.pop(); i += 1; textStart = i; continue; } // Semicolon ";" — clear typeDefPersist at top level (end of type declaration) if (ch === ';' && state.typeDefPersist && !currentOwner(state) && state.typeDefParenDepth === 0) { state.typeDefPersist = null; } // Clear stale pendingCssProperty at semicolons and braces if ((ch === ';' || ch === '{' || ch === '}') && state.pendingCssProperty) { state.pendingCssProperty = null; } // Newline — potential ASI boundary for complete expressions. // If pendingExpression ends with a value token (not an operator), mark // it as ready to flush. The actual flush is deferred so that next-line // continuation syntax (`.`, `[`, `(`) can invalidate the expression // before it is committed, matching the safeguards for pendingLiteralCandidate. if (ch === '\n' && linkScope && state.pendingExpression) { const { tokens } = state.pendingExpression; if (tokens.length > 0 && tokens[tokens.length - 1].kind !== 'operator') { state.expressionNewlineReady = true; } } // Semicolon ";" — fail-safe: clear any stuck import parsing state if (ch === ';' && state.sawJsImportKeyword) { resetImportState(state); } // Semicolon ";" — fail-safe: clear stuck export parsing state if (ch === ';' && state.sawExportKeyword) { if (!finalizePendingDefaultExport(state)) { resetExportState(state); } } // Semicolon ";" — clear pending export type/kind index (statement boundary) if (ch === ';') { state.pendingExportTypeIndex = null; state.pendingExportKindIndex = null; state.pendingMultiDeclKind = null; state.pendingReExportEntries = []; state.pendingStarReExport = false; } // Semicolon ";" — fail-safe: clear stuck CSS import state if (ch === ';' && state.sawCssImportKeyword) { state.sawCssImportKeyword = false; } // Semicolon ";" — scope ambiguity resets if (ch === ';' && linkScope) { flushLiteralCandidate(state); const exprResult = flushPendingExpression(state); if (exprResult) { state.lastFlushedExpression = exprResult; } state.expressionNewlineReady = false; state.lastDeclaredVarName = null; state.lastVarKeyword = null; state.expectingFunctionBody = false; state.sawArrowForBody = false; state.expressionArrowBody = false; state.pendingFunctionBindings = null; // Clear pending value tracking at statement boundaries state.pendingValueVar = null; // Only clear object/array literal collection when we're NOT inside a // nested construct. Semicolons inside nested braces (e.g., function // bodies within an object literal) should not abort the outer collection. if (!state.pendingObjectValue || state.pendingObjectValue.braceDepth <= 1) { state.pendingObjectValue = null; } if (!state.pendingArrayValue || state.pendingArrayValue.bracketDepth <= 1) { state.pendingArrayValue = null; } } // Open brace "{" if (ch === '{') { // Track nesting for multi-declarator export comma detection if (state.pendingMultiDeclKind) { state.multiDeclNestingDepth += 1; } // Object literal value tracking: track brace depth if (state.pendingObjectValue) { state.pendingObjectValue.braceDepth += 1; // At braceDepth > 1 we're inside a nested construct (e.g., a function // body within the object literal). Push scope frames so that inner // bindings don't leak into the outer scope. if (state.pendingObjectValue.braceDepth > 1 && linkScope) { if (state.expectingFunctionBody) { const bindings = state.pendingFunctionBindings ?? new Map(); const kind = state.expressionArrowBody ? 'block' : 'function'; state.scopeStack.push({ bindings, kind }); state.pendingFunctionBindings = null; state.expectingFunctionBody = false; state.sawArrowForBody = false; state.expressionArrowBody = false; } else { state.scopeStack.push({ bindings: new Map(), kind: 'block' }); } } i += 1; continue; } // Function body takes priority over object-value tracking so that // `const fn = () => { ... }` pushes a function scope instead of // entering pendingObjectValue mode. if (state.pendingValueVar && linkValues && !state.funcParamContext && !state.expectingFunctionBody) { state.pendingObjectValue = { varName: state.pendingValueVar, properties: new Map(), currentPropName: null, pendingSpanKey: null, braceDepth: 1, hasUnresolvedKeys: false }; state.pendingValueVar = null; i += 1; continue; } if (state.funcParamContext) { state.funcParamContext.nestedBracketDepth += 1; } else if (linkScope && state.expectingFunctionBody) { // Function body takes priority over owner tracking (e.g., arrow body // inside a pendingFuncCall should be a function scope, not an object arg). // For expression-bodied arrows `=> ({...})`, push a block scope instead // of a function scope to avoid trapping `var` hoisting. const bindings = state.pendingFunctionBindings ?? new Map(); const kind = state.expressionArrowBody ? 'block' : 'function'; state.scopeStack.push({ bindings, kind }); state.pendingFunctionBindings = null; state.expectingFunctionBody = false; state.sawArrowForBody = false; state.expressionArrowBody = false; // Clear any pending value tracking — this is a function body, not a value state.pendingValueVar = null; } else { const handled = handleOpenBrace(state, linkMap, linkProps); if (!handled && linkScope) { // Push a block scope state.scopeStack.push({ bindings: new Map(), kind: 'block' }); } } i += 1; continue; } // Close brace "}" if (ch === '}') { // Track nesting for multi-declarator export comma detection if (state.pendingMultiDeclKind && state.multiDeclNestingDepth > 0) { state.multiDeclNestingDepth -= 1; } // Object literal value tracking: track depth and flush at top level if (state.pendingObjectValue) { state.pendingObjectValue.braceDepth -= 1; if (state.pendingObjectValue.braceDepth === 0) { recordObjectValueBinding(state); i += 1; continue; } // At braceDepth > 0 we're closing a nested construct — pop the // scope frame that was pushed by the matching `{`. if (state.pendingObjectValue.braceDepth >= 1 && linkScope && state.scopeStack.length > 1) { state.scopeStack.pop(); } i += 1; continue; } if (state.funcParamContext && state.funcParamContext.nestedBracketDepth > 0) { state.funcParamContext.nestedBracketDepth -= 1; } else { const handled = handleCloseBrace(state); if (!handled && linkScope && state.scopeStack.length > 1) { state.scopeStack.pop(); } } i += 1; continue; } // Spread operator "..." — mark pending so the next identifier can be resolved. // If the spread target is a tracked array const, its elements are inlined; // otherwise the array tracking is invalidated at the identifier handler. if (ch === '.' && text[i + 1] === '.' && text[i + 2] === '.') { if (state.pendingArrayValue) { state.pendingArrayValue.pendingSpread = true; } i += 3; continue; } // Open bracket "[" — array literal value tracking or func param destructuring if (ch === '[') { // Track nesting for multi-declarator export comma detection if (state.pendingMultiDeclKind) { state.multiDeclNestingDepth += 1; } // An open bracket means the initializer is an array literal or index // access — not a simple literal — stop trying to infer a type. if (state.pendingExportTypeIndex !== null) { state.pendingExportTypeIndex = null; } // A `[` after a literal means index access (e.g., `'hello'[0]`), // not a standalone initializer. Invalidate the deferred candidate. if (state.pendingLiteralCandidate) { state.pendingLiteralCandidate = null; } if (state.pendingExpression) { state.pendingExpression = null; state.expressionNewlineReady = false; } // Array literal value tracking: start collecting elements if (state.pendingArrayValue) { state.pendingArrayValue.bracketDepth += 1; i += 1; continue; } if (state.pendingValueVar && linkArrays && !state.funcParamContext) { state.pendingArrayValue = { varName: state.pendingValueVar, elements: [], bracketDepth: 1, pendingSpread: false }; state.pendingValueVar = null; i += 1; continue; } if (state.funcParamContext) { state.funcParamContext.nestedBracketDepth += 1; i += 1; continue; } } // Close bracket "]" — array literal value tracking or func param destructuring if (ch === ']') { // Track nesting for multi-declarator export comma detection if (state.pendingMultiDeclKind && state.multiDeclNestingDepth > 0) { state.multiDeclNestingDepth -= 1; } if (state.pendingArrayValue) { state.pendingArrayValue.bracketDepth -= 1; if (state.pendingArrayValue.bracketDepth === 0) { recordArrayValueBinding(state); } i += 1; continue; } if (state.funcParamContext && state.funcParamContext.nestedBracketDepth > 0) { state.funcParamContext.nestedBracketDepth -= 1; i += 1; continue; } } // Open angle bracket "<" — track nesting inside func param context (generics) // Skip when inside a default value expression, where "<" is a comparison operator if (ch === '<' && state.funcParamContext && !state.funcParamContext.inDefaultValue) { state.funcParamContext.nestedAngleDepth += 1; i += 1; continue; } // Close angle bracket ">" — track nesting inside func param context (generics) if (ch === '>' && state.funcParamContext && state.funcParamContext.nestedAngleDepth > 0) { state.funcParamContext.nestedAngleDepth -= 1; i += 1; continue; } // Confirm a span-based property key when we see `:` (with optional leading whitespace). // This handles object keys emitted as span elements (e.g. <span class="pl-v">key</span>:). if (state.pendingObjectValue && state.pendingObjectValue.braceDepth === 1 && state.pendingObjectValue.pendingSpanKey) { if (ch === ':') { state.pendingObjectValue.currentPropName = state.pendingObjectValue.pendingSpanKey; state.pendingObjectValue.pendingSpanKey = null; i += 1; continue; } // Whitespace is allowed between the span and `:` if (ch !== ' ' && ch !== '\t') { // Shorthand property (no `:` value) — mark shape as incomplete state.pendingObjectValue.hasUnresolvedKeys = true; state.pendingObjectValue.pendingSpanKey = null; } } // Try to match a property name inside a pending object value literal if (state.pendingObjectValue && state.pendingObjectValue.braceDepth === 1 && /[a-zA-Z_$]/.test(ch)) { const rest = text.substring(i); const identMatch = rest.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(\s*:)/); if (identMatch) { state.pendingObjectValue.currentPropName = identMatch[1]; i += identMatch[0].length; continue; } } // Try to match a property name (identifier followed by ":") const owner = currentOwner(state); if (owner && linkProps && owner.braceDepth >= 1) { // In shallow mode, skip nested properties if (linkProps === 'shallow' && owner.braceDepth > 1) { i += 1; continue; } if (/[a-zA-Z_$]/.test(ch)) { const rest = text.substring(i); const identMatch = rest.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(\s*:)/); if (identMatch) { const propName = identMatch[1]; flush(i); const propPathStr = propPathToString(owner.propPath, propName); const anchor = buildPropHref(owner, propPathStr); const isDefinition = owner.kind === 'type-def'; output.push(createPropRefElement(anchor, [{ type: 'text', value: propName }], owner.name, propPathStr, isDefinition, undefined, typePropRefComponent)); state.lastLinkedProp = propName; i += propName.length; textStart = i; continue; } } } // Dot invalidates a pending literal candidate — it means property/method // access on the value (e.g., `'hello'.toUpperCase()`), so the // initializer is a compound expression, not a standalone literal. if (ch === '.') { if (state.pendingLiteralCandidate) { state.pendingLiteralCandidate = null; } if (state.pendingExpression) { state.pendingExpression = null; state.expressionNewlineReady = false; } } // Evaluable arithmetic/concat operators: promote a pending literal to a // compound expression, or push the operator onto an active expression. if (ch === '+' || ch === '-' || ch === '*' || ch === '/') { if (state.pendingLiteralCandidate) { // Promote: literal + operator → expression state.pendingExpression = { varName: state.pendingLiteralCandidate.varName, tokens: [tokenFromLiteral(state.pendingLiteralCandidate.value), { kind: 'operator', value: ch }], startChildIndex: state.pendingLiteralCandidate.startChildIndex ?? -1, targetChildren: state.pendingLiteralCandidate.targetChildren, endChildIndex: -1 }; state.pendingLiteralCandidate = null; } else if (state.pendingExpression) { // Consecutive operators (e.g. `++`, `+-`) → invalid expression const lastToken = state.pendingExpression.tokens[state.pendingExpression.tokens.length - 1]; if (lastToken && lastToken.kind === 'operator') { state.pendingExpression = null; state.expressionNewlineReady = false; } else { state.pendingExpression.tokens.push({ kind: 'operator', value: ch }); // Operator extends the expression — it's no longer complete state.expressionNewlineReady = false; } } else if (state.pendingValueVar) { // Unary prefix (e.g. `-1`, `+1`) — clear pendingValueVar state.pendingValueVar = null; } i += 1; continue; } // Non-evaluable operators invalidate all expression tracking. if (ch === '%' || ch === '?' || ch === '&' || ch === '|' || ch === '^' || ch === '~' || ch === '!' || ch === '<' || ch === '>') { if (state.pendingLiteralCandidate) { state.pendingLiteralCandidate = null; } if (state.pendingExpression) { state.pendingExpression = null; state.expressionNewlineReady = false; } if (state.pendingValueVar && !state.pendingObjectValue && !state.pendingArrayValue) { state.pendingValueVar = null; } } i += 1; } // Flush remaining text flush(text.length); return output.length > 0 ? output : [{ type: 'text', value: text }]; } /** * Converts a raw literal string (e.g., `'hello'`, `42`) into an expression token. */ export function tokenFromLiteral(value) { if (/^-?\d/.test(value)) { // Strip numeric separators (e.g. 1_000 → 1000) before storing return { kind: 'number', value: value.replace(/_/g, '') }; } return { kind: 'string', value }; } /** * Evaluates a list of expression tokens into a single value string. * Supports: * - Numeric arithmetic: `1 + 2` → `3`, `10 * 3` → `30` * - String concatenation: `'a' + 'b'` → `'ab'` * - Mixed string + number concat: `'item-' + 3` → `'item-3'` * - Partial evaluation with variables: `'a' + 'b' + x + 'c'` → `'ab' + x + 'c'` * where unresolved variables are kept and their type refs recorded. * * Returns null if the expression cannot be evaluated at all. */ export function evaluateExpression(tokens) { if (tokens.length === 0) { return null; } // Must start with a value token and end with a value token if (tokens[0].kind === 'operator' || tokens[tokens.length - 1].kind === 'operator') { return null; } // Validate proper alternation: value, operator, value, operator, value, ... for (let j = 0; j < tokens.length; j += 1) { const expectValue = j % 2 === 0; const isValue = tokens[j].kind !== 'operator'; if (expectValue !== isValue) { return null; } } const hasString = tokens.some(t => t.kind === 'string'); const hasVariable = tokens.some(t => t.kind === 'variable'); // Variables in pure numeric context are not evaluable if (hasVariable && !hasString) { return null; } // Partial evaluation: string concatenation with unresolved variables. // Collapse adjacent evaluable groups, keep variables in place. if (hasVariable) { // String concatenation with variables: only `+` is valid if (tokens.some(t => t.kind === 'operator' && t.value !== '+')) { return null; } return evaluatePartialConcat(tokens); } if (hasString) { // String concatenation: only `+` is valid if (tokens.some(t => t.kind === 'operator' && t.value !== '+')) { return null; } let result = ''; for (const token of tokens) { if (token.kind === 'string') { // Strip quotes to get inner value, then append result += stripQuotes(token.value); } else if (token.kind === 'number') { result += token.value; } // Skip operator tokens (they're all `+`) } return { value: `'${escapeQuotes(result)}'` }; } // Pure numeric arithmetic // Build a left-to-right evaluation respecting operator precedence const values = []; const ops = []; for (const token of tokens) { if (token.kind === 'number') { const num = Number(token.value); if (Number.isNaN(num)) { return null; } values.push(num); } else if (token.kind === 'operator') { ops.push(token.value); } } if (values.length !== ops.length + 1) { return null; } // Evaluate * and / first (left to right) let i = 0; while (i < ops.length) { if (ops[i] === '*' || ops[i] === '/') { const left = values[i]; const right = values[i + 1]; if (ops[i] === '/' && right === 0) { return null; } values[i] = ops[i] === '*' ? left * right : left / right; values.splice(i + 1, 1); ops.splice(i, 1); } else { i += 1; } } // Then + and - let result = values[0]; for (let j = 0; j < ops.length; j += 1) { if (ops[j] === '+') { result += values[j + 1]; } else { result -= values[j + 1]; } } // Format: avoid trailing decimals for integers const formatted = Number.isInteger(result) ? String(result) : String(result); return { value: formatted }; } /** * Evaluates a string concatenation expression that contains unresolved variable * tokens. Adjacent evaluable tokens (strings, numbers) are collapsed together, * while variable tokens remain in the output as-is. * * Example: `'a' + 'b' + test + 'c' + 'd'` → value `'ab' + test + 'cd'` * * Returns a refs map pairing each variable name to its type-ref anchor href * (if available). */ function evaluatePartialConcat(tokens) { // Collect runs of evaluable tokens separated by variable tokens const segments = []; const refs = {}; let pendingText = ''; for (const token of tokens) { if (token.kind === 'operator') { continue; } if (token.kind === 'variable') { // Flush preceding literal group if (pendingText.length > 0) { segments.push({ type: 'literal', text: pendingText }); pendingText = ''; } segments.push({ type: 'variable', name: token.value }); if (token.ref) { refs[token.value] = token.ref; } } else if (token.kind === 'string') { pendingText += stripQuotes(token.value); } else if (token.kind === 'number') { pendingText += token.value; } } // Flush trailing literal group if (pendingText.length > 0) { segments.push({ type: 'literal', text: pendingText }); } if (segments.length === 0) { return null; } // Build the simplified expression string const parts = segments.map(seg => seg.type === 'literal' ? `'${escapeQuotes(seg.text)}'` : seg.name); const value = parts.join(' + '); const hasRefs = Object.keys(refs).length > 0; return { value, refs: hasRefs ? refs : undefined }; } /** * Strips surrounding quotes from a string literal value. * `'hello'` → `hello`, `"world"` → `world` */ function stripQuotes(s) { if (s.startsWith("'") && s.endsWith("'") || s.startsWith('"') && s.endsWith('"')) { return s.slice(1, -1).replace(/\\'/g, "'"); } return s; } /** * Escapes single quotes in a string for inclusion in a single-quoted literal. */ function escapeQuotes(s) { return s.replace(/'/g, "\\'"); } /** * Commits a pending literal candidate as a scope binding and clears it. * Called at statement boundaries (`;`) and at the end of the top-level code block * traversal (covers ASI / no-semicolon code). */ export function flushLiteralCandidate(state) { if (!state.pendingLiteralCandidate) { return; } const binding = { refKind: 'value', value: state.pendingLiteralCandidate.value, varName: state.pendingLiteralCandidate.varName, declKind: 'const' }; const current = state.scopeStack[state.scopeStack.length - 1]; if (current) { current.bindings.set(state.pendingLiteralCandidate.varName, binding); } state.pendingLiteralCandidate = null; } /** * Evaluates and commits a pending compound expression as a scope binding. * Called at statement boundaries and at end of top-level traversal. * Returns the evaluated result (for wrapping) or null if evaluation failed. */ export function flushPendingExpression(state) { if (!state.pendingExpression) { return null; } const { varName, tokens, startChildIndex, endChildIndex, targetChildren } = state.pendingExpression; state.pendingExpression = null; state.expressionNewlineReady = false; const result = evaluateExpression(tokens); if (!result) { return null; } const binding = { refKind: 'value', value: result.value, varName, refs: result.refs, declKind: 'const' }; const current = state.scopeStack[state.scopeStack.length - 1]; if (current) { current.bindings.set(varName, binding); } return { value: result.value, varName, startChildIndex, endChildIndex, refs: result.refs, targetChildren }; } /** * Handles an open brace "{" in text, updating the scan state. * Returns true if the brace was consumed by owner logic, false otherwise. */ export function handleOpenBrace(state, linkMap, linkProps) { const owner = currentOwner(state); // Function call: pending function call with object argument if (!owner && state.pendingFuncCall) { const paramKey = `${state.pendingFuncCall.name}[${state.pendingFuncCall.paramIndex}]`; const paramAnchorHref = linkMap[paramKey] ?? null; state.ownerStack.push({ name: state.pendingFuncCall.name, anchorHref: state.pendingFuncCall.anchorHref, kind: 'func-call', braceDepth: 1, propPath: [], propPathDepths: [], paramIndex: state.pendingFuncCall.paramIndex, paramAnchorHref }); return true; } // Start of type definition body if (!owner && state.expectingTypeDefBrace && state.pendingTypeDefName && linkProps) { const lookup = lookupOwner(state.pendingTypeDefName, linkMap); if (lookup) { state.ownerStack.push({ name: lookup.ownerName, anchorHref: lookup.anchorHref, kind: 'type-def', braceDepth: 1, propPath: [], propPathDepths: [], paramIndex: 0, paramAnchorHref: null }); // Persist type def info for subsequent union/intersection braces state.typeDefPersist = { name: lookup.ownerName, anchorHref: lookup.anchorHref }; } state.pendingTypeDefName = null; state.expectingTypeDefBrace = false; return true; } // Reuse persisted type def context for union/intersection branches if (!owner && state.typeDefPersist && linkProps) { state.ownerStack.push({ name: state.typeDefPersist.name, anchorHref: state.typeDefPersist.anchorHref, kind: 'type-def', braceDepth: 1, propPath: [], propPathDepths: [], paramIndex: 0, paramAnchorHref: null }); return true; } // Start of type-annotated object literal body if (!owner && state.expectingAnnotationBrace && state.pendingAnnotationType && linkProps) { const lookup = lookupOwner(state.pendingAnnotationType, linkMap); if (lookup) { state.ownerStack.push({ name: lookup.ownerName, anchorHref: lookup.anchorHref, kind: 'type-annotation', braceDepth: 1, propPath: [], propPathDepths: [], paramIndex: 0, paramAnchorHref: null }); } state.pendingAnnotationType = null; state.expectingAnnotationBrace = false; return true; } // Nested brace inside an owner (skip CSS property owners — they end at `;`, not `}`) if (owner && owner.kind !== 'css-property') { owner.braceDepth += 1; if (linkProps === 'deep' && state.lastLinkedProp) { owner.propPath.push(state.lastLinkedProp); owner.propPathDepths.push(owner.braceDepth); state.lastLinkedProp = null; } return true; } return false; } /** * Handles a close brace "}" in text, updating the scan state. * Returns true if the brace was consumed by owner logic, false otherwise. */ export function handleCloseBrace(state) { const owner = currentOwner(state); if (!owner || owner.kind === 'css-property') { return false; } // Pop propPath entries at this brace depth while (owner.propPathDepths.length > 0 && owner.propPathDepths[owner.propPathDepths.length - 1] === owner.braceDepth) { owner.propPath.pop(); owner.propPathDepths.pop(); } owner.braceDepth -= 1; if (owner.braceDepth === 0) { state.ownerStack.pop(); } return true; } /** * Enriches a recorded export entry with type and kind information from scope bindings. * When identifiers in `export { ... }` resolve to a binding in the scope stack, * this populates the export's `type`, `typeHref`, and `kind` fields. */ function enrichExportFromScope(state, exportIndex, localName, linkMap) { for (let k = state.scopeStack.length - 1; k >= 0; k -= 1) { const binding = state.scopeStack[k].bindings.get(localName); if (binding) { const entry = getResolvedValueExportAt(state, exportIndex); if (!entry) { break; } if (binding.refKind === 'type') { entry.type = binding.typeName; entry.typeHref = linkMap[binding.typeName] ?? binding.href; } else if (binding.refKind === 'value') { entry.type = binding.value; } if ('declKind' in binding && binding.declKind) { entry.kind = binding.declKind; } break; } } }