@matthesketh/utopia-compiler 0.1.0 → 0.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/dist/index.cjs CHANGED
@@ -306,7 +306,9 @@ var TemplateParser = class {
306
306
  }
307
307
  expect(str) {
308
308
  if (!this.lookingAt(str)) {
309
- throw this.error(`Expected "${str}" but found "${this.source.slice(this.pos, this.pos + 20)}"`);
309
+ throw this.error(
310
+ `Expected "${str}" but found "${this.source.slice(this.pos, this.pos + 20)}"`
311
+ );
310
312
  }
311
313
  this.pos += str.length;
312
314
  }
@@ -370,7 +372,7 @@ function classifyDirective(name, value) {
370
372
  return null;
371
373
  }
372
374
  function isDirectiveKind(s) {
373
- return s === "on" || s === "bind" || s === "if" || s === "for" || s === "model";
375
+ return s === "on" || s === "bind" || s === "if" || s === "else" || s === "for" || s === "model";
374
376
  }
375
377
  var CodeGenerator = class {
376
378
  constructor(options) {
@@ -402,13 +404,7 @@ var CodeGenerator = class {
402
404
  this.helpers.add("setAttr");
403
405
  this.emit(`setAttr(${fragVar}, '${escapeStr(this.scopeId)}', '')`);
404
406
  }
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
- }
407
+ this.genChildren(fragVar, ast, scope);
412
408
  this.emit(`return ${fragVar}`);
413
409
  }
414
410
  const helperList = Array.from(this.helpers).sort();
@@ -464,21 +460,10 @@ ${fnBody}
464
460
  }
465
461
  }
466
462
  for (const dir of node.directives) {
467
- if (dir.kind === "if" || dir.kind === "for") continue;
463
+ if (dir.kind === "if" || dir.kind === "else" || dir.kind === "for") continue;
468
464
  this.genDirective(elVar, dir, scope);
469
465
  }
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
- }
466
+ this.genChildren(elVar, node.children, scope);
482
467
  return elVar;
483
468
  }
484
469
  // ---- Text & Interpolation -----------------------------------------------
@@ -518,7 +503,32 @@ ${fnBody}
518
503
  this.helpers.add("addEventListener");
519
504
  const event = dir.arg ?? "click";
520
505
  const handler = this.resolveExpression(dir.expression, scope);
521
- this.emit(`addEventListener(${elVar}, '${escapeStr(event)}', ${handler})`);
506
+ const modifiers = dir.modifiers;
507
+ const OPTION_MODS = /* @__PURE__ */ new Set(["once", "capture", "passive"]);
508
+ const handlerMods = modifiers.filter((m) => !OPTION_MODS.has(m));
509
+ const optionMods = modifiers.filter((m) => OPTION_MODS.has(m));
510
+ let handlerExpr = handler;
511
+ if (handlerMods.length > 0) {
512
+ const guards = [];
513
+ const calls = [];
514
+ for (const mod of handlerMods) {
515
+ switch (mod) {
516
+ case "prevent":
517
+ calls.push("_e.preventDefault()");
518
+ break;
519
+ case "stop":
520
+ calls.push("_e.stopPropagation()");
521
+ break;
522
+ case "self":
523
+ guards.push("if (_e.target !== _e.currentTarget) return");
524
+ break;
525
+ }
526
+ }
527
+ const body = [...guards, ...calls, `(${handler})(_e)`].join("; ");
528
+ handlerExpr = `(_e) => { ${body} }`;
529
+ }
530
+ const optionsStr = optionMods.length > 0 ? `, { ${optionMods.map((m) => `${m}: true`).join(", ")} }` : "";
531
+ this.emit(`addEventListener(${elVar}, '${escapeStr(event)}', ${handlerExpr}${optionsStr})`);
522
532
  }
523
533
  genBind(elVar, dir, scope) {
524
534
  this.helpers.add("setAttr");
@@ -533,12 +543,10 @@ ${fnBody}
533
543
  this.helpers.add("createEffect");
534
544
  const signalRef = this.resolveExpression(dir.expression, scope);
535
545
  this.emit(`createEffect(() => setAttr(${elVar}, 'value', ${signalRef}()))`);
536
- this.emit(
537
- `addEventListener(${elVar}, 'input', (e) => ${signalRef}.set(e.target.value))`
538
- );
546
+ this.emit(`addEventListener(${elVar}, 'input', (e) => ${signalRef}.set(e.target.value))`);
539
547
  }
