@matthesketh/utopia-compiler 0.1.0 → 0.3.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/dist/index.cjs CHANGED
@@ -245,7 +245,7 @@ var TemplateParser = class {
245
245
  if (this.lookingAt("{{")) {
246
246
  flush();
247
247
  this.pos += 2;
248
- const endIdx = this.source.indexOf("}}", this.pos);
248
+ const endIdx = this.findInterpolationEnd(this.pos);
249
249
  if (endIdx === -1) throw this.error("Unterminated interpolation {{ }}");
250
250
  const expression = this.source.slice(this.pos, endIdx).trim();
251
251
  nodes.push({ type: 3 /* Interpolation */, expression });
@@ -257,6 +257,39 @@ var TemplateParser = class {
257
257
  flush();
258
258
  return nodes;
259
259
  }
260
+ // ---- Interpolation end finder -------------------------------------------
261
+ /**
262
+ * Find the position of the closing `}}` for an interpolation expression,
263
+ * respecting JavaScript string literals so that `}}` inside quotes is not
264
+ * treated as the end delimiter.
265
+ *
266
+ * Handles single-quoted, double-quoted, and template literal strings,
267
+ * including escaped characters within them.
268
+ */
269
+ findInterpolationEnd(start) {
270
+ let pos = start;
271
+ let inSingle = false;
272
+ let inDouble = false;
273
+ let inBacktick = false;
274
+ while (pos < this.source.length - 1) {
275
+ const ch = this.source[pos];
276
+ if ((inSingle || inDouble || inBacktick) && ch === "\\") {
277
+ pos += 2;
278
+ continue;
279
+ }
280
+ if (!inDouble && !inBacktick && ch === "'") {
281
+ inSingle = !inSingle;
282
+ } else if (!inSingle && !inBacktick && ch === '"') {
283
+ inDouble = !inDouble;
284
+ } else if (!inSingle && !inDouble && ch === "`") {
285
+ inBacktick = !inBacktick;
286
+ } else if (!inSingle && !inDouble && !inBacktick && ch === "}" && this.source[pos + 1] === "}") {
287
+ return pos;
288
+ }
289
+ pos++;
290
+ }
291
+ return -1;
292
+ }
260
293
  // ---- Low-level helpers --------------------------------------------------
261
294
  readTagName() {
262
295
  const start = this.pos;
@@ -306,7 +339,9 @@ var TemplateParser = class {
306
339
  }
307
340
  expect(str) {
308
341
  if (!this.lookingAt(str)) {
309
- throw this.error(`Expected "${str}" but found "${this.source.slice(this.pos, this.pos + 20)}"`);
342
+ throw this.error(
343
+ `Expected "${str}" but found "${this.source.slice(this.pos, this.pos + 20)}"`
344
+ );
310
345
  }
311
346
  this.pos += str.length;
312
347
  }
@@ -370,7 +405,7 @@ function classifyDirective(name, value) {
370
405
  return null;
371
406
  }
372
407
  function isDirectiveKind(s) {
373
- return s === "on" || s === "bind" || s === "if" || s === "for" || s === "model";
408
+ return s === "on" || s === "bind" || s === "if" || s === "else" || s === "for" || s === "model";
374
409
  }
375
410
  var CodeGenerator = class {
376
411
  constructor(options) {
@@ -402,13 +437,7 @@ var CodeGenerator = class {
402
437
  this.helpers.add("setAttr");
403
438
  this.emit(`setAttr(${fragVar}, '${escapeStr(this.scopeId)}', '')`);
404
439
  }
405
- for (const node of ast) {
406
- const childVar = this.genNode(node, scope);
407
- if (childVar) {
408
- this.helpers.add("appendChild");
409
- this.emit(`appendChild(${fragVar}, ${childVar})`);
410
- }
411
- }
440
+ this.genChildren(fragVar, ast, scope);
412
441
  this.emit(`return ${fragVar}`);
413
442
  }
414
443
  const helperList = Array.from(this.helpers).sort();
@@ -416,7 +445,7 @@ var CodeGenerator = class {
416
445
 
417
446
  ` : "";
418
447
  const fnBody = this.code.map((l) => ` ${l}`).join("\n");
419
- const moduleCode = `${importLine}function __render() {
448
+ const moduleCode = `${importLine}function __render(_ctx) {
420
449
  ${fnBody}
421
450
  }
422
451
  `;
@@ -445,6 +474,9 @@ ${fnBody}
445
474
  if (forDir) {
446
475
  return this.genFor(node, forDir, scope);
447
476
  }
477
+ if (node.tag === "slot") {
478
+ return this.genSlot(node);
479
+ }
448
480
  if (isComponentTag(node.tag)) {
449
481
  return this.genComponent(node, scope);
450
482
  }
@@ -464,21 +496,10 @@ ${fnBody}
464
496
  }
465
497
  }
466
498
  for (const dir of node.directives) {
467
- if (dir.kind === "if" || dir.kind === "for") continue;
499
+ if (dir.kind === "if" || dir.kind === "else" || dir.kind === "for") continue;
468
500
  this.genDirective(elVar, dir, scope);
469
501
  }
470
- this.deferredCallsStack.push([]);
471
- for (const child of node.children) {
472
- const childVar = this.genNode(child, scope);
473
- if (childVar) {
474
- this.helpers.add("appendChild");
475
- this.emit(`appendChild(${elVar}, ${childVar})`);
476
- }
477
- }
478
- const deferred = this.deferredCallsStack.pop();
479
- for (const line of deferred) {
480
- this.emit(line);
481
- }
502
+ this.genChildren(elVar, node.children, scope);
482
503
  return elVar;
483
504
  }
484
505
  // ---- Text & Interpolation -----------------------------------------------
@@ -518,7 +539,32 @@ ${fnBody}
518
539
  this.helpers.add("addEventListener");
519
540
  const event = dir.arg ?? "click";
520
541
  const handler = this.resolveExpression(dir.expression, scope);
521
- this.emit(`addEventListener(${elVar}, '${escapeStr(event)}', ${handler})`);
542
+ const modifiers = dir.modifiers;
543
+ const OPTION_MODS = /* @__PURE__ */ new Set(["once", "capture", "passive"]);
544
+ const handlerMods = modifiers.filter((m) => !OPTION_MODS.has(m));
545
+ const optionMods = modifiers.filter((m) => OPTION_MODS.has(m));
546
+ let handlerExpr = handler;
547
+ if (handlerMods.length > 0) {
548
+ const guards = [];
549
+ const calls = [];
550
+ for (const mod of handlerMods) {
551
+ switch (mod) {
552
+ case "prevent":
553
+ calls.push("_e.preventDefault()");
554
+ break;
555
+ case "stop":
556
+ calls.push("_e.stopPropagation()");
557
+ break;
558
+ case "self":
559
+ guards.push("if (_e.target !== _e.currentTarget) return");
560
+ break;
561
+ }
562
+ }
563
+ const body = [...guards, ...calls, `(${handler})(_e)`].join("; ");
564
+ handlerExpr = `(_e) => { ${body} }`;
565
+ }
566
+ const optionsStr = optionMods.length > 0 ? `, { ${optionMods.map((m) => `${m}: true`).join(", ")} }` : "";
567
+ this.emit(`addEventListener(${elVar}, '${escapeStr(event)}', ${handlerExpr}${optionsStr})`);
522
568
  }
523
569
  genBind(elVar, dir, scope) {
524
570
  this.helpers.add("setAttr");
@@ -533,12 +579,10 @@ ${fnBody}
533
579
  this.helpers.add("createEffect");
534
580
  const signalRef = this.resolveExpression(dir.expression, scope);
535
581
  this.emit(`createEffect(() => setAttr(${elVar}, 'value', ${signalRef}()))`);
536
- this.emit(
537
- `addEventListener(${elVar}, 'input', (e) => ${signalRef}.set(e.target.value))`
538
- );
582
+ this.emit(`addEventListener(${elVar}, 'input', (e) => ${signalRef}.set(e.target.value))`);
539
583
  }
540
584
  // ---- Structural: u-if ---------------------------------------------------
541
- genIf(node, dir, scope) {
585
+ genIf(node, dir, scope, elseNode) {
542
586
  this.helpers.add("createIf");
543
587
  const anchorVar = this.freshVar();
544
588
  this.helpers.add("createComment");
@@ -560,7 +604,27 @@ ${fnBody}
560
604
  }
561
605
  this.emit(` return ${innerVar}`);
562
606
  this.emit(`}`);
563
- this.emitOrDefer(`createIf(${anchorVar}, () => Boolean(${condition}), ${trueFnVar})`);
607
+ let elseArg = "";
608
+ if (elseNode) {
609
+ const falseFnVar = this.freshVar();
610
+ const strippedElse = {
611
+ ...elseNode,
612
+ directives: elseNode.directives.filter((d) => d.kind !== "else")
613
+ };
614
+ const savedCode2 = this.code;
615
+ this.code = [];
616
+ const elseInnerVar = this.genElement(strippedElse, scope);
617
+ const elseLines = [...this.code];
618
+ this.code = savedCode2;
619
+ this.emit(`const ${falseFnVar} = () => {`);
620
+ for (const line of elseLines) {
621
+ this.emit(` ${line}`);
622
+ }
623
+ this.emit(` return ${elseInnerVar}`);
624
+ this.emit(`}`);
625
+ elseArg = `, ${falseFnVar}`;
626
+ }
627
+ this.emitOrDefer(`createIf(${anchorVar}, () => Boolean(${condition}), ${trueFnVar}${elseArg})`);
564
628
  return anchorVar;
565
629
  }
566
630
  // ---- Structural: u-for --------------------------------------------------
@@ -569,7 +633,9 @@ ${fnBody}
569
633
  const anchorVar = this.freshVar();
570
634
  this.helpers.add("createComment");
571
635
  this.emit(`const ${anchorVar} = createComment('u-for')`);
572
- const forMatch = dir.expression.match(/^\s*(?:\(\s*(\w+)\s*(?:,\s*(\w+)\s*)?\)|(\w+))\s+in\s+(.+)$/);
636
+ const forMatch = dir.expression.match(
637
+ /^\s*(?:\(\s*(\w+)\s*(?:,\s*(\w+)\s*)?\)|(\w+))\s+in\s+(.+)$/
638
+ );
573
639
  if (!forMatch) {
574
640
  throw new Error(`Invalid u-for expression: "${dir.expression}"`);
575
641
  }
@@ -578,9 +644,12 @@ ${fnBody}
578
644
  const listExpr = this.resolveExpression(forMatch[4].trim(), scope);
579
645
  const innerScope = new Set(scope);
580
646
  innerScope.add(itemName);
647
+ const keyDir = node.directives.find((d) => d.kind === "bind" && d.arg === "key");
581
648
  const strippedNode = {
582
649
  ...node,
583
- directives: node.directives.filter((d) => d.kind !== "for")
650
+ directives: node.directives.filter(
651
+ (d) => d.kind !== "for" && !(d.kind === "bind" && d.arg === "key")
652
+ )
584
653
  };
585
654
  const savedCode = this.code;
586
655
  this.code = [];
@@ -594,9 +663,81 @@ ${fnBody}
594
663
  }
595
664
  this.emit(` return ${innerVar}`);
596
665
  this.emit(`}`);
597
- this.emitOrDefer(`createFor(${anchorVar}, () => ${listExpr}, ${renderFnVar})`);
666
+ let keyArg = "";
667
+ if (keyDir) {
668
+ const keyExpr = this.resolveExpression(keyDir.expression, innerScope);
669
+ keyArg = `, (${itemName}, ${indexName}) => ${keyExpr}`;
670
+ }
671
+ this.emitOrDefer(`createFor(${anchorVar}, () => ${listExpr}, ${renderFnVar}${keyArg})`);
598
672
  return anchorVar;
599
673
  }
