legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
811 lines • 32.2 kB
JavaScript
/**
* Template Loop Processing Extension for Legal Markdown
*
* This extension adds support for template loops and conditional blocks using
* the {{#variable}}...{{/variable}} syntax. It enables iteration over arrays
* and conditional rendering of template sections.
*
* Features:
* - Array iteration: {{#items}}...{{/items}}
* - Conditional blocks: {{#loyaltyMember}}...{{/loyaltyMember}}
* - Nested loops support
* - Context management for loop variables
* - Integration with field tracking
*
* @example
* ```typescript
* import { processTemplateLoops } from './template-loops.js';
*
* const content = `
* {{#items}}
* - {{name}} {{onSale ? "(ON SALE!)" : ""}} - ${{price}}
* {{/items}}
* `;
*
* const metadata = {
* items: [
* { name: "Product A", price: 10.99, onSale: true },
* { name: "Product B", price: 15.50, onSale: false }
* ]
* };
*
* const result = processTemplateLoops(content, metadata);
* ```
*/
import { fieldTracker } from './tracking/field-tracker.js';
import { extensionHelpers as helpers } from './helpers/index.js';
/**
* Escapes HTML attribute values to prevent breaking HTML structure
* @param value - The value to escape
* @returns Escaped value safe for HTML attributes
*/
function escapeHtmlAttribute(value) {
return value
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>');
}
/**
* Processes template loops and conditional blocks in content
*
* @param content - The template content containing loop patterns
* @param metadata - The metadata object containing loop data
* @param context - Current loop context for nested loops
* @param enableFieldTracking - Whether to enable field tracking HTML spans
* @returns Processed content with loops expanded
*/
export function processTemplateLoops(content, metadata, context, enableFieldTracking = true) {
// Template loops now run before AST processor, so no need to skip based on spans
// Find all loop blocks in the content
const loopBlocks = findLoopBlocks(content);
if (loopBlocks.length === 0) {
return content;
}
// Process loop blocks from last to first to avoid position shifting
let processedContent = content;
for (let i = loopBlocks.length - 1; i >= 0; i--) {
const loopBlock = loopBlocks[i];
const expandedContent = expandLoopBlock(loopBlock, metadata, context, enableFieldTracking);
// Replace the loop block with expanded content
processedContent =
processedContent.slice(0, loopBlock.start) +
expandedContent +
processedContent.slice(loopBlock.end);
}
return processedContent;
}
/**
* Finds all loop blocks in the content using regex pattern matching
*
* @param content - The content to search for loop blocks
* @returns Array of parsed loop blocks
*/
function findLoopBlocks(content) {
const loopBlocks = [];
// Pattern for array loops: {{#variable}}...{{/variable}}
const loopPattern = /\{\{#([\w.]+)\}\}([\s\S]*?)\{\{\/\1\}\}/g;
// Pattern for conditionals: {{#if condition}}...{{/if}} (with optional {{else}})
const conditionalPattern = /\{\{#if\s*([^}]+)\}\}([\s\S]*?)\{\{\/if\}\}/g;
let match;
// Find array loops
while ((match = loopPattern.exec(content)) !== null) {
const [fullMatch, variable, loopContent] = match;
loopBlocks.push({
variable,
content: loopContent,
start: match.index,
end: match.index + fullMatch.length,
fullMatch,
});
}
// Find conditional blocks
while ((match = conditionalPattern.exec(content)) !== null) {
const [fullMatch, condition, conditionalContent] = match;
loopBlocks.push({
variable: `if ${condition}`, // Mark as conditional
content: conditionalContent,
start: match.index,
end: match.index + fullMatch.length,
fullMatch,
});
}
return loopBlocks;
}
/**
* Expands a single loop block into its final content
*
* @param loopBlock - The loop block to expand
* @param metadata - The metadata object containing loop data
* @param parentContext - Parent loop context for nested loops
* @param enableFieldTracking - Whether to enable field tracking HTML spans
* @returns Expanded content string
*/
function expandLoopBlock(loopBlock, metadata, parentContext, enableFieldTracking = true) {
const { variable, content } = loopBlock;
// Handle conditional blocks ({{#if condition}})
if (variable.startsWith('if ')) {
const condition = variable.substring(3).trim(); // Remove 'if '
const conditionValue = evaluateCondition(condition, metadata);
// Track field usage for conditional
fieldTracker.trackField(`if ${condition}`, {
value: conditionValue,
hasLogic: true,
mixinUsed: 'conditional',
});
// Check if content contains {{else}} clause
const elseIndex = content.indexOf('{{else}}');
if (elseIndex !== -1) {
// Split content into "if" and "else" parts
const ifContent = content.substring(0, elseIndex);
const elseContent = content.substring(elseIndex + 8); // 8 = length of '{{else}}'
const selectedContent = conditionValue ? ifContent : elseContent;
return processTemplateContent(selectedContent, metadata, parentContext, enableFieldTracking);
}
else {
// No {{else}} clause, use original behavior
if (conditionValue) {
return processTemplateContent(content, metadata, parentContext, enableFieldTracking);
}
else {
return ''; // Condition false, return empty
}
}
}
// Resolve the loop variable value from metadata or parent context
const loopValue = resolveLoopVariable(variable, metadata, parentContext);
// Track field usage
fieldTracker.trackField(variable, {
value: loopValue,
hasLogic: true,
mixinUsed: 'loop',
});
// Handle different types of loop values
if (Array.isArray(loopValue)) {
return expandArrayLoop(variable, content, loopValue, metadata, parentContext, enableFieldTracking);
}
else if (loopValue) {
// Conditional block - render if truthy
return expandConditionalBlock(variable, content, loopValue, metadata, parentContext, enableFieldTracking);
}
else {
// Falsy value - don't render
return '';
}
}
/**
* Expands an array loop, iterating over each item
*
* @param variable - The loop variable name
* @param content - The loop content template
* @param items - The array to iterate over
* @param metadata - The metadata object
* @param parentContext - Parent loop context
* @param enableFieldTracking - Whether to enable field tracking HTML spans
* @returns Expanded content for all array items
*/
function expandArrayLoop(variable, content, items, metadata, parentContext, enableFieldTracking = true) {
const expandedParts = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
// Create loop context for this iteration
const loopContext = {
variable,
item,
index: i,
total: items.length,
parent: parentContext,
};
// Create enhanced metadata with current item properties
const enhancedMetadata = createEnhancedMetadata(metadata, item, loopContext);
// Process the content with the enhanced metadata
let processedContent = content;
// First, process any nested loops
if (process.env.NODE_ENV === 'development') {
console.log(`Before nested loops processing: "${processedContent}"`);
}
processedContent = processTemplateLoops(processedContent, enhancedMetadata, loopContext, enableFieldTracking);
if (process.env.NODE_ENV === 'development') {
console.log(`After nested loops processing: "${processedContent}"`);
}
// Then process regular mixins in the content
processedContent = processItemMixins(processedContent, enhancedMetadata, loopContext, enableFieldTracking);
// Convert markdown list items to HTML li elements if content starts with -
processedContent = convertMarkdownListToHtml(processedContent);
expandedParts.push(processedContent);
}
return expandedParts.join('\n');
}
/**
* Expands a conditional block if the condition is truthy
*
* @param variable - The condition variable name
* @param content - The conditional content template
* @param value - The condition value
* @param metadata - The metadata object
* @param parentContext - Parent loop context
* @param enableFieldTracking - Whether to enable field tracking HTML spans
* @returns Expanded content if condition is truthy, empty string otherwise
*/
function expandConditionalBlock(variable, content, value, metadata, parentContext, enableFieldTracking = true) {
// Create enhanced metadata with the condition value
const enhancedMetadata = {
...metadata,
[variable]: value,
};
// Process any nested loops first
let processedContent = processTemplateLoops(content, enhancedMetadata, parentContext, enableFieldTracking);
// Then process regular mixins
processedContent = processItemMixins(processedContent, enhancedMetadata, parentContext, enableFieldTracking);
return processedContent;
}
/**
* Resolves a loop variable from metadata or parent context
*
* @param variable - The variable name to resolve
* @param metadata - The metadata object
* @param parentContext - Parent loop context
* @returns The resolved variable value
*/
function resolveLoopVariable(variable, metadata, parentContext) {
// First try to resolve using dot notation from metadata
const resolvedFromMetadata = resolvePath(metadata, variable);
if (resolvedFromMetadata !== undefined) {
return resolvedFromMetadata;
}
// Then check parent context (for nested loops)
if (parentContext && parentContext.item) {
const resolvedFromContext = resolvePath(parentContext.item, variable);
if (resolvedFromContext !== undefined) {
return resolvedFromContext;
}
}
// Check parent context's parent (for deeper nesting)
let ctx = parentContext;
while (ctx && ctx.parent) {
ctx = ctx.parent;
if (ctx.item) {
const resolvedFromNestedContext = resolvePath(ctx.item, variable);
if (resolvedFromNestedContext !== undefined) {
return resolvedFromNestedContext;
}
}
}
return undefined;
}
/**
* Creates enhanced metadata by merging current item properties
*
* @param metadata - Original metadata
* @param item - Current loop item
* @param context - Loop context
* @returns Enhanced metadata object
*/
function createEnhancedMetadata(metadata, item, context) {
const enhanced = { ...metadata };
// Add current item properties if it's an object
if (item && typeof item === 'object' && !Array.isArray(item)) {
Object.assign(enhanced, item);
// Debug: log enhanced metadata for template loops
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line max-len
console.log('Template loops enhanced metadata for item:', JSON.stringify({ item, enhanced }, null, 2));
}
}
// Add special dot notation for current item value
enhanced['.'] = item;
// Add loop context variables
enhanced['@index'] = context.index;
enhanced['@total'] = context.total;
enhanced['@first'] = context.index === 0;
enhanced['@last'] = context.index === context.total - 1;
return enhanced;
}
/**
* Evaluates a helper function expression with arguments
*
* @param expression - The helper function expression (e.g., "formatDate(@today, 'long')")
* @param metadata - Metadata context for argument resolution
* @returns The result of the helper function call, or undefined if invalid
*/
function evaluateHelperExpression(expression, metadata) {
try {
// Parse helper function call: helperName(arg1, arg2, ...)
const match = expression.match(/^(\w+)\((.*)\)$/);
if (!match)
return undefined;
const [, helperName, argsString] = match;
const helper = helpers[helperName];
if (!helper || typeof helper !== 'function') {
return undefined;
}
// Parse arguments
const args = argsString ? parseHelperArguments(argsString, metadata) : [];
// Call the helper function
return helper(...args);
}
catch (error) {
console.error(`Error evaluating helper expression: ${expression}`, error);
return undefined;
}
}
/**
* Parses helper function arguments, handling nested calls and variable references
*/
function parseHelperArguments(argsString, metadata) {
const args = [];
let currentArg = '';
let parenDepth = 0;
let inQuotes = false;
let quoteChar = '';
for (let i = 0; i < argsString.length; i++) {
const char = argsString[i];
// eslint-disable-next-line quotes
if ((char === '"' || char === "'") && !inQuotes) {
inQuotes = true;
quoteChar = char;
currentArg += char;
}
else if (char === quoteChar && inQuotes) {
inQuotes = false;
quoteChar = '';
currentArg += char;
}
else if (char === '(' && !inQuotes) {
parenDepth++;
currentArg += char;
}
else if (char === ')' && !inQuotes) {
parenDepth--;
currentArg += char;
}
else if (char === ',' && parenDepth === 0 && !inQuotes) {
args.push(resolveHelperArgument(currentArg.trim(), metadata));
currentArg = '';
}
else {
currentArg += char;
}
}
// Add the last argument
if (currentArg.trim()) {
args.push(resolveHelperArgument(currentArg.trim(), metadata));
}
return args;
}
/**
* Resolves a single helper argument, handling different types
*/
function resolveHelperArgument(arg, metadata) {
// Handle quoted strings
// eslint-disable-next-line quotes
if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) {
return arg.slice(1, -1);
}
// Handle boolean values
if (arg === 'true')
return true;
if (arg === 'false')
return false;
// Handle numbers
if (/^\d+$/.test(arg)) {
return parseInt(arg, 10);
}
if (/^\d+\.\d+$/.test(arg)) {
return parseFloat(arg);
}
// Handle @today special case
if (arg === '@today') {
// Check if @today is defined in metadata first, otherwise use current date
return metadata['@today'] ? new Date(metadata['@today']) : new Date();
}
// Handle nested helper function calls
if (arg.includes('(') && arg.includes(')')) {
const result = evaluateHelperExpression(arg, metadata);
if (result !== undefined) {
return result;
}
}
// Handle variable references
return resolvePath(metadata, arg);
}
/**
* Resolves a nested path in metadata (e.g., "client.name")
*/
function resolvePath(obj, path) {
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current && typeof current === 'object' && key in current) {
current = current[key];
}
else {
return undefined;
}
}
return current;
}
/**
* Processes mixins within loop content with enhanced metadata
*
* @param content - The content to process
* @param metadata - Enhanced metadata with loop context
* @param context - Loop context
* @param enableFieldTracking - Whether to enable field tracking HTML spans
* @returns Processed content with mixins resolved
*/
function processItemMixins(content, metadata, context, enableFieldTracking = true) {
// Template loops now run before AST processor, so no need to skip based on spans
if (process.env.NODE_ENV === 'development') {
console.log(`processItemMixins called with content: "${content}"`);
}
// Use a simple regex to find and replace mixins
const mixinPattern = /\{\{([^}]+)\}\}/g;
return content.replace(mixinPattern, (match, variable) => {
const trimmedVar = variable.trim();
// Debug: log each mixin being processed
if (process.env.NODE_ENV === 'development') {
console.log(`Processing mixin: "${trimmedVar}" with metadata keys:`, Object.keys(metadata));
}
// Handle conditional expressions (ternary operator)
if (trimmedVar.includes('?') && trimmedVar.includes(':')) {
const result = processConditionalExpression(trimmedVar, metadata, enableFieldTracking);
if (process.env.NODE_ENV === 'development') {
console.log(`Conditional result for "${trimmedVar}":`, result);
}
if (enableFieldTracking) {
return `<span class="highlight">${result}</span>`;
}
return result;
}
// Check if it's a helper function call
if (trimmedVar.includes('(') && trimmedVar.includes(')')) {
const result = evaluateHelperExpression(trimmedVar, metadata);
if (result !== undefined) {
// Track field with helper
fieldTracker.trackField(trimmedVar, {
value: result,
hasLogic: true,
mixinUsed: 'helper',
});
if (enableFieldTracking) {
const escapedField = escapeHtmlAttribute(trimmedVar);
const stringResult = String(result);
return ('<span class="highlight">' +
`<span class="imported-value" data-field="${escapedField}">${stringResult}</span>` +
'</span>');
}
return String(result);
}
// If helper evaluation failed, fall through to missing value handling
}
// Handle simple variable substitution
const value = resolveVariablePath(trimmedVar, metadata, context);
if (process.env.NODE_ENV === 'development') {
console.log(`Simple variable "${trimmedVar}" resolved to:`, value);
}
if (value === undefined || value === null) {
if (enableFieldTracking) {
return (`<span class="missing-value" data-field="${escapeHtmlAttribute(trimmedVar)}">` +
`[[${trimmedVar}]]</span>`);
}
return `{{${trimmedVar}}}`;
}
if (enableFieldTracking) {
return (`<span class="imported-value" data-field="${escapeHtmlAttribute(trimmedVar)}">` +
`${String(value)}</span>`);
}
return String(value);
});
}
/**
* Processes conditional expressions (ternary operator)
*
* @param expression - The conditional expression
* @param metadata - The metadata object
* @param enableFieldTracking - Whether to enable field tracking HTML spans
* @returns Resolved conditional value
*/
function processConditionalExpression(expression, metadata, enableFieldTracking = true) {
// Find the question mark and colon, but be careful about quoted strings
const questionIndex = findOperatorIndex(expression, '?');
const colonIndex = findOperatorIndex(expression, ':', questionIndex);
if (questionIndex === -1 || colonIndex === -1) {
return expression; // Invalid ternary, return as-is
}
const condition = expression.substring(0, questionIndex).trim();
const truePart = expression.substring(questionIndex + 1, colonIndex).trim();
const falsePart = expression.substring(colonIndex + 1).trim();
// Resolve condition value
const conditionValue = resolveVariablePath(condition, metadata);
if (process.env.NODE_ENV === 'development') {
console.log(`Condition "${condition}" resolved to:`, conditionValue);
console.log(
// eslint-disable-next-line max-len
`Selected part: ${conditionValue ? 'truePart' : 'falsePart'} = "${conditionValue ? truePart : falsePart}"`);
}
// Choose appropriate part based on condition
const selectedPart = conditionValue ? truePart : falsePart;
// Remove quotes if present
if ((selectedPart.startsWith('"') && selectedPart.endsWith('"')) ||
// eslint-disable-next-line quotes
(selectedPart.startsWith("'") && selectedPart.endsWith("'"))) {
const unquotedValue = selectedPart.slice(1, -1);
if (enableFieldTracking) {
return (`<span class="imported-value" data-field="${escapeHtmlAttribute(expression)}">` +
`${unquotedValue}</span>`);
}
return unquotedValue;
}
// Process as expression (might contain variables or concatenation)
const processedValue = processExpression(selectedPart, metadata);
if (enableFieldTracking) {
return (`<span class="imported-value" data-field="${escapeHtmlAttribute(expression)}">` +
`${processedValue}</span>`);
}
return processedValue;
}
/**
* Finds the index of an operator outside of quoted strings
*
* @param expression - The expression to search
* @param operator - The operator to find
* @param startIndex - Starting index for the search
* @returns The index of the operator or -1 if not found
*/
function findOperatorIndex(expression, operator, startIndex = 0) {
let inQuotes = false;
let quoteChar = '';
for (let i = startIndex; i < expression.length; i++) {
const char = expression[i];
// eslint-disable-next-line quotes
if (!inQuotes && (char === '"' || char === "'")) {
inQuotes = true;
quoteChar = char;
}
else if (inQuotes && char === quoteChar) {
inQuotes = false;
quoteChar = '';
}
else if (!inQuotes && char === operator) {
return i;
}
}
return -1;
}
/**
* Processes expressions that might contain variables or concatenation
*
* @param expression - The expression to process
* @param metadata - The metadata object
* @returns Processed expression result
*/
function processExpression(expression, metadata) {
// Handle string concatenation (if expression has quotes and +)
// eslint-disable-next-line quotes
if (expression.includes('+') && (expression.includes('"') || expression.includes("'"))) {
return evaluateStringConcatenation(expression, metadata);
}
// Handle mathematical operations (*, /, +, -)
if (expression.includes('*') ||
expression.includes('/') ||
expression.includes('+') ||
expression.includes('-')) {
return evaluateMathematicalExpression(expression, metadata);
}
// Handle single variable
const value = resolveVariablePath(expression, metadata);
return value !== undefined ? String(value) : expression;
}
/**
* Evaluates string concatenation expressions with variables
* @param expression - The concatenation expression (e.g., '"$" + price')
* @param metadata - The metadata object
* @returns The concatenated result as string
*/
function evaluateStringConcatenation(expression, metadata) {
try {
// Replace variables in the expression with their values
let processedExpression = expression.trim();
// Find all variable names (alphanumeric + underscores + dots, but not inside quotes)
const variablePattern = /\b[a-zA-Z_][a-zA-Z0-9_.]*\b/g;
const variables = new Set();
let match;
// Only capture variables that are not inside quoted strings
let inQuotes = false;
let quoteChar = '';
for (let i = 0; i < expression.length; i++) {
const char = expression[i];
// eslint-disable-next-line quotes
if (!inQuotes && (char === '"' || char === "'")) {
inQuotes = true;
quoteChar = char;
}
else if (inQuotes && char === quoteChar) {
inQuotes = false;
quoteChar = '';
}
}
// Reset and find variables outside quotes
while ((match = variablePattern.exec(expression)) !== null) {
// Check if this match is inside quotes
const beforeMatch = expression.substring(0, match.index);
const openQuotes = (beforeMatch.match(/"/g) || []).length;
const openSingleQuotes = (beforeMatch.match(/'/g) || []).length;
// If even number of quotes before this match, we're outside quotes
if (openQuotes % 2 === 0 && openSingleQuotes % 2 === 0) {
variables.add(match[0]);
}
}
// Replace each variable with its value
for (const variable of variables) {
const value = resolveVariablePath(variable, metadata);
if (value !== undefined) {
const regex = new RegExp(`\\b${variable.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
if (typeof value === 'string') {
processedExpression = processedExpression.replace(regex, `"${value}"`);
}
else {
processedExpression = processedExpression.replace(regex, String(value));
}
}
else {
// If any variable is undefined, return original expression
return expression;
}
}
// Safely evaluate the string concatenation expression
// Use Function constructor for safe evaluation
const result = new Function(`"use strict"; return (${processedExpression})`)();
return String(result);
}
catch (error) {
// If evaluation fails, return original expression
return expression;
}
}
/**
* Evaluates mathematical expressions with variables
* @param expression - The mathematical expression (e.g., "hours * rate")
* @param metadata - The metadata object
* @returns The calculated result as string
*/
function evaluateMathematicalExpression(expression, metadata) {
try {
// Replace variables in the expression with their values
let processedExpression = expression;
// Find all variable names (alphanumeric + underscores + dots)
const variablePattern = /[a-zA-Z_][a-zA-Z0-9_.]*(?![a-zA-Z0-9_.])/g;
const variables = new Set();
let match;
while ((match = variablePattern.exec(expression)) !== null) {
variables.add(match[0]);
}
// Replace each variable with its value
for (const variable of variables) {
const value = resolveVariablePath(variable, metadata);
if (value !== undefined && !isNaN(Number(value))) {
const regex = new RegExp(`\\b${variable.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
processedExpression = processedExpression.replace(regex, String(value));
}
else {
// If any variable is undefined or not a number, return original expression
return expression;
}
}
// Evaluate the mathematical expression safely
// Only allow numbers, operators, parentheses, and whitespace
if (!/^[\d\s+\-*/().]+$/.test(processedExpression)) {
return expression;
}
// Use Function constructor for safe evaluation (no access to global scope)
const result = new Function(`"use strict"; return (${processedExpression})`)();
return typeof result === 'number' && !isNaN(result) ? String(result) : expression;
}
catch (error) {
// If evaluation fails, return original expression
return expression;
}
}
/**
* Resolves a variable path with dot notation support
*
* @param path - The variable path (e.g., 'item.name')
* @param metadata - The metadata object
* @param context - Loop context
* @returns Resolved variable value
*/
function resolveVariablePath(path, metadata, context) {
// Handle special case for current item in template loops
if (path === '.') {
return metadata['.'];
}
// Split path by dots
const parts = path.split('.');
let current = metadata;
for (const part of parts) {
if (current &&
typeof current === 'object' &&
Object.prototype.hasOwnProperty.call(current, part)) {
current = current[part];
}
else {
return undefined;
}
}
return current;
}
/**
* Converts markdown list items to HTML li elements
*
* @param content - The content to convert
* @returns Content with markdown list items converted to HTML li elements
*/
function convertMarkdownListToHtml(content) {
// Check if content starts with markdown list item syntax
const trimmedContent = content.trim();
if (trimmedContent.startsWith('- ')) {
// Convert "- content" to "<li>content</li>"
const listItemContent = trimmedContent.substring(2).trim();
return `<li>${listItemContent}</li>`;
}
return content;
}
/**
* Evaluates a conditional expression
* @param condition - The condition to evaluate (e.g., "includeSupport", "client.active")
* @param metadata - The metadata object
* @returns Boolean result of the condition
*/
function evaluateCondition(condition, metadata) {
const value = resolveVariablePath(condition, metadata);
// Consider truthy values: non-empty strings, non-zero numbers, true, non-empty arrays, non-empty objects
if (value === null || value === undefined)
return false;
if (typeof value === 'boolean')
return value;
if (typeof value === 'number')
return value !== 0;
if (typeof value === 'string')
return value.length > 0;
if (Array.isArray(value))
return value.length > 0;
if (typeof value === 'object')
return Object.keys(value).length > 0;
return Boolean(value);
}
/**
* Processes template content with field substitution
* @param content - The content to process
* @param metadata - The metadata object
* @param context - Current loop context
* @param enableFieldTracking - Whether to enable field tracking
* @returns Processed content with fields substituted
*/
function processTemplateContent(content, metadata, context, enableFieldTracking = true) {
// Process any nested template loops first
let processedContent = processTemplateLoops(content, metadata, context, enableFieldTracking);
// Process field substitutions {{variable}}
processedContent = processedContent.replace(/\{\{([^#/][^}]*)\}\}/g, (match, variable) => {
const trimmedVar = variable.trim();
const value = resolveVariablePath(trimmedVar, metadata);
if (value !== undefined) {
if (enableFieldTracking) {
const isEmptyValue = value === null || value === '' || (typeof value === 'string' && value.trim() === '');
const cssClass = isEmptyValue ? 'missing-value' : 'imported-value';
fieldTracker.trackField(trimmedVar, {
value: value,
originalValue: match,
hasLogic: false,
mixinUsed: 'conditional',
});
return `<span class="${cssClass}" data-field="${escapeHtmlAttribute(trimmedVar)}">${String(value)}</span>`;
}
return String(value);
}
return match; // Keep original if not found
});
return processedContent;
}
//# sourceMappingURL=template-loops.js.map