540
548
  // ---- Structural: u-if ---------------------------------------------------
541
- genIf(node, dir, scope) {
549
+ genIf(node, dir, scope, elseNode) {
542
550
  this.helpers.add("createIf");
543
551
  const anchorVar = this.freshVar();
544
552
  this.helpers.add("createComment");
@@ -560,7 +568,27 @@ ${fnBody}
560
568
  }
561
569
  this.emit(` return ${innerVar}`);
562
570
  this.emit(`}`);
563
- this.emitOrDefer(`createIf(${anchorVar}, () => Boolean(${condition}), ${trueFnVar})`);
571
+ let elseArg = "";
572
+ if (elseNode) {
573
+ const falseFnVar = this.freshVar();
574
+ const strippedElse = {
575
+ ...elseNode,
576
+ directives: elseNode.directives.filter((d) => d.kind !== "else")
577
+ };
578
+ const savedCode2 = this.code;
579
+ this.code = [];
580
+ const elseInnerVar = this.genElement(strippedElse, scope);
581
+ const elseLines = [...this.code];
582
+ this.code = savedCode2;
583
+ this.emit(`const ${falseFnVar} = () => {`);
584
+ for (const line of elseLines) {
585
+ this.emit(` ${line}`);
586
+ }
587
+ this.emit(` return ${elseInnerVar}`);
588
+ this.emit(`}`);
589
+ elseArg = `, ${falseFnVar}`;
590
+ }
591
+ this.emitOrDefer(`createIf(${anchorVar}, () => Boolean(${condition}), ${trueFnVar}${elseArg})`);
564
592
  return anchorVar;
565
593
  }
566
594
  // ---- Structural: u-for --------------------------------------------------
@@ -569,7 +597,9 @@ ${fnBody}
569
597
  const anchorVar = this.freshVar();
570
598
  this.helpers.add("createComment");
571
599
  this.emit(`const ${anchorVar} = createComment('u-for')`);
572
- const forMatch = dir.expression.match(/^\s*(?:\(\s*(\w+)\s*(?:,\s*(\w+)\s*)?\)|(\w+))\s+in\s+(.+)$/);
600
+ const forMatch = dir.expression.match(
601
+ /^\s*(?:\(\s*(\w+)\s*(?:,\s*(\w+)\s*)?\)|(\w+))\s+in\s+(.+)$/
602
+ );
573
603
  if (!forMatch) {
574
604
  throw new Error(`Invalid u-for expression: "${dir.expression}"`);
575
605
  }
@@ -578,9 +608,12 @@ ${fnBody}
578
608
  const listExpr = this.resolveExpression(forMatch[4].trim(), scope);
579
609
  const innerScope = new Set(scope);
580
610
  innerScope.add(itemName);
611
+ const keyDir = node.directives.find((d) => d.kind === "bind" && d.arg === "key");
581
612
  const strippedNode = {
582
613
  ...node,
583
- directives: node.directives.filter((d) => d.kind !== "for")
614
+ directives: node.directives.filter(
615
+ (d) => d.kind !== "for" && !(d.kind === "bind" && d.arg === "key")
616
+ )
584
617
  };
585
618
  const savedCode = this.code;
586
619
  this.code = [];
@@ -594,9 +627,57 @@ ${fnBody}
594
627
  }
595
628
  this.emit(` return ${innerVar}`);
596
629
  this.emit(`}`);
597
- this.emitOrDefer(`createFor(${anchorVar}, () => ${listExpr}, ${renderFnVar})`);
630
+ let keyArg = "";
631
+ if (keyDir) {
632
+ const keyExpr = this.resolveExpression(keyDir.expression, innerScope);
633
+ keyArg = `, (${itemName}, ${indexName}) => ${keyExpr}`;
634
+ }
635
+ this.emitOrDefer(`createFor(${anchorVar}, () => ${listExpr}, ${renderFnVar}${keyArg})`);
598
636
  return anchorVar;
