@webmate-studio/builder 0.2.110 → 0.2.113

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,623 +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
73
 
120
- if (nextClose === -1) return -1; // No closing tag found
121
-
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
- *
149
- * @param {string} html - HTML template with {#each} blocks
150
- * @param {Object} contextData - Data context for evaluation
151
- * @param {number} depth - Nesting depth (for logging)
152
- * @param {string|null} parentLoopVar - Parent loop variable name
153
- * @returns {string} Processed HTML
82
+ * @deprecated Use process() instead
154
83
  */
155
- processEachLoops(html, contextData, depth = 0, parentLoopVar = null) {
156
- const indent = ' '.repeat(depth);
157
- console.log(`${indent}[processEachLoops] depth=${depth}, parentLoopVar=${parentLoopVar}, contextData keys:`, Object.keys(contextData));
158
-
159
- if (depth === 0) {
160
- console.log(`${indent}[DEBUG] 🎯 processEachLoops INPUT HTML (first 600 chars):`, html.substring(0, 600));
161
- }
162
-
163
- // Find all {#each} opening tags with optional index parameter and key
164
- // Matches: {#each items as item} or {#each items as item, index} or {#each items as item (item.id)}
165
- const openPattern = /\{#each\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s+as\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:,\s*([a-zA-Z_][a-zA-Z0-9_]*))?\s*(?:\(([^)]+)\))?\}/g;
166
-
167
- let result = html;
168
- let iteration = 0;
169
- const maxIterations = 20;
170
-
171
- // Process loops from innermost to outermost by repeatedly finding and replacing
172
- while (iteration < maxIterations) {
173
- iteration++;
174
- let foundMatch = false;
175
-
176
- // Reset regex
177
- openPattern.lastIndex = 0;
178
- let match;
179
-
180
- // Process ONLY the first match to avoid index issues
181
- if ((match = openPattern.exec(html)) !== null) {
182
- const arrayPath = match[1];
183
- const loopVar = match[2];
184
- const indexVar = match[3]; // Optional index variable
185
- const keyExpr = match[4]; // Optional key expression
186
- const openStart = match.index;
187
- const openEnd = match.index + match[0].length;
188
-
189
- if (indexVar) {
190
- console.log(`${indent}[processEachLoops] Found index variable: ${indexVar}`);
191
- }
192
- if (keyExpr) {
193
- console.log(`${indent}[processEachLoops] Found key expression: ${keyExpr} (used for tracking, no effect in preview mode)`);
194
- }
195
-
196
- // Find matching closing tag
197
- const closeStart = this.findMatchingEachEnd(html, openEnd);
198
- if (closeStart === -1) {
199
- console.warn(`${indent}[processEachLoops] No matching {/each} for ${loopVar} in ${arrayPath}`);
200
- break;
201
- }
202
-
203
- const closeEnd = closeStart + 7; // length of '{/each}'
204
- let content = html.substring(openEnd, closeStart);
205
- let elseContent = '';
206
-
207
- console.log(`${indent}[DEBUG] Extracted content for ${loopVar} in ${arrayPath}, content snippet:`, content.substring(0, 300));
208
-
209
- // Check for {:else} within this each block (for empty state)
210
- // IMPORTANT: Only match {:else} at depth 0 (not inside nested {#if} blocks)
211
- let elsePos = -1;
212
- let ifDepth = 0; // Track {#if} nesting depth to avoid matching {:else} inside conditionals
213
- for (let i = 0; i < content.length; i++) {
214
- // Check for {#if} opening
215
- if (content.substring(i, i + 4) === '{#if' && (content[i + 4] === ' ' || content[i + 4] === '}')) {
216
- ifDepth++;
217
- i += 3; // Skip ahead
218
- continue;
219
- }
220
- // Check for {/if} closing
221
- if (ifDepth > 0 && content.substring(i, i + 5) === '{/if}') {
222
- ifDepth--;
223
- i += 4; // Skip ahead
224
- continue;
225
- }
226
- // Check for {:else} at depth 0 (belongs to the each block)
227
- if (ifDepth === 0 && content.substring(i, i + 7) === '{:else}') {
228
- elsePos = i;
229
- break;
230
- }
231
- }
232
-
233
- if (elsePos !== -1) {
234
- elseContent = content.substring(elsePos + 7); // Content after {:else}
235
- content = content.substring(0, elsePos); // Content before {:else}
236
- console.log(`${indent}[processEachLoops] Found {:else} in each block at depth 0`);
237
- }
238
-
239
- foundMatch = true;
240
- console.log(`${indent}[processEachLoops] iteration=${iteration}, FOUND: ${loopVar} in ${arrayPath}`);
241
-
242
- // Parse array path: "items" or "item.subitems"
243
- let pathParts = arrayPath.split('.');
244
-
245
- // If we're in a nested context and the path starts with the parent loop variable,
246
- // strip it since the context IS that variable
247
- if (depth > 0 && parentLoopVar && pathParts.length > 1 && pathParts[0] === parentLoopVar) {
248
- console.log(`${indent}[processEachLoops] Stripping parent loop var "${parentLoopVar}" from path "${arrayPath}"`);
249
- pathParts = pathParts.slice(1);
250
- }
251
-
252
- let arrayValue = contextData;
253
-
254
- // Navigate through path
255
- let pathValid = true;
256
- for (const part of pathParts) {
257
- if (arrayValue === null || arrayValue === undefined) {
258
- console.warn(`[Preview] Array path not found: ${arrayPath} (stopped at ${part})`);
259
- pathValid = false;
260
- break;
261
- }
262
- arrayValue = arrayValue[part];
263
- }
264
-
265
- let replacement = '';
266
-
267
- // If array doesn't exist or path invalid, use else content or remove
268
- if (!pathValid || !Array.isArray(arrayValue)) {
269
- if (!pathValid || !Array.isArray(arrayValue)) {
270
- console.warn(`[Preview] Not an array: ${arrayPath}`, arrayValue);
271
- }
272
- replacement = elseContent;
273
- }
274
- // If array is empty, use else content or remove
275
- else if (arrayValue.length === 0) {
276
- console.log(`${indent}[Preview] Array ${arrayPath} is empty, using else content:`, elseContent ? 'yes' : 'no');
277
- replacement = elseContent;
278
- }
279
- // Render each item
280
- else {
281
- replacement = arrayValue.map((item, index) => {
282
- console.log(`${indent}[Preview] Processing loop item #${index}: ${loopVar} in ${arrayPath}`, item);
283
- let itemHtml = content;
284
-
285
- // First, recursively process any nested loops with this item as context
286
- // Pass loopVar as parentLoopVar so nested loops can strip it from paths
287
- console.log(`${indent}[DEBUG] BEFORE nested processEachLoops - HTML snippet:`, itemHtml.substring(0, 300));
288
- itemHtml = this.processEachLoops(itemHtml, item, depth + 1, loopVar);
289
- console.log(`${indent}[DEBUG] AFTER nested processEachLoops - HTML snippet:`, itemHtml.substring(0, 300));
290
-
291
- // Process class:name={loopVar.field} conditionals BEFORE replacing {loopVar.field}
292
- // Use loop item as context so loop variables are available
293
- const loopContext = { [loopVar]: item };
294
- if (indexVar) {
295
- loopContext[indexVar] = index;
296
- }
297
- // Add the array itself to context (use the last part of the path as name)
298
- const arrayName = arrayPath.split('.').pop();
299
- loopContext[arrayName] = arrayValue;
300
- itemHtml = this.processClassConditionals(itemHtml, loopContext);
301
-
302
- // First: Replace attribute expressions with quoted values (e.g., alt={item.image.alt} or alt="{item.image.alt}" → alt="value")
303
- // Pattern matches both: attr={expr} and attr="{expr}"
304
- itemHtml = itemHtml.replace(new RegExp(`(\\w+)=["']?\\{\\s*${loopVar}\\.([a-zA-Z0-9_.]+)\\s*\\}["']?`, 'g'), (m, attrName, path) => {
305
- // Navigate through nested path (e.g., "image.src")
306
- const pathParts = path.split('.');
307
- let value = item;
308
- for (const part of pathParts) {
309
- if (value === null || value === undefined) {
310
- value = '';
311
- break;
312
- }
313
- value = value[part];
314
- }
315
-
316
- // Handle link objects (from LinkField) - extract path for preview/production
317
- if (typeof value === 'object' && value !== null && value.type === 'page' && value.pageUuid) {
318
- // In preview mode: use /preview/{pageUuid}
319
- value = `/preview/${value.pageUuid}`;
320
- }
321
- // Handle image objects - extract URL
322
- else if (typeof value === 'object' && value !== null && (value.src || value.filename)) {
323
- value = value.src || value.filename || '';
324
- }
325
-
326
- // Ensure value is string
327
- if (value === undefined || value === null) {
328
- value = '';
329
- } else {
330
- value = String(value);
331
- }
332
-
333
- // Escape quotes
334
- value = value.replace(/"/g, '&quot;');
335
-
336
- // Always return quoted attribute
337
- return `${attrName}="${value}"`;
338
- });
339
-
340
- // Second: Replace {loopVar.field} and {loopVar.nested.path} in text content
341
- itemHtml = itemHtml.replace(new RegExp(`\\{\\s*${loopVar}\\.([a-zA-Z0-9_.]+)\\s*\\}`, 'g'), (m, path) => {
342
- // Navigate through nested path (e.g., "image.src")
343
- const pathParts = path.split('.');
344
- let value = item;
345
- for (const part of pathParts) {
346
- if (value === null || value === undefined) {
347
- return '';
348
- }
349
- value = value[part];
350
- }
351
-
352
- // Handle link objects (from LinkField) - extract path for preview
353
- if (typeof value === 'object' && value !== null && value.type === 'page' && value.pageUuid) {
354
- return `/preview/${value.pageUuid}`;
355
- }
356
-
357
- // Handle image objects - extract URL
358
- if (typeof value === 'object' && value !== null && (value.src || value.filename)) {
359
- return value.src || value.filename || '';
360
- }
361
-
362
- return value !== undefined ? value : '';
363
- });
364
-
365
- // Replace {@html loopVar.field} (unescaped) - keep this for compatibility
366
- itemHtml = itemHtml.replace(new RegExp(`{@html\\s+${loopVar}\\.([a-zA-Z0-9_.]+)}`, 'g'), (m, path) => {
367
- // Navigate through nested path (e.g., "image.src")
368
- const pathParts = path.split('.');
369
- let value = item;
370
- for (const part of pathParts) {
371
- if (value === null || value === undefined) {
372
- return '';
373
- }
374
- value = value[part];
375
- }
376
- return value !== undefined ? value : '';
377
- });
378
-
379
- // Replace {indexVar} with the current loop index (if indexVar is defined) - single braces
380
- if (indexVar) {
381
- itemHtml = itemHtml.replace(new RegExp(`{\\s*${indexVar}\\s*}`, 'g'), () => {
382
- return String(index);
383
- });
384
- }
385
-
386
- // Replace {loopVar} with the item value (for primitive arrays like ['A', 'B']) - single braces
387
- itemHtml = itemHtml.replace(new RegExp(`{\\s*${loopVar}\\s*}`, 'g'), () => {
388
- // If item is object, convert to string representation
389
- if (typeof item === 'object' && item !== null) {
390
- return JSON.stringify(item);
391
- }
392
- return String(item !== undefined ? item : '');
393
- });
394
-
395
- // Process {#if} conditionals within loop (supports {:else if})
396
- // Merge loop variable with parent context for nested conditionals
397
- const evalContext = { ...contextData, [loopVar]: item };
398
- if (indexVar) {
399
- evalContext[indexVar] = index;
400
- }
401
- // Add the array itself to context
402
- evalContext[arrayName] = arrayValue;
403
- itemHtml = this.processIfConditionals(itemHtml, evalContext, depth + 2);
404
-
405
- // Process complex expressions within loop (e.g., {item.image.src ? "Ja" : "Nein"})
406
- // This handles expressions that couldn't be processed by the simple regex replacements above
407
- itemHtml = itemHtml.replace(/\{(?![#/@:])[^}]+\}/g, (match) => {
408
- const expression = match.slice(1, -1).trim();
409
-
410
- // Skip JSON-like patterns
411
- if (/^["']/.test(expression) && expression.includes(':')) {
412
- return match;
413
- }
414
-
415
- // Skip simple variable references already handled above
416
- if (/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z0-9_]+)*$/.test(expression)) {
417
- return match; // Already processed
418
- }
419
-
420
- // Evaluate complex expressions with loop context
421
- try {
422
- const result = this.evaluateExpressionValue(expression, evalContext);
423
- if (result === undefined || result === null) {
424
- return '';
425
- }
426
- // Handle link objects (from LinkField) - extract path for preview
427
- if (typeof result === 'object' && result !== null && result.type === 'page' && result.pageUuid) {
428
- return `/preview/${result.pageUuid}`;
429
- }
430
- // Handle image objects
431
- if (typeof result === 'object' && result !== null && (result.src || result.filename)) {
432
- return result.src || result.filename || '';
433
- }
434
- return String(result);
435
- } catch (err) {
436
- console.warn(`[Loop] Failed to evaluate expression "${expression}":`, err.message);
437
- return match; // Keep as-is on error
438
- }
439
- });
440
-
441
- return itemHtml;
442
- }).join('');
443
- }
444
-
445
- // Replace in html
446
- html = html.substring(0, openStart) + replacement + html.substring(closeEnd);
447
- }
448
-
449
- if (!foundMatch) break;
450
- }
451
-
452
- if (iteration >= maxIterations) {
453
- console.warn(`${indent}[processEachLoops] Max iterations reached - possible infinite loop`);
454
- }
455
-
456
- return html;
84
+ processEachLoops(html, contextData) {
85
+ return this.process(html, contextData);
457
86
  }
458
87
 
459
88
  /**
460
- * Process {#if} conditionals recursively
461
- *
462
- * Syntax supported:
463
- * - {#if condition}...{/if}
464
- * - {#if condition}...{:else}...{/if}
465
- * - {#if condition}...{:else if condition2}...{:else}...{/if}
466
- *
467
- * @param {string} html - HTML template with {#if} blocks
468
- * @param {Object} contextData - Data context for evaluation
469
- * @param {number} depth - Nesting depth (for logging)
470
- * @returns {string} Processed HTML
89
+ * @deprecated Use process() instead
471
90
  */
472
- processIfConditionals(html, contextData, depth = 0) {
473
- const indent = ' '.repeat(depth);
474
- if (depth === 0) {
475
- console.log('[processIfConditionals] ✅ NEW VERSION v3.0 - {:else if} support');
476
- }
477
- let result = html;
478
- let iteration = 0;
479
- const maxIterations = 50;
480
-
481
- // Process from innermost to outermost by repeatedly finding and replacing
482
- while (iteration < maxIterations) {
483
- iteration++;
484
- let foundMatch = false;
485
-
486
- // Find {#if} opening tag
487
- const openPattern = /\{#if\s+([^}]+)\}/g;
488
- let match;
489
-
490
- // Find the first {#if}
491
- if ((match = openPattern.exec(result)) !== null) {
492
- const condition = match[1];
493
- const openStart = match.index;
494
- const openEnd = match.index + match[0].length;
495
-
496
- // Find matching closing tag and all {:else if} / {:else} branches
497
- let depth = 1;
498
- let pos = openEnd;
499
- let branches = []; // Array of {type: 'if'|'else if'|'else', condition: string, start: number, end: number}
500
- let closeStart = -1;
501
-
502
- console.log(`${indent}[DEBUG] Looking for matching {/if} for condition: ${condition}, starting at pos ${openEnd}, html length: ${result.length}`);
503
-
504
- // Add initial {#if} branch
505
- branches.push({ type: 'if', condition: condition, start: openEnd, end: -1 });
506
-
507
- while (pos < result.length && depth > 0) {
508
- // Check for nested {#if} - must have space or } after to avoid false matches
509
- if (result.substring(pos, pos + 4) === '{#if' && (result[pos + 4] === ' ' || result[pos + 4] === '}')) {
510
- depth++;
511
- console.log(`${indent}[DEBUG] Found nested {#if} at pos ${pos}, depth now: ${depth}`);
512
- console.log(`${indent}[DEBUG] Context:`, JSON.stringify(result.substring(pos, pos + 20)));
513
- pos += 4;
514
- continue;
515
- }
516
-
517
- // Check for {:else if condition} at current depth
518
- if (depth === 1 && result.substring(pos, pos + 9) === '{:else if') {
519
- // Find the closing } to get the full condition
520
- const condEnd = result.indexOf('}', pos + 9);
521
- if (condEnd !== -1) {
522
- const elseIfCondition = result.substring(pos + 10, condEnd).trim();
523
- // Close previous branch
524
- branches[branches.length - 1].end = pos;
525
- // Add new else if branch
526
- branches.push({ type: 'else if', condition: elseIfCondition, start: condEnd + 1, end: -1 });
527
- console.log(`${indent}[DEBUG] Found {:else if ${elseIfCondition}} at pos ${pos}`);
528
- pos = condEnd + 1;
529
- continue;
530
- }
531
- }
532
-
533
- // Check for {:else} at current depth
534
- if (depth === 1 && result.substring(pos, pos + 7) === '{:else}') {
535
- // Close previous branch
536
- branches[branches.length - 1].end = pos;
537
- // Add else branch
538
- branches.push({ type: 'else', condition: null, start: pos + 7, end: -1 });
539
- console.log(`${indent}[DEBUG] Found {:else} at pos ${pos}`);
540
- pos += 7;
541
- continue;
542
- }
543
-
544
- // Check for {/if} - CORRECTED: {/if} is 5 chars, not 6!
545
- const substr = result.substring(pos, pos + 5);
546
- if (substr === '{/if}') {
547
- depth--;
548
- console.log(`${indent}[DEBUG] Found {/if} at pos ${pos}, depth now: ${depth}`);
549
- if (depth === 0) {
550
- closeStart = pos;
551
- // Close last branch
552
- branches[branches.length - 1].end = pos;
553
- break;
554
- }
555
- pos += 5;
556
- continue;
557
- }
558
-
559
- // Debug: Log when we see something that looks like {/
560
- if (result[pos] === '{' && result[pos + 1] === '/') {
561
- console.log(`${indent}[DEBUG] At pos ${pos}, found '{/' but full check failed. Next 10 chars:`, JSON.stringify(result.substring(pos, pos + 10)));
562
- }
563
- pos++;
564
- }
565
-
566
- console.log(`${indent}[DEBUG] Scan complete. closeStart: ${closeStart}, final depth: ${depth}, branches:`, branches.length);
567
-
568
- if (closeStart === -1) {
569
- console.warn(`${indent}[processIfConditionals] No matching {/if} for condition: ${condition}`);
570
- console.log(`${indent}[DEBUG] HTML snippet around openEnd:`, result.substring(openEnd, Math.min(openEnd + 200, result.length)));
571
- break;
572
- }
573
-
574
- foundMatch = true;
575
- const closeEnd = closeStart + 5; // length of '{/if}' - CORRECTED: 5 chars, not 6!
576
-
577
- // Process each branch recursively and evaluate conditions
578
- let replacement = '';
579
- for (const branch of branches) {
580
- let branchContent = result.substring(branch.start, branch.end);
581
-
582
- // Recursively process nested ifs
583
- branchContent = this.processIfConditionals(branchContent, contextData, depth + 1);
584
-
585
- // Evaluate condition
586
- if (branch.type === 'if' || branch.type === 'else if') {
587
- const shouldShow = this.evaluateExpression(branch.condition.trim(), contextData);
588
- console.log(`${indent}[If] ${branch.type}="${branch.condition}" result=${shouldShow}`);
91
+ processIfConditionals(html, contextData) {
92
+ return this.process(html, contextData);
93
+ }
589
94
 
590
- if (shouldShow) {
591
- replacement = branchContent;
592
- break; // First matching condition wins
593
- }
594
- } else if (branch.type === 'else') {
595
- // Else branch - use if no previous condition matched
596
- console.log(`${indent}[If] Using else branch`);
597
- replacement = branchContent;
598
- break;
599
- }
600
- }
95
+ /**
96
+ * @deprecated Use process() instead
97
+ */
98
+ processClassConditionals(html, contextData) {
99
+ return this.process(html, contextData);
100
+ }
601
101
 
602
- // Replace in result
603
- result = result.substring(0, openStart) + replacement + result.substring(closeEnd);
604
- }
102
+ /**
103
+ * Clear the template AST cache
104
+ */
105
+ clearCache() {
106
+ this.templateCache.clear();
107
+ expressionEvaluator.clearCache();
108
+ }
605
109
 
606
- if (!foundMatch) break;
607
- }
110
+ // ========================================
111
+ // Internal
112
+ // ========================================
608
113
 
609
- if (iteration >= maxIterations) {
610
- 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);
611
119
  }
612
-
613
- return result;
120
+ this.templateCache.set(key, ast);
614
121
  }
615
122
  }
616
123
 
617
- // Universal export - works in both Browser and Node.js
618
- // For Browser: Use as ES6 module or create global via script
619
- // For Node.js: Use as ES6 module import
620
- export default TemplateProcessor;
124
+ // Singleton
125
+ const templateProcessor = new TemplateProcessor();
621
126
 
622
- // Also export singleton instance for convenience
623
- export const templateProcessor = new TemplateProcessor();
127
+ export default TemplateProcessor;
128
+ export { templateProcessor, TemplateProcessor };