323 lines
9.9 KiB
JavaScript
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));
|
|
}
|
|
}; |