599
637
  }
638
+ // ---- Children processing (handles u-if / u-else pairing) ----------------
639
+ genChildren(parentVar, children, scope) {
640
+ this.deferredCallsStack.push([]);
641
+ let i = 0;
642
+ while (i < children.length) {
643
+ const child = children[i];
644
+ if (child.type === 1 /* Element */) {
645
+ const ifDir = child.directives.find((d) => d.kind === "if");
646
+ if (ifDir) {
647
+ let elseNode;
648
+ let skipTo = i + 1;
649
+ for (let j = i + 1; j < children.length; j++) {
650
+ const next = children[j];
651
+ if (next.type === 2 /* Text */ && !next.content.trim()) continue;
652
+ if (next.type === 1 /* Element */ && next.directives.some((d) => d.kind === "else")) {
653
+ elseNode = next;
654
+ skipTo = j + 1;
655
+ }
656
+ break;
657
+ }
658
+ const childVar2 = this.genIf(child, ifDir, scope, elseNode);
659
+ this.helpers.add("appendChild");
660
+ this.emit(`appendChild(${parentVar}, ${childVar2})`);
661
+ i = skipTo;
662
+ continue;
663
+ }
664
+ if (child.directives.some((d) => d.kind === "else")) {
665
+ i++;
666
+ continue;
667
+ }
668
+ }
669
+ const childVar = this.genNode(child, scope);
670
+ if (childVar) {
671
+ this.helpers.add("appendChild");
672
+ this.emit(`appendChild(${parentVar}, ${childVar})`);
673
+ }
674
+ i++;
675
+ }
676
+ const deferred = this.deferredCallsStack.pop();
677
+ for (const line of deferred) {
678
+ this.emit(line);
679
+ }
680
+ }
600
681
  // ---- Component generation -----------------------------------------------
601
682
  genComponent(node, scope) {
602
683
  const compVar = this.freshVar();
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
@@ -274,7 +274,9 @@ var TemplateParser = class {
274
274
  }
275
275
  expect(str) {
276
276
  if (!this.lookingAt(str)) {
277
- throw this.error(`Expected "${str}" but found "${this.source.slice(this.pos, this.pos + 20)}"`);
277
+ throw this.error(
278
+ `Expected "${str}" but found "${this.source.slice(this.pos, this.pos + 20)}"`
279
+ );
278
280
  }
279
281
  this.pos += str.length;
280
282
  }
@@ -338,7 +340,7 @@ function classifyDirective(name, value) {
338
340
  return null;
339
341
  }
340
342
  function isDirectiveKind(s) {
341
- return s === "on" || s === "bind" || s === "if" || s === "for" || s === "model";
343
+ return s === "on" || s === "bind" || s === "if" || s === "else" || s === "for" || s === "model";
342
344
  }
343
345
  var CodeGenerator = class {
344
346
  constructor(options) {
@@ -370,13 +372,7 @@ var CodeGenerator = class {
370
372
  this.helpers.add("setAttr");
371
373
  this.emit(`setAttr(${fragVar}, '${escapeStr(this.scopeId)}', '')`);
372
374
  }
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
- }
375
+ this.genChildren(fragVar, ast, scope);
380
376
  this.emit(`return ${fragVar}`);
381
377
  }
382
378
  const helperList = Array.from(this.helpers).sort();
@@ -432,21 +428,10 @@ ${fnBody}
432
428
  }
433
429
  }
434
430
  for (const dir of node.directives) {
435
- if (dir.kind === "if" || dir.kind === "for") continue;
431
+ if (dir.kind === "if" || dir.kind === "else" || dir.kind === "for") continue;
436
432
  this.genDirective(elVar, dir, scope);
437
433
  }
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
- }
434
+ this.genChildren(elVar, node.children, scope);
450
435
  return elVar;
451
436
  }
452
437
  // ---- Text & Interpolation -----------------------------------------------
@@ -486,7 +471,32 @@ ${fnBody}
486
471
  this.helpers.add("addEventListener");
