import * as fs from 'fs'; import * as path from 'path'; export interface TransformResult { content: string; modified: boolean; } export class TemplateTransformer { transformInterpolation(content: string): string { let result = content; result = this.transformAttributeInterpolation(result); result = this.transformContentInterpolation(result); return result; } private transformAttributeInterpolation(content: string): string { const tagRegex = /<([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[^>]*?)?)>/g; return content.replace(tagRegex, (fullMatch, tagName, attributesPart) => { if (!attributesPart || !attributesPart.includes('{{')) { return fullMatch; } const interpolationRegex = /([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*"([^"]*\{\{[^"]*\}\}[^"]*)"/g; const bindings: { attr: string; expr: string }[] = []; let newAttributes = attributesPart; newAttributes = attributesPart.replace(interpolationRegex, (_attrMatch: string, attrName: string, attrValue: string) => { const hasInterpolation = /\{\{.*?\}\}/.test(attrValue); if (!hasInterpolation) { return _attrMatch; } const parts: string[] = []; let lastIndex = 0; const exprRegex = /\{\{\s*([^}]+?)\s*\}\}/g; let match; while ((match = exprRegex.exec(attrValue)) !== null) { if (match.index > lastIndex) { const literal = attrValue.substring(lastIndex, match.index); if (literal) { parts.push(`'${literal}'`); } } parts.push(`(${match[1].trim()})`); lastIndex = exprRegex.lastIndex; } if (lastIndex < attrValue.length) { const literal = attrValue.substring(lastIndex); if (literal) { parts.push(`'${literal}'`); } } const expression = parts.length === 1 ? parts[0] : parts.join(' + '); bindings.push({ attr: attrName, expr: expression }); return ''; }); if (bindings.length === 0) { return fullMatch; } const bindingsJson = JSON.stringify(bindings).replace(/"/g, "'"); const dataAttr = ` data-quarc-attr-bindings="${bindingsJson.replace(/'/g, ''')}"`; newAttributes = newAttributes.trim(); return `<${tagName}${newAttributes ? ' ' + newAttributes : ''}${dataAttr}>`; }); } private transformContentInterpolation(content: string): string { return content.replace( /\{\{\s*([^}]+?)\s*\}\}/g, (_, expr) => ``, ); } transformControlFlowIf(content: string): string { let result = content; let modified = true; while (modified) { modified = false; result = result.replace( /@if\s*\(([^)]+)\)\s*\{([\s\S]*?)\}(?:\s*@else\s+if\s*\(([^)]+)\)\s*\{([\s\S]*?)\})*(?:\s*@else\s*\{([\s\S]*?)\})?/, (match) => { modified = true; return this.parseIfBlock(match); }, ); } return result; } transformControlFlowFor(content: string): string { let result = content; let startIndex = 0; while (startIndex < result.length) { const forIndex = result.indexOf('@for', startIndex); if (forIndex === -1) break; const block = this.extractForBlock(result, forIndex); if (!block) { startIndex = forIndex + 4; continue; } const replacement = this.buildForDirective(block.header, block.body); result = result.substring(0, forIndex) + replacement + result.substring(block.endIndex); startIndex = forIndex + replacement.length; } return result; } transformNgIfDirective(content: string): string { // Keep *ngIf as is - runtime handles it return content; } transformNgForDirective(content: string): string { // Keep *ngFor as is - runtime handles it return content; } transformInputBindings(content: string): string { return content.replace(/\[([a-zA-Z][a-zA-Z0-9]*)\]="/g, (match, propName) => { const kebabName = this.camelToKebab(propName); return `[${kebabName}]="`; }); } private camelToKebab(str: string): string { return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } transformOutputBindings(content: string): string { // Keep (event) as is - runtime handles it return content; } transformTwoWayBindings(content: string): string { // Keep [(model)] as is - runtime handles it return content; } transformAll(content: string): string { let result = content; result = this.transformInterpolation(result); result = this.transformControlFlowFor(result); result = this.transformControlFlowIf(result); result = this.transformSelectNgFor(result); result = this.transformNgIfDirective(result); result = this.transformNgForDirective(result); result = this.transformInputBindings(result); result = this.transformOutputBindings(result); result = this.transformTwoWayBindings(result); return result; } async loadExternalTemplate(templatePath: string, fileDir: string): Promise { const fullPath = path.resolve(fileDir, templatePath); if (!fs.existsSync(fullPath)) { throw new Error(`Template file not found: ${fullPath}`); } return fs.promises.readFile(fullPath, 'utf8'); } private parseIfBlock(match: string): string { const blocks: Array<{ condition: string | null; content: string }> = []; let remaining = match; const ifMatch = remaining.match(/@if\s*\(([^)]+)\)\s*\{([\s\S]*?)\}/); if (ifMatch) { blocks.push({ condition: ifMatch[1].trim(), content: ifMatch[2] }); remaining = remaining.substring(ifMatch[0].length); } let elseIfMatch; const elseIfRegex = /@else\s+if\s*\(([^)]+)\)\s*\{([\s\S]*?)\}/g; while ((elseIfMatch = elseIfRegex.exec(remaining)) !== null) { blocks.push({ condition: elseIfMatch[1].trim(), content: elseIfMatch[2] }); } const elseMatch = remaining.match(/@else\s*\{([\s\S]*?)\}$/); if (elseMatch) { blocks.push({ condition: null, content: elseMatch[1] }); } return this.buildIfDirectives(blocks); } private buildIfDirectives(blocks: Array<{ condition: string | null; content: string }>): string { const negated: string[] = []; let result = ''; for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; let condition: string; if (block.condition === null) { condition = negated.map(c => `!(${c})`).join(' && '); } else if (negated.length > 0) { condition = negated.map(c => `!(${c})`).join(' && ') + ` && ${block.condition}`; } else { condition = block.condition; } result += `${block.content}`; if (i < blocks.length - 1) result += '\n'; if (block.condition) { negated.push(block.condition); } } return result; } private extractForBlock(content: string, startIndex: number): { header: string; body: string; endIndex: number } | null { const openParenIndex = content.indexOf('(', startIndex); if (openParenIndex === -1) return null; const closeParenIndex = this.findMatchingParen(content, openParenIndex); if (closeParenIndex === -1) return null; const openBraceIndex = content.indexOf('{', closeParenIndex); if (openBraceIndex === -1) return null; const closeBraceIndex = this.findMatchingBrace(content, openBraceIndex); if (closeBraceIndex === -1) return null; return { header: content.substring(openParenIndex + 1, closeParenIndex).trim(), body: content.substring(openBraceIndex + 1, closeBraceIndex), endIndex: closeBraceIndex + 1, }; } private buildForDirective(header: string, body: string): string { const parts = header.split(';'); const forPart = parts[0].trim(); const trackPart = parts[1]?.trim(); const forMatch = forPart.match(/^\s*(\w+)\s+of\s+(.+)\s*$/); if (!forMatch) return ``; const variable = forMatch[1]; const iterable = forMatch[2].trim(); let ngForExpr = `let ${variable} of ${iterable}`; if (trackPart) { const trackMatch = trackPart.match(/^track\s+(.+)$/); if (trackMatch) { ngForExpr += `; trackBy: ${trackMatch[1].trim()}`; } } return `${body}`; } transformSelectNgFor(content: string): string { // Transform ng-container *ngFor inside