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.
- package/CHANGELOG.md +47 -5
- package/README.md +48 -3
- package/bin/rip +24 -2
- package/docs/RIP-INTERNALS.md +22 -14
- package/docs/RIP-LANG.md +48 -8
- package/docs/RIP-REACTIVITY.md +21 -0
- package/docs/RIP-TYPES.md +1345 -595
- package/docs/dist/rip.browser.js +2281 -345
- package/docs/dist/rip.browser.min.js +283 -207
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/package.json +6 -2
- package/src/browser.js +1 -1
- package/src/compiler.js +80 -9
- package/src/components.js +1239 -0
- package/src/grammar/grammar.rip +47 -1
- package/src/grammar/solar.rip +11 -11
- package/src/lexer.js +300 -4
- package/src/parser.js +217 -214
- package/src/repl.js +53 -1
- package/src/tags.js +62 -0
- package/src/types.js +718 -0
|
@@ -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
|
+
}
|