487
472
  const event = dir.arg ?? "click";
488
473
  const handler = this.resolveExpression(dir.expression, scope);
489
- this.emit(`addEventListener(${elVar}, '${escapeStr(event)}', ${handler})`);
474
+ const modifiers = dir.modifiers;
475
+ const OPTION_MODS = /* @__PURE__ */ new Set(["once", "capture", "passive"]);
476
+ const handlerMods = modifiers.filter((m) => !OPTION_MODS.has(m));
477
+ const optionMods = modifiers.filter((m) => OPTION_MODS.has(m));
478
+ let handlerExpr = handler;
479
+ if (handlerMods.length > 0) {
480
+ const guards = [];
481
+ const calls = [];
482
+ for (const mod of handlerMods) {
483
+ switch (mod) {
484
+ case "prevent":
485
+ calls.push("_e.preventDefault()");
486
+ break;
487
+ case "stop":
488
+ calls.push("_e.stopPropagation()");
489
+ break;
490
+ case "self":
491
+ guards.push("if (_e.target !== _e.currentTarget) return");
492
+ break;
493
+ }
494
+ }
495
+ const body = [...guards, ...calls, `(${handler})(_e)`].join("; ");
496
+ handlerExpr = `(_e) => { ${body} }`;
497
+ }
498
+ const optionsStr = optionMods.length > 0 ? `, { ${optionMods.map((m) => `${m}: true`).join(", ")} }` : "";
499
+ this.emit(`addEventListener(${elVar}, '${escapeStr(event)}', ${handlerExpr}${optionsStr})`);
490
500
  }
491
501
  genBind(elVar, dir, scope) {
492
502
  this.helpers.add("setAttr");
@@ -501,12 +511,10 @@ ${fnBody}
501
511
  this.helpers.add("createEffect");
502
512
  const signalRef = this.resolveExpression(dir.expression, scope);
503
513
  this.emit(`createEffect(() => setAttr(${elVar}, 'value', ${signalRef}()))`);
504
- this.emit(
505
- `addEventListener(${elVar}, 'input', (e) => ${signalRef}.set(e.target.value))`
506
- );
514
+ this.emit(`addEventListener(${elVar}, 'input', (e) => ${signalRef}.set(e.target.value))`);
507
515
  }
508
516
  // ---- Structural: u-if ---------------------------------------------------
509
- genIf(node, dir, scope) {
517
+ genIf(node, dir, scope, elseNode) {
510
518
  this.helpers.add("createIf");
511
519
  const anchorVar = this.freshVar();
512
520
  this.helpers.add("createComment");
@@ -528,7 +536,27 @@ ${fnBody}
528
536
  }
529
537
  this.emit(` return ${innerVar}`);
530
538
  this.emit(`}`);
531
- this.emitOrDefer(`createIf(${anchorVar}, () => Boolean(${condition}), ${trueFnVar})`);
539
+ let elseArg = "";
540
+ if (elseNode) {
541
+ const falseFnVar = this.freshVar();
542
+ const strippedElse = {
543
+ ...elseNode,
544
+ directives: elseNode.directives.filter((d) => d.kind !== "else")
545
+ };
546
+ const savedCode2 = this.code;
547
+ this.code = [];
548
+ const elseInnerVar = this.genElement(strippedElse, scope);
549
+ const elseLines = [...this.code];
550
+ this.code = savedCode2;
551
+ this.emit(`const ${falseFnVar} = () => {`);
552
+ for (const line of elseLines) {
553
+ this.emit(` ${line}`);
554
+ }
555
+ this.emit(` return ${elseInnerVar}`);
556
+ this.emit(`}`);
557
+ elseArg = `, ${falseFnVar}`;
558
+ }
559
+ this.emitOrDefer(`createIf(${anchorVar}, () => Boolean(${condition}), ${trueFnVar}${elseArg})`);
532
560
  return anchorVar;
533
561
  }
534
562
  // ---- Structural: u-for --------------------------------------------------
@@ -537,7 +565,9 @@ ${fnBody}
537
565
  const anchorVar = this.freshVar();
538
566
  this.helpers.add("createComment");