674
+ // ---- Children processing (handles u-if / u-else pairing) ----------------
675
+ genChildren(parentVar, children, scope) {
676
+ this.deferredCallsStack.push([]);
677
+ let i = 0;
678
+ while (i < children.length) {
679
+ const child = children[i];
680
+ if (child.type === 1 /* Element */) {
681
+ const ifDir = child.directives.find((d) => d.kind === "if");
682
+ if (ifDir) {
683
+ let elseNode;
684
+ let skipTo = i + 1;
685
+ for (let j = i + 1; j < children.length; j++) {
686
+ const next = children[j];
687
+ if (next.type === 2 /* Text */ && !next.content.trim()) continue;
688
+ if (next.type === 1 /* Element */ && next.directives.some((d) => d.kind === "else")) {
689
+ elseNode = next;
690
+ skipTo = j + 1;
691
+ }
692
+ break;
693
+ }
694
+ const childVar2 = this.genIf(child, ifDir, scope, elseNode);
695
+ this.helpers.add("appendChild");
696
+ this.emit(`appendChild(${parentVar}, ${childVar2})`);
697
+ i = skipTo;
698
+ continue;
699
+ }
700
+ if (child.directives.some((d) => d.kind === "else")) {
701
+ i++;
702
+ continue;
703
+ }
704
+ }
705
+ const childVar = this.genNode(child, scope);
706
+ if (childVar) {
707
+ this.helpers.add("appendChild");
708
+ this.emit(`appendChild(${parentVar}, ${childVar})`);
709
+ }
710
+ i++;
711
+ }
712
+ const deferred = this.deferredCallsStack.pop();
713
+ for (const line of deferred) {
714
+ this.emit(line);
715
+ }
716
+ }
717
+ // ---- Slot rendering -----------------------------------------------------
718
+ /**
719
+ * Generate code for a `<slot />` element.
720
+ *
721
+ * Named slots use `<slot name="foo" />`, defaulting to "default".
722
+ * The generated code reads from `_ctx.$slots[name]()` if available,
723
+ * otherwise renders a comment placeholder.
724
+ */
725
+ genSlot(node) {
726
+ const nameAttr = node.attrs.find((a) => a.name === "name");
727
+ const slotName = nameAttr?.value ?? "default";
728
+ const slotVar = this.freshVar();
729
+ this.helpers.add("createComment");
730
+ if (slotName === "default") {
731
+ this.emit(
732
+ `const ${slotVar} = _ctx && _ctx.$slots && _ctx.$slots['default'] ? _ctx.$slots['default']() : (_ctx && _ctx.children instanceof Node ? _ctx.children : createComment('slot'))`
733
+ );
734
+ } else {
735
+ this.emit(
736
+ `const ${slotVar} = _ctx && _ctx.$slots && _ctx.$slots['${escapeStr(slotName)}'] ? _ctx.$slots['${escapeStr(slotName)}']() : createComment('slot')`
737
+ );
738
+ }
739
+ return slotVar;
740
+ }
600
741
  // ---- Component generation -----------------------------------------------
