@webmate-studio/builder 0.2.112 → 0.2.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,658 +1,128 @@
1
1
  /**
2
- * Template Processor (Universal Version)
2
+ * Template Processor Unified entry point for template processing.
3
3
  *
4
- * Handles all Svelte-style template syntax processing:
5
- * - {#if} conditionals with {:else if} and {:else}
6
- * - {#each} loops with index, keys, and {:else}
7
- * - Expression evaluation (delegates to ExpressionEvaluator)
4
+ * Single API: process(template, data) → HTML
8
5
  *
9
- * This is the canonical version used by:
10
- * - Browser preview (via script tag)
11
- * - Build service (Node.js)
6
+ * Used identically by:
7
+ * - Preview (browser, via window global)
8
+ * - Build service (Node.js, via npm import)
12
9
  * - CMS rendering engine
13
10
  *
11
+ * Internally:
12
+ * 1. Parses template into AST (cached)
13
+ * 2. Evaluates AST with data context → HTML
14
+ *
14
15
  * @module template-processor
15
16
  */
16
17
 
18
+ import { TemplateParser } from './template-parser.js';
19
+ import { TemplateEvaluator } from './template-evaluator.js';
20
+ import expressionEvaluator from './expression-evaluator.js';
21
+
17
22
  class TemplateProcessor {
18
23
  constructor() {
19
- // Will be set by init()
20
- this.evaluateExpression = null;
21
- this.evaluateExpressionValue = null;
22
- this.propSchema = null;
23
- }
24
-
25
- /**
26
- * Initialize with expression evaluator functions
27
- * @param {Function} evaluateExprFn - Function to evaluate expressions (returns boolean)
28
- * @param {Function} evaluateExprValueFn - Function to evaluate expressions (returns value)
29
- * @param {Object} propSchema - Optional: Prop schema for markdown detection
30
- */
31
- init(evaluateExprFn, evaluateExprValueFn, propSchema = null) {
32
- this.evaluateExpression = evaluateExprFn;
33
- this.evaluateExpressionValue = evaluateExprValueFn;
34
- this.propSchema = propSchema;
24
+ this.parser = new TemplateParser();
25
+ this.evaluator = new TemplateEvaluator(expressionEvaluator);
26
+ this.templateCache = new Map();
27
+ this.maxCacheSize = 200;
35
28
  }
36
29
 
37
30
  /**
38
- * Process class:name={condition} conditionals
31
+ * Process a template with data and return HTML.
39
32
  *
40
- * Finds all class: directives in HTML tags and evaluates their conditions.
41
- * If condition is truthy, adds the class to the element's class attribute.
33
+ * @param {string} template - HTML template with Svelte-style syntax
34
+ * @param {Object} data - Data context (props)
35
+ * @param {Object} [options] - Optional configuration
36
+ * @param {boolean} [options.cache=true] - Whether to cache the parsed AST
37
+ * @returns {string} Processed HTML
42
38
  *
43
- * @param {string} html - HTML template with class: directives
44
- * @param {Object} contextData - Data context for evaluation
45
- * @returns {string} Processed HTML with class: directives replaced
39
+ * @example
40
+ * const html = processor.process(
41
+ * '<h1>{title}</h1>{#if show}<p>{text}</p>{/if}',
42
+ * { title: 'Hello', show: true, text: 'World' }
43
+ * );
44
+ * // → '<h1>Hello</h1><p>World</p>'
46
45
  */
47
- processClassConditionals(html, contextData) {
48
- const classCondPattern = /class:((?:[^\s=[]|\[[^\]]*\])+)=\{([^}]+)\}/g;
49
-
50
- // Debug: ALWAYS log to verify this function is called
51
- console.log('[processClassConditionals] ✨ v0.2.23 ALWAYS CALLED! HTML length:', html.length, 'contains class:?', html.includes('class:'));
52
-
53
- // Debug: Check if HTML contains class: directives
54
- if (html.includes('class:')) {
55
- console.log('[processClassConditionals] Input HTML snippet:', html.substring(0, 300));
56
- }
57
-
58
- return html.replace(/<([a-z][a-z0-9-]*)([^>]*)>/gi, (match, tagName, attrs) => {
59
- const conditionalClasses = [];
60
- let processedAttrs = attrs;
61
-
62
- let classMatch;
63
- classCondPattern.lastIndex = 0;
64
- while ((classMatch = classCondPattern.exec(attrs)) !== null) {
65
- let className = classMatch[1];
66
- const condition = classMatch[2];
67
-
68
- // Normalize calc() expressions
69
- className = className.replace(/calc\([^)]*\)/g, (m) => m.replace(/\s+/g, ''));
70
-
71
- // Evaluate condition
72
- let conditionResult = false;
73
- try {
74
- conditionResult = this.evaluateExpression(condition.trim(), contextData);
75
- console.log(`[Class:] "${className}" condition="${condition}" result=${conditionResult}`);
76
- } catch (err) {
77
- console.warn(`[Preview] Failed to evaluate class condition "${condition}":`, err);
78
- }
79
-
80
- if (conditionResult) {
81
- conditionalClasses.push(className);
82
- }
83
-
84
- // Remove the class:name={} from attributes
85
- processedAttrs = processedAttrs.replace(classMatch[0], '');
86
- }
87
-
88
- // Merge conditional classes into class attribute
89
- if (conditionalClasses.length > 0) {
90
- const classAttrMatch = processedAttrs.match(/class="([^"]*)"/);
91
- if (classAttrMatch) {
92
- const existingClasses = classAttrMatch[1];
93
- const mergedClasses = existingClasses + ' ' + conditionalClasses.join(' ');
94
- processedAttrs = processedAttrs.replace(/class="[^"]*"/, `class="${mergedClasses.trim()}"`);
95
- } else {
96
- processedAttrs += ` class="${conditionalClasses.join(' ')}"`;
97
- }
46
+ process(template, data = {}, options = {}) {
47
+ const useCache = options.cache !== false;
48
+
49
+ // 1. Parse template into AST (or retrieve from cache)
50
+ let ast;
51
+ if (useCache) {
52
+ ast = this.templateCache.get(template);
53
+ if (!ast) {
54
+ ast = this.parser.parse(template);
55
+ this.cacheTemplate(template, ast);
98
56
  }
57
+ } else {
58
+ ast = this.parser.parse(template);
59
+ }
99
60
 
100
- return `<${tagName}${processedAttrs}>`;
101
- });
61
+ // 2. Evaluate AST with data → HTML
62
+ // Create a shallow copy of data so @const doesn't pollute the original
63
+ const ctx = { ...data };
64
+ return this.evaluator.evaluate(ast, ctx);
102
65
  }
