jsonblade
Version:
A powerful and modular JSON template engine with extensible filters
305 lines • 13.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.processCommentsAsync = processCommentsAsync;
exports.processConditionalsAsync = processConditionalsAsync;
exports.processIfElseAsync = processIfElseAsync;
exports.processLoopsAsync = processLoopsAsync;
exports.processVariablesAsync = processVariablesAsync;
exports.processUnlessAsync = processUnlessAsync;
exports.compileAdvancedTemplateAsync = compileAdvancedTemplateAsync;
const json_template_utils_1 = require("./json-template.utils");
async function processCommentsAsync(template) {
return template.replace(/\{\{!.*?\}\}/gs, "");
}
async function processConditionalsAsync(template, data) {
const conditionalRegex = /\{\{#if\s+([^}]+)\}\}(.*?)\{\{\/if\}\}/gs;
let result = template;
const matches = Array.from(template.matchAll(conditionalRegex));
for (const match of matches) {
const [fullMatch, condition, content] = match;
const conditionResult = await evaluateConditionAsync(condition.trim(), data);
const replacement = conditionResult ? content : "";
result = result.replace(fullMatch, replacement);
}
return result;
}
async function processIfElseAsync(template, data) {
let result = template;
// Keep processing until no more if/else patterns are found
let hasMatches = true;
while (hasMatches) {
hasMatches = false;
// Find all if statements and their matching else/endif with proper nesting
const ifMatches = [];
const ifRegex = /\{\{#if\s+([^}]+)\}\}/g;
let match;
while ((match = ifRegex.exec(result)) !== null) {
const startPos = match.index;
const condition = match[1];
// Find the matching {{#else}} and {{/if}} considering nesting
let nestLevel = 1;
let pos = match.index + match[0].length;
let elsePos = -1;
let endPos = -1;
while (pos < result.length && nestLevel > 0) {
// Look for next if, else, or endif
const nextIf = result.indexOf("{{#if", pos);
const nextElse = result.indexOf("{{#else}}", pos);
const nextEndIf = result.indexOf("{{/if}}", pos);
// Find the earliest occurrence
let nextPos = result.length;
let nextType = "";
if (nextIf !== -1 && nextIf < nextPos) {
nextPos = nextIf;
nextType = "if";
}
if (nextElse !== -1 &&
nextElse < nextPos &&
nestLevel === 1 &&
elsePos === -1) {
nextPos = nextElse;
nextType = "else";
}
if (nextEndIf !== -1 && nextEndIf < nextPos) {
nextPos = nextEndIf;
nextType = "endif";
}
if (nextType === "if") {
nestLevel++;
pos = nextPos + 5; // Skip "{{#if"
}
else if (nextType === "else" && nestLevel === 1) {
elsePos = nextPos;
pos = nextPos + 9; // Skip "{{#else}}"
}
else if (nextType === "endif") {
nestLevel--;
if (nestLevel === 0) {
endPos = nextPos;
}
pos = nextPos + 7; // Skip "{{/if}}"
}
else {
break; // No more matches found
}
}
if (elsePos !== -1 && endPos !== -1) {
const ifContent = result.substring(startPos + match[0].length, elsePos);
const elseContent = result.substring(elsePos + 9, endPos);
const fullMatch = result.substring(startPos, endPos + 7);
ifMatches.push({
fullMatch,
condition,
ifContent,
elseContent,
startPos,
length: fullMatch.length,
});
}
}
if (ifMatches.length > 0) {
// Process the innermost (shortest) match first
ifMatches.sort((a, b) => a.length - b.length);
const shortestMatch = ifMatches[0];
const conditionResult = await evaluateConditionAsync(shortestMatch.condition.trim(), data);
const replacement = conditionResult
? shortestMatch.ifContent
: shortestMatch.elseContent;
result = result.replace(shortestMatch.fullMatch, replacement);
hasMatches = true;
}
}
return result;
}
async function processLoopsAsync(template, data) {
const loopRegex = /\{\{#each\s+([^}]+)\}\}(.*?)\{\{\/each\}\}/gs;
let result = template;
const matches = Array.from(template.matchAll(loopRegex));
for (const match of matches) {
const [fullMatch, arrayPath, content] = match;
const array = (0, json_template_utils_1.getObjectPath)(arrayPath.trim(), data);
if (!Array.isArray(array)) {
result = result.replace(fullMatch, "");
continue;
}
const iterations = [];
for (let i = 0; i < array.length; i++) {
const item = array[i];
const loopData = {
...data,
...item,
".": item, // Add the current item as "." for filters
"@index": i,
"@first": i === 0,
"@last": i === array.length - 1,
};
let iterationContent = content;
// Process variables within the loop context first
const loopVariables = await extractVariablesAsync(iterationContent, loopData);
const enrichedLoopData = { ...loopData, ...loopVariables };
iterationContent = replaceVariablesAsync(iterationContent, loopVariables);
// Process conditionals within the loop
iterationContent = await processIfElseAsync(iterationContent, enrichedLoopData);
iterationContent = await processConditionalsAsync(iterationContent, enrichedLoopData);
iterationContent = await processUnlessAsync(iterationContent, enrichedLoopData);
// Process variable interpolations
const { compileJSONTemplateAsync, } = require("./async-json-template.utils");
iterationContent = await compileJSONTemplateAsync(iterationContent, enrichedLoopData);
iterations.push(iterationContent);
}
result = result.replace(fullMatch, iterations.join(""));
}
return result;
}
async function processVariablesAsync(template, data) {
const variables = await extractVariablesAsync(template, data);
const templateWithoutSets = template.replace(/\{\{#set\s+(\w+)\s*=\s*([^}]+)\}\}/g, "");
let result = templateWithoutSets;
Object.keys(variables).forEach((varName) => {
// Use negative lookahead to avoid matching property references like {{userData.active}}
const regex = new RegExp(`\\{\\{${varName}(?!\\.)\\}\\}`, "g");
const value = variables[varName];
result = result.replace(regex, typeof value === "string" ? value : JSON.stringify(value));
});
return result;
}
async function processUnlessAsync(template, data) {
const unlessRegex = /\{\{#unless\s+([^}]+)\}\}(.*?)\{\{\/unless\}\}/gs;
let result = template;
const matches = Array.from(template.matchAll(unlessRegex));
for (const match of matches) {
const [fullMatch, condition, content] = match;
const conditionResult = await evaluateConditionAsync(condition.trim(), data);
const replacement = !conditionResult ? content : "";
result = result.replace(fullMatch, replacement);
}
return result;
}
async function evaluateConditionAsync(condition, data) {
const { evaluateExpressionAsync: evalExprAsync, } = require("./async-json-template.utils");
const result = await evalExprAsync(condition, data);
if (typeof result === "boolean")
return result;
if (typeof result === "number")
return result !== 0;
if (typeof result === "string")
return result.length > 0;
if (Array.isArray(result))
return result.length > 0;
if (result && typeof result === "object")
return Object.keys(result).length > 0;
return !!result;
}
async function extractVariablesAsync(template, data) {
const { evaluateExpressionAsync: evalExprAsync, } = require("./async-json-template.utils");
const variables = {};
// First, find all {{#each}}...{{/each}} blocks to avoid extracting variables inside them
const eachBlocks = [];
const eachRegex = /\{\{#each\s+[^}]+\}\}/g;
const eachEndRegex = /\{\{\/each\}\}/g;
let eachMatch;
while ((eachMatch = eachRegex.exec(template)) !== null) {
const startPos = eachMatch.index;
// Find matching {{/each}}
let nestLevel = 1;
let pos = eachMatch.index + eachMatch[0].length;
while (pos < template.length && nestLevel > 0) {
const nextEach = template.indexOf("{{#each", pos);
const nextEndEach = template.indexOf("{{/each}}", pos);
if (nextEndEach === -1)
break;
if (nextEach !== -1 && nextEach < nextEndEach) {
nestLevel++;
pos = nextEach + 7; // length of '{{#each'
}
else {
nestLevel--;
if (nestLevel === 0) {
eachBlocks.push({ start: startPos, end: nextEndEach + 9 }); // +9 for '{{/each}}'
}
pos = nextEndEach + 9;
}
}
}
// Function to check if a position is inside any each block
const isInsideEachBlock = (pos) => {
return eachBlocks.some((block) => pos >= block.start && pos <= block.end);
};
const setRegex = /\{\{#set\s+(\w+)\s*=\s*([^}]+)\}\}/g;
const matches = Array.from(template.matchAll(setRegex));
for (const match of matches) {
// Skip if this variable definition is inside an each block
if (isInsideEachBlock(match.index)) {
continue;
}
const [, varName, expression] = match;
const result = await evalExprAsync(expression.trim(), {
...data,
...variables,
});
variables[varName] = result;
}
return variables;
}
function replaceVariablesAsync(template, variables) {
// Find all {{#each}}...{{/each}} blocks to avoid removing {{#set}} inside them
const eachBlocks = [];
const eachRegex = /\{\{#each\s+[^}]+\}\}/g;
let eachMatch;
while ((eachMatch = eachRegex.exec(template)) !== null) {
const startPos = eachMatch.index;
// Find matching {{/each}}
let nestLevel = 1;
let pos = eachMatch.index + eachMatch[0].length;
while (pos < template.length && nestLevel > 0) {
const nextEach = template.indexOf("{{#each", pos);
const nextEndEach = template.indexOf("{{/each}}", pos);
if (nextEndEach === -1)
break;
if (nextEach !== -1 && nextEach < nextEndEach) {
nestLevel++;
pos = nextEach + 7; // length of '{{#each'
}
else {
nestLevel--;
if (nestLevel === 0) {
eachBlocks.push({ start: startPos, end: nextEndEach + 9 }); // +9 for '{{/each}}'
}
pos = nextEndEach + 9;
}
}
}
// Function to check if a position is inside any each block
const isInsideEachBlock = (pos) => {
return eachBlocks.some((block) => pos >= block.start && pos <= block.end);
};
// Remove {{#set}} declarations that are NOT inside each blocks
let result = template.replace(/\{\{#set\s+(\w+)\s*=\s*([^}]+)\}\}/g, (match, varName, expression, offset) => {
if (isInsideEachBlock(offset)) {
return match; // Keep the {{#set}} if it's inside an each block
}
return ""; // Remove it if it's global
});
Object.keys(variables).forEach((varName) => {
// Use negative lookahead to avoid matching property references like {{userData.active}}
const regex = new RegExp(`\\{\\{${varName}(?!\\.)\\}\\}`, "g");
const value = variables[varName];
result = result.replace(regex, typeof value === "string" ? value : JSON.stringify(value));
});
return result;
}
async function compileAdvancedTemplateAsync(template, data) {
const { compileJSONTemplateAsync } = require("./async-json-template.utils");
let result = template;
result = await processCommentsAsync(result);
const variables = await extractVariablesAsync(result, data);
const enrichedData = { ...data, ...variables };
result = replaceVariablesAsync(result, variables);
result = await processLoopsAsync(result, enrichedData);
result = await processIfElseAsync(result, enrichedData);
result = await processConditionalsAsync(result, enrichedData);
result = await processUnlessAsync(result, enrichedData);
result = await compileJSONTemplateAsync(result, enrichedData);
return result;
}
//# sourceMappingURL=async-advanced-templating.js.map