Files
inkject/inkject.js
2025-05-29 13:35:21 +03:00

284 lines
11 KiB
JavaScript

class inkject {
render(template, data = {}, delimiter = '&&') {
const [openDelim, closeDelim] = this.parseDelimiter(delimiter);
let result = template;
result = this.processConditionals(result, data, openDelim, closeDelim);
result = this.processVariables(result, data, openDelim, closeDelim);
return result;
}
parseDelimiter(delimiter) {
const len = delimiter.length;
if (len % 2 === 0) {
const mid = len / 2;
return [delimiter.slice(0, mid), delimiter.slice(mid)];
} else {
return [delimiter, delimiter];
}
}
processConditionals(template, data, openDelim, closeDelim) {
let result = template;
let changed = true;
while (changed) {
changed = false;
const match = this.findInnermostConditional(result, openDelim, closeDelim);
if (match) {
const replacement = this.evaluateConditionalBlock(match.content, data, openDelim, closeDelim);
const beforeMatch = result.slice(0, match.start);
const afterMatch = result.slice(match.end);
const isInline = this.isInlineConditional(beforeMatch, afterMatch, match.content);
if (isInline) {
result = beforeMatch + replacement + afterMatch;
} else {
let cleanedReplacement = replacement.trim();
let cleanedBefore = beforeMatch;
let cleanedAfter = afterMatch;
if (cleanedReplacement === '') {
if (cleanedBefore.endsWith('\n')) {
cleanedBefore = cleanedBefore.slice(0, -1);
}
if (cleanedAfter.startsWith('\n')) {
cleanedAfter = cleanedAfter.slice(1);
}
result = cleanedBefore + cleanedAfter;
} else {
const needsNewlineBefore = cleanedBefore.length > 0 && !cleanedBefore.endsWith('\n') && cleanedReplacement.length > 0;
const needsNewlineAfter = cleanedAfter.length > 0 && !cleanedAfter.startsWith('\n') && cleanedReplacement.length > 0;
result = cleanedBefore +
(needsNewlineBefore ? '\n' : '') +
cleanedReplacement +
(needsNewlineAfter ? '\n' : '') +
cleanedAfter;
}
}
changed = true;
}
}
return result;
}
isInlineConditional(beforeMatch, afterMatch, conditionalContent) {
const beforeLastNewline = beforeMatch.lastIndexOf('\n');
const afterFirstNewline = afterMatch.indexOf('\n');
const beforeText = beforeLastNewline === -1 ? beforeMatch : beforeMatch.slice(beforeLastNewline + 1);
const afterText = afterFirstNewline === -1 ? afterMatch : afterMatch.slice(0, afterFirstNewline);
const hasContentBefore = beforeText.trim().length > 0;
const hasContentAfter = afterText.trim().length > 0;
const conditionalHasNewlines = conditionalContent.includes('\n');
return (hasContentBefore || hasContentAfter) && !conditionalHasNewlines;
}
findInnermostConditional(template, openDelim, closeDelim) {
const ifPattern = openDelim + '#if ';
const endifPattern = openDelim + '/if' + closeDelim;
let deepestIf = null;
let maxDepth = 0;
for (let i = 0; i < template.length; i++) {
if (template.substr(i, ifPattern.length) === ifPattern) {
let depth = 0;
let currentPos = i;
let foundMatching = false;
while (currentPos < template.length) {
if (template.substr(currentPos, ifPattern.length) === ifPattern) {
depth++;
currentPos += ifPattern.length;
} else if (template.substr(currentPos, endifPattern.length) === endifPattern) {
depth--;
if (depth === 0) {
if (!foundMatching || depth >= maxDepth) {
let hasNestedIf = false;
const blockContent = template.slice(i + ifPattern.length, currentPos);
if (blockContent.indexOf(ifPattern) !== -1) {
hasNestedIf = true;
}
if (!hasNestedIf) {
deepestIf = {
start: i,
end: currentPos + endifPattern.length,
content: template.slice(i, currentPos + endifPattern.length)
};
maxDepth = depth;
}
}
foundMatching = true;
break;
}
currentPos += endifPattern.length;
} else {
currentPos++;
}
}
}
}
return deepestIf;
}
evaluateConditionalBlock(block, data, openDelim, closeDelim) {
const sections = this.parseConditionalSections(block, openDelim, closeDelim);
for (const section of sections) {
if (section.type === 'else' || this.evaluateCondition(section.condition, data)) {
return section.content;
}
}
return '';
}
parseConditionalSections(block, openDelim, closeDelim) {
const sections = [];
const ifRegex = new RegExp(`^${this.escapeRegex(openDelim)}#if\\s+([^${this.escapeRegex(closeDelim)}]+)${this.escapeRegex(closeDelim)}`);
const elseifRegex = new RegExp(`${this.escapeRegex(openDelim)}#elseif\\s+([^${this.escapeRegex(closeDelim)}]+)${this.escapeRegex(closeDelim)}`, 'g');
const elseRegex = new RegExp(`${this.escapeRegex(openDelim)}#else${this.escapeRegex(closeDelim)}`);
const endifRegex = new RegExp(`${this.escapeRegex(openDelim)}/if${this.escapeRegex(closeDelim)}$`);
const ifMatch = block.match(ifRegex);
if (!ifMatch) return sections;
let content = block.replace(ifRegex, '').replace(endifRegex, '');
let pos = 0;
let currentCondition = ifMatch[1].trim();
let currentType = 'if';
const elseifMatches = [];
let elseifMatch;
while ((elseifMatch = elseifRegex.exec(content)) !== null) {
elseifMatches.push({
index: elseifMatch.index,
condition: elseifMatch[1].trim(),
fullMatch: elseifMatch[0]
});
}
const elseMatch = content.match(elseRegex);
const elsePos = elseMatch ? content.search(elseRegex) : -1;
const allBreakpoints = [
...elseifMatches.map(m => ({ pos: m.index, type: 'elseif', condition: m.condition, length: m.fullMatch.length })),
...(elsePos !== -1 ? [{ pos: elsePos, type: 'else', condition: null, length: elseMatch[0].length }] : [])
].sort((a, b) => a.pos - b.pos);
for (let i = 0; i <= allBreakpoints.length; i++) {
const start = i === 0 ? 0 : allBreakpoints[i - 1].pos + allBreakpoints[i - 1].length;
const end = i === allBreakpoints.length ? content.length : allBreakpoints[i].pos;
const sectionContent = content.slice(start, end);
sections.push({
type: currentType,
condition: currentCondition,
content: sectionContent
});
if (i < allBreakpoints.length) {
currentType = allBreakpoints[i].type;
currentCondition = allBreakpoints[i].condition;
}
}
return sections;
}
processVariables(template, data, openDelim, closeDelim) {
const openPattern = this.escapeRegex(openDelim);
const closePattern = this.escapeRegex(closeDelim);
const varRegex = new RegExp(`${openPattern}([^#/][^${closePattern.slice(-1)}]*)${closePattern}`, 'g');
return template.replace(varRegex, (match, variable) => {
const value = this.getValue(variable.trim(), data);
return value !== undefined ? String(value) : '';
}).replace(/\n\s*\n\s*\n/g, '\n\n');
}
evaluateCondition(condition, data) {
if (!condition) return false;
condition = condition.trim();
if (condition.startsWith('!')) {
return !this.evaluateCondition(condition.slice(1), data);
}
const operators = ['===', '!==', '==', '!=', '>=', '<=', '>', '<'];
for (const op of operators) {
const opIndex = condition.indexOf(op);
if (opIndex !== -1) {
const left = condition.slice(0, opIndex).trim();
const right = condition.slice(opIndex + op.length).trim();
const leftValue = this.getValue(left, data);
const rightValue = this.parseValue(right);
switch (op) {
case '===': return leftValue === rightValue;
case '!==': return leftValue !== rightValue;
case '==': return leftValue == rightValue;
case '!=': return leftValue != rightValue;
case '>=': return leftValue >= rightValue;
case '<=': return leftValue <= rightValue;
case '>': return leftValue > rightValue;
case '<': return leftValue < rightValue;
}
}
}
const value = this.getValue(condition, data);
return Boolean(value);
}
getValue(path, data) {
const keys = path.split('.');
let current = data;
for (const key of keys) {
if (current === null || current === undefined) {
return undefined;
}
current = current[key];
}
return current;
}
parseValue(value) {
value = value.trim();
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
return value.slice(1, -1);
}
if (!isNaN(value) && !isNaN(parseFloat(value)) && value !== '') {
return parseFloat(value);
}
if (value === 'true') return true;
if (value === 'false') return false;
if (value === 'null') return null;
if (value === 'undefined') return undefined;
return value;
}
escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
}
module.exports = inkject;