601
742
  genComponent(node, scope) {
602
743
  const compVar = this.freshVar();
@@ -615,7 +756,42 @@ ${fnBody}
615
756
  }
616
757
  const propsStr = propEntries.length > 0 ? `{ ${propEntries.join(", ")} }` : "{}";
617
758
  this.helpers.add("createComponent");
618
- this.emit(`const ${compVar} = createComponent(${node.tag}, ${propsStr})`);
759
+ const substantiveChildren = node.children.filter(
760
+ (c) => c.type === 1 /* Element */ || c.type === 3 /* Interpolation */ || c.type === 2 /* Text */ && c.content.trim() !== ""
761
+ );
762
+ if (substantiveChildren.length > 0) {
763
+ const slotFnVar = this.freshVar();
764
+ const savedCode = this.code;
765
+ this.code = [];
766
+ if (substantiveChildren.length === 1 && substantiveChildren[0].type === 1 /* Element */) {
767
+ const innerVar = this.genNode(substantiveChildren[0], scope);
768
+ this.emit(`return ${innerVar}`);
769
+ } else {
770
+ this.helpers.add("createElement");
771
+ this.helpers.add("appendChild");
772
+ const fragVar = this.freshVar();
773
+ this.emit(`const ${fragVar} = createElement('div')`);
774
+ for (const child of node.children) {
775
+ const childVar = this.genNode(child, scope);
776
+ if (childVar) {
777
+ this.emit(`appendChild(${fragVar}, ${childVar})`);
778
+ }
779
+ }
780
+ this.emit(`return ${fragVar}`);
781
+ }
782
+ const slotLines = [...this.code];
783
+ this.code = savedCode;
784
+ this.emit(`const ${slotFnVar} = () => {`);
785
+ for (const line of slotLines) {
786
+ this.emit(` ${line}`);
787
+ }
788
+ this.emit(`}`);
789
+ this.emit(
790
+ `const ${compVar} = createComponent(${node.tag}, ${propsStr}, { default: ${slotFnVar} })`
791
+ );
792
+ } else {
793
+ this.emit(`const ${compVar} = createComponent(${node.tag}, ${propsStr})`);
794
+ }
619
795
  return compVar;
