UNPKG

jsonblade

Version:

A powerful and modular JSON template engine with extensible filters

306 lines 13.1 kB
"use strict"; 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