jsonblade
Version:
A powerful and modular JSON template engine with extensible filters
306 lines • 13.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.processConditionals = processConditionals;
exports.processIfElse = processIfElse;
exports.processLoops = processLoops;
exports.processVariables = processVariables;
exports.processComments = processComments;
exports.processUnless = processUnless;
exports.compileAdvancedTemplate = compileAdvancedTemplate;
const filter_registry_1 = require("./filter-registry");
// Conditional templating
function processConditionals(template, data) {
// Process {{#if condition}} ... {{/if}} blocks
const conditionalRegex = /\{\{#if\s+([^}]+)\}\}(.*?)\{\{\/if\}\}/gs;
return template.replace(conditionalRegex, (match, condition, content) => {
const result = evaluateCondition(condition.trim(), data);
return result ? content : "";
});
}
// Process {{#if condition}} ... {{#else}} ... {{/if}} blocks
function processIfElse(template, data) {
const ifElseRegex = /\{\{#if\s+([^}]+)\}\}(.*?)\{\{#else\}\}(.*?)\{\{\/if\}\}/gs;
return template.replace(ifElseRegex, (match, condition, ifContent, elseContent) => {
const result = evaluateCondition(condition.trim(), data);
return result ? ifContent : elseContent;
});
}
// Process loops {{#each items}} ... {{/each}}
function processLoops(template, data) {
const loopRegex = /\{\{#each\s+([^}]+)\}\}(.*?)\{\{\/each\}\}/gs;
return template.replace(loopRegex, (match, arrayPath, content) => {
const array = getObjectPath(arrayPath.trim(), data);
if (!Array.isArray(array))
return "";
return array
.map((item, index) => {
let itemContent = content;
// Replace {{this}} with current item
itemContent = itemContent.replace(/\{\{this\}\}/g, typeof item === "string" ? item : JSON.stringify(item));
// Handle {{this | filters...}} expressions
itemContent = itemContent.replace(/\{\{this(\s*\|[^}]+)\}\}/g, (match, filterPart) => {
const { evaluateExpression } = require("./json-template.utils");
const expression = `this${filterPart}`;
const result = evaluateExpression(expression, { this: item });
return typeof result === "string" ? result : JSON.stringify(result);
});
// Replace {{@index}} with current index
itemContent = itemContent.replace(/\{\{@index\}\}/g, String(index));
// Replace {{@first}} and {{@last}}
itemContent = itemContent.replace(/\{\{@first\}\}/g, String(index === 0));
itemContent = itemContent.replace(/\{\{@last\}\}/g, String(index === array.length - 1));
// Handle {{#unless @last}} within loops
const isLast = index === array.length - 1;
itemContent = itemContent.replace(/\{\{#unless @last\}\}(.*?)\{\{\/unless\}\}/g, (match, content) => (!isLast ? content : ""));
// Create enriched context for this iteration
const itemContext = {
...data,
...(typeof item === "object" && item !== null ? item : {}),
};
// Process conditionals within this loop iteration using the item context
itemContent = processIfElse(itemContent, itemContext);
itemContent = processConditionals(itemContent, itemContext);
itemContent = processUnless(itemContent, itemContext);
// Process nested loops recursively
itemContent = processLoops(itemContent, itemContext);
// Replace item properties {{propertyName}} and {{propertyName | filters}}
if (item && typeof item === "object") {
const { evaluateExpression } = require("./json-template.utils");
// Find all {{...}} expressions that aren't special (@index, @first, @last, this)
// and aren't control structures (#if, #else, #unless, #each, etc.)
itemContent = itemContent.replace(/\{\{(?!@|this\b|#|\/)([\w\s|().'",-]+)\}\}/g, (match, expression) => {
const result = evaluateExpression(expression.trim(), itemContext);
return typeof result === "string"
? result
: JSON.stringify(result);
});
}
return itemContent;
})
.join("");
});
}
// Variables support {{#set varName = value}} ... {{varName}}
function processVariables(template, data) {
// Import the filter-aware evaluateExpression from json-template.utils
const { evaluateExpression: evaluateExpressionWithFilters, } = require("./json-template.utils");
// Ensure filters are initialized before using evaluateExpression
const { initializeFilters } = require("./json-template.utils");
initializeFilters();
const variables = {};
// Extract variable definitions
const setRegex = /\{\{#set\s+(\w+)\s*=\s*([^}]+)\}\}/g;
const templateWithoutSets = template.replace(setRegex, (match, varName, expression) => {
const result = evaluateExpressionWithFilters(expression.trim(), {
...data,
...variables,
});
variables[varName] = result;
return "";
});
// Replace variable references
let result = templateWithoutSets;
Object.keys(variables).forEach((varName) => {
const regex = new RegExp(`\\{\\{${varName}\\}\\}`, "g");
const value = variables[varName];
result = result.replace(regex, typeof value === "string" ? value : JSON.stringify(value));
});
return result;
}
// Comments support {{!-- comment --}}
function processComments(template) {
let result = template;
// Handle nested comments by processing them iteratively
let hasComments = true;
while (hasComments) {
hasComments = false;
// Find the first comment start
const startIndex = result.indexOf("{{!--");
if (startIndex === -1)
break;
// Find the matching comment end, considering nesting
let nestLevel = 1;
let pos = startIndex + 5; // Skip '{{!--'
let endIndex = -1;
while (pos < result.length && nestLevel > 0) {
const nextStart = result.indexOf("{{!--", pos);
const nextEnd = result.indexOf("--}}", pos);
if (nextEnd === -1)
break; // No more comment ends
if (nextStart !== -1 && nextStart < nextEnd) {
// Found nested comment start
nestLevel++;
pos = nextStart + 5;
}
else {
// Found comment end
nestLevel--;
if (nestLevel === 0) {
endIndex = nextEnd + 4; // Include '--}}'
}
pos = nextEnd + 4;
}
}
if (endIndex !== -1) {
// Remove the complete comment
result = result.substring(0, startIndex) + result.substring(endIndex);
hasComments = true;
}
else {
// Malformed comment, remove everything from start to end of string
result = result.substring(0, startIndex);
break;
}
}
return result;
}
function evaluateCondition(condition, data) {
// Handle simple comparisons
const comparisonRegex = /^(.+?)\s*(===|!==|==|!=|>=|<=|>|<)\s*(.+)$/;
const match = condition.match(comparisonRegex);
if (match) {
const [, left, operator, right] = match;
const leftValue = evaluateExpression(left.trim(), data);
const rightValue = evaluateExpression(right.trim(), data);
switch (operator) {
case "===":
return leftValue === rightValue;
case "!==":
return leftValue !== rightValue;
case "==":
return leftValue == rightValue;
case "!=":
return leftValue != rightValue;
case ">=":
return Number(leftValue) >= Number(rightValue);
case "<=":
return Number(leftValue) <= Number(rightValue);
case ">":
return Number(leftValue) > Number(rightValue);
case "<":
return Number(leftValue) < Number(rightValue);
default:
return false;
}
}
// Handle simple boolean expressions
const value = evaluateExpression(condition, data);
return Boolean(value);
}
function evaluateExpression(expression, data) {
// Handle filters
if (expression.includes("|")) {
const parts = expression.split("|").map((p) => p.trim());
const [rawPath, ...filterParts] = parts;
let value = getObjectPath(rawPath, data);
for (const part of filterParts) {
const match = part.match(/^(\w+)(?:\((.*?)\))?$/);
if (!match)
continue;
const [, name, argsString] = match;
const fn = (0, filter_registry_1.getFilter)(name);
if (!fn)
continue;
let args = [];
if (argsString) {
args = argsString.split(",").map((arg) => {
const trimmed = arg.trim();
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
return trimmed.slice(1, -1);
}
return trimmed;
});
}
value = fn(value, ...args);
}
return value;
}
// Handle literals
if (expression.startsWith('"') && expression.endsWith('"')) {
return expression.slice(1, -1);
}
if (expression.startsWith("'") && expression.endsWith("'")) {
return expression.slice(1, -1);
}
if (!isNaN(Number(expression))) {
return Number(expression);
}
if (expression === "true")
return true;
if (expression === "false")
return false;
if (expression === "null")
return null;
// Handle object path
return getObjectPath(expression, data);
}
function getObjectPath(path, data) {
const parts = path.split(".");
let current = data;
for (const part of parts) {
if (current == null || typeof current !== "object")
return null;
current = current[part];
}
return current;
}
// Extract variables from {{#set}} declarations
function extractVariables(template, data) {
const { evaluateExpression: evaluateExpressionWithFilters, } = require("./json-template.utils");
const { initializeFilters } = require("./json-template.utils");
initializeFilters();
const variables = {};
const setRegex = /\{\{#set\s+(\w+)\s*=\s*([^}]+)\}\}/g;
let match;
while ((match = setRegex.exec(template)) !== null) {
const [, varName, expression] = match;
const result = evaluateExpressionWithFilters(expression.trim(), {
...data,
...variables,
});
variables[varName] = result;
}
return variables;
}
// Replace variables and remove {{#set}} declarations
function replaceVariables(template, variables) {
// Remove set declarations
let result = template.replace(/\{\{#set\s+(\w+)\s*=\s*([^}]+)\}\}/g, "");
// Replace variable references
Object.keys(variables).forEach((varName) => {
const regex = new RegExp(`\\{\\{${varName}\\}\\}`, "g");
const value = variables[varName];
result = result.replace(regex, typeof value === "string" ? value : JSON.stringify(value));
});
return result;
}
// Process unless blocks {{#unless condition}} ... {{/unless}}
function processUnless(template, data) {
const unlessRegex = /\{\{#unless\s+([^}]+)\}\}(.*?)\{\{\/unless\}\}/gs;
return template.replace(unlessRegex, (match, condition, content) => {
const result = evaluateCondition(condition.trim(), data);
return !result ? content : "";
});
}
function compileAdvancedTemplate(template, data) {
// Import the basic template compiler
const { compileJSONTemplate } = require("./json-template.utils");
let result = template;
// Process in order
result = processComments(result);
// Extract variables first and merge them with data
const variables = extractVariables(result, data);
const enrichedData = { ...data, ...variables };
result = replaceVariables(result, variables);
// Process loops first (which will handle conditionals within loops)
result = processLoops(result, enrichedData);
// Then process any remaining conditionals at the global level
result = processIfElse(result, enrichedData);
result = processConditionals(result, enrichedData);
result = processUnless(result, enrichedData);
// Apply basic template compilation for filters
result = compileJSONTemplate(result, enrichedData);
return result;
}
//# sourceMappingURL=advanced-templating.js.map