@webmate-studio/builder 0.2.62 → 0.2.63

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmate-studio/builder",
3
- "version": "0.2.62",
3
+ "version": "0.2.63",
4
4
  "type": "module",
5
5
  "description": "Webmate Studio Component Builder",
6
6
  "keywords": [
package/src/index.js CHANGED
@@ -4,6 +4,7 @@ import { cleanComponentHTML } from './html-cleaner.js';
4
4
  import { bundleIsland, bundleComponentIslands } from './bundler.js';
5
5
  import { deduplicateCSS } from './css-deduplicator.js';
6
6
  import { markdownToHtml, processMarkdownProps } from './markdown.js';
7
+ import TemplateProcessor, { templateProcessor } from './template-processor.js';
7
8
  import { readFileSync } from 'fs';
8
9
  import { fileURLToPath } from 'url';
9
10
  import { dirname, join } from 'path';
@@ -16,4 +17,4 @@ function getMotionRuntime() {
16
17
  return readFileSync(join(__dirname, '../dist/motion-runtime.min.js'), 'utf-8');
17
18
  }
18
19
 
19
- export { build, generateComponentCSS, generateTailwindCSS, extractTailwindClasses, cleanComponentHTML, bundleIsland, bundleComponentIslands, deduplicateCSS, markdownToHtml, processMarkdownProps, getMotionRuntime };
20
+ export { build, generateComponentCSS, generateTailwindCSS, extractTailwindClasses, cleanComponentHTML, bundleIsland, bundleComponentIslands, deduplicateCSS, markdownToHtml, processMarkdownProps, getMotionRuntime, TemplateProcessor, templateProcessor };
@@ -0,0 +1,608 @@
1
+ /**
2
+ * Template Processor (Universal Version)
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)
8
+ *
9
+ * This is the canonical version used by:
10
+ * - Browser preview (via script tag)
11
+ * - Build service (Node.js)
12
+ * - CMS rendering engine
13
+ *
14
+ * @module template-processor
15
+ */
16
+
17
+ class TemplateProcessor {
18
+ 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;
35
+ }
36
+
37
+ /**
38
+ * Process class:name={condition} conditionals
39
+ *
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.
42
+ *
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
46
+ */
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
+ }
98
+ }
99
+
100
+ return `<${tagName}${processedAttrs}>`;
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Find the matching {/each} for a {#each} opening tag
106
+ * Handles nested {#each} blocks correctly
107
+ *
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
111
+ */
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
+
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;
137
+ }
138
+
139
+ /**
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
154
+ */
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} → alt="value")
303
+ itemHtml = itemHtml.replace(new RegExp(`(\\w+)=\\{\\s*${loopVar}\\.([a-zA-Z0-9_.]+)\\s*\\}`, 'g'), (m, attrName, path) => {
304
+ // Navigate through nested path (e.g., "image.src")
305
+ const pathParts = path.split('.');
306
+ let value = item;
307
+ for (const part of pathParts) {
308
+ if (value === null || value === undefined) {
309
+ value = '';
310
+ break;
311
+ }
312
+ value = value[part];
313
+ }
314
+
315
+ // Handle image objects - extract URL
316
+ if (typeof value === 'object' && value !== null && (value.src || value.filename)) {
317
+ value = value.src || value.filename || '';
318
+ }
319
+
320
+ // Ensure value is string
321
+ if (value === undefined || value === null) {
322
+ value = '';
323
+ } else {
324
+ value = String(value);
325
+ }
326
+
327
+ // Escape quotes
328
+ value = value.replace(/"/g, '&quot;');
329
+
330
+ // Always return quoted attribute
331
+ return `${attrName}="${value}"`;
332
+ });
333
+
334
+ // Second: Replace {loopVar.field} and {loopVar.nested.path} in text content
335
+ itemHtml = itemHtml.replace(new RegExp(`\\{\\s*${loopVar}\\.([a-zA-Z0-9_.]+)\\s*\\}`, 'g'), (m, path) => {
336
+ // Navigate through nested path (e.g., "image.src")
337
+ const pathParts = path.split('.');
338
+ let value = item;
339
+ for (const part of pathParts) {
340
+ if (value === null || value === undefined) {
341
+ return '';
342
+ }
343
+ value = value[part];
344
+ }
345
+
346
+ // Handle image objects - extract URL
347
+ if (typeof value === 'object' && value !== null && (value.src || value.filename)) {
348
+ return value.src || value.filename || '';
349
+ }
350
+
351
+ return value !== undefined ? value : '';
352
+ });
353
+
354
+ // Replace {@html loopVar.field} (unescaped) - keep this for compatibility
355
+ itemHtml = itemHtml.replace(new RegExp(`{@html\\s+${loopVar}\\.([a-zA-Z0-9_.]+)}`, 'g'), (m, path) => {
356
+ // Navigate through nested path (e.g., "image.src")
357
+ const pathParts = path.split('.');
358
+ let value = item;
359
+ for (const part of pathParts) {
360
+ if (value === null || value === undefined) {
361
+ return '';
362
+ }
363
+ value = value[part];
364
+ }
365
+ return value !== undefined ? value : '';
366
+ });
367
+
368
+ // Replace {indexVar} with the current loop index (if indexVar is defined) - single braces
369
+ if (indexVar) {
370
+ itemHtml = itemHtml.replace(new RegExp(`{\\s*${indexVar}\\s*}`, 'g'), () => {
371
+ return String(index);
372
+ });
373
+ }
374
+
375
+ // Replace {loopVar} with the item value (for primitive arrays like ['A', 'B']) - single braces
376
+ itemHtml = itemHtml.replace(new RegExp(`{\\s*${loopVar}\\s*}`, 'g'), () => {
377
+ // If item is object, convert to string representation
378
+ if (typeof item === 'object' && item !== null) {
379
+ return JSON.stringify(item);
380
+ }
381
+ return String(item !== undefined ? item : '');
382
+ });
383
+
384
+ // Process {#if} conditionals within loop (supports {:else if})
385
+ // Merge loop variable with parent context for nested conditionals
386
+ const evalContext = { ...contextData, [loopVar]: item };
387
+ if (indexVar) {
388
+ evalContext[indexVar] = index;
389
+ }
390
+ // Add the array itself to context
391
+ evalContext[arrayName] = arrayValue;
392
+ itemHtml = this.processIfConditionals(itemHtml, evalContext, depth + 2);
393
+
394
+ // Process complex expressions within loop (e.g., {item.image.src ? "Ja" : "Nein"})
395
+ // This handles expressions that couldn't be processed by the simple regex replacements above
396
+ itemHtml = itemHtml.replace(/\{(?![#/@:])[^}]+\}/g, (match) => {
397
+ const expression = match.slice(1, -1).trim();
398
+
399
+ // Skip JSON-like patterns
400
+ if (/^["']/.test(expression) && expression.includes(':')) {
401
+ return match;
402
+ }
403
+
404
+ // Skip simple variable references already handled above
405
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z0-9_]+)*$/.test(expression)) {
406
+ return match; // Already processed
407
+ }
408
+
409
+ // Evaluate complex expressions with loop context
410
+ try {
411
+ const result = this.evaluateExpressionValue(expression, evalContext);
412
+ if (result === undefined || result === null) {
413
+ return '';
414
+ }
415
+ // Handle image objects
416
+ if (typeof result === 'object' && result !== null && (result.src || result.filename)) {
417
+ return result.src || result.filename || '';
418
+ }
419
+ return String(result);
420
+ } catch (err) {
421
+ console.warn(`[Loop] Failed to evaluate expression "${expression}":`, err.message);
422
+ return match; // Keep as-is on error
423
+ }
424
+ });
425
+
426
+ return itemHtml;
427
+ }).join('');
428
+ }
429
+
430
+ // Replace in html
431
+ html = html.substring(0, openStart) + replacement + html.substring(closeEnd);
432
+ }
433
+
434
+ if (!foundMatch) break;
435
+ }
436
+
437
+ if (iteration >= maxIterations) {
438
+ console.warn(`${indent}[processEachLoops] Max iterations reached - possible infinite loop`);
439
+ }
440
+
441
+ return html;
442
+ }
443
+
444
+ /**
445
+ * Process {#if} conditionals recursively
446
+ *
447
+ * Syntax supported:
448
+ * - {#if condition}...{/if}
449
+ * - {#if condition}...{:else}...{/if}
450
+ * - {#if condition}...{:else if condition2}...{:else}...{/if}
451
+ *
452
+ * @param {string} html - HTML template with {#if} blocks
453
+ * @param {Object} contextData - Data context for evaluation
454
+ * @param {number} depth - Nesting depth (for logging)
455
+ * @returns {string} Processed HTML
456
+ */
457
+ processIfConditionals(html, contextData, depth = 0) {
458
+ const indent = ' '.repeat(depth);
459
+ if (depth === 0) {
460
+ console.log('[processIfConditionals] ✅ NEW VERSION v3.0 - {:else if} support');
461
+ }
462
+ let result = html;
463
+ let iteration = 0;
464
+ const maxIterations = 50;
465
+
466
+ // Process from innermost to outermost by repeatedly finding and replacing
467
+ while (iteration < maxIterations) {
468
+ iteration++;
469
+ let foundMatch = false;
470
+
471
+ // Find {#if} opening tag
472
+ const openPattern = /\{#if\s+([^}]+)\}/g;
473
+ let match;
474
+
475
+ // Find the first {#if}
476
+ if ((match = openPattern.exec(result)) !== null) {
477
+ const condition = match[1];
478
+ const openStart = match.index;
479
+ const openEnd = match.index + match[0].length;
480
+
481
+ // Find matching closing tag and all {:else if} / {:else} branches
482
+ let depth = 1;
483
+ let pos = openEnd;
484
+ let branches = []; // Array of {type: 'if'|'else if'|'else', condition: string, start: number, end: number}
485
+ let closeStart = -1;
486
+
487
+ console.log(`${indent}[DEBUG] Looking for matching {/if} for condition: ${condition}, starting at pos ${openEnd}, html length: ${result.length}`);
488
+
489
+ // Add initial {#if} branch
490
+ branches.push({ type: 'if', condition: condition, start: openEnd, end: -1 });
491
+
492
+ while (pos < result.length && depth > 0) {
493
+ // Check for nested {#if} - must have space or } after to avoid false matches
494
+ if (result.substring(pos, pos + 4) === '{#if' && (result[pos + 4] === ' ' || result[pos + 4] === '}')) {
495
+ depth++;
496
+ console.log(`${indent}[DEBUG] Found nested {#if} at pos ${pos}, depth now: ${depth}`);
497
+ console.log(`${indent}[DEBUG] Context:`, JSON.stringify(result.substring(pos, pos + 20)));
498
+ pos += 4;
499
+ continue;
500
+ }
501
+
502
+ // Check for {:else if condition} at current depth
503
+ if (depth === 1 && result.substring(pos, pos + 9) === '{:else if') {
504
+ // Find the closing } to get the full condition
505
+ const condEnd = result.indexOf('}', pos + 9);
506
+ if (condEnd !== -1) {
507
+ const elseIfCondition = result.substring(pos + 10, condEnd).trim();
508
+ // Close previous branch
509
+ branches[branches.length - 1].end = pos;
510
+ // Add new else if branch
511
+ branches.push({ type: 'else if', condition: elseIfCondition, start: condEnd + 1, end: -1 });
512
+ console.log(`${indent}[DEBUG] Found {:else if ${elseIfCondition}} at pos ${pos}`);
513
+ pos = condEnd + 1;
514
+ continue;
515
+ }
516
+ }
517
+
518
+ // Check for {:else} at current depth
519
+ if (depth === 1 && result.substring(pos, pos + 7) === '{:else}') {
520
+ // Close previous branch
521
+ branches[branches.length - 1].end = pos;
522
+ // Add else branch
523
+ branches.push({ type: 'else', condition: null, start: pos + 7, end: -1 });
524
+ console.log(`${indent}[DEBUG] Found {:else} at pos ${pos}`);
525
+ pos += 7;
526
+ continue;
527
+ }
528
+
529
+ // Check for {/if} - CORRECTED: {/if} is 5 chars, not 6!
530
+ const substr = result.substring(pos, pos + 5);
531
+ if (substr === '{/if}') {
532
+ depth--;
533
+ console.log(`${indent}[DEBUG] Found {/if} at pos ${pos}, depth now: ${depth}`);
534
+ if (depth === 0) {
535
+ closeStart = pos;
536
+ // Close last branch
537
+ branches[branches.length - 1].end = pos;
538
+ break;
539
+ }
540
+ pos += 5;
541
+ continue;
542
+ }
543
+
544
+ // Debug: Log when we see something that looks like {/
545
+ if (result[pos] === '{' && result[pos + 1] === '/') {
546
+ console.log(`${indent}[DEBUG] At pos ${pos}, found '{/' but full check failed. Next 10 chars:`, JSON.stringify(result.substring(pos, pos + 10)));
547
+ }
548
+ pos++;
549
+ }
550
+
551
+ console.log(`${indent}[DEBUG] Scan complete. closeStart: ${closeStart}, final depth: ${depth}, branches:`, branches.length);
552
+
553
+ if (closeStart === -1) {
554
+ console.warn(`${indent}[processIfConditionals] No matching {/if} for condition: ${condition}`);
555
+ console.log(`${indent}[DEBUG] HTML snippet around openEnd:`, result.substring(openEnd, Math.min(openEnd + 200, result.length)));
556
+ break;
557
+ }
558
+
559
+ foundMatch = true;
560
+ const closeEnd = closeStart + 5; // length of '{/if}' - CORRECTED: 5 chars, not 6!
561
+
562
+ // Process each branch recursively and evaluate conditions
563
+ let replacement = '';
564
+ for (const branch of branches) {
565
+ let branchContent = result.substring(branch.start, branch.end);
566
+
567
+ // Recursively process nested ifs
568
+ branchContent = this.processIfConditionals(branchContent, contextData, depth + 1);
569
+
570
+ // Evaluate condition
571
+ if (branch.type === 'if' || branch.type === 'else if') {
572
+ const shouldShow = this.evaluateExpression(branch.condition.trim(), contextData);
573
+ console.log(`${indent}[If] ${branch.type}="${branch.condition}" result=${shouldShow}`);
574
+
575
+ if (shouldShow) {
576
+ replacement = branchContent;
577
+ break; // First matching condition wins
578
+ }
579
+ } else if (branch.type === 'else') {
580
+ // Else branch - use if no previous condition matched
581
+ console.log(`${indent}[If] Using else branch`);
582
+ replacement = branchContent;
583
+ break;
584
+ }
585
+ }
586
+
587
+ // Replace in result
588
+ result = result.substring(0, openStart) + replacement + result.substring(closeEnd);
589
+ }
590
+
591
+ if (!foundMatch) break;
592
+ }
593
+
594
+ if (iteration >= maxIterations) {
595
+ console.warn(`${indent}[processIfConditionals] Max iterations reached - possible infinite loop`);
596
+ }
597
+
598
+ return result;
599
+ }
600
+ }
601
+
602
+ // Universal export - works in both Browser and Node.js
603
+ // For Browser: Use as ES6 module or create global via script
604
+ // For Node.js: Use as ES6 module import
605
+ export default TemplateProcessor;
606
+
607
+ // Also export singleton instance for convenience
608
+ export const templateProcessor = new TemplateProcessor();