@matthesketh/utopia-compiler 0.0.5 → 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) {
@@ -400,15 +402,9 @@ var CodeGenerator = class {
400
402
  this.emit(`const ${fragVar} = createElement('div')`);
401
403
  if (this.scopeId) {
402
404
  this.helpers.add("setAttr");
403
- this.emit(`setAttr(${fragVar}, '${this.scopeId}', '')`);
404
- }
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
- }
405
+ this.emit(`setAttr(${fragVar}, '${escapeStr(this.scopeId)}', '')`);
411
406
  }
407
+ this.genChildren(fragVar, ast, scope);
412
408
  this.emit(`return ${fragVar}`);
413
409
  }
414
410
  const helperList = Array.from(this.helpers).sort();
@@ -453,7 +449,7 @@ ${fnBody}
453
449
  this.emit(`const ${elVar} = createElement('${node.tag}')`);
454
450
  if (this.scopeId) {
455
451
  this.helpers.add("setAttr");
456
- this.emit(`setAttr(${elVar}, '${this.scopeId}', '')`);
452
+ this.emit(`setAttr(${elVar}, '${escapeStr(this.scopeId)}', '')`);
457
453
  }
458
454
  for (const attr of node.attrs) {
459
455
  this.helpers.add("setAttr");
@@ -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,23 +627,71 @@ ${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();
603
684
  const propEntries = [];
604
685
  for (const a of node.attrs) {
605
686
  if (a.value !== null) {
606
- propEntries.push(`${a.name}: '${escapeStr(a.value)}'`);
687
+ propEntries.push(`'${escapeStr(a.name)}': '${escapeStr(a.value)}'`);
607
688
  } else {
608
- propEntries.push(`${a.name}: true`);
689
+ propEntries.push(`'${escapeStr(a.name)}': true`);
609
690
  }
610
691
  }
611
692
  for (const d of node.directives) {
612
693
  if (d.kind === "bind" && d.arg) {
613
- propEntries.push(`${d.arg}: ${this.resolveExpression(d.expression, scope)}`);
694
+ propEntries.push(`'${escapeStr(d.arg)}': ${this.resolveExpression(d.expression, scope)}`);
614
695
  }
615
696
  }
616
697
  const propsStr = propEntries.length > 0 ? `{ ${propEntries.join(", ")} }` : "{}";
@@ -647,7 +728,7 @@ ${fnBody}
647
728
  }
648
729
  };
649
730
  function isComponentTag(tag) {
650
- return /^[A-Z]/.test(tag);
731
+ return /^[A-Z][a-zA-Z0-9_$]*$/.test(tag);
651
732
  }
652
733
  function escapeStr(s) {
653
734
  return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
@@ -680,8 +761,28 @@ var ENTITY_MAP = {
680
761
  };
681
762
  function decodeEntities(text) {
682
763
  return text.replace(/&(?:#(\d+)|#x([0-9a-fA-F]+)|(\w+));/g, (match, dec, hex, named) => {
683
- if (dec) return String.fromCodePoint(parseInt(dec, 10));
684
- if (hex) return String.fromCodePoint(parseInt(hex, 16));
764
+ if (dec) {
765
+ const code = parseInt(dec, 10);
766
+ if (code >= 0 && code <= 1114111) {
767
+ try {
768
+ return String.fromCodePoint(code);
769
+ } catch {
770
+ return match;
771
+ }
772
+ }
773
+ return match;
774
+ }
775
+ if (hex) {
776
+ const code = parseInt(hex, 16);
777
+ if (code >= 0 && code <= 1114111) {
778
+ try {
779
+ return String.fromCodePoint(code);
780
+ } catch {
781
+ return match;
782
+ }
783
+ }
784
+ return match;
785
+ }
685
786
  if (named) return ENTITY_MAP[`&${named};`] ?? match;
686
787
  return match;
687
788
  });
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) {
@@ -368,15 +370,9 @@ var CodeGenerator = class {
368
370
  this.emit(`const ${fragVar} = createElement('div')`);
369
371
  if (this.scopeId) {
370
372
  this.helpers.add("setAttr");
371
- this.emit(`setAttr(${fragVar}, '${this.scopeId}', '')`);
372
- }
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
- }
373
+ this.emit(`setAttr(${fragVar}, '${escapeStr(this.scopeId)}', '')`);
379
374
  }
375
+ this.genChildren(fragVar, ast, scope);
380
376
  this.emit(`return ${fragVar}`);
381
377
  }
