import * as fs from 'fs'; import * as path from 'path'; import { ControlFlowTransformer } from '../../helpers/control-flow-transformer'; export interface TransformResult { content: string; modified: boolean; } export class TemplateTransformer { private controlFlowTransformer = new ControlFlowTransformer(); 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 { return this.controlFlowTransformer.transform(content); } 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 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