103
66
 
104
67
  /**
105
- * Find the matching {/each} for a {#each} opening tag
106
- * Handles nested {#each} blocks correctly
68
+ * Backwards-compatible methods for gradual migration.
69
+ * These all delegate to process() internally.
107
70
  *
108
- * @param {string} html - HTML string to search
109
- * @param {number} startPos - Position after {#each ...}
110
- * @returns {number} Position of matching {/each} or -1 if not found
71
+ * @deprecated Use process() instead
111
72
  */
112
- findMatchingEachEnd(html, startPos) {
113
- let depth = 1;
114
- let pos = startPos;
115
-
116
- while (depth > 0 && pos < html.length) {
117
- const nextOpen = html.indexOf('{#each', pos);
118
- const nextClose = html.indexOf('{/each}', pos);
119
-
120
- if (nextClose === -1) return -1; // No closing tag found
121
73
 
122
- if (nextOpen !== -1 && nextOpen < nextClose) {
123
- // Found opening tag first
124
- depth++;
125
- pos = nextOpen + 6; // length of '{#each'
126
- } else {
127
- // Found closing tag first
128
- depth--;
129
- if (depth === 0) {
130
- return nextClose;
131
- }
132
- pos = nextClose + 7; // length of '{/each}'
133
- }
134
- }
135
-
136
- return -1;
74
+ /**
75
+ * @deprecated Use process() instead
76
+ */
77
+ init(evaluateExprFn, evaluateExprValueFn, propSchema = null) {
78
+ // No-op — expression evaluator is built in now
137
79
  }
138
80
 
139
81
  /**
140
- * Process {#each} loops recursively
141
- *
142
- * Syntax supported:
143
- * - {#each items as item}
144
- * - {#each items as item, index}
145
- * - {#each items as item (item.id)}
146
- * - {#each items as item, index (item.id)}
147
- * - {#each items as item}{:else}Empty state{/each}
148
- * - {#each Array(n) as _} or {#each Array(n) as _, index} (creates array of n elements)
149
- *
150
- * @param {string} html - HTML template with {#each} blocks
151
- * @param {Object} contextData - Data context for evaluation
152
- * @param {number} depth - Nesting depth (for logging)
153
- * @param {string|null} parentLoopVar - Parent loop variable name
154
- * @returns {string} Processed HTML
82
+ * @deprecated Use process() instead
155
83
  */
156
- processEachLoops(html, contextData, depth = 0, parentLoopVar = null) {
157
- const indent = ' '.repeat(depth);
158
- console.log(`${indent}[processEachLoops] depth=${depth}, parentLoopVar=${parentLoopVar}, contextData keys:`, Object.keys(contextData));
159
-
160
- if (depth === 0) {
161
- console.log(`${indent}[DEBUG] 🎯 processEachLoops INPUT HTML (first 600 chars):`, html.substring(0, 600));
162
- }
163
-
164
- // Find all {#each} opening tags with optional index parameter and key
165
- // Matches: {#each items as item} or {#each items as item, index} or {#each items as item (item.id)}
166
- // Also matches: {#each Array(n) as _} or {#each Array(expr) as _, index} for generating arrays
167
- const openPattern = /\{#each\s+((?:[a-zA-Z_][a-zA-Z0-9_.]*|Array\([^)]+\)))\s+as\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:,\s*([a-zA-Z_][a-zA-Z0-9_]*))?\s*(?:\(([^)]+)\))?\}/g;
168
-
169
- let result = html;
170
- let iteration = 0;
171
- const maxIterations = 20;
172
-
173
- // Process loops from innermost to outermost by repeatedly finding and replacing
174
- while (iteration < maxIterations) {
175
- iteration++;
176
- let foundMatch = false;
177
-
178
- // Reset regex
179
- openPattern.lastIndex = 0;
180
- let match;
181
-
182
- // Process ONLY the first match to avoid index issues
183
- if ((match = openPattern.exec(html)) !== null) {
184
- const arrayPath = match[1];
185
- const loopVar = match[2];
186
- const indexVar = match[3]; // Optional index variable
187
- const keyExpr = match[4]; // Optional key expression
188
- const openStart = match.index;
189
- const openEnd = match.index + match[0].length;
190
-
191
- if (indexVar) {
192
- console.log(`${indent}[processEachLoops] Found index variable: ${indexVar}`);
193
- }
194
- if (keyExpr) {
195
- console.log(`${indent}[processEachLoops] Found key expression: ${keyExpr} (used for tracking, no effect in preview mode)`);
196
- }
197
-
198
- // Find matching closing tag
199
- const closeStart = this.findMatchingEachEnd(html, openEnd);
200
- if (closeStart === -1) {
201
- console.warn(`${indent}[processEachLoops] No matching {/each} for ${loopVar} in ${arrayPath}`);
202
- break;
203
- }
204
-
205
- const closeEnd = closeStart + 7; // length of '{/each}'
206
- let content = html.substring(openEnd, closeStart);
207
- let elseContent = '';
208
-
209
- console.log(`${indent}[DEBUG] Extracted content for ${loopVar} in ${arrayPath}, content snippet:`, content.substring(0, 300));
210
-
211
- // Check for {:else} within this each block (for empty state)
212
- // IMPORTANT: Only match {:else} at depth 0 (not inside nested {#if} blocks)
213
- let elsePos = -1;
214
- let ifDepth = 0; // Track {#if} nesting depth to avoid matching {:else} inside conditionals
215
- for (let i = 0; i < content.length; i++) {
216
- // Check for {#if} opening
217
- if (content.substring(i, i + 4) === '{#if' && (content[i + 4] === ' ' || content[i + 4] === '}')) {
218
- ifDepth++;
219
- i += 3; // Skip ahead
220
- continue;
221
- }
222
- // Check for {/if} closing
223
- if (ifDepth > 0 && content.substring(i, i + 5) === '{/if}') {
224
- ifDepth--;
225
- i += 4; // Skip ahead
226
- continue;
227
- }
228
- // Check for {:else} at depth 0 (belongs to the each block)
229
- if (ifDepth === 0 && content.substring(i, i + 7) === '{:else}') {
230
- elsePos = i;
231
- break;
232
- }
233
- }
234
-
235
- if (elsePos !== -1) {
236
- elseContent = content.substring(elsePos + 7); // Content after {:else}
237
- content = content.substring(0, elsePos); // Content before {:else}
238
- console.log(`${indent}[processEachLoops] Found {:else} in each block at depth 0`);
239
- }
240
-
241
- foundMatch = true;
242
- console.log(`${indent}[processEachLoops] iteration=${iteration}, FOUND: ${loopVar} in ${arrayPath}`);
243
-
244
- let arrayValue;
245
- let pathValid = true;
246
-
247
- // Check for Array(n) pattern - creates an array of n elements
248
- const arrayConstructorMatch = arrayPath.match(/^Array\(([^)]+)\)$/);
249
- if (arrayConstructorMatch) {
250
- const countExpr = arrayConstructorMatch[1].trim();
251
- console.log(`${indent}[processEachLoops] Array constructor pattern detected: Array(${countExpr})`);
252
-
253
- // Evaluate the expression to get the count
254
- let count = 0;
255
- try {
256
- // First try to parse as a simple number
257
- if (/^\d+$/.test(countExpr)) {
258
- count = parseInt(countExpr, 10);
259
- } else {
260
- // Otherwise evaluate as expression (e.g., item.rating)
261
- const rawValue = this.evaluateExpressionValue(countExpr, contextData);
262
- // Convert to number (handles string values like "3" from CMS)
263
- count = typeof rawValue === 'string' ? parseInt(rawValue, 10) : Number(rawValue);
264
- }
265
- console.log(`${indent}[processEachLoops] Array count evaluated to: ${count}`);
266
- } catch (err) {
267
- console.warn(`${indent}[processEachLoops] Failed to evaluate Array count expression "${countExpr}":`, err.message);
268
- pathValid = false;
269
- }
270
-
271
- // Create array with 'count' undefined elements (like Array(n) in JavaScript)
272
- // Use Number.isFinite to handle NaN and non-numeric values
273
- if (pathValid && Number.isFinite(count) && count > 0) {
274
- arrayValue = Array(count).fill(undefined);
275
- } else {
276
- arrayValue = [];
277
- }
278
- } else {
279
- // Parse array path: "items" or "item.subitems"
280
- let pathParts = arrayPath.split('.');
281
-
282
- // If we're in a nested context and the path starts with the parent loop variable,
283
- // strip it since the context IS that variable
284
- if (depth > 0 && parentLoopVar && pathParts.length > 1 && pathParts[0] === parentLoopVar) {
285
- console.log(`${indent}[processEachLoops] Stripping parent loop var "${parentLoopVar}" from path "${arrayPath}"`);
286
- pathParts = pathParts.slice(1);
287
- }
288
-
289
- arrayValue = contextData;
290
-
291
- // Navigate through path
292
- for (const part of pathParts) {
293
- if (arrayValue === null || arrayValue === undefined) {
294
- console.warn(`[Preview] Array path not found: ${arrayPath} (stopped at ${part})`);
295
- pathValid = false;
296
- break;
297
- }
298
- arrayValue = arrayValue[part];
299
- }
300
- }
301
-
302
- let replacement = '';
303
-
304
- // If array doesn't exist or path invalid, use else content or remove
305
- if (!pathValid || !Array.isArray(arrayValue)) {
306
- if (!pathValid || !Array.isArray(arrayValue)) {
307
- console.warn(`[Preview] Not an array: ${arrayPath}`, arrayValue);
308
- }
309
- replacement = elseContent;
310
- }
311
- // If array is empty, use else content or remove
312
- else if (arrayValue.length === 0) {
313
- console.log(`${indent}[Preview] Array ${arrayPath} is empty, using else content:`, elseContent ? 'yes' : 'no');
314
- replacement = elseContent;
315
- }
316
- // Render each item
317
- else {
318
- replacement = arrayValue.map((item, index) => {
319
- console.log(`${indent}[Preview] Processing loop item #${index}: ${loopVar} in ${arrayPath}`, item);
320
- let itemHtml = content;
321
-
322
- // Build loop context with the loop variable and index
323
- // Also merge with parent context so nested expressions can access parent data
324
- const loopContext = { ...contextData, [loopVar]: item };
325
- if (indexVar) {
326
- loopContext[indexVar] = index;
327
- }
328
- // Add the array itself to context (use the last part of the path as name)
329
- const arrayName = arrayPath.split('.').pop();
330
- if (arrayName && !arrayName.includes('(')) {
331
- loopContext[arrayName] = arrayValue;
332
- }
333
-
334
- // First, recursively process any nested loops with the merged context
335
- // Pass loopVar as parentLoopVar so nested loops can strip it from paths
336
- console.log(`${indent}[DEBUG] BEFORE nested processEachLoops - HTML snippet:`, itemHtml.substring(0, 300));
337
- itemHtml = this.processEachLoops(itemHtml, loopContext, depth + 1, loopVar);
338
- console.log(`${indent}[DEBUG] AFTER nested processEachLoops - HTML snippet:`, itemHtml.substring(0, 300));
339
-
340
- // Process class:name={loopVar.field} conditionals BEFORE replacing {loopVar.field}
341
- itemHtml = this.processClassConditionals(itemHtml, loopContext);
342
-
343
- // First: Replace attribute expressions with quoted values (e.g., alt={item.image.alt} or alt="{item.image.alt}" → alt="value")
344
- // Pattern matches both: attr={expr} and attr="{expr}"
345
- itemHtml = itemHtml.replace(new RegExp(`(\\w+)=["']?\\{\\s*${loopVar}\\.([a-zA-Z0-9_.]+)\\s*\\}["']?`, 'g'), (m, attrName, path) => {
346
- // Navigate through nested path (e.g., "image.src")
347
- const pathParts = path.split('.');
348
- let value = item;
349
- for (const part of pathParts) {
350
- if (value === null || value === undefined) {
351
- value = '';
352
- break;
353
- }
354
- value = value[part];
355
- }
356
-
357
- // Handle link objects (from LinkField) - extract path for preview/production
358
- if (typeof value === 'object' && value !== null && value.type === 'page' && value.pageUuid) {
359
- // In preview mode: use /preview/{pageUuid}
360
- value = `/preview/${value.pageUuid}`;
361
- }
362
- // Handle image objects - extract URL
363
- else if (typeof value === 'object' && value !== null && (value.src || value.filename)) {
364
- value = value.src || value.filename || '';
365
- }
366
-
367
- // Ensure value is string
368
- if (value === undefined || value === null) {
369
- value = '';
370
- } else {
371
- value = String(value);
372
- }
373
-
374
- // Escape quotes
375
- value = value.replace(/"/g, '&quot;');
376
-
377
- // Always return quoted attribute
378
- return `${attrName}="${value}"`;
379
- });
380
-
381
- // Second: Replace {loopVar.field} and {loopVar.nested.path} in text content
382
- itemHtml = itemHtml.replace(new RegExp(`\\{\\s*${loopVar}\\.([a-zA-Z0-9_.]+)\\s*\\}`, 'g'), (m, path) => {
383
- // Navigate through nested path (e.g., "image.src")
384
- const pathParts = path.split('.');
385
- let value = item;
386
- for (const part of pathParts) {
387
- if (value === null || value === undefined) {
388
- return '';
389
- }
390
- value = value[part];
391
- }
392
-
393
- // Handle link objects (from LinkField) - extract path for preview
394
- if (typeof value === 'object' && value !== null && value.type === 'page' && value.pageUuid) {
395
- return `/preview/${value.pageUuid}`;
396
- }
397
-
398
- // Handle image objects - extract URL
399
- if (typeof value === 'object' && value !== null && (value.src || value.filename)) {
400
- return value.src || value.filename || '';
401
- }
402
-
403
- return value !== undefined ? value : '';
404
- });
405
-
406
- // Replace {@html loopVar.field} (unescaped) - keep this for compatibility
407
- itemHtml = itemHtml.replace(new RegExp(`{@html\\s+${loopVar}\\.([a-zA-Z0-9_.]+)}`, 'g'), (m, path) => {
408
- // Navigate through nested path (e.g., "image.src")
409
- const pathParts = path.split('.');
410
- let value = item;
411
- for (const part of pathParts) {
412
- if (value === null || value === undefined) {
413
- return '';
414
- }
415
- value = value[part];
416
- }
417
- return value !== undefined ? value : '';
418
- });
419
-
420
- // Replace {indexVar} with the current loop index (if indexVar is defined) - single braces
421
- if (indexVar) {
422
- itemHtml = itemHtml.replace(new RegExp(`{\\s*${indexVar}\\s*}`, 'g'), () => {
423
- return String(index);
424
- });
425
- }
426
-
427
- // Replace {loopVar} with the item value (for primitive arrays like ['A', 'B']) - single braces
428
- itemHtml = itemHtml.replace(new RegExp(`{\\s*${loopVar}\\s*}`, 'g'), () => {
429
- // If item is object, convert to string representation
430
- if (typeof item === 'object' && item !== null) {
431
- return JSON.stringify(item);
432
- }
433
- return String(item !== undefined ? item : '');
434
- });
435
-
436
- // Process {#if} conditionals within loop (supports {:else if})
437
- // Use loopContext which already has parent context + loop variable + index
438
- itemHtml = this.processIfConditionals(itemHtml, loopContext, depth + 2);
439
-
440
- // Process complex expressions within loop (e.g., {item.image.src ? "Ja" : "Nein"})
441
- // This handles expressions that couldn't be processed by the simple regex replacements above
442
- itemHtml = itemHtml.replace(/\{(?![#/@:])[^}]+\}/g, (match) => {
443
- const expression = match.slice(1, -1).trim();
444
-
445
- // Skip JSON-like patterns
446
- if (/^["']/.test(expression) && expression.includes(':')) {
447
- return match;
448
- }
449
-
450
- // Skip simple variable references already handled above
451
- if (/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z0-9_]+)*$/.test(expression)) {
452
- return match; // Already processed
453
- }
454
-
455
- // Evaluate complex expressions with loop context
456
- try {
457
- const result = this.evaluateExpressionValue(expression, loopContext);
458
- if (result === undefined || result === null) {
459
- return '';
460
- }
461
- // Handle link objects (from LinkField) - extract path for preview
462
- if (typeof result === 'object' && result !== null && result.type === 'page' && result.pageUuid) {
463
- return `/preview/${result.pageUuid}`;
464
- }
465
- // Handle image objects
466
- if (typeof result === 'object' && result !== null && (result.src || result.filename)) {
467
- return result.src || result.filename || '';
468
- }
469
- return String(result);
470
- } catch (err) {
471
- console.warn(`[Loop] Failed to evaluate expression "${expression}":`, err.message);
472
- return match; // Keep as-is on error
473
- }
474
- });
475
-
476
- return itemHtml;
477
- }).join('');
478
- }
479
-
480
- // Replace in html
481
- html = html.substring(0, openStart) + replacement + html.substring(closeEnd);
482
- }
483
-
484
- if (!foundMatch) break;
485
- }
486
-
487
- if (iteration >= maxIterations) {
488
- console.warn(`${indent}[processEachLoops] Max iterations reached - possible infinite loop`);
489
- }
490
-
491
- return html;
84
+ processEachLoops(html, contextData) {
85
+ return this.process(html, contextData);
492
86
  }
493
87
 
494
88
  /**
495
- * Process {#if} conditionals recursively
496
- *
497
- * Syntax supported:
498
- * - {#if condition}...{/if}
499
- * - {#if condition}...{:else}...{/if}
500
- * - {#if condition}...{:else if condition2}...{:else}...{/if}
501
- *
502
- * @param {string} html - HTML template with {#if} blocks
503
- * @param {Object} contextData - Data context for evaluation
504
- * @param {number} depth - Nesting depth (for logging)
505
- * @returns {string} Processed HTML
89
+ * @deprecated Use process() instead
506
90
  */
507
- processIfConditionals(html, contextData, depth = 0) {
508
- const indent = ' '.repeat(depth);
509
- if (depth === 0) {
510
- console.log('[processIfConditionals] ✅ NEW VERSION v3.0 - {:else if} support');
511
- }
512
- let result = html;
513
- let iteration = 0;
514
- const maxIterations = 50;
515
-
516
- // Process from innermost to outermost by repeatedly finding and replacing
517
- while (iteration < maxIterations) {
518
- iteration++;
519
- let foundMatch = false;
520
-
521
- // Find {#if} opening tag
522
- const openPattern = /\{#if\s+([^}]+)\}/g;
523
- let match;
524
-
525
- // Find the first {#if}
526
- if ((match = openPattern.exec(result)) !== null) {
527
- const condition = match[1];
528
- const openStart = match.index;
529
- const openEnd = match.index + match[0].length;
530
-
531
- // Find matching closing tag and all {:else if} / {:else} branches
532
- let depth = 1;
533
- let pos = openEnd;
534
- let branches = []; // Array of {type: 'if'|'else if'|'else', condition: string, start: number, end: number}
535
- let closeStart = -1;
536
-
537
- console.log(`${indent}[DEBUG] Looking for matching {/if} for condition: ${condition}, starting at pos ${openEnd}, html length: ${result.length}`);
538
-
539
- // Add initial {#if} branch
540
- branches.push({ type: 'if', condition: condition, start: openEnd, end: -1 });
541
-
542
- while (pos < result.length && depth > 0) {
543
- // Check for nested {#if} - must have space or } after to avoid false matches
544
- if (result.substring(pos, pos + 4) === '{#if' && (result[pos + 4] === ' ' || result[pos + 4] === '}')) {
545
- depth++;
546
- console.log(`${indent}[DEBUG] Found nested {#if} at pos ${pos}, depth now: ${depth}`);
547
- console.log(`${indent}[DEBUG] Context:`, JSON.stringify(result.substring(pos, pos + 20)));
548
- pos += 4;
549
- continue;
550
- }
551
-
552
- // Check for {:else if condition} at current depth
553
- if (depth === 1 && result.substring(pos, pos + 9) === '{:else if') {
554
- // Find the closing } to get the full condition
555
- const condEnd = result.indexOf('}', pos + 9);
556
- if (condEnd !== -1) {
557
- const elseIfCondition = result.substring(pos + 10, condEnd).trim();
558
- // Close previous branch
559
- branches[branches.length - 1].end = pos;
560
- // Add new else if branch
561
- branches.push({ type: 'else if', condition: elseIfCondition, start: condEnd + 1, end: -1 });
562
- console.log(`${indent}[DEBUG] Found {:else if ${elseIfCondition}} at pos ${pos}`);
563
- pos = condEnd + 1;
564
- continue;
565
- }
566
- }
567
-
568
- // Check for {:else} at current depth
569
- if (depth === 1 && result.substring(pos, pos + 7) === '{:else}') {
570
- // Close previous branch
571
- branches[branches.length - 1].end = pos;
572
- // Add else branch
573
- branches.push({ type: 'else', condition: null, start: pos + 7, end: -1 });
574
- console.log(`${indent}[DEBUG] Found {:else} at pos ${pos}`);
575
- pos += 7;
576
- continue;
577
- }
578
-
579
- // Check for {/if} - CORRECTED: {/if} is 5 chars, not 6!
580
- const substr = result.substring(pos, pos + 5);
581
- if (substr === '{/if}') {
582
- depth--;
583
- console.log(`${indent}[DEBUG] Found {/if} at pos ${pos}, depth now: ${depth}`);
584
- if (depth === 0) {
585
- closeStart = pos;
586
- // Close last branch
587
- branches[branches.length - 1].end = pos;
588
- break;
589
- }
590
- pos += 5;
591
- continue;
592
- }
593
-
594
- // Debug: Log when we see something that looks like {/
595
- if (result[pos] === '{' && result[pos + 1] === '/') {
596
- console.log(`${indent}[DEBUG] At pos ${pos}, found '{/' but full check failed. Next 10 chars:`, JSON.stringify(result.substring(pos, pos + 10)));
597
- }
598
- pos++;
599
- }
600
-
601
- console.log(`${indent}[DEBUG] Scan complete. closeStart: ${closeStart}, final depth: ${depth}, branches:`, branches.length);
602
-
603
- if (closeStart === -1) {
604
- console.warn(`${indent}[processIfConditionals] No matching {/if} for condition: ${condition}`);
605
- console.log(`${indent}[DEBUG] HTML snippet around openEnd:`, result.substring(openEnd, Math.min(openEnd + 200, result.length)));
606
- break;
607
- }
608
-
609
- foundMatch = true;
610
- const closeEnd = closeStart + 5; // length of '{/if}' - CORRECTED: 5 chars, not 6!
611
-
612
- // Process each branch recursively and evaluate conditions
613
- let replacement = '';
614
- for (const branch of branches) {
615
- let branchContent = result.substring(branch.start, branch.end);
616
-
617
- // Recursively process nested ifs
618
- branchContent = this.processIfConditionals(branchContent, contextData, depth + 1);
619
-
620
- // Evaluate condition
621
- if (branch.type === 'if' || branch.type === 'else if') {
622
- const shouldShow = this.evaluateExpression(branch.condition.trim(), contextData);
623
- console.log(`${indent}[If] ${branch.type}="${branch.condition}" result=${shouldShow}`);
91
+ processIfConditionals(html, contextData) {
92
+ return this.process(html, contextData);
93
+ }
624
94
 
625
- if (shouldShow) {
626
- replacement = branchContent;
627
- break; // First matching condition wins
628
- }
629
- } else if (branch.type === 'else') {
630
- // Else branch - use if no previous condition matched
631
- console.log(`${indent}[If] Using else branch`);
632
- replacement = branchContent;
633
- break;
634
- }
635
- }
95
+ /**
96
+ * @deprecated Use process() instead
97
+ */
98
+ processClassConditionals(html, contextData) {
99
+ return this.process(html, contextData);
100
+ }
636
101
 
637
- // Replace in result
638
- result = result.substring(0, openStart) + replacement + result.substring(closeEnd);
639
- }
102
+ /**
103
+ * Clear the template AST cache
104
+ */
105
+ clearCache() {
106
+ this.templateCache.clear();
107
+ expressionEvaluator.clearCache();
108
+ }
640
109
 
641
- if (!foundMatch) break;
642
- }
110
+ // ========================================
111
+ // Internal
112
+ // ========================================
643
113
 
644
- if (iteration >= maxIterations) {
645
- console.warn(`${indent}[processIfConditionals] Max iterations reached - possible infinite loop`);
114
+ cacheTemplate(key, ast) {
115
+ if (this.templateCache.size >= this.maxCacheSize) {
116
+ // Remove oldest entry
117
+ const firstKey = this.templateCache.keys().next().value;
118
+ this.templateCache.delete(firstKey);
646
119
  }
647
-
648
- return result;
120
+ this.templateCache.set(key, ast);
649
121
  }
650
122
  }
651
123
 
652
- // Universal export - works in both Browser and Node.js
653
- // For Browser: Use as ES6 module or create global via script
654
- // For Node.js: Use as ES6 module import
655
- export default TemplateProcessor;
124
+ // Singleton
125
+ const templateProcessor = new TemplateProcessor();
656
126
 
657
- // Also export singleton instance for convenience
658
- export const templateProcessor = new TemplateProcessor();
127
+ export default TemplateProcessor;
128
+ export { templateProcessor, TemplateProcessor };