@fluffjs/cli 0.0.1

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.
@@ -0,0 +1,767 @@
1
+ import * as parse5 from 'parse5';
2
+ import { addThisPrefix, addThisPrefixSafe, expressionUsesVariable, extractRootIdentifier, parseInterpolations, parsePipedExpression, renameVariable, transformForExpression, transformForExpressionKeepIterator, transformPipedExpression } from './ExpressionTransformer.js';
3
+ export class CodeGenerator {
4
+ reactiveProperties = new Set();
5
+ templateRefs = [];
6
+ setReactiveProperties(props) {
7
+ this.reactiveProperties = props;
8
+ }
9
+ setTemplateRefs(refs) {
10
+ this.templateRefs = refs;
11
+ }
12
+ generateRenderMethod(html, styles) {
13
+ let content = '';
14
+ if (styles) {
15
+ content += `<style>${styles}</style>`;
16
+ }
17
+ content += html;
18
+ content = this.escapeForTemplateLiteral(content);
19
+ return `this.__getShadowRoot().innerHTML = \`${content}\`;`;
20
+ }
21
+ generateBindingsSetup(bindings, controlFlows) {
22
+ const lines = [];
23
+ for (const binding of bindings) {
24
+ lines.push(this.generateBindingCode(binding));
25
+ }
26
+ lines.push('const __renderById = {};');
27
+ lines.push(`const __triggerNested = (container) => {
28
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_COMMENT);
29
+ while (walker.nextNode()) {
30
+ const id = walker.currentNode.textContent;
31
+ if (__renderById[id]) __renderById[id]();
32
+ }
33
+ };`);
34
+ for (const cf of controlFlows) {
35
+ lines.push(this.generateControlFlowCode(cf));
36
+ }
37
+ return lines.join('\n ');
38
+ }
39
+ getPropertyRef(propName) {
40
+ if (this.reactiveProperties.has(propName)) {
41
+ return `__${propName}`;
42
+ }
43
+ return propName;
44
+ }
45
+ extractBaseProp(expr) {
46
+ const result = extractRootIdentifier(expr);
47
+ if (result === null) {
48
+ throw new Error(`Failed to extract base property from expression: "${expr}"`);
49
+ }
50
+ return result;
51
+ }
52
+ escapeForTemplateLiteral(content) {
53
+ return content.replace(/`/g, '\\`')
54
+ .replace(/\$/g, '\\$');
55
+ }
56
+ escapeForTemplateLiteralPreservingExpressions(content) {
57
+ let result = content.replace(/`/g, '\\`');
58
+ result = result.replace(/\$(?!\{)/g, '\\$');
59
+ return result;
60
+ }
61
+ dotsToDashes(str) {
62
+ return str.replace(/\./g, '-');
63
+ }
64
+ removeIndexSuffix(str) {
65
+ return str.replace(/-\d+$/, '');
66
+ }
67
+ generateBindingCode(binding) {
68
+ const { id, type, expression, target, eventName, className, styleProp } = binding;
69
+ switch (type) {
70
+ case 'text':
71
+ return this.generateTextBinding(id, expression);
72
+ case 'property':
73
+ return target ? this.generatePropertyBinding(id, target, expression) : '';
74
+ case 'event':
75
+ return eventName ? this.generateEventBinding(id, eventName, expression) : '';
76
+ case 'class':
77
+ return className ? this.generateClassBinding(id, className, expression) : '';
78
+ case 'style':
79
+ return styleProp ? this.generateStyleBinding(id, styleProp, expression) : '';
80
+ default:
81
+ return '';
82
+ }
83
+ }
84
+ generateTextBinding(id, expression) {
85
+ const { expression: baseExpr, pipes } = parsePipedExpression(expression);
86
+ const hasPipes = pipes.length > 0;
87
+ const baseProp = this.extractBaseProp(baseExpr);
88
+ const propRef = this.getPropertyRef(baseProp);
89
+ const restPath = baseExpr.split('.')
90
+ .slice(1);
91
+ const safeAccess = restPath.length > 0 ? `Array.isArray(val) ? val.${restPath.join('.')} : (val && typeof val === 'object' ? val.${restPath.join('.')} : val)` : 'val';
92
+ const reactivePropsArray = Array.from(this.reactiveProperties);
93
+ const subscribeToAll = reactivePropsArray.map(p => `if (this.__${p}?.onChange) this.__bindPropertyChange(this.__${p}, __update);`)
94
+ .join('\n ');
95
+ if (hasPipes) {
96
+ const transformedExpr = transformPipedExpression(expression);
97
+ return `{
98
+ const __update = () => {
99
+ const result = ${transformedExpr};
100
+ this.__setText("${id}", String(result ?? ''));
101
+ };
102
+ ${subscribeToAll}
103
+ __update();
104
+ }`;
105
+ }
106
+ return `{
107
+ const prop = this.${propRef};
108
+ if (prop && prop.onChange) {
109
+ const __getText = (val) => {
110
+ if (val == null) return '';
111
+ ${restPath.length > 0 ? `return String(${safeAccess} ?? '');` : 'return String(val ?? \'\');'}
112
+ };
113
+ this.__bindPropertyChange(prop, (val) => {
114
+ this.__setText("${id}", __getText(val));
115
+ });
116
+ } else {
117
+ const __update = () => {
118
+ const raw = this.${propRef};
119
+ const val = (raw && typeof raw === 'object' && raw.getValue) ? raw.getValue() : raw;
120
+ ${restPath.length > 0 ? `const result = val != null ? val.${restPath.join('.')} : undefined;` : 'const result = val;'}
121
+ this.__setText("${id}", String(result ?? ''));
122
+ };
123
+ ${subscribeToAll}
124
+ __update();
125
+ }
126
+ }`;
127
+ }
128
+ generatePropertyBinding(id, target, expression) {
129
+ const baseProp = this.extractBaseProp(expression);
130
+ const propRef = this.getPropertyRef(baseProp);
131
+ const targetCamel = this.kebabToCamel(target);
132
+ const restPath = expression.split('.')
133
+ .slice(1);
134
+ const valueExpr = restPath.length > 0 ? `val?.${restPath.join('.')}` : 'val';
135
+ return `{
136
+ const prop = this.${propRef};
137
+ if (prop && prop.onChange) {
138
+ this.__bindPropertyChange(prop, (val) => {
139
+ this.__bindToChild("${id}", "${targetCamel}", ${valueExpr});
140
+ });
141
+ } else {
142
+ const val = (this.${propRef} && typeof this.${propRef} === 'object' && this.${propRef}.getValue) ? this.${propRef}.getValue() : this.${propRef};
143
+ this.__bindToChild("${id}", "${targetCamel}", ${restPath.length > 0 ? `val?.${restPath.join('.')}` : 'val'});
144
+ }
145
+ }`;
146
+ }
147
+ generateEventBinding(id, eventName, expression) {
148
+ const [baseEvent, ...modifiers] = eventName.split('.');
149
+ const handlerCode = renameVariable(expression, '$event', '__ev');
150
+ const eventCamel = this.kebabToCamel(baseEvent);
151
+ const refLookups = this.generateRefLookups(expression);
152
+ if (modifiers.length > 0) {
153
+ const conditions = modifiers.map(m => {
154
+ if (m === 'enter')
155
+ return '__ev.key === "Enter"';
156
+ if (m === 'escape')
157
+ return '__ev.key === "Escape"';
158
+ if (m === 'space')
159
+ return '__ev.key === " "';
160
+ return 'true';
161
+ })
162
+ .join(' && ');
163
+ return `this.__bindEvent("${id}", "${baseEvent}", (__ev) => { ${refLookups}if (${conditions}) { this.${handlerCode}; } });`;
164
+ }
165
+ return `this.__bindOutput("${id}", "${eventCamel}", (__ev: any) => { ${refLookups}this.${handlerCode}; });`;
166
+ }
167
+ generateRefLookups(expression) {
168
+ const usedRefs = this.templateRefs.filter(ref => new RegExp(`\\b${ref}\\b`).test(expression));
169
+ if (usedRefs.length === 0)
170
+ return '';
171
+ return usedRefs.map(ref => `const ${ref} = this.__getShadowRoot().querySelector('[data-ref="${ref}"]');`)
172
+ .join(' ') + ' ';
173
+ }
174
+ generateClassBinding(id, className, expression) {
175
+ const baseProp = this.extractBaseProp(expression);
176
+ const propRef = this.getPropertyRef(baseProp);
177
+ const restPath = expression.split('.')
178
+ .slice(1);
179
+ const safeExpr = restPath.length > 0 ? `this.${baseProp}?.${restPath.join('.')}` : `this.${baseProp}`;
180
+ return `{
181
+ const prop = this.${propRef};
182
+ const el = this.__getElement("${id}");
183
+ if (prop && prop.onChange) {
184
+ this.__bindPropertyChange(prop, (val) => {
185
+ const finalVal = ${restPath.length > 0 ? `val?.${restPath.join('.')}` : 'val'};
186
+ if (finalVal) { this.__addClass("${id}", "${className}"); }
187
+ else { this.__removeClass("${id}", "${className}"); }
188
+ });
189
+ } else if (el) {
190
+ if (${safeExpr}) { el.classList.add("${className}"); }
191
+ else { el.classList.remove("${className}"); }
192
+ }
193
+ }`;
194
+ }
195
+ generateStyleBinding(id, styleProp, expression) {
196
+ const reactivePropsArray = Array.from(this.reactiveProperties);
197
+ const subscribeToAll = reactivePropsArray.map(p => `if (this.__${p}?.onChange) this.__bindPropertyChange(this.__${p}, __update);`)
198
+ .join('\n ');
199
+ const transformedExpr = addThisPrefix(expression);
200
+ const stylePropCamel = this.kebabToCamel(styleProp);
201
+ return `{
202
+ const el = this.__getElement("${id}");
203
+ const __update = () => {
204
+ if (el) {
205
+ (el as HTMLElement).style.${stylePropCamel} = ${transformedExpr};
206
+ }
207
+ };
208
+ ${subscribeToAll}
209
+ __update();
210
+ }`;
211
+ }
212
+ generateControlFlowCode(cf) {
213
+ if (cf.type === 'if') {
214
+ return this.generateIfCode(cf);
215
+ }
216
+ else if (cf.type === 'for') {
217
+ return this.generateForCode(cf);
218
+ }
219
+ else if (cf.type === 'switch') {
220
+ return this.generateSwitchCode(cf);
221
+ }
222
+ return '';
223
+ }
224
+ processContentBindings(content, prefix, skipEscape = false) {
225
+ const events = new Set();
226
+ let bindIdx = 0;
227
+ const fragment = parse5.parseFragment(content);
228
+ this.walkAndTransformBindings(fragment.childNodes, prefix, events, () => bindIdx++);
229
+ let processed = parse5.serialize(fragment);
230
+ if (!skipEscape) {
231
+ processed = this.escapeForTemplateLiteral(processed);
232
+ }
233
+ return { processed, events: [...events] };
234
+ }
235
+ walkAndTransformBindings(nodes, prefix, events, getBindIdx) {
236
+ for (const node of nodes) {
237
+ if (this.isParse5Element(node)) {
238
+ const attrsToRemove = [];
239
+ const attrsToAdd = [];
240
+ const subscribeMap = new Map();
241
+ for (const attr of node.attrs) {
242
+ if (attr.name === '[subscribe]') {
243
+ const parts = attr.value.split(':');
244
+ if (parts.length === 2) {
245
+ subscribeMap.set(parts[0].trim(), parts[1].trim());
246
+ }
247
+ }
248
+ }
249
+ for (const attr of node.attrs) {
250
+ if (attr.name === '[subscribe]') {
251
+ attrsToRemove.push(attr.name);
252
+ continue;
253
+ }
254
+ if (attr.name.startsWith('#')) {
255
+ const refName = attr.name.slice(1);
256
+ attrsToRemove.push(attr.name);
257
+ attrsToAdd.push({ name: 'data-ref', value: refName });
258
+ continue;
259
+ }
260
+ if (attr.name.startsWith('(') && attr.name.endsWith(')')) {
261
+ let event = '';
262
+ let handler = '';
263
+ if (attr.value.startsWith('[')) {
264
+ try {
265
+ const parsed = JSON.parse(attr.value);
266
+ if (Array.isArray(parsed) && typeof parsed[0] === 'string' && typeof parsed[1] === 'string') {
267
+ [event, handler] = parsed;
268
+ }
269
+ }
270
+ catch {
271
+ event = attr.name.slice(1, -1);
272
+ handler = attr.value;
273
+ }
274
+ }
275
+ else {
276
+ event = attr.name.slice(1, -1);
277
+ handler = attr.value;
278
+ }
279
+ const safeEvent = this.dotsToDashes(event);
280
+ events.add(safeEvent);
281
+ attrsToRemove.push(attr.name);
282
+ attrsToAdd.push({
283
+ name: `data-${prefix}-event-${safeEvent}`,
284
+ value: JSON.stringify([event, handler])
285
+ });
286
+ }
287
+ else if (attr.name.startsWith('[') && attr.name.endsWith(']')) {
288
+ let prop = '';
289
+ let expr = '';
290
+ if (attr.value.startsWith('[')) {
291
+ try {
292
+ const parsed = JSON.parse(attr.value);
293
+ if (Array.isArray(parsed) && typeof parsed[0] === 'string' && typeof parsed[1] === 'string') {
294
+ [prop, expr] = parsed;
295
+ }
296
+ }
297
+ catch {
298
+ prop = attr.name.slice(1, -1);
299
+ expr = attr.value;
300
+ }
301
+ }
302
+ else {
303
+ prop = attr.name.slice(1, -1);
304
+ expr = attr.value;
305
+ }
306
+ const dangerousProps = ['innerHTML', 'outerHTML', 'href', 'src'];
307
+ const isUnsafe = prop.endsWith('.unsafe');
308
+ const baseProp = isUnsafe ? prop.slice(0, -7) : prop;
309
+ if (dangerousProps.includes(baseProp) && !isUnsafe) {
310
+ throw new Error(`XSS Protection: [${prop}] binding is blocked. ` + `Use [${prop}.unsafe] if you understand the security implications ` + 'and have sanitized user input.');
311
+ }
312
+ const safeProp = this.dotsToDashes(baseProp);
313
+ attrsToRemove.push(attr.name);
314
+ const subscribeTo = subscribeMap.get(prop) ?? subscribeMap.get(baseProp);
315
+ const bindingData = subscribeTo ? [baseProp, expr, subscribeTo] : [baseProp, expr];
316
+ attrsToAdd.push({
317
+ name: `data-${prefix}-bind-${safeProp}-${getBindIdx()}`,
318
+ value: JSON.stringify(bindingData)
319
+ });
320
+ }
321
+ }
322
+ node.attrs = node.attrs.filter(a => !attrsToRemove.includes(a.name));
323
+ for (const newAttr of attrsToAdd) {
324
+ node.attrs.push(newAttr);
325
+ }
326
+ if (node.childNodes) {
327
+ this.walkAndTransformBindings(node.childNodes, prefix, events, getBindIdx);
328
+ }
329
+ }
330
+ }
331
+ }
332
+ isParse5Element(node) {
333
+ return 'tagName' in node;
334
+ }
335
+ processAndGenerateBindings(content, prefix) {
336
+ const result = this.processContentBindings(content, prefix, true);
337
+ const withInterpolations = this.transformInterpolationsInContent(result.processed);
338
+ const escaped = this.escapeForTemplateLiteralPreservingExpressions(withInterpolations);
339
+ const bindingSetup = this.generateBindingSetupCode(prefix, result.events, withInterpolations);
340
+ return { processed: escaped, bindingSetup };
341
+ }
342
+ transformInterpolationsInContent(content, iteratorVar) {
343
+ const interpolations = parseInterpolations(content);
344
+ if (interpolations.length === 0)
345
+ return content;
346
+ let newContent = '';
347
+ let lastEnd = 0;
348
+ for (const { start, end, expr } of interpolations) {
349
+ newContent += content.slice(lastEnd, start);
350
+ const { expression: baseExpr, pipes } = parsePipedExpression(expr);
351
+ if (pipes.length > 0) {
352
+ let result = iteratorVar && expressionUsesVariable(baseExpr, iteratorVar) ? baseExpr : `this.${baseExpr}`;
353
+ for (const pipe of pipes) {
354
+ const argsStr = pipe.args.length > 0 ? ', ' + pipe.args.join(', ') : '';
355
+ result = `this.__pipe('${pipe.name}', ${result}${argsStr})`;
356
+ }
357
+ newContent += `\${${result} ?? ''}`;
358
+ }
359
+ else {
360
+ const usesIterator = iteratorVar && expressionUsesVariable(expr, iteratorVar);
361
+ if (usesIterator) {
362
+ newContent += `\${${expr}}`;
363
+ }
364
+ else {
365
+ newContent += `\${this.${expr} ?? ''}`;
366
+ }
367
+ }
368
+ lastEnd = end;
369
+ }
370
+ newContent += content.slice(lastEnd);
371
+ return newContent;
372
+ }
373
+ generateBindingSetupCode(prefix, events, content) {
374
+ const lines = [];
375
+ for (const event of events) {
376
+ lines.push(`this.__wireEvents(container, 'data-${prefix}-event-${event}');`);
377
+ }
378
+ const textBindings = this.extractTextBindings(content);
379
+ for (const { id, expr } of textBindings) {
380
+ const subscriptions = Array.from(this.reactiveProperties)
381
+ .map(p => `if (this.__${p}?.onChange) this.__bindPropertyChange(this.__${p}, __update);`)
382
+ .join('\n ');
383
+ lines.push(`{
384
+ const el = container.querySelector('[data-lid="${id}"]');
385
+ if (el) {
386
+ const __update = () => {
387
+ const __val = ${expr};
388
+ el.textContent = __val != null ? String(__val) : '';
389
+ };
390
+ ${subscriptions}
391
+ __update();
392
+ }
393
+ }`);
394
+ }
395
+ const propBindings = this.extractPropertyBindings(content, prefix);
396
+ for (const { prop, expr, fullAttr } of propBindings) {
397
+ const transformedExpr = addThisPrefix(expr);
398
+ const baseProp = this.extractBaseProp(expr);
399
+ const propRef = this.reactiveProperties.has(baseProp) ? `__${baseProp}` : baseProp;
400
+ if (prop.startsWith('class.')) {
401
+ const className = prop.slice(6);
402
+ lines.push(`{
403
+ const __updateClass = () => {
404
+ container.querySelectorAll('[data-${prefix}-bind-${fullAttr}]').forEach(el => {
405
+ if (${transformedExpr}) el.classList.add('${className}');
406
+ else el.classList.remove('${className}');
407
+ });
408
+ };
409
+ if (this.${propRef}?.onChange) this.__bindPropertyChange(this.${propRef}, __updateClass);
410
+ __updateClass();
411
+ }`);
412
+ }
413
+ else {
414
+ const propCamel = this.kebabToCamel(prop);
415
+ lines.push(`{
416
+ const __updateProp = () => {
417
+ container.querySelectorAll('[data-${prefix}-bind-${fullAttr}]').forEach(el => {
418
+ this.__setChildPropertyDeferred(el, '${propCamel}', ${transformedExpr});
419
+ });
420
+ };
421
+ if (this.${propRef}?.onChange) this.__bindPropertyChange(this.${propRef}, __updateProp);
422
+ __updateProp();
423
+ }`);
424
+ }
425
+ }
426
+ return lines.join('\n ');
427
+ }
428
+ extractPropertyBindings(content, prefix) {
429
+ const bindings = [];
430
+ const fragment = parse5.parseFragment(content);
431
+ const attrPrefix = `data-${prefix}-bind-`;
432
+ this.walkAndExtractPropertyBindings(fragment.childNodes, attrPrefix, bindings);
433
+ return bindings;
434
+ }
435
+ generateUnifiedRender(id, baseProp, contentEvalCode, bindingSetupCode) {
436
+ const propRef = this.getPropertyRef(baseProp);
437
+ return `{
438
+ const __findMarkers = () => {
439
+ const walker = document.createTreeWalker(this.__getShadowRoot(), NodeFilter.SHOW_COMMENT);
440
+ let start = null, end = null;
441
+ while (walker.nextNode()) {
442
+ const text = walker.currentNode.textContent;
443
+ if (text === '${id}') start = walker.currentNode;
444
+ else if (text === '/${id}') end = walker.currentNode;
445
+ }
446
+ return { start, end };
447
+ };
448
+ const prop = this.${propRef};
449
+
450
+ const __render = () => {
451
+ const { start: marker, end: endMarker } = __findMarkers();
452
+ if (!marker) return;
453
+
454
+ if (endMarker) {
455
+ while (marker.nextSibling && marker.nextSibling !== endMarker) {
456
+ marker.nextSibling.remove();
457
+ }
458
+ }
459
+
460
+ ${contentEvalCode}
461
+
462
+ const __tmp = document.createElement('div');
463
+ __tmp.innerHTML = __content;
464
+ const __frag = document.createDocumentFragment();
465
+ const __newNodes = [];
466
+ while (__tmp.firstChild) {
467
+ __newNodes.push(__tmp.firstChild);
468
+ __frag.appendChild(__tmp.firstChild);
469
+ }
470
+ marker.parentNode.insertBefore(__frag, endMarker || marker.nextSibling);
471
+ const container = marker.parentNode;
472
+
473
+ ${bindingSetupCode}
474
+
475
+ __newNodes.forEach(n => {
476
+ if (n.nodeType === 1) __triggerNested(n);
477
+ else if (n.nodeType === 8 && __renderById[n.textContent]) __renderById[n.textContent]();
478
+ });
479
+ };
480
+
481
+ __renderById['${id}'] = __render;
482
+ if (prop && prop.onChange) {
483
+ this.__bindPropertyChange(prop, __render);
484
+ }
485
+ __render();
486
+ }`;
487
+ }
488
+ generateIfCode(cf) {
489
+ const { id, condition, ifContent, elseContent } = cf;
490
+ if (!condition)
491
+ return '';
492
+ const baseProp = this.extractBaseProp(condition);
493
+ const transformedCondition = addThisPrefixSafe(condition);
494
+ const ifBranch = this.processAndGenerateBindings(ifContent ?? '', `${id}-if`);
495
+ const elseBranch = this.processAndGenerateBindings(elseContent ?? '', `${id}-else`);
496
+ const contentEvalCode = `const rawCond = ${transformedCondition};
497
+ const cond = (rawCond && typeof rawCond === 'object' && rawCond.getValue) ? rawCond.getValue() : rawCond;
498
+ const __content = cond ? \`${ifBranch.processed}\` : \`${elseBranch.processed}\`;
499
+ const __bindingFn = cond
500
+ ? () => { ${ifBranch.bindingSetup} }
501
+ : () => { ${elseBranch.bindingSetup} };`;
502
+ const bindingSetupCode = '__bindingFn();';
503
+ return this.generateUnifiedRender(id, baseProp, contentEvalCode, bindingSetupCode);
504
+ }
505
+ generateForCode(cf) {
506
+ const { id, iterator, iterable, content } = cf;
507
+ if (!iterator || !iterable)
508
+ return '';
509
+ const baseProp = this.extractBaseProp(iterable);
510
+ const isOptionContent = (content ?? '').trim()
511
+ .startsWith('<option');
512
+ const markerId = isOptionContent ? `for-${id}` : id;
513
+ const prefix = `${id}-for`;
514
+ const processed = this.processContentBindings(content ?? '', prefix, true);
515
+ let forContent = processed.processed;
516
+ const interpolations = parseInterpolations(forContent);
517
+ if (interpolations.length > 0) {
518
+ let newContent = '';
519
+ let lastEnd = 0;
520
+ for (const { start, end, expr } of interpolations) {
521
+ newContent += forContent.slice(lastEnd, start);
522
+ if (expr.includes(iterator)) {
523
+ newContent += `\${${expr}}`;
524
+ }
525
+ else {
526
+ newContent += `\${this.${expr}}`;
527
+ }
528
+ lastEnd = end;
529
+ }
530
+ newContent += forContent.slice(lastEnd);
531
+ forContent = newContent;
532
+ }
533
+ const bindingSetup = this.generateForBindingSetup(prefix, processed.events, forContent, iterator);
534
+ const escapedForContent = this.escapeForTemplateLiteralPreservingExpressions(forContent);
535
+ const contentEvalCode = `const __items = this.${iterable};
536
+ if (!Array.isArray(__items)) return;
537
+ const __content = __items.map((${iterator}, __idx) => \`${escapedForContent}\`).join('');`;
538
+ return this.generateUnifiedRender(markerId, baseProp, contentEvalCode, bindingSetup);
539
+ }
540
+ generateForBindingSetup(prefix, _events, content, iterator) {
541
+ const lines = [];
542
+ const eventBindings = this.extractEventBindings(content, prefix);
543
+ for (const { event, handler, safeEvent } of eventBindings) {
544
+ lines.push(`container.querySelectorAll('[data-${prefix}-event-${safeEvent}]').forEach((el, __idx) => {
545
+ const __item = __items[__idx];
546
+ this.__bindOutputOnElement(el, '${event}', ($event) => {
547
+ const fn = new Function('${iterator}', '$event', 'return this.${handler}').bind(this);
548
+ fn(__item, $event);
549
+ });
550
+ });`);
551
+ }
552
+ const propBindings = this.extractPropertyBindings(content, prefix);
553
+ for (const { prop, expr, fullAttr, subscribeTo } of propBindings) {
554
+ const propCamel = this.kebabToCamel(prop);
555
+ const evalExpr = expr === iterator ? '__items[__idx]' : transformForExpression(expr, iterator, '__items[__idx]');
556
+ const baseProp = this.extractBaseProp(expr);
557
+ const exprUsesIterator = expressionUsesVariable(expr, iterator);
558
+ if (subscribeTo) {
559
+ const exprWithThis = transformForExpressionKeepIterator(expr, iterator);
560
+ const subscribeProps = subscribeTo.split(',')
561
+ .map(p => p.trim());
562
+ const subscriptions = subscribeProps
563
+ .map(p => `if (this.__${p}?.onChange) this.__bindPropertyChange(this.__${p}, __updateProp);`)
564
+ .join('\n ');
565
+ lines.push(`{
566
+ const __updateProp = () => {
567
+ const __currentItems = prop?.getValue ? prop.getValue() : __items;
568
+ if (!Array.isArray(__currentItems)) return;
569
+ container.querySelectorAll('[data-${prefix}-bind-${fullAttr}]').forEach((el, __idx) => {
570
+ if (__idx < __currentItems.length) {
571
+ const ${iterator} = __currentItems[__idx];
572
+ this.__setChildPropertyDeferred(el, '${propCamel}', ${exprWithThis});
573
+ }
574
+ });
575
+ };
576
+ ${subscriptions}
577
+ __updateProp();
578
+ }`);
579
+ }
580
+ else if (baseProp !== iterator && this.reactiveProperties.has(baseProp)) {
581
+ const propRef = `__${baseProp}`;
582
+ if (exprUsesIterator) {
583
+ const exprWithThis = transformForExpressionKeepIterator(expr, iterator);
584
+ lines.push(`{
585
+ const __updateProp = () => {
586
+ const __currentItems = prop?.getValue ? prop.getValue() : __items;
587
+ if (!Array.isArray(__currentItems)) return;
588
+ container.querySelectorAll('[data-${prefix}-bind-${fullAttr}]').forEach((el, __idx) => {
589
+ if (__idx < __currentItems.length) {
590
+ const ${iterator} = __currentItems[__idx];
591
+ this.__setChildPropertyDeferred(el, '${propCamel}', ${exprWithThis});
592
+ }
593
+ });
594
+ };
595
+ if (this.${propRef}?.onChange) this.__bindPropertyChange(this.${propRef}, __updateProp);
596
+ __updateProp();
597
+ }`);
598
+ }
599
+ else {
600
+ lines.push(`{
601
+ const __updateProp = () => {
602
+ container.querySelectorAll('[data-${prefix}-bind-${fullAttr}]').forEach((el) => {
603
+ this.__setChildPropertyDeferred(el, '${propCamel}', ${evalExpr});
604
+ });
605
+ };
606
+ if (this.${propRef}?.onChange) this.__bindPropertyChange(this.${propRef}, __updateProp);
607
+ __updateProp();
608
+ }`);
609
+ }
610
+ }
611
+ else {
612
+ lines.push(`container.querySelectorAll('[data-${prefix}-bind-${fullAttr}]').forEach((el, __idx) => {
613
+ this.__setChildPropertyDeferred(el, '${propCamel}', ${evalExpr});
614
+ });`);
615
+ }
616
+ }
617
+ return lines.join('\n ');
618
+ }
619
+ extractEventBindings(content, prefix) {
620
+ const bindings = [];
621
+ const fragment = parse5.parseFragment(content);
622
+ const attrPrefix = `data-${prefix}-event-`;
623
+ this.walkAndExtractEventBindings(fragment.childNodes, attrPrefix, bindings);
624
+ return bindings;
625
+ }
626
+ walkAndExtractEventBindings(nodes, attrPrefix, bindings) {
627
+ for (const node of nodes) {
628
+ if (this.isParse5Element(node)) {
629
+ for (const attr of node.attrs) {
630
+ if (attr.name.startsWith(attrPrefix)) {
631
+ const safeEvent = attr.name.slice(attrPrefix.length);
632
+ let event = '';
633
+ let handler = '';
634
+ if (attr.value.startsWith('[')) {
635
+ try {
636
+ const unescaped = attr.value.replace(/\\\$/g, '$');
637
+ const parsed = JSON.parse(unescaped);
638
+ if (Array.isArray(parsed) && typeof parsed[0] === 'string' && typeof parsed[1] === 'string') {
639
+ [event, handler] = parsed;
640
+ }
641
+ }
642
+ catch (e) {
643
+ console.error('Failed to parse event binding JSON:', attr.value, e);
644
+ event = this.kebabToCamel(safeEvent);
645
+ handler = attr.value;
646
+ }
647
+ }
648
+ else {
649
+ event = this.kebabToCamel(safeEvent);
650
+ handler = attr.value;
651
+ }
652
+ bindings.push({ event, handler, safeEvent });
653
+ }
654
+ }
655
+ if (node.childNodes) {
656
+ this.walkAndExtractEventBindings(node.childNodes, attrPrefix, bindings);
657
+ }
658
+ }
659
+ }
660
+ }
661
+ generateSwitchCode(cf) {
662
+ const { id, expression, cases } = cf;
663
+ if (!expression)
664
+ return '';
665
+ const baseProp = this.extractBaseProp(expression);
666
+ const transformedExpr = addThisPrefixSafe(expression);
667
+ const processedCases = (cases ?? []).map((c, idx) => {
668
+ const { processed, bindingSetup } = this.processAndGenerateBindings(c.content, `${id}-case-${idx}`);
669
+ return {
670
+ value: c.value, fallthrough: c.fallthrough, content: processed, bindingSetup
671
+ };
672
+ });
673
+ const caseContents = processedCases.map(c => `\`${c.content}\``)
674
+ .join(', ');
675
+ const caseConditions = processedCases.map((c, idx) => {
676
+ if (c.value === null)
677
+ return `{ idx: ${idx}, match: true }`;
678
+ return `{ idx: ${idx}, match: __switchVal === (${addThisPrefixSafe(c.value)}) }`;
679
+ })
680
+ .join(', ');
681
+ const caseBindings = processedCases.map(c => `(container) => { ${c.bindingSetup} }`)
682
+ .join(', ');
683
+ const fallthrough = processedCases.map(c => c.fallthrough.toString())
684
+ .join(', ');
685
+ const contentEvalCode = `const __switchVal = ${transformedExpr};
686
+ const __cases = [${caseContents}];
687
+ const __conditions = [${caseConditions}];
688
+ const __bindings = [${caseBindings}];
689
+ const __fallthrough = [${fallthrough}];
690
+
691
+ let __content = '';
692
+ let __bindingFns = [];
693
+ let __matched = false;
694
+ for (let i = 0; i < __conditions.length; i++) {
695
+ if (__matched || __conditions[i].match) {
696
+ __matched = true;
697
+ __content += __cases[i];
698
+ __bindingFns.push(__bindings[i]);
699
+ if (!__fallthrough[i]) break;
700
+ }
701
+ }`;
702
+ const bindingSetupCode = '__bindingFns.forEach(fn => fn(container));';
703
+ return this.generateUnifiedRender(id, baseProp, contentEvalCode, bindingSetupCode);
704
+ }
705
+ kebabToCamel(str) {
706
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
707
+ }
708
+ extractTextBindings(content) {
709
+ const bindings = [];
710
+ const fragment = parse5.parseFragment(content);
711
+ this.walkAndExtractTextBindings(fragment.childNodes, bindings);
712
+ return bindings;
713
+ }
714
+ walkAndExtractTextBindings(nodes, bindings) {
715
+ for (const node of nodes) {
716
+ if (this.isParse5Element(node)) {
717
+ if (node.tagName === 'span') {
718
+ const textBindAttr = node.attrs.find(a => a.name === 'data-text-bind');
719
+ const lidAttr = node.attrs.find(a => a.name === 'data-lid');
720
+ if (textBindAttr && lidAttr) {
721
+ bindings.push({ expr: textBindAttr.value, id: lidAttr.value });
722
+ }
723
+ }
724
+ if (node.childNodes) {
725
+ this.walkAndExtractTextBindings(node.childNodes, bindings);
726
+ }
727
+ }
728
+ }
729
+ }
730
+ walkAndExtractPropertyBindings(nodes, attrPrefix, bindings) {
731
+ for (const node of nodes) {
732
+ if (this.isParse5Element(node)) {
733
+ for (const attr of node.attrs) {
734
+ if (attr.name.startsWith(attrPrefix)) {
735
+ const fullAttr = attr.name.slice(attrPrefix.length);
736
+ let prop = '';
737
+ let expr = '';
738
+ let subscribeTo = undefined;
739
+ if (attr.value.startsWith('[')) {
740
+ try {
741
+ const parsed = JSON.parse(attr.value);
742
+ if (Array.isArray(parsed) && typeof parsed[0] === 'string' && typeof parsed[1] === 'string') {
743
+ [prop, expr] = parsed;
744
+ if (typeof parsed[2] === 'string') {
745
+ [, , subscribeTo] = parsed;
746
+ }
747
+ }
748
+ }
749
+ catch {
750
+ prop = this.removeIndexSuffix(fullAttr);
751
+ expr = attr.value;
752
+ }
753
+ }
754
+ else {
755
+ prop = this.removeIndexSuffix(fullAttr);
756
+ expr = attr.value;
757
+ }
758
+ bindings.push({ prop, expr, fullAttr, subscribeTo });
759
+ }
760
+ }
761
+ if (node.childNodes) {
762
+ this.walkAndExtractPropertyBindings(node.childNodes, attrPrefix, bindings);
763
+ }
764
+ }
765
+ }
766
+ }
767
+ }