@mkdevofficial/liquid-compiler 0.0.0-stage → 0.1.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/package.json CHANGED
@@ -1,6 +1,24 @@
1
1
  {
2
2
  "name": "@mkdevofficial/liquid-compiler",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "🫟 Compiler for the Liquid frontend framework",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "src"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "keywords": [
17
+ "liquid",
18
+ "compiler",
19
+ "reactive",
20
+ "signals",
21
+ "parser"
22
+ ],
23
+ "license": "MIT"
24
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Analyzer: scope resolution, component resolution, scoped style hashing, warnings.
3
+ */
4
+
5
+ // Simple deterministic string hash for scope attributes
6
+ export function hashString(str) {
7
+ let hash = 5381;
8
+ let i = str.length;
9
+ while (i) {
10
+ hash = (hash * 33) ^ str.charCodeAt(--i);
11
+ }
12
+ return (hash >>> 0).toString(36).slice(0, 8);
13
+ }
14
+
15
+ export function analyze(ast, options = {}) {
16
+ const file = options.file || 'Component.liq';
17
+ const warnings = [];
18
+ const referencedComponents = new Set();
19
+ const explicitImports = new Set();
20
+
21
+ // Scan explicit imports in logic section
22
+ const logicContent = ast.logic.content || '';
23
+ const importRegex = /import\s+(?:([a-zA-Z_$][a-zA-Z0-9_$]*)|(?:\{([^}]+)\}))\s+from\s+['"][^'"]+['"]/g;
24
+ let match;
25
+ while ((match = importRegex.exec(logicContent)) !== null) {
26
+ if (match[1]) {
27
+ explicitImports.add(match[1].trim());
28
+ }
29
+ if (match[2]) {
30
+ const named = match[2].split(',').map(s => s.trim().split(/\s+as\s+/)[0]);
31
+ for (const n of named) {
32
+ if (n) explicitImports.add(n);
33
+ }
34
+ }
35
+ }
36
+
37
+ // Traverse template
38
+ function visit(node) {
39
+ if (!node) return;
40
+
41
+ if (node.type === 'Element') {
42
+ if (node.isComponent) {
43
+ referencedComponents.add(node.tag);
44
+ }
45
+ for (const child of node.children) {
46
+ visit(child);
47
+ }
48
+ } else if (node.type === 'IfBlock') {
49
+ for (const c of node.consequent) visit(c);
50
+ if (node.alternate) {
51
+ if (Array.isArray(node.alternate)) {
52
+ for (const a of node.alternate) visit(a);
53
+ } else {
54
+ visit(node.alternate);
55
+ }
56
+ }
57
+ } else if (node.type === 'EachBlock') {
58
+ if (!node.key) {
59
+ warnings.push({
60
+ message: `'each' block for '${node.collection}' does not have a key expression. Provide a key like '(${node.item}.id)' for optimal reconciliation.`,
61
+ loc: node.loc
62
+ });
63
+ }
64
+ for (const child of node.children) {
65
+ visit(child);
66
+ }
67
+ }
68
+ }
69
+
70
+ for (const node of ast.template) {
71
+ visit(node);
72
+ }
73
+
74
+ // Components that are referenced but not explicitly imported
75
+ const autoImportComponents = [];
76
+ for (const comp of referencedComponents) {
77
+ if (!explicitImports.has(comp)) {
78
+ autoImportComponents.push(comp);
79
+ }
80
+ }
81
+
82
+ // Generate unique scoped attribute
83
+ const scopeId = hashString(file + (ast.styles.content || '') + (ast.logic.content || ''));
84
+ const scopeAttr = `data-l-${scopeId}`;
85
+
86
+ return {
87
+ scopeId,
88
+ scopeAttr,
89
+ referencedComponents: Array.from(referencedComponents),
90
+ autoImportComponents,
91
+ warnings
92
+ };
93
+ }
package/src/codegen.js ADDED
@@ -0,0 +1,581 @@
1
+ import { SourceMapGenerator } from './sourcemap.js';
2
+
3
+ /**
4
+ * Generate readable ES module code from Liquid AST and Analysis.
5
+ */
6
+ export function generate(ast, analysis, options = {}) {
7
+ const file = options.file || 'Component.liq';
8
+ const componentName = options.name || getComponentNameFromFile(file);
9
+ const runtimeModule = options.runtimeModule || 'liquid';
10
+ const scopeAttr = analysis.scopeAttr;
11
+
12
+ const mapGen = new SourceMapGenerator({
13
+ file: `${componentName}.js`,
14
+ sourceFile: file
15
+ });
16
+
17
+ const imports = [];
18
+ const autoImports = [];
19
+ const bodyLines = [];
20
+ let varCounter = 0;
21
+
22
+ function nextVar(prefix = '_v') {
23
+ return `${prefix}${varCounter++}`;
24
+ }
25
+
26
+ // Runtime helper tracking
27
+ const usedRuntimeHelpers = new Set(['element', 'text']);
28
+
29
+ // Separate logic into top-level imports and component body
30
+ const rawLogic = ast.logic.content || '';
31
+ const logicLines = rawLogic.split(/\r?\n/);
32
+ const userImports = [];
33
+ const userBody = [];
34
+
35
+ let inMultiLineImport = false;
36
+ for (let idx = 0; idx < logicLines.length; idx++) {
37
+ const line = logicLines[idx];
38
+ const trimmed = line.trim();
39
+
40
+ if (inMultiLineImport) {
41
+ userImports.push(line);
42
+ if (/['"][^'"]+['"]\s*;?$/.test(trimmed)) {
43
+ inMultiLineImport = false;
44
+ }
45
+ } else if (trimmed.startsWith('import ') || trimmed.startsWith('import{')) {
46
+ userImports.push(line);
47
+ if (!/['"][^'"]+['"]\s*;?$/.test(trimmed)) {
48
+ inMultiLineImport = true;
49
+ }
50
+ } else {
51
+ userBody.push({ text: line, lineNum: ast.logic.line + idx });
52
+ }
53
+ }
54
+
55
+ // Handle auto-imported components
56
+ if (options.resolveComponent) {
57
+ for (const comp of analysis.autoImportComponents) {
58
+ const resolved = options.resolveComponent(comp, file);
59
+ if (resolved) {
60
+ autoImports.push(resolved);
61
+ }
62
+ }
63
+ } else {
64
+ for (const comp of analysis.autoImportComponents) {
65
+ autoImports.push(`import ${comp} from './${comp}.liq';`);
66
+ }
67
+ }
68
+
69
+ // Generate DOM tree code
70
+ const rootVar = '_root';
71
+ bodyLines.push(` const ${rootVar} = [];`);
72
+
73
+ for (const node of ast.template) {
74
+ generateNode(node, rootVar, true);
75
+ }
76
+
77
+ bodyLines.push(` return ${rootVar}.length === 1 ? ${rootVar}[0] : ${rootVar};`);
78
+
79
+ // Build final code
80
+ const outputLines = [];
81
+
82
+ // 1. Runtime imports
83
+ outputLines.push(`import { ${Array.from(usedRuntimeHelpers).join(', ')} } from '${runtimeModule}';`);
84
+
85
+ // 2. User & Auto imports
86
+ if (userImports.length > 0) {
87
+ outputLines.push(userImports.join('\n'));
88
+ }
89
+ if (autoImports.length > 0) {
90
+ outputLines.push(autoImports.join('\n'));
91
+ }
92
+
93
+ outputLines.push('');
94
+
95
+ // 3. Component function definition
96
+ const funcHeaderLine = outputLines.length + 1;
97
+ outputLines.push(`export default function ${componentName}(props = {}, context = {}) {`);
98
+
99
+ // Inject props accessor helper
100
+ outputLines.push(` function defineProps(defaults = {}) { return Object.assign({}, defaults, props); }`);
101
+
102
+ // User logic body
103
+ for (const item of userBody) {
104
+ const genLine = outputLines.length + 1;
105
+ outputLines.push(` ${item.text}`);
106
+ if (item.text.trim()) {
107
+ mapGen.addMapping({
108
+ generatedLine: genLine,
109
+ generatedColumn: 2,
110
+ originalLine: item.lineNum,
111
+ originalColumn: 1
112
+ });
113
+ }
114
+ }
115
+
116
+ outputLines.push('');
117
+
118
+ // UI DOM creation body
119
+ for (const line of bodyLines) {
120
+ outputLines.push(line);
121
+ }
122
+
123
+ outputLines.push('}');
124
+
125
+ const finalCode = outputLines.join('\n');
126
+ const sourceMap = mapGen.generate();
127
+
128
+ return {
129
+ code: finalCode,
130
+ map: sourceMap
131
+ };
132
+
133
+ /**
134
+ * Helper to recursively generate node creation instructions.
135
+ */
136
+ function generateNode(node, parentVar, isRoot = false) {
137
+ if (!node) return;
138
+
139
+ if (node.type === 'Text') {
140
+ generateTextNode(node, parentVar, isRoot);
141
+ } else if (node.type === 'Element') {
142
+ if (node.isComponent) {
143
+ generateComponentNode(node, parentVar, isRoot);
144
+ } else {
145
+ generateElementNode(node, parentVar, isRoot);
146
+ }
147
+ } else if (node.type === 'IfBlock') {
148
+ generateIfBlock(node, parentVar, isRoot);
149
+ } else if (node.type === 'EachBlock') {
150
+ generateEachBlock(node, parentVar, isRoot);
151
+ } else if (node.type === 'Slot') {
152
+ generateSlotNode(node, parentVar, isRoot);
153
+ }
154
+ }
155
+
156
+ function generateTextNode(node, parentVar, isRoot) {
157
+ // If text has only one static raw part
158
+ if (node.parts.length === 1 && node.parts[0].type === 'raw') {
159
+ const tVar = nextVar('_t');
160
+ bodyLines.push(` const ${tVar} = text(${JSON.stringify(node.parts[0].value)});`);
161
+ appendOrPush(parentVar, tVar, isRoot);
162
+ return;
163
+ }
164
+
165
+ // Dynamic text parts
166
+ for (const part of node.parts) {
167
+ const tVar = nextVar('_t');
168
+ if (part.type === 'raw') {
169
+ bodyLines.push(` const ${tVar} = text(${JSON.stringify(part.value)});`);
170
+ } else {
171
+ usedRuntimeHelpers.add('bindText');
172
+ bodyLines.push(` const ${tVar} = text('');`);
173
+ bodyLines.push(` bindText(${tVar}, () => (${part.value}));`);
174
+ }
175
+ appendOrPush(parentVar, tVar, isRoot);
176
+ }
177
+ }
178
+
179
+ function generateElementNode(node, parentVar, isRoot) {
180
+ const elVar = nextVar('_el');
181
+ const classList = node.classes.length > 0 ? JSON.stringify(node.classes) : 'null';
182
+ bodyLines.push(` const ${elVar} = element(${JSON.stringify(node.tag)}, ${JSON.stringify(scopeAttr)}, ${classList});`);
183
+
184
+ if (node.id) {
185
+ bodyLines.push(` ${elVar}.id = ${JSON.stringify(node.id)};`);
186
+ }
187
+
188
+ // Static and dynamic attributes
189
+ for (const attr of node.attributes) {
190
+ if (attr.isBoolean) {
191
+ bodyLines.push(` ${elVar}.setAttribute(${JSON.stringify(attr.name)}, '');`);
192
+ } else if (!attr.isDynamic) {
193
+ if (attr.name === 'class' && node.classes.length > 0) {
194
+ const combined = node.classes.join(' ') + (attr.value ? ' ' + attr.value : '');
195
+ bodyLines.push(` ${elVar}.setAttribute('class', ${JSON.stringify(combined)});`);
196
+ } else {
197
+ bodyLines.push(` ${elVar}.setAttribute(${JSON.stringify(attr.name)}, ${JSON.stringify(attr.value)});`);
198
+ }
199
+ } else {
200
+ usedRuntimeHelpers.add('bindAttr');
201
+ if (attr.name === 'class' && node.classes.length > 0) {
202
+ const baseClasses = JSON.stringify(node.classes.join(' '));
203
+ bodyLines.push(` bindAttr(${elVar}, "class", () => (${baseClasses} + ((${attr.value}) ? ' ' + (${attr.value}) : '')));`);
204
+ } else {
205
+ bodyLines.push(` bindAttr(${elVar}, ${JSON.stringify(attr.name)}, () => (${attr.value}));`);
206
+ }
207
+ }
208
+ }
209
+
210
+ // Event listeners
211
+ for (const ev of node.events) {
212
+ usedRuntimeHelpers.add('listen');
213
+ const mods = ev.modifiers.length > 0 ? JSON.stringify(ev.modifiers) : '[]';
214
+ const rawHandler = (ev.handler || '').trim();
215
+ // If handler is empty, use a no-op function (common with standalone modifiers like @click.stop)
216
+ const handlerExpr = !rawHandler
217
+ ? '() => {}'
218
+ : (/[\(\)=]/.test(rawHandler) && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(rawHandler)
219
+ ? `(e) => { ${rawHandler}; }`
220
+ : rawHandler);
221
+ bodyLines.push(` listen(${elVar}, ${JSON.stringify(ev.name)}, ${handlerExpr}, ${mods});`);
222
+ }
223
+
224
+ // Two-way bindings
225
+ for (const bind of node.bindings) {
226
+ usedRuntimeHelpers.add('bindInput');
227
+ bodyLines.push(` bindInput(${elVar}, ${bind.target}, ${JSON.stringify(bind.prop)});`);
228
+ }
229
+
230
+ // Children
231
+ for (const child of node.children) {
232
+ generateNode(child, elVar, false);
233
+ }
234
+
235
+ appendOrPush(parentVar, elVar, isRoot);
236
+ }
237
+
238
+ function generateComponentNode(node, parentVar, isRoot) {
239
+ usedRuntimeHelpers.add('append');
240
+ const compVar = nextVar('_c');
241
+ const propsObj = [];
242
+
243
+ for (const attr of node.attributes) {
244
+ const val = attr.isDynamic ? attr.value : JSON.stringify(attr.value);
245
+ propsObj.push(`${JSON.stringify(attr.name)}: ${val}`);
246
+ }
247
+
248
+ // Default slot content if children exist
249
+ if (node.children.length > 0) {
250
+ const slotChildrenVar = nextVar('_slot');
251
+ const slotBody = [];
252
+ slotBody.push(`() => {`);
253
+ slotBody.push(` const ${slotChildrenVar} = [];`);
254
+
255
+ // Temporary redirect
256
+ const origBodyLines = bodyLines;
257
+ const subLines = [];
258
+ // Replace bodyLines temporarily
259
+ // We will generate children into subLines
260
+ generateSubtreeInto(node.children, slotChildrenVar, subLines);
261
+ slotBody.push(...subLines.map(l => ' ' + l));
262
+ slotBody.push(` return ${slotChildrenVar};`);
263
+ slotBody.push(`}`);
264
+
265
+ propsObj.push(`default: ${slotBody.join('\n')}`);
266
+ }
267
+
268
+ bodyLines.push(` const ${compVar} = ${node.tag}({ ${propsObj.join(', ')} });`);
269
+ if (isRoot) {
270
+ bodyLines.push(` if (Array.isArray(${compVar})) ${parentVar}.push(...${compVar}); else if (${compVar}) ${parentVar}.push(${compVar});`);
271
+ } else {
272
+ bodyLines.push(` if (Array.isArray(${compVar})) append(${parentVar}, ...${compVar}); else if (${compVar}) append(${parentVar}, ${compVar});`);
273
+ }
274
+ }
275
+
276
+ function generateIfBlock(node, parentVar, isRoot) {
277
+ usedRuntimeHelpers.add('createIfBlock');
278
+ usedRuntimeHelpers.add('append');
279
+ const anchorVar = nextVar('_anchor');
280
+ bodyLines.push(` const ${anchorVar} = document.createComment('if');`);
281
+ appendOrPush(parentVar, anchorVar, isRoot);
282
+
283
+ const thenNodesVar = nextVar('_thenNodes');
284
+ const thenSubLines = [];
285
+ generateSubtreeInto(node.consequent, thenNodesVar, thenSubLines);
286
+
287
+ let elseSubLines = null;
288
+ let elseNodesVar = null;
289
+ if (node.alternate) {
290
+ elseNodesVar = nextVar('_elseNodes');
291
+ elseSubLines = [];
292
+ const altNodes = Array.isArray(node.alternate) ? node.alternate : [node.alternate];
293
+ generateSubtreeInto(altNodes, elseNodesVar, elseSubLines);
294
+ }
295
+
296
+ const thenFn = `() => {\n const ${thenNodesVar} = [];\n${thenSubLines.map(l => ' ' + l).join('\n')}\n return ${thenNodesVar};\n }`;
297
+ const elseFn = elseSubLines
298
+ ? `() => {\n const ${elseNodesVar} = [];\n${elseSubLines.map(l => ' ' + l).join('\n')}\n return ${elseNodesVar};\n }`
299
+ : 'null';
300
+
301
+ bodyLines.push(` createIfBlock(${anchorVar}, () => (${node.test}), ${thenFn}, ${elseFn});`);
302
+ }
303
+
304
+ function generateEachBlock(node, parentVar, isRoot) {
305
+ usedRuntimeHelpers.add('createEachBlock');
306
+ usedRuntimeHelpers.add('append');
307
+ const anchorVar = nextVar('_anchor');
308
+ bodyLines.push(` const ${anchorVar} = document.createComment('each');`);
309
+ appendOrPush(parentVar, anchorVar, isRoot);
310
+
311
+ const itemArg = node.item;
312
+ const indexArg = node.index || '_idx';
313
+ const keyExpr = node.key ? `(${itemArg}, ${indexArg}) => (${node.key})` : `(${itemArg}, ${indexArg}) => ${indexArg}`;
314
+
315
+ const eachNodesVar = nextVar('_eachNodes');
316
+ const eachSubLines = [];
317
+ generateSubtreeInto(node.children, eachNodesVar, eachSubLines);
318
+
319
+ // Create item proxy so item.prop, item(), and primitive {item} both work cleanly!
320
+ const itemProxySetup = ` const ${itemArg} = new Proxy(${itemArg}Sig, {
321
+ get(target, prop) {
322
+ if (prop === 'isSignal') return true;
323
+ if (prop === Symbol.toPrimitive || prop === 'valueOf' || prop === 'toString') {
324
+ return () => target();
325
+ }
326
+ const val = target();
327
+ if (val != null && prop in Object(val)) {
328
+ const res = val[prop];
329
+ return typeof res === 'function' ? res.bind(val) : res;
330
+ }
331
+ return val;
332
+ },
333
+ apply(target) { return target(); }
334
+ });
335
+ const ${indexArg} = new Proxy(${indexArg}Sig, {
336
+ apply(target) { return target(); },
337
+ get(target, prop) {
338
+ if (prop === Symbol.toPrimitive || prop === 'valueOf' || prop === 'toString') return () => target();
339
+ const val = target();
340
+ if (val != null && prop in Object(val)) {
341
+ const res = val[prop];
342
+ return typeof res === 'function' ? res.bind(val) : res;
343
+ }
344
+ return val;
345
+ }
346
+ });`;
347
+
348
+ bodyLines.push(` createEachBlock(${anchorVar}, () => (${node.collection}), ${keyExpr}, (${itemArg}Sig, ${indexArg}Sig) => {
349
+ ${itemProxySetup}
350
+ const ${eachNodesVar} = [];
351
+ ${eachSubLines.map(l => ' ' + l).join('\n')}
352
+ return ${eachNodesVar};
353
+ });`);
354
+ }
355
+
356
+ function generateSlotNode(node, parentVar, isRoot) {
357
+ usedRuntimeHelpers.add('createSlot');
358
+ usedRuntimeHelpers.add('append');
359
+ const slotVar = nextVar('_slot');
360
+ const slotName = node.name ? JSON.stringify(node.name) : "'default'";
361
+ let fallbackFn = 'null';
362
+
363
+ if (node.fallback.length > 0) {
364
+ const fbVar = nextVar('_fb');
365
+ const fbLines = [];
366
+ generateSubtreeInto(node.fallback, fbVar, fbLines);
367
+ fallbackFn = `() => {\n const ${fbVar} = [];\n${fbLines.map(l => ' ' + l).join('\n')}\n return ${fbVar};\n }`;
368
+ }
369
+
370
+ bodyLines.push(` const ${slotVar} = createSlot(props, ${slotName}, ${fallbackFn});`);
371
+ if (isRoot) {
372
+ bodyLines.push(` ${parentVar}.push(...${slotVar});`);
373
+ } else {
374
+ bodyLines.push(` append(${parentVar}, ...${slotVar});`);
375
+ }
376
+ }
377
+
378
+ function generateSubtreeInto(nodes, containerVar, lineBuffer) {
379
+ for (const n of nodes) {
380
+ generateSubNode(n, containerVar, true, lineBuffer);
381
+ }
382
+ }
383
+
384
+ function generateSubNode(n, parent, isRootSub, buf) {
385
+ if (!n) return;
386
+ if (n.type === 'Text') {
387
+ if (n.parts.length === 1 && n.parts[0].type === 'raw') {
388
+ const tVar = nextVar('_t');
389
+ buf.push(`const ${tVar} = text(${JSON.stringify(n.parts[0].value)});`);
390
+ if (isRootSub) {
391
+ buf.push(`${parent}.push(${tVar});`);
392
+ } else {
393
+ buf.push(`${parent}.appendChild(${tVar});`);
394
+ }
395
+ } else {
396
+ for (const p of n.parts) {
397
+ const tVar = nextVar('_t');
398
+ if (p.type === 'raw') {
399
+ buf.push(`const ${tVar} = text(${JSON.stringify(p.value)});`);
400
+ } else {
401
+ usedRuntimeHelpers.add('bindText');
402
+ buf.push(`const ${tVar} = text('');`);
403
+ buf.push(`bindText(${tVar}, () => (${p.value}));`);
404
+ }
405
+ if (isRootSub) {
406
+ buf.push(`${parent}.push(${tVar});`);
407
+ } else {
408
+ buf.push(`${parent}.appendChild(${tVar});`);
409
+ }
410
+ }
411
+ }
412
+ } else if (n.type === 'Element') {
413
+ if (n.isComponent) {
414
+ usedRuntimeHelpers.add('append');
415
+ const compVar = nextVar('_c');
416
+ const propsObj = [];
417
+ for (const attr of n.attributes) {
418
+ const val = attr.isDynamic ? attr.value : JSON.stringify(attr.value);
419
+ propsObj.push(`${JSON.stringify(attr.name)}: ${val}`);
420
+ }
421
+ if (n.children.length > 0) {
422
+ const slotChildrenVar = nextVar('_slot');
423
+ const slotBody = [];
424
+ slotBody.push(`() => {`);
425
+ slotBody.push(` const ${slotChildrenVar} = [];`);
426
+ const subLines = [];
427
+ generateSubtreeInto(n.children, slotChildrenVar, subLines);
428
+ slotBody.push(...subLines.map(l => ' ' + l));
429
+ slotBody.push(` return ${slotChildrenVar};`);
430
+ slotBody.push(`}`);
431
+ propsObj.push(`default: ${slotBody.join('\n')}`);
432
+ }
433
+ buf.push(`const ${compVar} = ${n.tag}({ ${propsObj.join(', ')} });`);
434
+ if (isRootSub) {
435
+ buf.push(`if (Array.isArray(${compVar})) ${parent}.push(...${compVar}); else if (${compVar}) ${parent}.push(${compVar});`);
436
+ } else {
437
+ buf.push(`if (Array.isArray(${compVar})) append(${parent}, ...${compVar}); else if (${compVar}) append(${parent}, ${compVar});`);
438
+ }
439
+ } else {
440
+ const elVar = nextVar('_el');
441
+ const classList = n.classes.length > 0 ? JSON.stringify(n.classes) : 'null';
442
+ buf.push(`const ${elVar} = element(${JSON.stringify(n.tag)}, ${JSON.stringify(scopeAttr)}, ${classList});`);
443
+ if (n.id) buf.push(`${elVar}.id = ${JSON.stringify(n.id)};`);
444
+
445
+ for (const attr of n.attributes) {
446
+ if (attr.isBoolean) {
447
+ buf.push(`${elVar}.setAttribute(${JSON.stringify(attr.name)}, '');`);
448
+ } else if (!attr.isDynamic) {
449
+ if (attr.name === 'class' && n.classes.length > 0) {
450
+ const combined = n.classes.join(' ') + (attr.value ? ' ' + attr.value : '');
451
+ buf.push(`${elVar}.setAttribute('class', ${JSON.stringify(combined)});`);
452
+ } else {
453
+ buf.push(`${elVar}.setAttribute(${JSON.stringify(attr.name)}, ${JSON.stringify(attr.value)});`);
454
+ }
455
+ } else {
456
+ usedRuntimeHelpers.add('bindAttr');
457
+ if (attr.name === 'class' && n.classes.length > 0) {
458
+ const baseClasses = JSON.stringify(n.classes.join(' '));
459
+ buf.push(`bindAttr(${elVar}, "class", () => (${baseClasses} + ((${attr.value}) ? ' ' + (${attr.value}) : '')));`);
460
+ } else {
461
+ buf.push(`bindAttr(${elVar}, ${JSON.stringify(attr.name)}, () => (${attr.value}));`);
462
+ }
463
+ }
464
+ }
465
+ for (const ev of n.events) {
466
+ usedRuntimeHelpers.add('listen');
467
+ const mods = ev.modifiers.length > 0 ? JSON.stringify(ev.modifiers) : '[]';
468
+ const rawHandler = (ev.handler || '').trim();
469
+ const handlerExpr = !rawHandler
470
+ ? '() => {}'
471
+ : (/[\(\)=]/.test(rawHandler) && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(rawHandler)
472
+ ? `(e) => { ${rawHandler}; }`
473
+ : rawHandler);
474
+ buf.push(`listen(${elVar}, ${JSON.stringify(ev.name)}, ${handlerExpr}, ${mods});`);
475
+ }
476
+ for (const bind of n.bindings) {
477
+ usedRuntimeHelpers.add('bindInput');
478
+ buf.push(`bindInput(${elVar}, ${bind.target}, ${JSON.stringify(bind.prop)});`);
479
+ }
480
+ for (const child of n.children) {
481
+ const childBuf = [];
482
+ generateSubNode(child, elVar, false, childBuf);
483
+ buf.push(...childBuf);
484
+ }
485
+ if (isRootSub) {
486
+ buf.push(`${parent}.push(${elVar});`);
487
+ } else {
488
+ buf.push(`${parent}.appendChild(${elVar});`);
489
+ }
490
+ }
491
+ } else if (n.type === 'IfBlock') {
492
+ usedRuntimeHelpers.add('createIfBlock');
493
+ const anchorVar = nextVar('_anchor');
494
+ buf.push(`const ${anchorVar} = document.createComment('if');`);
495
+ if (isRootSub) {
496
+ buf.push(`${parent}.push(${anchorVar});`);
497
+ } else {
498
+ buf.push(`${parent}.appendChild(${anchorVar});`);
499
+ }
500
+ const thenNodesVar = nextVar('_thenNodes');
501
+ const thenSub = [];
502
+ generateSubtreeInto(n.consequent, thenNodesVar, thenSub);
503
+ let elseSub = null;
504
+ let elseNodesVar = null;
505
+ if (n.alternate) {
506
+ elseNodesVar = nextVar('_elseNodes');
507
+ const altNodes = Array.isArray(n.alternate) ? n.alternate : [n.alternate];
508
+ elseSub = [];
509
+ generateSubtreeInto(altNodes, elseNodesVar, elseSub);
510
+ }
511
+ const thenFn = `() => {\n const ${thenNodesVar} = [];\n${thenSub.map(l => ' ' + l).join('\n')}\n return ${thenNodesVar};\n }`;
512
+ const elseFn = elseSub
513
+ ? `() => {\n const ${elseNodesVar} = [];\n${elseSub.map(l => ' ' + l).join('\n')}\n return ${elseNodesVar};\n }`
514
+ : 'null';
515
+ buf.push(`createIfBlock(${anchorVar}, () => (${n.test}), ${thenFn}, ${elseFn});`);
516
+ } else if (n.type === 'EachBlock') {
517
+ usedRuntimeHelpers.add('createEachBlock');
518
+ const anchorVar = nextVar('_anchor');
519
+ buf.push(`const ${anchorVar} = document.createComment('each');`);
520
+ if (isRootSub) {
521
+ buf.push(`${parent}.push(${anchorVar});`);
522
+ } else {
523
+ buf.push(`${parent}.appendChild(${anchorVar});`);
524
+ }
525
+ const itemArg = n.item;
526
+ const indexArg = n.index || '_idx';
527
+ const keyExpr = n.key ? `(${itemArg}, ${indexArg}) => (${n.key})` : `(${itemArg}, ${indexArg}) => ${indexArg}`;
528
+ const eachNodesVar = nextVar('_eachNodes');
529
+ const eachSub = [];
530
+ generateSubtreeInto(n.children, eachNodesVar, eachSub);
531
+
532
+ const itemProxySetup = ` const ${itemArg} = new Proxy(${itemArg}Sig, {
533
+ get(target, prop) {
534
+ if (prop === 'isSignal') return true;
535
+ if (prop === Symbol.toPrimitive || prop === 'valueOf' || prop === 'toString') {
536
+ return () => target();
537
+ }
538
+ const val = target();
539
+ if (val != null && prop in Object(val)) {
540
+ const res = val[prop];
541
+ return typeof res === 'function' ? res.bind(val) : res;
542
+ }
543
+ return val;
544
+ },
545
+ apply(target) { return target(); }
546
+ });
547
+ const ${indexArg} = new Proxy(${indexArg}Sig, {
548
+ apply(target) { return target(); },
549
+ get(target, prop) {
550
+ if (prop === Symbol.toPrimitive || prop === 'valueOf' || prop === 'toString') return () => target();
551
+ const val = target();
552
+ if (val != null && prop in Object(val)) {
553
+ const res = val[prop];
554
+ return typeof res === 'function' ? res.bind(val) : res;
555
+ }
556
+ return val;
557
+ }
558
+ });`;
559
+
560
+ buf.push(`createEachBlock(${anchorVar}, () => (${n.collection}), ${keyExpr}, (${itemArg}Sig, ${indexArg}Sig) => {
561
+ ${itemProxySetup}
562
+ const ${eachNodesVar} = [];
563
+ ${eachSub.map(l => ' ' + l).join('\n')}
564
+ return ${eachNodesVar};
565
+ });`);
566
+ }
567
+ }
568
+
569
+ function appendOrPush(parent, childVar, isRoot) {
570
+ if (isRoot) {
571
+ bodyLines.push(` ${parent}.push(${childVar});`);
572
+ } else {
573
+ bodyLines.push(` ${parent}.appendChild(${childVar});`);
574
+ }
575
+ }
576
+ }
577
+
578
+ function getComponentNameFromFile(filePath) {
579
+ const base = filePath.split(/[/\\]/).pop().replace(/\.liq$/, '');
580
+ return base.replace(/[^a-zA-Z0-9_$]/g, '_') || 'Component';
581
+ }