382
378
  const helperList = Array.from(this.helpers).sort();
@@ -421,7 +417,7 @@ ${fnBody}
421
417
  this.emit(`const ${elVar} = createElement('${node.tag}')`);
422
418
  if (this.scopeId) {
423
419
  this.helpers.add("setAttr");
424
- this.emit(`setAttr(${elVar}, '${this.scopeId}', '')`);
420
+ this.emit(`setAttr(${elVar}, '${escapeStr(this.scopeId)}', '')`);
425
421
  }
426
422
  for (const attr of node.attrs) {
427
423
  this.helpers.add("setAttr");
@@ -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,23 +595,71 @@ ${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();
571
652
  const propEntries = [];
572
653
  for (const a of node.attrs) {
573
654
  if (a.value !== null) {
574
- propEntries.push(`${a.name}: '${escapeStr(a.value)}'`);
655
+ propEntries.push(`'${escapeStr(a.name)}': '${escapeStr(a.value)}'`);
575
656
  } else {
576
- propEntries.push(`${a.name}: true`);
657
+ propEntries.push(`'${escapeStr(a.name)}': true`);
577
658
  }
578
659
  }
579
660
  for (const d of node.directives) {
580
661
  if (d.kind === "bind" && d.arg) {
581
- propEntries.push(`${d.arg}: ${this.resolveExpression(d.expression, scope)}`);
662
+ propEntries.push(`'${escapeStr(d.arg)}': ${this.resolveExpression(d.expression, scope)}`);
582
663
  }
583
664
  }
584
665
  const propsStr = propEntries.length > 0 ? `{ ${propEntries.join(", ")} }` : "{}";
@@ -615,7 +696,7 @@ ${fnBody}
615
696
  }
616
697
  };
617
698
  function isComponentTag(tag) {
618
- return /^[A-Z]/.test(tag);
699
+ return /^[A-Z][a-zA-Z0-9_$]*$/.test(tag);
619
700
  }
620
701
  function escapeStr(s) {
621
702
  return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
@@ -648,8 +729,28 @@ var ENTITY_MAP = {
648
729
  };
649
730
  function decodeEntities(text) {
650
731
  return text.replace(/&(?:#(\d+)|#x([0-9a-fA-F]+)|(\w+));/g, (match, dec, hex, named) => {
651
- if (dec) return String.fromCodePoint(parseInt(dec, 10));
652
- if (hex) return String.fromCodePoint(parseInt(hex, 16));
732
+ if (dec) {
733
+ const code = parseInt(dec, 10);
734
+ if (code >= 0 && code <= 1114111) {
735
+ try {
736
+ return String.fromCodePoint(code);
737
+ } catch {
738
+ return match;
739
+ }
740
+ }
741
+ return match;
742
+ }
743
+ if (hex) {
744
+ const code = parseInt(hex, 16);
745
+ if (code >= 0 && code <= 1114111) {
746
+ try {
747
+ return String.fromCodePoint(code);
748
+ } catch {
749
+ return match;
750
+ }
751
+ }
752
+ return match;
753
+ }
653
754
  if (named) return ENTITY_MAP[`&${named};`] ?? match;
654
755
  return match;
655
756
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthesketh/utopia-compiler",
3
- "version": "0.0.5",
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.0.5"
42
+ "@matthesketh/utopia-core": "0.2.0"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsup src/index.ts --format esm,cjs --dts",