import type { TemplateChildNode, ElementNode, DirectiveNode, JsxContext } from "../types"; import { rewriteTemplateGlobals } from "./utils"; // NodeTypes from @vue/compiler-core const ELEMENT = 1; const TEXT = 2; const DIRECTIVE = 7; /** * Check if an element node has a directive with the given name. */ function findDirective(node: ElementNode, name: string): DirectiveNode | undefined { return node.props.find((p): p is DirectiveNode => p.type === DIRECTIVE && p.name === name); } /** * Check if a TemplateChildNode is a whitespace-only text node. */ function isWhitespaceText(node: TemplateChildNode): boolean { return node.type === TEXT && "content" in node && (node as any).content.trim() === ""; } /** * Process a conditional chain starting from a v-if element. * Scans siblings forward to collect v-else-if and v-else branches. * * Returns the JSX ternary expression and the number of sibling nodes consumed * (including whitespace text nodes between branches). */ export function processConditionalChain( siblings: TemplateChildNode[], startIndex: number, ctx: JsxContext, renderElement: (node: ElementNode, ctx: JsxContext) => string, ): { jsx: string; consumed: number } { const branches: { condition: string | null; node: ElementNode }[] = []; let consumed = 1; // the v-if node itself const ifNode = siblings[startIndex] as ElementNode; const ifDir = findDirective(ifNode, "if")!; branches.push({ condition: ifDir.exp ? rewriteTemplateGlobals((ifDir.exp as any).content, ctx) : "true", node: ifNode, }); // Scan forward for v-else-if / v-else let i = startIndex + 1; while (i < siblings.length) { const sibling = siblings[i]; // Skip whitespace-only text nodes if (isWhitespaceText(sibling)) { i++; consumed++; continue; } // Must be an element node if (sibling.type !== ELEMENT) break; const elseIfDir = findDirective(sibling as ElementNode, "else-if"); const elseDir = findDirective(sibling as ElementNode, "else"); if (elseIfDir) { branches.push({ condition: elseIfDir.exp ? rewriteTemplateGlobals((elseIfDir.exp as any).content, ctx) : "true", node: sibling as ElementNode, }); consumed++; i++; continue; } if (elseDir) { branches.push({ condition: null, // v-else has no condition node: sibling as ElementNode, }); consumed++; i++; break; } // Not a conditional sibling, stop break; } // Build ternary chain const jsx = buildTernary(branches, ctx, renderElement); return { jsx: `{${jsx}}`, consumed }; } function buildTernary( branches: { condition: string | null; node: ElementNode }[], ctx: JsxContext, renderElement: (node: ElementNode, ctx: JsxContext) => string, ): string { if (branches.length === 1) { // Only v-if, no else const b = branches[0]; return `${b.condition} ? ${renderElement(b.node, ctx)} : null`; } const parts: string[] = []; for (let i = 0; i < branches.length; i++) { const b = branches[i]; if (b.condition === null) { // v-else (final branch) parts.push(renderElement(b.node, ctx)); } else if (i === branches.length - 1) { // Last branch with condition, no v-else parts.push(`${b.condition} ? ${renderElement(b.node, ctx)} : null`); } else { parts.push(`${b.condition} ? ${renderElement(b.node, ctx)}`); } } return parts.join(" : "); } /** * Parse a v-for expression like "item in items" or "(item, index) in items". * Handles multiline expressions by normalizing whitespace first. */ function parseVForExpression(expr: string): { iterator: string; iterable: string } { // Normalize whitespace (collapse newlines/multiple spaces from multiline expressions) const normalized = expr.replace(/\s+/g, " ").trim(); // Match "X in Y" or "X of Y" — use greedy .+ for iterable to capture full expression const match = normalized.match(/^(.+?)\s+(?:in|of)\s+(.+)$/); if (!match) { return { iterator: "_item", iterable: normalized }; } return { iterator: match[1].trim(), iterable: match[2].trim() }; } /** * Process a v-for directive on an element. * Generates: `{items.map((item, index) => ())}` * * If v-if is also present on the same element, the v-if wraps the map body: * `{items.map((item) => (show ? : null))}` */ export function processVFor( node: ElementNode, ctx: JsxContext, renderElement: (node: ElementNode, ctx: JsxContext) => string, ): string { const forDir = findDirective(node, "for")!; const rawExpr = forDir.exp ? (forDir.exp as any).content : ""; // Parse BEFORE rewriting globals — so the iterator variable doesn't get props. prefixed const { iterator: rawIterator, iterable: rawIterable } = parseVForExpression(rawExpr); const iterable = rewriteTemplateGlobals(rawIterable, ctx); const iterator = rawIterator; // Check for :key binding const keyDir = node.props.find( (p): p is DirectiveNode => p.type === DIRECTIVE && p.name === "bind" && p.arg != null && (p.arg as any).content === "key", ); const _keyExpr = keyDir?.exp ? (keyDir.exp as any).content : null; // Check for v-if on same element const ifDir = findDirective(node, "if"); const rendered = renderElement(node, ctx); let body: string; if (ifDir) { const condition = ifDir.exp ? rewriteTemplateGlobals((ifDir.exp as any).content, ctx) : "true"; body = `${condition} ? ${rendered} : null`; } else { body = rendered; } const arrowParams = iterator.startsWith("(") ? iterator : `(${iterator})`; // Mark that v-for was used so the _renderList helper gets emitted ctx.hasVFor = true; return `{_renderList(${iterable}, ${arrowParams} => (${body}))}`; } export { findDirective, isWhitespaceText };