620
796
  }
621
797
  // ---- Expression resolution ----------------------------------------------
package/dist/index.d.cts CHANGED
@@ -84,7 +84,7 @@ interface Directive {
84
84
  expression: string;
85
85
  modifiers: string[];
86
86
  }
87
- type DirectiveKind = 'on' | 'bind' | 'if' | 'for' | 'model';
87
+ type DirectiveKind = 'on' | 'bind' | 'if' | 'else' | 'for' | 'model';
88
88
  /** Exported for testing — parse a template string into an AST. */
89
89
  declare function parseTemplate(source: string): TemplateNode[];
90
90
 
package/dist/index.d.ts CHANGED
@@ -84,7 +84,7 @@ interface Directive {
84
84
  expression: string;
85
85
  modifiers: string[];
86
86
  }
87
- type DirectiveKind = 'on' | 'bind' | 'if' | 'for' | 'model';
87
+ type DirectiveKind = 'on' | 'bind' | 'if' | 'else' | 'for' | 'model';
88
88
  /** Exported for testing — parse a template string into an AST. */
89
89
  declare function parseTemplate(source: string): TemplateNode[];
90
90
 
package/dist/index.js CHANGED
@@ -213,7 +213,7 @@ var TemplateParser = class {
213
213
  if (this.lookingAt("{{")) {
214
214
  flush();
215
215
  this.pos += 2;
216
- const endIdx = this.source.indexOf("}}", this.pos);
216
+ const endIdx = this.findInterpolationEnd(this.pos);
217
217
  if (endIdx === -1) throw this.error("Unterminated interpolation {{ }}");
218
218
  const expression = this.source.slice(this.pos, endIdx).trim();
219
219
  nodes.push({ type: 3 /* Interpolation */, expression });
@@ -225,6 +225,39 @@ var TemplateParser = class {
225
225
  flush();
226
226
  return nodes;
227
227
  }
228
+ // ---- Interpolation end finder -------------------------------------------
229
+ /**
230
+ * Find the position of the closing `}}` for an interpolation expression,
231
+ * respecting JavaScript string literals so that `}}` inside quotes is not
232
+ * treated as the end delimiter.
233
+ *
234
+ * Handles single-quoted, double-quoted, and template literal strings,
235
+ * including escaped characters within them.
236
+ */
237
+ findInterpolationEnd(start) {
238
+ let pos = start;
239
+ let inSingle = false;
240
+ let inDouble = false;
241
+ let inBacktick = false;
242
+ while (pos < this.source.length - 1) {
243
+ const ch = this.source[pos];
244
+ if ((inSingle || inDouble || inBacktick) && ch === "\\") {
245
+ pos += 2;
246
+ continue;
247
+ }
248
+ if (!inDouble && !inBacktick && ch === "'") {
249
+ inSingle = !inSingle;
250
+ } else if (!inSingle && !inBacktick && ch === '"') {
251
+ inDouble = !inDouble;
252
+ } else if (!inSingle && !inDouble && ch === "`") {
253
+ inBacktick = !inBacktick;
254
+ } else if (!inSingle && !inDouble && !inBacktick && ch === "}" && this.source[pos + 1] === "}") {
255
+ return pos;
256
+ }
257
+ pos++;
258
+ }
259
+ return -1;
260
+ }
228
261
  // ---- Low-level helpers --------------------------------------------------
229
262
  readTagName() {
230
263
  const start = this.pos;
@@ -274,7 +307,9 @@ var TemplateParser = class {
274
307
  }
275
308
  expect(str) {
276
309
  if (!this.lookingAt(str)) {
277
- throw this.error(`Expected "${str}" but found "${this.source.slice(this.pos, this.pos + 20)}"`);
310
+ throw this.error(
311
+ `Expected "${str}" but found "${this.source.slice(this.pos, this.pos + 20)}"`
312
+ );
278
313
  }
279
314
  this.pos += str.length;
280
315
  }
@@ -338,7 +373,7 @@ function classifyDirective(name, value) {
338
373
  return null;
339
374
  }
340
375
  function isDirectiveKind(s) {
341
- return s === "on" || s === "bind" || s === "if" || s === "for" || s === "model";
376
+ return s === "on" || s === "bind" || s === "if" || s === "else" || s === "for" || s === "model";
342
377
  }
343
378
  var CodeGenerator = class {
344
379
  constructor(options) {
@@ -370,13 +405,7 @@ var CodeGenerator = class {
370
405
  this.helpers.add("setAttr");
371
406
  this.emit(`setAttr(${fragVar}, '${escapeStr(this.scopeId)}', '')`);
372
407
  }
373
- for (const node of ast) {
374
- const childVar = this.genNode(node, scope);
375
- if (childVar) {
376
- this.helpers.add("appendChild");
377
- this.emit(`appendChild(${fragVar}, ${childVar})`);
378
- }
379
- }
408
+ this.genChildren(fragVar, ast, scope);
380
409
  this.emit(`return ${fragVar}`);
381
410
  }
382
411
  const helperList = Array.from(this.helpers).sort();
@@ -384,7 +413,7 @@ var CodeGenerator = class {
384
413
 
385
414
  ` : "";
386
415
  const fnBody = this.code.map((l) => ` ${l}`).join("\n");
387
- const moduleCode = `${importLine}function __render() {
416
+ const moduleCode = `${importLine}function __render(_ctx) {
388
417
  ${fnBody}
389
418
  }
390
419
  `;
@@ -413,6 +442,9 @@ ${fnBody}
413
442
  if (forDir) {
414
443
  return this.genFor(node, forDir, scope);
415
444
  }
445
+ if (node.tag === "slot") {
446
+ return this.genSlot(node);
447
+ }
416
448
  if (isComponentTag(node.tag)) {
417
449
  return this.genComponent(node, scope);
418
450
  }
@@ -432,21 +464,10 @@ ${fnBody}
432
464
  }
433
465
  }
434
466
  for (const dir of node.directives) {
435
- if (dir.kind === "if" || dir.kind === "for") continue;
467
+ if (dir.kind === "if" || dir.kind === "else" || dir.kind === "for") continue;
436
468
  this.genDirective(elVar, dir, scope);
437
469
  }
438
- this.deferredCallsStack.push([]);
439
- for (const child of node.children) {
440
- const childVar = this.genNode(child, scope);
441
- if (childVar) {
442
- this.helpers.add("appendChild");
443
- this.emit(`appendChild(${elVar}, ${childVar})`);
444
- }
445
- }
446
- const deferred = this.deferredCallsStack.pop();
447
- for (const line of deferred) {
448
- this.emit(line);
449
- }
470
+ this.genChildren(elVar, node.children, scope);
450
471
  return elVar;
451
472
  }
452
473
  // ---- Text & Interpolation -----------------------------------------------
@@ -486,7 +507,32 @@ ${fnBody}
486
507
  this.helpers.add("addEventListener");
487
508
  const event = dir.arg ?? "click";
488
509
  const handler = this.resolveExpression(dir.expression, scope);
489
- this.emit(`addEventListener(${elVar}, '${escapeStr(event)}', ${handler})`);
510
+ const modifiers = dir.modifiers;
511
+ const OPTION_MODS = /* @__PURE__ */ new Set(["once", "capture", "passive"]);
512
+ const handlerMods = modifiers.filter((m) => !OPTION_MODS.has(m));
513
+ const optionMods = modifiers.filter((m) => OPTION_MODS.has(m));
514
+ let handlerExpr = handler;
515
+ if (handlerMods.length > 0) {
516
+ const guards = [];
517
+ const calls = [];
518
+ for (const mod of handlerMods) {
519
+ switch (mod) {
520
+ case "prevent":
521
+ calls.push("_e.preventDefault()");
522
+ break;
523
+ case "stop":
524
+ calls.push("_e.stopPropagation()");
525
+ break;
526
+ case "self":
527
+ guards.push("if (_e.target !== _e.currentTarget) return");
528
+ break;
529
+ }
530
+ }
531
+ const body = [...guards, ...calls, `(${handler})(_e)`].join("; ");
532
+ handlerExpr = `(_e) => { ${body} }`;
533
+ }
534
+ const optionsStr = optionMods.length > 0 ? `, { ${optionMods.map((m) => `${m}: true`).join(", ")} }` : "";
535
+ this.emit(`addEventListener(${elVar}, '${escapeStr(event)}', ${handlerExpr}${optionsStr})`);
490
536
  }
491
537
  genBind(elVar, dir, scope) {
492
538
  this.helpers.add("setAttr");
@@ -501,12 +547,10 @@ ${fnBody}
501
547
  this.helpers.add("createEffect");
502
548
  const signalRef = this.resolveExpression(dir.expression, scope);
503
549
  this.emit(`createEffect(() => setAttr(${elVar}, 'value', ${signalRef}()))`);
504
- this.emit(
505
- `addEventListener(${elVar}, 'input', (e) => ${signalRef}.set(e.target.value))`
506
- );
550
+ this.emit(`addEventListener(${elVar}, 'input', (e) => ${signalRef}.set(e.target.value))`);
507
551
  }
508
552
  // ---- Structural: u-if ---------------------------------------------------
509
- genIf(node, dir, scope) {
553
+ genIf(node, dir, scope, elseNode) {
510
554
  this.helpers.add("createIf");
511
555
  const anchorVar = this.freshVar();
512
556
  this.helpers.add("createComment");
@@ -528,7 +572,27 @@ ${fnBody}
528
572
  }
529
573
  this.emit(` return ${innerVar}`);
530
574
  this.emit(`}`);
531
- this.emitOrDefer(`createIf(${anchorVar}, () => Boolean(${condition}), ${trueFnVar})`);
575
+ let elseArg = "";
576
+ if (elseNode) {
577
+ const falseFnVar = this.freshVar();
578
+ const strippedElse = {
579
+ ...elseNode,
580
+ directives: elseNode.directives.filter((d) => d.kind !== "else")
581
+ };
582
+ const savedCode2 = this.code;
583
+ this.code = [];
584
+ const elseInnerVar = this.genElement(strippedElse, scope);
585
+ const elseLines = [...this.code];
586
+ this.code = savedCode2;
587
+ this.emit(`const ${falseFnVar} = () => {`);
588
+ for (const line of elseLines) {
589
+ this.emit(` ${line}`);
590
+ }
591
+ this.emit(` return ${elseInnerVar}`);
592
+ this.emit(`}`);
593
+ elseArg = `, ${falseFnVar}`;
594
+ }
595
+ this.emitOrDefer(`createIf(${anchorVar}, () => Boolean(${condition}), ${trueFnVar}${elseArg})`);
532
596
  return anchorVar;
533
597
  }
534
598
  // ---- Structural: u-for --------------------------------------------------
@@ -537,7 +601,9 @@ ${fnBody}
537
601
  const anchorVar = this.freshVar();
538
602
  this.helpers.add("createComment");
539
603
  this.emit(`const ${anchorVar} = createComment('u-for')`);
540
- const forMatch = dir.expression.match(/^\s*(?:\(\s*(\w+)\s*(?:,\s*(\w+)\s*)?\)|(\w+))\s+in\s+(.+)$/);
604
+ const forMatch = dir.expression.match(
605
+ /^\s*(?:\(\s*(\w+)\s*(?:,\s*(\w+)\s*)?\)|(\w+))\s+in\s+(.+)$/
606
+ );
541
607
  if (!forMatch) {
542
608
  throw new Error(`Invalid u-for expression: "${dir.expression}"`);
543
609
  }
@@ -546,9 +612,12 @@ ${fnBody}
546
612
  const listExpr = this.resolveExpression(forMatch[4].trim(), scope);
547
613
  const innerScope = new Set(scope);
548
614
  innerScope.add(itemName);
615
+ const keyDir = node.directives.find((d) => d.kind === "bind" && d.arg === "key");
549
616
  const strippedNode = {
550
617
  ...node,
551
- directives: node.directives.filter((d) => d.kind !== "for")
618
+ directives: node.directives.filter(
619
+ (d) => d.kind !== "for" && !(d.kind === "bind" && d.arg === "key")
620
+ )
552
621
  };
553
622
  const savedCode = this.code;
554
623
  this.code = [];
@@ -562,9 +631,81 @@ ${fnBody}
562
631
  }
563
632
  this.emit(` return ${innerVar}`);
564
633
  this.emit(`}`);
565
- this.emitOrDefer(`createFor(${anchorVar}, () => ${listExpr}, ${renderFnVar})`);
634
+ let keyArg = "";
635
+ if (keyDir) {
636
+ const keyExpr = this.resolveExpression(keyDir.expression, innerScope);
637
+ keyArg = `, (${itemName}, ${indexName}) => ${keyExpr}`;
638
+ }
639
+ this.emitOrDefer(`createFor(${anchorVar}, () => ${listExpr}, ${renderFnVar}${keyArg})`);
566
640
  return anchorVar;
567
641
  }
