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