323 lines
9.9 KiB
JavaScript

export const TemplateEngine = {
state: new Proxy({}, {
set(target, property, value) {
target[property] = value;
TemplateEngine.updateDependents(property);
return true;
}
}),
dependencyMap: new Map(),
components: new Map(),
init() {
this.registerComponents();
this.processDOM(document.body);
this.setupEventListeners();
},
registerComponent(name, template) {
this.components.set(name, template);
},
registerComponents() {
const templates = document.querySelectorAll('template[data-component]');
templates.forEach(template => {
const name = template.dataset.component;
this.components.set(name, template.innerHTML);
template.remove();
});
},
setupTwoWayBinding(element) {
if (element.hasAttribute('bind')) {
const bindExpression = element.getAttribute('bind');
const [stateKey] = bindExpression.split('.');
element.value = this.evaluateExpression(bindExpression);
this.addDependency(stateKey, element);
element.addEventListener('input', (e) => {
const value = element.type === 'checkbox' ? element.checked : element.value;
this.setState(bindExpression, value);
});
}
},
setupEventListeners() {
document.addEventListener('click', (e) => {
let target = e.target;
while (target && target !== document) {
const eventAttrs = [...target.attributes].filter(attr =>
attr.name.startsWith('@')
);
eventAttrs.forEach(attr => {
const handler = attr.value;
try {
const func = new Function('event', `
const target = event.target;
${handler}
`);
func(e);
} catch (error) {
console.error(`Error in event handler "${handler}":`, error);
}
});
target = target.parentNode;
}
});
},
processDOM(rootElement) {
this.processComponents(rootElement);
this.evaluateAttributes(rootElement);
this.processControlFlow(rootElement);
this.processLoops(rootElement);
this.setupReactivity(rootElement);
},
processComponents(element) {
this.components.forEach((template, name) => {
const components = element.getElementsByTagName(name);
[...components].forEach(component => {
const props = [...component.attributes].reduce((acc, attr) => {
acc[attr.name] = this.evaluateExpression(attr.value);
return acc;
}, {});
const content = template.replace(/\${([^}]+)}/g, (_, expr) => {
return this.evaluateExpression(expr, props);
});
component.innerHTML = content;
});
});
},
processLoops(element) {
const processForLoop = (node) => {
const forExpr = node.textContent.trim().slice(4);
const matches = forExpr.match(/(\w+)\s+of\s+(.+)/);
if (!matches) return;
const [_, itemName, arrayExpr] = matches;
const array = this.evaluateExpression(arrayExpr);
if (!Array.isArray(array)) {
throw new Error(`Expression "${arrayExpr}" must evaluate to an array`);
}
const template = document.createDocumentFragment();
let currentNode = node.nextSibling;
while (currentNode && currentNode.textContent.trim() !== 'endfor') {
template.appendChild(currentNode.cloneNode(true));
currentNode = currentNode.nextSibling;
}
const parent = node.parentNode;
array.forEach((item, index) => {
const context = {
[itemName]: item,
index
};
const instance = template.cloneNode(true);
this.evaluateWithContext(instance, context);
parent.insertBefore(instance, node);
});
while (currentNode && currentNode.textContent.trim() !== 'endfor') {
const next = currentNode.nextSibling;
parent.removeChild(currentNode);
currentNode = next;
}
parent.removeChild(node);
if (currentNode) parent.removeChild(currentNode);
};
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_COMMENT,
null,
false
);
const forNodes = [];
let currentNode;
while (currentNode = walker.nextNode()) {
if (currentNode.textContent.trim().startsWith('for ')) {
forNodes.push(currentNode);
}
}
forNodes.forEach(node => processForLoop(node));
},
setupReactivity(element) {
[...element.attributes || []].forEach(attr => {
const match = attr.value.match(/^{(.+)}$/);
if (match) {
const expression = match[1];
const stateKeys = this.extractStateKeys(expression);
stateKeys.forEach(key => {
this.addDependency(key, element);
});
}
});
[...element.children].forEach(child => this.setupReactivity(child));
},
extractStateKeys(expression) {
const stateKeys = new Set();
const matches = expression.match(/\b\w+\b/g) || [];
matches.forEach(match => {
if (this.state.hasOwnProperty(match)) {
stateKeys.add(match);
}
});
return Array.from(stateKeys);
},
addDependency(stateKey, element) {
if (!this.dependencyMap.has(stateKey)) {
this.dependencyMap.set(stateKey, new Set());
}
this.dependencyMap.get(stateKey).add(element);
},
updateDependents(stateKey) {
const dependents = this.dependencyMap.get(stateKey);
if (dependents) {
dependents.forEach(element => {
this.evaluateAttributes(element);
});
}
},
setState(path, value) {
const parts = path.split('.');
let current = this.state;
for (let i = 0; i < parts.length - 1; i++) {
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
},
evaluateExpression(expression, context = {}) {
try {
const func = new Function(
...Object.keys(context),
`return ${expression}`
);
return func(...Object.values(context));
} catch (error) {
throw new Error(`Error evaluating expression "${expression}": ${error.message}`);
}
},
evaluateWithContext(element, context) {
[...element.attributes || []].forEach(attr => {
const match = attr.value.match(/^{(.+)}$/);
if (match) {
const expression = match[1];
try {
const result = this.evaluateExpression(expression, context);
element.setAttribute(attr.name, result);
} catch (error) {
console.error(error);
}
}
});
[...element.children].forEach(child => this.evaluateWithContext(child, context));
},
evaluateAttributes(element) {
const attributes = [...element.attributes || []];
attributes.forEach(attr => {
const match = attr.value.match(/^{(.+)}$/);
if (match) {
const expression = match[1];
try {
const result = this.evaluateExpression(expression);
if (result === undefined || result === null) {
throw new Error(`Variable "${expression}" is undefined or null`);
}
element.setAttribute(attr.name, result);
} catch (error) {
throw new Error(`Error evaluating expression "${expression}" in attribute "${attr.name}": ${error.message}`);
}
}
});
[...element.children].forEach(child => this.evaluateAttributes(child));
},
processControlFlow(element) {
const processIfStatements = (startNode) => {
let currentNode = startNode;
let removeNodes = [];
let keepContent = false;
let foundTruthy = false;
while (currentNode) {
if (currentNode.nodeType === Node.COMMENT_NODE) {
const content = currentNode.textContent.trim();
if (content.startsWith('if ')) {
const condition = content.slice(3);
try {
keepContent = this.evaluateExpression(condition);
foundTruthy = keepContent;
} catch (error) {
throw new Error(`Error evaluating if condition "${condition}": ${error.message}`);
}
} else if (content === 'else if' || content === 'else') {
keepContent = !foundTruthy;
if (keepContent && content === 'else if') {
foundTruthy = true;
}
} else if (content === 'endif') {
break;
}
removeNodes.push(currentNode);
} else if (!keepContent && currentNode.nodeType === Node.ELEMENT_NODE) {
removeNodes.push(currentNode);
}
currentNode = currentNode.nextSibling;
}
removeNodes.forEach(node => node.parentNode.removeChild(node));
};
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_COMMENT,
null,
false
);
const ifNodes = [];
let currentNode;
while (currentNode = walker.nextNode()) {
if (currentNode.textContent.trim().startsWith('if ')) {
ifNodes.push(currentNode);
}
}
ifNodes.forEach(node => processIfStatements(node));
}
};