642
+ // ---- Children processing (handles u-if / u-else pairing) ----------------
643
+ genChildren(parentVar, children, scope) {
644
+ this.deferredCallsStack.push([]);
645
+ let i = 0;
646
+ while (i < children.length) {
647
+ const child = children[i];
648
+ if (child.type === 1 /* Element */) {
649
+ const ifDir = child.directives.find((d) => d.kind === "if");
650
+ if (ifDir) {
651
+ let elseNode;
652
+ let skipTo = i + 1;
653
+ for (let j = i + 1; j < children.length; j++) {
654
+ const next = children[j];
655
+ if (next.type === 2 /* Text */ && !next.content.trim()) continue;
656
+ if (next.type === 1 /* Element */ && next.directives.some((d) => d.kind === "else")) {
657
+ elseNode = next;
658
+ skipTo = j + 1;
659
+ }
660
+ break;
661
+ }
662
+ const childVar2 = this.genIf(child, ifDir, scope, elseNode);
663
+ this.helpers.add("appendChild");
664
+ this.emit(`appendChild(${parentVar}, ${childVar2})`);
665
+ i = skipTo;
666
+ continue;
667
+ }
668
+ if (child.directives.some((d) => d.kind === "else")) {
669
+ i++;
670
+ continue;
671
+ }
672
+ }
673
+ const childVar = this.genNode(child, scope);
674
+ if (childVar) {
675
+ this.helpers.add("appendChild");
676
+ this.emit(`appendChild(${parentVar}, ${childVar})`);
677
+ }
678
+ i++;
679
+ }
680
+ const deferred = this.deferredCallsStack.pop();
681
+ for (const line of deferred) {
682
+ this.emit(line);
683
+ }
684
+ }
685
+ // ---- Slot rendering -----------------------------------------------------
686
+ /**
687
+ * Generate code for a `<slot />` element.
688
+ *
689
+ * Named slots use `<slot name="foo" />`, defaulting to "default".
690
+ * The generated code reads from `_ctx.$slots[name]()` if available,
691
+ * otherwise renders a comment placeholder.
692
+ */
693
+ genSlot(node) {
694
+ const nameAttr = node.attrs.find((a) => a.name === "name");
695
+ const slotName = nameAttr?.value ?? "default";
696
+ const slotVar = this.freshVar();
697
+ this.helpers.add("createComment");
698
+ if (slotName === "default") {
699
+ this.emit(
700
+ `const ${slotVar} = _ctx && _ctx.$slots && _ctx.$slots['default'] ? _ctx.$slots['default']() : (_ctx && _ctx.children instanceof Node ? _ctx.children : createComment('slot'))`
701
+ );
702
+ } else {
703
+ this.emit(
704
+ `const ${slotVar} = _ctx && _ctx.$slots && _ctx.$slots['${escapeStr(slotName)}'] ? _ctx.$slots['${escapeStr(slotName)}']() : createComment('slot')`
705
+ );
706
+ }
707
+ return slotVar;
708
+ }
568
709
  // ---- Component generation -----------------------------------------------
