@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 +1 -1
- package/src/index.js +2 -1
- package/src/template-processor.js +608 -0
package/package.json
CHANGED
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, '"');
|
|
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();
|