539
567
  this.emit(`const ${anchorVar} = createComment('u-for')`);
540
- const forMatch = dir.expression.match(/^\s*(?:\(\s*(\w+)\s*(?:,\s*(\w+)\s*)?\)|(\w+))\s+in\s+(.+)$/);
568
+ const forMatch = dir.expression.match(
569
+ /^\s*(?:\(\s*(\w+)\s*(?:,\s*(\w+)\s*)?\)|(\w+))\s+in\s+(.+)$/
570
+ );
541
571
  if (!forMatch) {
542
572
  throw new Error(`Invalid u-for expression: "${dir.expression}"`);
543
573
  }
@@ -546,9 +576,12 @@ ${fnBody}
546
576
  const listExpr = this.resolveExpression(forMatch[4].trim(), scope);
547
577
  const innerScope = new Set(scope);
548
578
  innerScope.add(itemName);
579
+ const keyDir = node.directives.find((d) => d.kind === "bind" && d.arg === "key");
549
580
  const strippedNode = {
550
581
  ...node,
551
- directives: node.directives.filter((d) => d.kind !== "for")
582
+ directives: node.directives.filter(
583
+ (d) => d.kind !== "for" && !(d.kind === "bind" && d.arg === "key")
584
+ )
552
585
  };
553
586
  const savedCode = this.code;
554
587
  this.code = [];
@@ -562,9 +595,57 @@ ${fnBody}
562
595
  }
563
596
  this.emit(` return ${innerVar}`);
564
597
  this.emit(`}`);
565
- this.emitOrDefer(`createFor(${anchorVar}, () => ${listExpr}, ${renderFnVar})`);
598
+ let keyArg = "";
599
+ if (keyDir) {
600
+ const keyExpr = this.resolveExpression(keyDir.expression, innerScope);
601
+ keyArg = `, (${itemName}, ${indexName}) => ${keyExpr}`;
602
+ }
603
+ this.emitOrDefer(`createFor(${anchorVar}, () => ${listExpr}, ${renderFnVar}${keyArg})`);
566
604
  return anchorVar;
567
605
  }
606
+ // ---- Children processing (handles u-if / u-else pairing) ----------------
607
+ genChildren(parentVar, children, scope) {
608
+ this.deferredCallsStack.push([]);
609
+ let i = 0;
610
+ while (i < children.length) {
611
+ const child = children[i];
612
+ if (child.type === 1 /* Element */) {
613
+ const ifDir = child.directives.find((d) => d.kind === "if");
614
+ if (ifDir) {
615
+ let elseNode;
616
+ let skipTo = i + 1;
617
+ for (let j = i + 1; j < children.length; j++) {
618
+ const next = children[j];
619
+ if (next.type === 2 /* Text */ && !next.content.trim()) continue;
620
+ if (next.type === 1 /* Element */ && next.directives.some((d) => d.kind === "else")) {
621
+ elseNode = next;
622
+ skipTo = j + 1;
623
+ }
624
+ break;
625
+ }
626
+ const childVar2 = this.genIf(child, ifDir, scope, elseNode);
627
+ this.helpers.add("appendChild");
628
+ this.emit(`appendChild(${parentVar}, ${childVar2})`);
629
+ i = skipTo;
630
+ continue;
631
+ }
632
+ if (child.directives.some((d) => d.kind === "else")) {
633
+ i++;
634
+ continue;
635
+ }
636
+ }
637
+ const childVar = this.genNode(child, scope);
638
+ if (childVar) {
639
+ this.helpers.add("appendChild");
640
+ this.emit(`appendChild(${parentVar}, ${childVar})`);
641
+ }
642
+ i++;
643
+ }
644
+ const deferred = this.deferredCallsStack.pop();
645
+ for (const line of deferred) {
646
+ this.emit(line);
647
+ }
648
+ }
568
649
  // ---- Component generation -----------------------------------------------
569
650
  genComponent(node, scope) {
570
651
  const compVar = this.freshVar();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthesketh/utopia-compiler",
3
- "version": "0.1.0",
3
+ "version": "0.2.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.2.0"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsup src/index.ts --format esm,cjs --dts",