rip-lang 3.0.2 → 3.2.0

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,1239 @@
1
+ // Component System — Fine-grained reactive components for Rip
2
+ //
3
+ // Architecture: installComponentSupport(CodeGenerator) adds methods to the
4
+ // CodeGenerator prototype, enabling component compilation. A separate
5
+ // getComponentRuntime() emits runtime helpers only when components are used.
6
+ //
7
+ // Naming: All render-tree emitters use emit* (ported from v2.5.1's fg* methods).
8
+
9
+ import { TEMPLATE_TAGS } from './tags.js';
10
+
11
+ // ============================================================================
12
+ // Constants
13
+ // ============================================================================
14
+
15
+ const BIND_PREFIX = '__bind_';
16
+ const BIND_SUFFIX = '__';
17
+
18
+ const LIFECYCLE_HOOKS = new Set(['mounted', 'unmounted', 'updated']);
19
+
20
+ // ============================================================================
21
+ // Standalone Utilities
22
+ // ============================================================================
23
+
24
+ /**
25
+ * Extract input type from attribute pairs for smart binding (valueAsNumber for number/range)
26
+ * @param {Array} pairs - Array of [key, value] pairs from object expression
27
+ * @returns {string|null} - The input type value or null
28
+ */
29
+ function extractInputType(pairs) {
30
+ for (const pair of pairs) {
31
+ if (!Array.isArray(pair)) continue;
32
+ const key = pair[0] instanceof String ? pair[0].valueOf() : pair[0];
33
+ const val = pair[1] instanceof String ? pair[1].valueOf() : pair[1];
34
+ if (key === 'type' && typeof val === 'string') {
35
+ return val.replace(/^["']|["']$/g, '');
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Extract member name from s-expression target.
43
+ * Handles both [".", "this", name] (@property) and plain string.
44
+ */
45
+ function getMemberName(target) {
46
+ if (typeof target === 'string') return target;
47
+ if (Array.isArray(target) && target[0] === '.' && target[1] === 'this' && typeof target[2] === 'string') {
48
+ return target[2];
49
+ }
50
+ return null;
51
+ }
52
+
53
+ // ============================================================================
54
+ // Prototype Installation
55
+ // ============================================================================
56
+
57
+ export function installComponentSupport(CodeGenerator) {
58
+ const proto = CodeGenerator.prototype;
59
+
60
+ // ==========================================================================
61
+ // Utilities
62
+ // ==========================================================================
63
+
64
+ /**
65
+ * Check if name is an HTML/SVG tag
66
+ */
67
+ proto.isHtmlTag = function(name) {
68
+ const tagPart = name.split('#')[0];
69
+ return TEMPLATE_TAGS.has(tagPart.toLowerCase());
70
+ };
71
+
72
+ /**
73
+ * Check if name is a component (PascalCase)
74
+ */
75
+ proto.isComponent = function(name) {
76
+ if (!name || typeof name !== 'string') return false;
77
+ return /^[A-Z]/.test(name);
78
+ };
79
+
80
+ /**
81
+ * Collect tag name and static classes from dot-chain s-expression.
82
+ * e.g. [".", [".", "div", "card"], "active"] → { tag: "div", classes: ["card", "active"] }
83
+ */
84
+ proto.collectTemplateClasses = function(sexpr) {
85
+ const classes = [];
86
+ let current = sexpr;
87
+ while (Array.isArray(current) && current[0] === '.') {
88
+ const prop = current[2];
89
+ if (typeof prop === 'string' || prop instanceof String) {
90
+ classes.unshift(prop.valueOf());
91
+ }
92
+ current = current[1];
93
+ }
94
+ const tag = typeof current === 'string' ? current : (current instanceof String ? current.valueOf() : 'div');
95
+ return { tag, classes };
96
+ };
97
+
98
+ // ==========================================================================
99
+ // Member Transformation
100
+ // ==========================================================================
101
+
102
+ /**
103
+ * Recursively transform s-expression to replace member identifiers with this.X.value.
104
+ * For component context where state variables are signals.
105
+ */
106
+ proto.transformComponentMembers = function(sexpr) {
107
+ if (!Array.isArray(sexpr)) {
108
+ if (typeof sexpr === 'string' && this.reactiveMembers && this.reactiveMembers.has(sexpr)) {
109
+ return ['.', ['.', 'this', sexpr], 'value'];
110
+ }
111
+ return sexpr;
112
+ }
113
+
114
+ // Special case: (. this memberName) for @member syntax
115
+ if (sexpr[0] === '.' && sexpr[1] === 'this' && typeof sexpr[2] === 'string') {
116
+ const memberName = sexpr[2];
117
+ if (this.reactiveMembers && this.reactiveMembers.has(memberName)) {
118
+ return ['.', sexpr, 'value']; // this.X → this.X.value
119
+ }
120
+ return sexpr;
121
+ }
122
+
123
+ // Force thin arrows to fat arrows inside components to preserve this binding
124
+ if (sexpr[0] === '->') {
125
+ return ['=>', ...sexpr.slice(1).map(item => this.transformComponentMembers(item))];
126
+ }
127
+
128
+ return sexpr.map(item => this.transformComponentMembers(item));
129
+ };
130
+
131
+ // ==========================================================================
132
+ // Component Generation (entry points)
133
+ // ==========================================================================
134
+
135
+ /**
136
+ * Generate component: produces an anonymous ES6 class expression.
137
+ * Pattern: ["component", null, ["block", ...statements]]
138
+ */
139
+ proto.generateComponent = function(head, rest, context, sexpr) {
140
+ const [, body] = rest;
141
+
142
+ this.usesTemplates = true;
143
+ this.usesReactivity = true;
144
+
145
+ // Extract component body statements
146
+ const statements = Array.isArray(body) && body[0] === 'block' ? body.slice(1) : [];
147
+
148
+ // Categorize statements
149
+ const stateVars = [];
150
+ const derivedVars = [];
151
+ const readonlyVars = [];
152
+ const methods = [];
153
+ const lifecycleHooks = [];
154
+ const effects = [];
155
+ let renderBlock = null;
156
+
157
+ const memberNames = new Set();
158
+ const reactiveMembers = new Set();
159
+
160
+ for (const stmt of statements) {
161
+ if (!Array.isArray(stmt)) continue;
162
+ const [op] = stmt;
163
+
164
+ if (op === 'state') {
165
+ const varName = getMemberName(stmt[1]);
166
+ if (varName) {
167
+ stateVars.push({ name: varName, value: stmt[2] });
168
+ memberNames.add(varName);
169
+ reactiveMembers.add(varName);
170
+ }
171
+ } else if (op === 'computed') {
172
+ const varName = getMemberName(stmt[1]);
173
+ if (varName) {
174
+ derivedVars.push({ name: varName, expr: stmt[2] });
175
+ memberNames.add(varName);
176
+ reactiveMembers.add(varName);
177
+ }
178
+ } else if (op === 'readonly') {
179
+ const varName = getMemberName(stmt[1]);
180
+ if (varName) {
181
+ readonlyVars.push({ name: varName, value: stmt[2] });
182
+ memberNames.add(varName);
183
+ }
184
+ } else if (op === '=') {
185
+ const varName = getMemberName(stmt[1]);
186
+ if (varName) {
187
+ if (LIFECYCLE_HOOKS.has(varName)) {
188
+ lifecycleHooks.push({ name: varName, value: stmt[2] });
189
+ } else {
190
+ const val = stmt[2];
191
+ if (Array.isArray(val) && (val[0] === '->' || val[0] === '=>')) {
192
+ methods.push({ name: varName, func: val });
193
+ memberNames.add(varName);
194
+ } else {
195
+ stateVars.push({ name: varName, value: val });
196
+ memberNames.add(varName);
197
+ reactiveMembers.add(varName);
198
+ }
199
+ }
200
+ }
201
+ } else if (op === 'effect') {
202
+ effects.push(stmt);
203
+ } else if (op === 'render') {
204
+ renderBlock = stmt;
205
+ } else if (op === 'object') {
206
+ for (let i = 1; i < stmt.length; i++) {
207
+ const pair = stmt[i];
208
+ if (!Array.isArray(pair)) continue;
209
+ const [methodName, funcDef] = pair;
210
+ if (typeof methodName === 'string' && LIFECYCLE_HOOKS.has(methodName)) {
211
+ lifecycleHooks.push({ name: methodName, value: funcDef });
212
+ } else if (typeof methodName === 'string') {
213
+ methods.push({ name: methodName, func: funcDef });
214
+ memberNames.add(methodName);
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // Save and set component context
221
+ const prevComponentMembers = this.componentMembers;
222
+ const prevReactiveMembers = this.reactiveMembers;
223
+ this.componentMembers = memberNames;
224
+ this.reactiveMembers = reactiveMembers;
225
+
226
+ const lines = [];
227
+ let blockFactoriesCode = '';
228
+
229
+ lines.push('class {');
230
+
231
+ // --- Constructor ---
232
+ lines.push(' constructor(props = {}) {');
233
+ lines.push(' const __prevComponent = __pushComponent(this);');
234
+ lines.push('');
235
+
236
+ // Constants (readonly)
237
+ for (const { name, value } of readonlyVars) {
238
+ const val = this.generateInComponent(value, 'value');
239
+ lines.push(` this.${name} = props.${name} ?? ${val};`);
240
+ }
241
+
242
+ // State variables (with isSignal prop merging)
243
+ for (const { name, value } of stateVars) {
244
+ const val = this.generateInComponent(value, 'value');
245
+ lines.push(` this.${name} = isSignal(props.${name}) ? props.${name} : __state(props.${name} ?? ${val});`);
246
+ }
247
+
248
+ // Computed (derived)
249
+ for (const { name, expr } of derivedVars) {
250
+ const val = this.generateInComponent(expr, 'value');
251
+ lines.push(` this.${name} = __computed(() => ${val});`);
252
+ }
253
+
254
+ // Effects
255
+ for (const effect of effects) {
256
+ const effectBody = effect[1];
257
+ const effectCode = this.generateInComponent(effectBody, 'value');
258
+ lines.push(` __effect(${effectCode});`);
259
+ }
260
+
261
+ lines.push('');
262
+ lines.push(' __popComponent(__prevComponent);');
263
+ lines.push(' }');
264
+
265
+ // --- Methods ---
266
+ for (const { name, func } of methods) {
267
+ if (Array.isArray(func) && (func[0] === '->' || func[0] === '=>')) {
268
+ const [, params, methodBody] = func;
269
+ const paramStr = Array.isArray(params) ? params.map(p => this.formatParam(p)).join(', ') : '';
270
+ const bodyCode = this.generateInComponent(methodBody, 'value');
271
+ lines.push(` ${name}(${paramStr}) { return ${bodyCode}; }`);
272
+ }
273
+ }
274
+
275
+ // --- Lifecycle hooks ---
276
+ for (const { name, value } of lifecycleHooks) {
277
+ if (Array.isArray(value) && (value[0] === '->' || value[0] === '=>')) {
278
+ const [, , hookBody] = value;
279
+ const bodyCode = this.generateInComponent(hookBody, 'value');
280
+ lines.push(` ${name}() { return ${bodyCode}; }`);
281
+ }
282
+ }
283
+
284
+ // --- Render block (fine-grained) ---
285
+ if (renderBlock) {
286
+ const renderBody = renderBlock[1];
287
+ const result = this.buildRender(renderBody);
288
+
289
+ if (result.blockFactories.length > 0) {
290
+ blockFactoriesCode = result.blockFactories.join('\n\n') + '\n\n';
291
+ }
292
+
293
+ lines.push(' _create() {');
294
+ for (const line of result.createLines) {
295
+ lines.push(` ${line}`);
296
+ }
297
+ lines.push(` return ${result.rootVar};`);
298
+ lines.push(' }');
299
+
300
+ if (result.setupLines.length > 0) {
301
+ lines.push(' _setup() {');
302
+ for (const line of result.setupLines) {
303
+ lines.push(` ${line}`);
304
+ }
305
+ lines.push(' }');
306
+ }
307
+ }
308
+
309
+ // --- Mount ---
310
+ lines.push(' mount(target) {');
311
+ lines.push(' if (typeof target === "string") target = document.querySelector(target);');
312
+ lines.push(' this._target = target;');
313
+ lines.push(' this._root = this._create();');
314
+ lines.push(' target.appendChild(this._root);');
315
+ lines.push(' if (this._setup) this._setup();');
316
+ lines.push(' if (this.mounted) this.mounted();');
317
+ lines.push(' return this;');
318
+ lines.push(' }');
319
+
320
+ // --- Unmount ---
321
+ lines.push(' unmount() {');
322
+ lines.push(' if (this.unmounted) this.unmounted();');
323
+ lines.push(' if (this._root && this._root.parentNode) {');
324
+ lines.push(' this._root.parentNode.removeChild(this._root);');
325
+ lines.push(' }');
326
+ lines.push(' }');
327
+
328
+ lines.push('}');
329
+
330
+ // Restore context
331
+ this.componentMembers = prevComponentMembers;
332
+ this.reactiveMembers = prevReactiveMembers;
333
+
334
+ // If block factories exist, wrap in IIFE so they're in scope
335
+ if (blockFactoriesCode) {
336
+ return `(() => {\n${blockFactoriesCode}return ${lines.join('\n')};\n})()`;
337
+ }
338
+
339
+ return lines.join('\n');
340
+ };
341
+
342
+ /**
343
+ * Generate code inside component context (transforms member access to this.X.value)
344
+ */
345
+ proto.generateInComponent = function(sexpr, context) {
346
+ if (typeof sexpr === 'string' && this.reactiveMembers && this.reactiveMembers.has(sexpr)) {
347
+ return `this.${sexpr}.value`;
348
+ }
349
+ if (Array.isArray(sexpr) && this.reactiveMembers) {
350
+ const transformed = this.transformComponentMembers(sexpr);
351
+ return this.generate(transformed, context);
352
+ }
353
+ return this.generate(sexpr, context);
354
+ };
355
+
356
+ /**
357
+ * Handle standalone render (outside component): error
358
+ */
359
+ proto.generateRender = function(head, rest, context, sexpr) {
360
+ throw new Error('render blocks can only be used inside a component');
361
+ };
362
+
363
+ // ==========================================================================
364
+ // Render Tree Emission
365
+ // ==========================================================================
366
+
367
+ /**
368
+ * Build the fine-grained render output: create lines, setup lines, block factories.
369
+ * Entry point for processing an entire render block.
370
+ */
371
+ proto.buildRender = function(body) {
372
+ this._emitElementCount = 0;
373
+ this._emitTextCount = 0;
374
+ this._emitBlockCount = 0;
375
+ this._emitCreateLines = [];
376
+ this._emitSetupLines = [];
377
+ this._emitBlockFactories = [];
378
+
379
+ const statements = Array.isArray(body) && body[0] === 'block' ? body.slice(1) : [body];
380
+
381
+ let rootVar;
382
+ if (statements.length === 0) {
383
+ rootVar = 'null';
384
+ } else if (statements.length === 1) {
385
+ rootVar = this.emitNode(statements[0]);
386
+ } else {
387
+ rootVar = this.newElementVar('frag');
388
+ this._emitCreateLines.push(`${rootVar} = document.createDocumentFragment();`);
389
+ for (const stmt of statements) {
390
+ const childVar = this.emitNode(stmt);
391
+ this._emitCreateLines.push(`${rootVar}.appendChild(${childVar});`);
392
+ }
393
+ }
394
+
395
+ return {
396
+ createLines: this._emitCreateLines,
397
+ setupLines: this._emitSetupLines,
398
+ blockFactories: this._emitBlockFactories,
399
+ rootVar
400
+ };
401
+ };
402
+
403
+ /** Generate a unique block factory name */
404
+ proto.newBlockVar = function() {
405
+ return `create_block_${this._emitBlockCount++}`;
406
+ };
407
+
408
+ /** Generate a unique element variable name */
409
+ proto.newElementVar = function(hint = 'el') {
410
+ return `this._${hint}${this._emitElementCount++}`;
411
+ };
412
+
413
+ /** Generate a unique text node variable name */
414
+ proto.newTextVar = function() {
415
+ return `this._t${this._emitTextCount++}`;
416
+ };
417
+
418
+ // --------------------------------------------------------------------------
419
+ // emitNode — main dispatch for all render tree nodes
420
+ // --------------------------------------------------------------------------
421
+
422
+ proto.emitNode = function(sexpr) {
423
+ // String literal → text node (handle both primitive and String objects)
424
+ if (typeof sexpr === 'string' || sexpr instanceof String) {
425
+ const str = sexpr.valueOf();
426
+ if (str.startsWith('"') || str.startsWith("'") || str.startsWith('`')) {
427
+ const textVar = this.newTextVar();
428
+ this._emitCreateLines.push(`${textVar} = document.createTextNode(${str});`);
429
+ return textVar;
430
+ }
431
+ // Dynamic text binding (reactive member)
432
+ if (this.reactiveMembers && this.reactiveMembers.has(str)) {
433
+ const textVar = this.newTextVar();
434
+ this._emitCreateLines.push(`${textVar} = document.createTextNode('');`);
435
+ this._emitSetupLines.push(`__effect(() => { ${textVar}.data = this.${str}.value; });`);
436
+ return textVar;
437
+ }
438
+ // Static tag without content
439
+ const elVar = this.newElementVar();
440
+ this._emitCreateLines.push(`${elVar} = document.createElement('${str}');`);
441
+ return elVar;
442
+ }
443
+
444
+ if (!Array.isArray(sexpr)) {
445
+ const commentVar = this.newElementVar('c');
446
+ this._emitCreateLines.push(`${commentVar} = document.createComment('unknown');`);
447
+ return commentVar;
448
+ }
449
+
450
+ const [head, ...rest] = sexpr;
451
+ const headStr = typeof head === 'string' ? head : (head instanceof String ? head.valueOf() : null);
452
+
453
+ // Component instantiation (PascalCase)
454
+ if (headStr && this.isComponent(headStr)) {
455
+ return this.emitChildComponent(headStr, rest);
456
+ }
457
+
458
+ // HTML tag
459
+ if (headStr && this.isHtmlTag(headStr)) {
460
+ return this.emitTag(headStr, [], rest);
461
+ }
462
+
463
+ // Property chain (div.class or item.name)
464
+ if (headStr === '.') {
465
+ const [, obj, prop] = sexpr;
466
+
467
+ // Property access on this (e.g., @prop, @children)
468
+ if (obj === 'this' && typeof prop === 'string') {
469
+ if (this.reactiveMembers && this.reactiveMembers.has(prop)) {
470
+ const textVar = this.newTextVar();
471
+ this._emitCreateLines.push(`${textVar} = document.createTextNode('');`);
472
+ this._emitSetupLines.push(`__effect(() => { ${textVar}.data = this.${prop}.value; });`);
473
+ return textVar;
474
+ }
475
+ if (this.componentMembers && this.componentMembers.has(prop)) {
476
+ const slotVar = this.newElementVar('slot');
477
+ this._emitCreateLines.push(`${slotVar} = this.${prop} instanceof Node ? this.${prop} : (this.${prop} != null ? document.createTextNode(String(this.${prop})) : document.createComment(''));`);
478
+ return slotVar;
479
+ }
480
+ }
481
+
482
+ // HTML tag with classes (div.class)
483
+ const { tag, classes } = this.collectTemplateClasses(sexpr);
484
+ if (tag && this.isHtmlTag(tag)) {
485
+ return this.emitTag(tag, classes, []);
486
+ }
487
+
488
+ // General property access (e.g., item.name in a loop)
489
+ const textVar = this.newTextVar();
490
+ const exprCode = this.generateInComponent(sexpr, 'value');
491
+ this._emitCreateLines.push(`${textVar} = document.createTextNode(String(${exprCode}));`);
492
+ return textVar;
493
+ }
494
+
495
+ // Call expression: (tag.class args...) or ((tag.class) args...)
496
+ if (Array.isArray(head)) {
497
+ // Nested dynamic class call: (((. div __cx__) "classes") children)
498
+ if (Array.isArray(head[0]) && head[0][0] === '.' &&
499
+ (head[0][2] === '__cx__' || (head[0][2] instanceof String && head[0][2].valueOf() === '__cx__'))) {
500
+ const tag = typeof head[0][1] === 'string' ? head[0][1] : head[0][1].valueOf();
501
+ const classExprs = head.slice(1);
502
+ return this.emitDynamicTag(tag, classExprs, rest);
503
+ }
504
+
505
+ const { tag, classes } = this.collectTemplateClasses(head);
506
+ if (tag && this.isHtmlTag(tag)) {
507
+ // Dynamic class syntax: div.("classes") → (. div __cx__) "classes"
508
+ if (classes.length === 1 && classes[0] === '__cx__') {
509
+ return this.emitDynamicTag(tag, rest, []);
510
+ }
511
+ return this.emitTag(tag, classes, rest);
512
+ }
513
+ }
514
+
515
+ // Arrow function (children block)
516
+ if (headStr === '->' || headStr === '=>') {
517
+ return this.emitBlock(rest[1]);
518
+ }
519
+
520
+ // Conditional: if/else
521
+ if (headStr === 'if') {
522
+ return this.emitConditional(sexpr);
523
+ }
524
+
525
+ // For loop
526
+ if (headStr === 'for' || headStr === 'for-in' || headStr === 'for-of' || headStr === 'for-as') {
527
+ return this.emitLoop(sexpr);
528
+ }
529
+
530
+ // General expression (computed value, function call, binary op, etc.)
531
+ const textVar = this.newTextVar();
532
+ const exprCode = this.generateInComponent(sexpr, 'value');
533
+ if (this.hasReactiveDeps(sexpr)) {
534
+ this._emitCreateLines.push(`${textVar} = document.createTextNode('');`);
535
+ this._emitSetupLines.push(`__effect(() => { ${textVar}.data = ${exprCode}; });`);
536
+ } else {
537
+ this._emitCreateLines.push(`${textVar} = document.createTextNode(String(${exprCode}));`);
538
+ }
539
+ return textVar;
540
+ };
541
+
542
+ // --------------------------------------------------------------------------
543
+ // emitTag — HTML element with static classes and children
544
+ // --------------------------------------------------------------------------
545
+
546
+ proto.emitTag = function(tag, classes, args) {
547
+ const elVar = this.newElementVar();
548
+ this._emitCreateLines.push(`${elVar} = document.createElement('${tag}');`);
549
+
550
+ if (classes.length > 0) {
551
+ this._emitCreateLines.push(`${elVar}.className = '${classes.join(' ')}';`);
552
+ }
553
+
554
+ for (const arg of args) {
555
+ // Arrow function = children
556
+ if (Array.isArray(arg) && (arg[0] === '->' || arg[0] === '=>')) {
557
+ const block = arg[2];
558
+ if (Array.isArray(block) && block[0] === 'block') {
559
+ for (const child of block.slice(1)) {
560
+ const childVar = this.emitNode(child);
561
+ this._emitCreateLines.push(`${elVar}.appendChild(${childVar});`);
562
+ }
563
+ } else if (block) {
564
+ const childVar = this.emitNode(block);
565
+ this._emitCreateLines.push(`${elVar}.appendChild(${childVar});`);
566
+ }
567
+ }
568
+ // Object = attributes/events
569
+ else if (Array.isArray(arg) && arg[0] === 'object') {
570
+ this.emitAttributes(elVar, arg);
571
+ }
572
+ // String = text child
573
+ else if (typeof arg === 'string') {
574
+ const textVar = this.newTextVar();
575
+ if (arg.startsWith('"') || arg.startsWith("'") || arg.startsWith('`')) {
576
+ this._emitCreateLines.push(`${textVar} = document.createTextNode(${arg});`);
577
+ } else if (this.reactiveMembers && this.reactiveMembers.has(arg)) {
578
+ this._emitCreateLines.push(`${textVar} = document.createTextNode('');`);
579
+ this._emitSetupLines.push(`__effect(() => { ${textVar}.data = this.${arg}.value; });`);
580
+ } else if (this.componentMembers && this.componentMembers.has(arg)) {
581
+ this._emitCreateLines.push(`${textVar} = document.createTextNode(String(this.${arg}));`);
582
+ } else {
583
+ this._emitCreateLines.push(`${textVar} = document.createTextNode(String(${arg}));`);
584
+ }
585
+ this._emitCreateLines.push(`${elVar}.appendChild(${textVar});`);
586
+ }
587
+ // String object (from parser)
588
+ else if (arg instanceof String) {
589
+ const val = arg.valueOf();
590
+ const textVar = this.newTextVar();
591
+ if (val.startsWith('"') || val.startsWith("'") || val.startsWith('`')) {
592
+ this._emitCreateLines.push(`${textVar} = document.createTextNode(${val});`);
593
+ } else if (this.reactiveMembers && this.reactiveMembers.has(val)) {
594
+ this._emitCreateLines.push(`${textVar} = document.createTextNode('');`);
595
+ this._emitSetupLines.push(`__effect(() => { ${textVar}.data = this.${val}.value; });`);
596
+ } else {
597
+ this._emitCreateLines.push(`${textVar} = document.createTextNode(String(${val}));`);
598
+ }
599
+ this._emitCreateLines.push(`${elVar}.appendChild(${textVar});`);
600
+ }
601
+ // Other = nested element
602
+ else if (arg) {
603
+ const childVar = this.emitNode(arg);
604
+ this._emitCreateLines.push(`${elVar}.appendChild(${childVar});`);
605
+ }
606
+ }
607
+
608
+ return elVar;
609
+ };
610
+
611
+ // --------------------------------------------------------------------------
612
+ // emitDynamicTag — tag with .() CLSX dynamic classes
613
+ // --------------------------------------------------------------------------
614
+
615
+ proto.emitDynamicTag = function(tag, classExprs, children) {
616
+ const elVar = this.newElementVar();
617
+ this._emitCreateLines.push(`${elVar} = document.createElement('${tag}');`);
618
+
619
+ if (classExprs.length > 0) {
620
+ const classArgs = classExprs.map(e => this.generateInComponent(e, 'value')).join(', ');
621
+ const hasReactive = classExprs.some(e => this.hasReactiveDeps(e));
622
+ if (hasReactive) {
623
+ this._emitSetupLines.push(`__effect(() => { ${elVar}.className = __cx__(${classArgs}); });`);
624
+ } else {
625
+ this._emitCreateLines.push(`${elVar}.className = __cx__(${classArgs});`);
626
+ }
627
+ }
628
+
629
+ for (const arg of children) {
630
+ const argHead = Array.isArray(arg) ? (arg[0] instanceof String ? arg[0].valueOf() : arg[0]) : null;
631
+ if (argHead === '->' || argHead === '=>') {
632
+ const block = arg[2];
633
+ const blockHead = Array.isArray(block) ? (block[0] instanceof String ? block[0].valueOf() : block[0]) : null;
634
+ if (blockHead === 'block') {
635
+ for (const child of block.slice(1)) {
636
+ const childVar = this.emitNode(child);
637
+ this._emitCreateLines.push(`${elVar}.appendChild(${childVar});`);
638
+ }
639
+ } else if (block) {
640
+ const childVar = this.emitNode(block);
641
+ this._emitCreateLines.push(`${elVar}.appendChild(${childVar});`);
642
+ }
643
+ }
644
+ else if (Array.isArray(arg) && arg[0] === 'object') {
645
+ this.emitAttributes(elVar, arg);
646
+ }
647
+ else if (typeof arg === 'string' || arg instanceof String) {
648
+ const textVar = this.newTextVar();
649
+ const argStr = arg.valueOf();
650
+ if (argStr.startsWith('"') || argStr.startsWith("'") || argStr.startsWith('`')) {
651
+ this._emitCreateLines.push(`${textVar} = document.createTextNode(${argStr});`);
652
+ } else if (this.reactiveMembers && this.reactiveMembers.has(argStr)) {
653
+ this._emitCreateLines.push(`${textVar} = document.createTextNode('');`);
654
+ this._emitSetupLines.push(`__effect(() => { ${textVar}.data = this.${argStr}.value; });`);
655
+ } else {
656
+ this._emitCreateLines.push(`${textVar} = document.createTextNode(${this.generateInComponent(arg, 'value')});`);
657
+ }
658
+ this._emitCreateLines.push(`${elVar}.appendChild(${textVar});`);
659
+ }
660
+ else {
661
+ const childVar = this.emitNode(arg);
662
+ this._emitCreateLines.push(`${elVar}.appendChild(${childVar});`);
663
+ }
664
+ }
665
+
666
+ return elVar;
667
+ };
668
+
669
+ // --------------------------------------------------------------------------
670
+ // emitAttributes — attributes, events, and bindings on an element
671
+ // --------------------------------------------------------------------------
672
+
673
+ proto.emitAttributes = function(elVar, objExpr) {
674
+ const inputType = extractInputType(objExpr.slice(1));
675
+
676
+ for (let i = 1; i < objExpr.length; i++) {
677
+ let [key, value] = objExpr[i];
678
+
679
+ // Event handler: @click or (. this eventName)
680
+ if (Array.isArray(key) && key[0] === '.' && key[1] === 'this') {
681
+ const eventName = key[2];
682
+ const handlerCode = this.generateInComponent(value, 'value');
683
+ this._emitCreateLines.push(`${elVar}.addEventListener('${eventName}', (e) => (${handlerCode})(e));`);
684
+ continue;
685
+ }
686
+
687
+ // Regular attribute
688
+ if (typeof key === 'string') {
689
+ // Strip quotes from string keys (e.g., "data-slot" → data-slot)
690
+ if (key.startsWith('"') && key.endsWith('"')) {
691
+ key = key.slice(1, -1);
692
+ }
693
+
694
+ // Two-way binding: __bind_value__ pattern
695
+ if (key.startsWith(BIND_PREFIX) && key.endsWith(BIND_SUFFIX)) {
696
+ const prop = key.slice(BIND_PREFIX.length, -BIND_SUFFIX.length);
697
+ const valueCode = this.generateInComponent(value, 'value');
698
+
699
+ let event, valueAccessor;
700
+ if (prop === 'checked') {
701
+ event = 'change';
702
+ valueAccessor = 'e.target.checked';
703
+ } else {
704
+ event = 'input';
705
+ valueAccessor = (inputType === 'number' || inputType === 'range')
706
+ ? 'e.target.valueAsNumber' : 'e.target.value';
707
+ }
708
+
709
+ this._emitSetupLines.push(`__effect(() => { ${elVar}.${prop} = ${valueCode}; });`);
710
+ this._emitCreateLines.push(`${elVar}.addEventListener('${event}', (e) => ${valueCode} = ${valueAccessor});`);
711
+ continue;
712
+ }
713
+
714
+ const valueCode = this.generateInComponent(value, 'value');
715
+
716
+ // Smart two-way binding for value/checked when bound to reactive state
717
+ if ((key === 'value' || key === 'checked') && this.hasReactiveDeps(value)) {
718
+ // Reactive effect: signal → DOM property
719
+ this._emitSetupLines.push(`__effect(() => { ${elVar}.${key} = ${valueCode}; });`);
720
+ // Event listener: DOM → signal (two-way)
721
+ const event = key === 'checked' ? 'change' : 'input';
722
+ const accessor = key === 'checked' ? 'e.target.checked'
723
+ : (inputType === 'number' || inputType === 'range') ? 'e.target.valueAsNumber'
724
+ : 'e.target.value';
725
+ this._emitCreateLines.push(`${elVar}.addEventListener('${event}', (e) => { ${valueCode} = ${accessor}; });`);
726
+ continue;
727
+ }
728
+
729
+ if (this.hasReactiveDeps(value)) {
730
+ this._emitSetupLines.push(`__effect(() => { ${elVar}.setAttribute('${key}', ${valueCode}); });`);
731
+ } else {
732
+ this._emitCreateLines.push(`${elVar}.setAttribute('${key}', ${valueCode});`);
733
+ }
734
+ }
735
+ }
736
+ };
737
+
738
+ // --------------------------------------------------------------------------
739
+ // emitBlock — a block of template children
740
+ // --------------------------------------------------------------------------
741
+
742
+ proto.emitBlock = function(body) {
743
+ if (!Array.isArray(body) || body[0] !== 'block') {
744
+ return this.emitNode(body);
745
+ }
746
+
747
+ const statements = body.slice(1);
748
+ if (statements.length === 0) {
749
+ const commentVar = this.newElementVar('empty');
750
+ this._emitCreateLines.push(`${commentVar} = document.createComment('');`);
751
+ return commentVar;
752
+ }
753
+ if (statements.length === 1) {
754
+ return this.emitNode(statements[0]);
755
+ }
756
+
757
+ const fragVar = this.newElementVar('frag');
758
+ this._emitCreateLines.push(`${fragVar} = document.createDocumentFragment();`);
759
+ for (const stmt of statements) {
760
+ const childVar = this.emitNode(stmt);
761
+ this._emitCreateLines.push(`${fragVar}.appendChild(${childVar});`);
762
+ }
763
+ return fragVar;
764
+ };
765
+
766
+ // --------------------------------------------------------------------------
767
+ // emitConditional — reactive if/else using block factories
768
+ // --------------------------------------------------------------------------
769
+
770
+ proto.emitConditional = function(sexpr) {
771
+ const [, condition, thenBlock, elseBlock] = sexpr;
772
+
773
+ const anchorVar = this.newElementVar('anchor');
774
+ this._emitCreateLines.push(`${anchorVar} = document.createComment('if');`);
775
+
776
+ const condCode = this.generateInComponent(condition, 'value');
777
+
778
+ const thenBlockName = this.newBlockVar();
779
+ this.emitConditionBranch(thenBlockName, thenBlock);
780
+
781
+ let elseBlockName = null;
782
+ if (elseBlock) {
783
+ elseBlockName = this.newBlockVar();
784
+ this.emitConditionBranch(elseBlockName, elseBlock);
785
+ }
786
+
787
+ const setupLines = [];
788
+ setupLines.push(`// Conditional: ${thenBlockName}${elseBlockName ? ' / ' + elseBlockName : ''}`);
789
+ setupLines.push(`{`);
790
+ setupLines.push(` const anchor = ${anchorVar};`);
791
+ setupLines.push(` let currentBlock = null;`);
792
+ setupLines.push(` let showing = null;`);
793
+ setupLines.push(` __effect(() => {`);
794
+ setupLines.push(` const show = !!(${condCode});`);
795
+ setupLines.push(` const want = show ? 'then' : ${elseBlock ? "'else'" : 'null'};`);
796
+ setupLines.push(` if (want === showing) return;`);
797
+ setupLines.push(``);
798
+ setupLines.push(` if (currentBlock) {`);
799
+ setupLines.push(` currentBlock.d(true);`);
800
+ setupLines.push(` currentBlock = null;`);
801
+ setupLines.push(` }`);
802
+ setupLines.push(` showing = want;`);
803
+ setupLines.push(``);
804
+ setupLines.push(` if (want === 'then') {`);
805
+ setupLines.push(` currentBlock = ${thenBlockName}(this);`);
806
+ setupLines.push(` currentBlock.c();`);
807
+ setupLines.push(` currentBlock.m(anchor.parentNode, anchor.nextSibling);`);
808
+ setupLines.push(` currentBlock.p(this);`);
809
+ setupLines.push(` }`);
810
+ if (elseBlock) {
811
+ setupLines.push(` if (want === 'else') {`);
812
+ setupLines.push(` currentBlock = ${elseBlockName}(this);`);
813
+ setupLines.push(` currentBlock.c();`);
814
+ setupLines.push(` currentBlock.m(anchor.parentNode, anchor.nextSibling);`);
815
+ setupLines.push(` currentBlock.p(this);`);
816
+ setupLines.push(` }`);
817
+ }
818
+ setupLines.push(` });`);
819
+ setupLines.push(`}`);
820
+
821
+ this._emitSetupLines.push(setupLines.join('\n '));
822
+
823
+ return anchorVar;
824
+ };
825
+
826
+ // --------------------------------------------------------------------------
827
+ // emitConditionBranch — block factory for a conditional branch
828
+ // --------------------------------------------------------------------------
829
+
830
+ proto.emitConditionBranch = function(blockName, block) {
831
+ const savedCreateLines = this._emitCreateLines;
832
+ const savedSetupLines = this._emitSetupLines;
833
+
834
+ this._emitCreateLines = [];
835
+ this._emitSetupLines = [];
836
+
837
+ const rootVar = this.emitBlock(block);
838
+ const createLines = this._emitCreateLines;
839
+ const setupLines = this._emitSetupLines;
840
+
841
+ this._emitCreateLines = savedCreateLines;
842
+ this._emitSetupLines = savedSetupLines;
843
+
844
+ const localizeVar = (line) => {
845
+ // First localize template element refs (this._elN → _elN)
846
+ let result = line.replace(/this\.(_el\d+|_t\d+|_anchor\d+|_frag\d+|_slot\d+|_c\d+|_inst\d+|_empty\d+)/g, '$1');
847
+ // Then replace remaining this. with ctx. (component instance in block context)
848
+ result = result.replace(/\bthis\./g, 'ctx.');
849
+ return result;
850
+ };
851
+
852
+ const factoryLines = [];
853
+ factoryLines.push(`function ${blockName}(ctx) {`);
854
+
855
+ // Declare local variables
856
+ const localVars = new Set();
857
+ for (const line of createLines) {
858
+ const match = line.match(/^this\.(_(?:el|t|anchor|frag|slot|c|inst|empty)\d+)\s*=/);
859
+ if (match) localVars.add(match[1]);
860
+ }
861
+ if (localVars.size > 0) {
862
+ factoryLines.push(` let ${[...localVars].join(', ')};`);
863
+ }
864
+
865
+ const hasEffects = setupLines.length > 0;
866
+ if (hasEffects) {
867
+ factoryLines.push(` let disposers = [];`);
868
+ }
869
+
870
+ factoryLines.push(` return {`);
871
+
872
+ // c() - create
873
+ factoryLines.push(` c() {`);
874
+ for (const line of createLines) {
875
+ factoryLines.push(` ${localizeVar(line)}`);
876
+ }
877
+ factoryLines.push(` },`);
878
+
879
+ // m() - mount
880
+ factoryLines.push(` m(target, anchor) {`);
881
+ factoryLines.push(` target.insertBefore(${localizeVar(rootVar)}, anchor);`);
882
+ factoryLines.push(` },`);
883
+
884
+ // p() - update/patch
885
+ factoryLines.push(` p(ctx) {`);
886
+ if (hasEffects) {
887
+ factoryLines.push(` disposers.forEach(d => d());`);
888
+ factoryLines.push(` disposers = [];`);
889
+ for (const line of setupLines) {
890
+ const localizedLine = localizeVar(line);
891
+ const wrappedLine = localizedLine.replace(
892
+ /__effect\(\(\) => \{/g,
893
+ 'disposers.push(__effect(() => {'
894
+ ).replace(
895
+ /\}\);$/g,
896
+ '}));'
897
+ );
898
+ factoryLines.push(` ${wrappedLine}`);
899
+ }
900
+ }
901
+ factoryLines.push(` },`);
902
+
903
+ // d() - destroy
904
+ factoryLines.push(` d(detaching) {`);
905
+ if (hasEffects) {
906
+ factoryLines.push(` disposers.forEach(d => d());`);
907
+ }
908
+ factoryLines.push(` if (detaching) ${localizeVar(rootVar)}.remove();`);
909
+ factoryLines.push(` }`);
910
+
911
+ factoryLines.push(` };`);
912
+ factoryLines.push(`}`);
913
+
914
+ this._emitBlockFactories.push(factoryLines.join('\n'));
915
+ };
916
+
917
+ // --------------------------------------------------------------------------
918
+ // emitLoop — reactive for-loop with keyed reconciliation
919
+ // --------------------------------------------------------------------------
920
+
921
+ proto.emitLoop = function(sexpr) {
922
+ const [head, vars, collection, guard, step, body] = sexpr;
923
+
924
+ const blockName = this.newBlockVar();
925
+
926
+ const anchorVar = this.newElementVar('anchor');
927
+ this._emitCreateLines.push(`${anchorVar} = document.createComment('for');`);
928
+
929
+ const varNames = Array.isArray(vars) ? vars : [vars];
930
+ const itemVar = varNames[0];
931
+ const indexVar = varNames[1] || 'i';
932
+
933
+ const collectionCode = this.generateInComponent(collection, 'value');
934
+
935
+ // Extract key expression from body if present
936
+ let keyExpr = itemVar;
937
+ if (Array.isArray(body) && body[0] === 'block' && body.length > 1) {
938
+ const firstChild = body[1];
939
+ if (Array.isArray(firstChild)) {
940
+ for (const arg of firstChild) {
941
+ if (Array.isArray(arg) && arg[0] === 'object') {
942
+ for (let i = 1; i < arg.length; i++) {
943
+ const [k, v] = arg[i];
944
+ if (k === 'key') {
945
+ keyExpr = this.generate(v, 'value');
946
+ break;
947
+ }
948
+ }
949
+ }
950
+ if (keyExpr !== itemVar) break;
951
+ }
952
+ }
953
+ }
954
+
955
+ // Save state and generate item template in isolation
956
+ const savedCreateLines = this._emitCreateLines;
957
+ const savedSetupLines = this._emitSetupLines;
958
+
959
+ this._emitCreateLines = [];
960
+ this._emitSetupLines = [];
961
+
962
+ const itemNode = this.emitBlock(body);
963
+ const itemCreateLines = this._emitCreateLines;
964
+ const itemSetupLines = this._emitSetupLines;
965
+
966
+ this._emitCreateLines = savedCreateLines;
967
+ this._emitSetupLines = savedSetupLines;
968
+
969
+ const localizeVar = (line) => {
970
+ // First localize template element refs (this._elN → _elN)
971
+ let result = line.replace(/this\.(_el\d+|_t\d+|_anchor\d+|_frag\d+|_slot\d+|_c\d+|_inst\d+|_empty\d+)/g, '$1');
972
+ // Then replace remaining this. with ctx. (component instance in block context)
973
+ result = result.replace(/\bthis\./g, 'ctx.');
974
+ return result;
975
+ };
976
+
977
+ // Generate block factory
978
+ const factoryLines = [];
979
+ factoryLines.push(`function ${blockName}(ctx, ${itemVar}, ${indexVar}) {`);
980
+
981
+ const localVars = new Set();
982
+ for (const line of itemCreateLines) {
983
+ const match = line.match(/^this\.(_(?:el|t|anchor|frag|slot|c|inst|empty)\d+)\s*=/);
984
+ if (match) localVars.add(match[1]);
985
+ }
986
+ if (localVars.size > 0) {
987
+ factoryLines.push(` let ${[...localVars].join(', ')};`);
988
+ }
989
+
990
+ const hasEffects = itemSetupLines.length > 0;
991
+ if (hasEffects) {
992
+ factoryLines.push(` let disposers = [];`);
993
+ }
994
+
995
+ factoryLines.push(` return {`);
996
+
997
+ // c() - create
998
+ factoryLines.push(` c() {`);
999
+ for (const line of itemCreateLines) {
1000
+ factoryLines.push(` ${localizeVar(line)}`);
1001
+ }
1002
+ factoryLines.push(` },`);
1003
+
1004
+ // m() - mount
1005
+ factoryLines.push(` m(target, anchor) {`);
1006
+ factoryLines.push(` target.insertBefore(${localizeVar(itemNode)}, anchor);`);
1007
+ factoryLines.push(` },`);
1008
+
1009
+ // p() - update
1010
+ factoryLines.push(` p(ctx, ${itemVar}, ${indexVar}) {`);
1011
+ if (hasEffects) {
1012
+ factoryLines.push(` disposers.forEach(d => d());`);
1013
+ factoryLines.push(` disposers = [];`);
1014
+ for (const line of itemSetupLines) {
1015
+ const localizedLine = localizeVar(line);
1016
+ const wrappedLine = localizedLine.replace(
1017
+ /__effect\(\(\) => \{/g,
1018
+ 'disposers.push(__effect(() => {'
1019
+ ).replace(
1020
+ /\}\);$/g,
1021
+ '}));'
1022
+ );
1023
+ factoryLines.push(` ${wrappedLine}`);
1024
+ }
1025
+ }
1026
+ factoryLines.push(` },`);
1027
+
1028
+ // d() - destroy
1029
+ factoryLines.push(` d(detaching) {`);
1030
+ if (hasEffects) {
1031
+ factoryLines.push(` disposers.forEach(d => d());`);
1032
+ }
1033
+ factoryLines.push(` if (detaching) ${localizeVar(itemNode)}.remove();`);
1034
+ factoryLines.push(` }`);
1035
+
1036
+ factoryLines.push(` };`);
1037
+ factoryLines.push(`}`);
1038
+
1039
+ this._emitBlockFactories.push(factoryLines.join('\n'));
1040
+
1041
+ // Generate reconciliation code in _setup()
1042
+ const setupLines = [];
1043
+ setupLines.push(`// Loop: ${blockName}`);
1044
+ setupLines.push(`{`);
1045
+ setupLines.push(` const anchor = ${anchorVar};`);
1046
+ setupLines.push(` const map = new Map();`);
1047
+ setupLines.push(` __effect(() => {`);
1048
+ setupLines.push(` const items = ${collectionCode};`);
1049
+ setupLines.push(` const parent = anchor.parentNode;`);
1050
+ setupLines.push(` const newMap = new Map();`);
1051
+ setupLines.push(``);
1052
+ setupLines.push(` for (let ${indexVar} = 0; ${indexVar} < items.length; ${indexVar}++) {`);
1053
+ setupLines.push(` const ${itemVar} = items[${indexVar}];`);
1054
+ setupLines.push(` const key = ${keyExpr};`);
1055
+ setupLines.push(` let block = map.get(key);`);
1056
+ setupLines.push(` if (block) {`);
1057
+ setupLines.push(` block.p(this, ${itemVar}, ${indexVar});`);
1058
+ setupLines.push(` } else {`);
1059
+ setupLines.push(` block = ${blockName}(this, ${itemVar}, ${indexVar});`);
1060
+ setupLines.push(` block.c();`);
1061
+ setupLines.push(` block.m(parent, anchor);`);
1062
+ setupLines.push(` block.p(this, ${itemVar}, ${indexVar});`);
1063
+ setupLines.push(` }`);
1064
+ setupLines.push(` newMap.set(key, block);`);
1065
+ setupLines.push(` }`);
1066
+ setupLines.push(``);
1067
+ setupLines.push(` for (const [key, block] of map) {`);
1068
+ setupLines.push(` if (!newMap.has(key)) block.d(true);`);
1069
+ setupLines.push(` }`);
1070
+ setupLines.push(``);
1071
+ setupLines.push(` map.clear();`);
1072
+ setupLines.push(` for (const [k, v] of newMap) map.set(k, v);`);
1073
+ setupLines.push(` });`);
1074
+ setupLines.push(`}`);
1075
+
1076
+ this._emitSetupLines.push(setupLines.join('\n '));
1077
+
1078
+ return anchorVar;
1079
+ };
1080
+
1081
+ // --------------------------------------------------------------------------
1082
+ // emitChildComponent — instantiate a child component
1083
+ // --------------------------------------------------------------------------
1084
+
1085
+ proto.emitChildComponent = function(componentName, args) {
1086
+ const instVar = this.newElementVar('inst');
1087
+ const elVar = this.newElementVar('el');
1088
+ const { propsCode, childrenSetupLines } = this.buildComponentProps(args);
1089
+
1090
+ this._emitCreateLines.push(`${instVar} = new ${componentName}(${propsCode});`);
1091
+ this._emitCreateLines.push(`${elVar} = ${instVar}._create();`);
1092
+
1093
+ this._emitSetupLines.push(`if (${instVar}._setup) ${instVar}._setup();`);
1094
+
1095
+ for (const line of childrenSetupLines) {
1096
+ this._emitSetupLines.push(line);
1097
+ }
1098
+
1099
+ return elVar;
1100
+ };
1101
+
1102
+ // --------------------------------------------------------------------------
1103
+ // buildComponentProps — build props object for component instantiation
1104
+ // --------------------------------------------------------------------------
1105
+
1106
+ proto.buildComponentProps = function(args) {
1107
+ const props = [];
1108
+ let childrenVar = null;
1109
+ const childrenSetupLines = [];
1110
+
1111
+ for (const arg of args) {
1112
+ if (Array.isArray(arg) && arg[0] === 'object') {
1113
+ for (let i = 1; i < arg.length; i++) {
1114
+ const [key, value] = arg[i];
1115
+ if (typeof key === 'string') {
1116
+ const valueCode = this.generateInComponent(value, 'value');
1117
+ props.push(`${key}: ${valueCode}`);
1118
+ }
1119
+ }
1120
+ } else if (Array.isArray(arg) && (arg[0] === '->' || arg[0] === '=>')) {
1121
+ const block = arg[2];
1122
+ if (block) {
1123
+ const savedCreateLines = this._emitCreateLines;
1124
+ const savedSetupLines = this._emitSetupLines;
1125
+ this._emitCreateLines = [];
1126
+ this._emitSetupLines = [];
1127
+
1128
+ childrenVar = this.emitBlock(block);
1129
+
1130
+ const childCreateLines = this._emitCreateLines;
1131
+ const childSetupLinesCopy = this._emitSetupLines;
1132
+
1133
+ this._emitCreateLines = savedCreateLines;
1134
+ this._emitSetupLines = savedSetupLines;
1135
+
1136
+ for (const line of childCreateLines) {
1137
+ this._emitCreateLines.push(line);
1138
+ }
1139
+ childrenSetupLines.push(...childSetupLinesCopy);
1140
+ props.push(`children: ${childrenVar}`);
1141
+ }
1142
+ }
1143
+ }
1144
+
1145
+ const propsCode = props.length > 0 ? `{ ${props.join(', ')} }` : '{}';
1146
+ return { propsCode, childrenSetupLines };
1147
+ };
1148
+
1149
+ // --------------------------------------------------------------------------
1150
+ // hasReactiveDeps — check if an s-expression references reactive members
1151
+ // --------------------------------------------------------------------------
1152
+
1153
+ proto.hasReactiveDeps = function(sexpr) {
1154
+ if (!this.reactiveMembers || this.reactiveMembers.size === 0) return false;
1155
+
1156
+ if (typeof sexpr === 'string') {
1157
+ return this.reactiveMembers.has(sexpr);
1158
+ }
1159
+
1160
+ if (!Array.isArray(sexpr)) return false;
1161
+
1162
+ if (sexpr[0] === '.' && sexpr[1] === 'this' && typeof sexpr[2] === 'string') {
1163
+ return this.reactiveMembers.has(sexpr[2]);
1164
+ }
1165
+
1166
+ for (const child of sexpr) {
1167
+ if (this.hasReactiveDeps(child)) return true;
1168
+ }
1169
+
1170
+ return false;
1171
+ };
1172
+
1173
+ // ==========================================================================
1174
+ // Component Runtime
1175
+ // ==========================================================================
1176
+
1177
+ /**
1178
+ * Returns runtime code for the component system.
1179
+ * Only emitted when `component` keyword is used (this.usesTemplates === true).
1180
+ */
1181
+ proto.getComponentRuntime = function() {
1182
+ return `
1183
+ // ============================================================================
1184
+ // Rip Component Runtime
1185
+ // ============================================================================
1186
+
1187
+ function isSignal(v) {
1188
+ return v != null && typeof v === 'object' && typeof v.read === 'function';
1189
+ }
1190
+
1191
+ let __currentComponent = null;
1192
+
1193
+ function __pushComponent(component) {
1194
+ component._parent = __currentComponent;
1195
+ const prev = __currentComponent;
1196
+ __currentComponent = component;
1197
+ return prev;
1198
+ }
1199
+
1200
+ function __popComponent(prev) {
1201
+ __currentComponent = prev;
1202
+ }
1203
+
1204
+ function setContext(key, value) {
1205
+ if (!__currentComponent) throw new Error('setContext must be called during component initialization');
1206
+ if (!__currentComponent._context) __currentComponent._context = new Map();
1207
+ __currentComponent._context.set(key, value);
1208
+ }
1209
+
1210
+ function getContext(key) {
1211
+ let component = __currentComponent;
1212
+ while (component) {
1213
+ if (component._context && component._context.has(key)) return component._context.get(key);
1214
+ component = component._parent;
1215
+ }
1216
+ return undefined;
1217
+ }
1218
+
1219
+ function hasContext(key) {
1220
+ let component = __currentComponent;
1221
+ while (component) {
1222
+ if (component._context && component._context.has(key)) return true;
1223
+ component = component._parent;
1224
+ }
1225
+ return false;
1226
+ }
1227
+
1228
+ function __cx__(...args) {
1229
+ return args.filter(Boolean).join(' ');
1230
+ }
1231
+
1232
+ // Register on globalThis for runtime deduplication
1233
+ if (typeof globalThis !== 'undefined') {
1234
+ globalThis.__ripComponent = { isSignal, __pushComponent, __popComponent, setContext, getContext, hasContext, __cx__ };
1235
+ }
1236
+
1237
+ `;
1238
+ };
1239
+ }