569
710
  genComponent(node, scope) {
570
711
  const compVar = this.freshVar();
@@ -583,7 +724,42 @@ ${fnBody}
583
724
  }
584
725
  const propsStr = propEntries.length > 0 ? `{ ${propEntries.join(", ")} }` : "{}";
585
726
  this.helpers.add("createComponent");
586
- this.emit(`const ${compVar} = createComponent(${node.tag}, ${propsStr})`);
727
+ const substantiveChildren = node.children.filter(
728
+ (c) => c.type === 1 /* Element */ || c.type === 3 /* Interpolation */ || c.type === 2 /* Text */ && c.content.trim() !== ""
729
+ );
730
+ if (substantiveChildren.length > 0) {
731
+ const slotFnVar = this.freshVar();
732
+ const savedCode = this.code;
733
+ this.code = [];
734
+ if (substantiveChildren.length === 1 && substantiveChildren[0].type === 1 /* Element */) {
735
+ const innerVar = this.genNode(substantiveChildren[0], scope);
736
+ this.emit(`return ${innerVar}`);
737
+ } else {
738
+ this.helpers.add("createElement");
739
+ this.helpers.add("appendChild");
740
+ const fragVar = this.freshVar();
741
+ this.emit(`const ${fragVar} = createElement('div')`);
742
+ for (const child of node.children) {
743
+ const childVar = this.genNode(child, scope);
744
+ if (childVar) {
745
+ this.emit(`appendChild(${fragVar}, ${childVar})`);
746
+ }
747
+ }
748
+ this.emit(`return ${fragVar}`);
749
+ }
750
+ const slotLines = [...this.code];
751
+ this.code = savedCode;
752
+ this.emit(`const ${slotFnVar} = () => {`);
753
+ for (const line of slotLines) {
754
+ this.emit(` ${line}`);
755
+ }
756
+ this.emit(`}`);
757
+ this.emit(
758
+ `const ${compVar} = createComponent(${node.tag}, ${propsStr}, { default: ${slotFnVar} })`
759
+ );
760
+ } else {
761
+ this.emit(`const ${compVar} = createComponent(${node.tag}, ${propsStr})`);
762
+ }
587
763
  return compVar;
588
764
  }
589
765
  // ---- Expression resolution ----------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthesketh/utopia-compiler",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Compiler for .utopia single-file components",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,7 +39,7 @@
39
39
  "dist"
40
40
  ],
41
41
  "dependencies": {
42
- "@matthesketh/utopia-core": "0.1.0"
42
+ "@matthesketh/utopia-core": "0.3.0"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsup src/index.ts --format esm,cjs --dts",