@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 +140 -39
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +140 -39
- package/package.json +2 -2
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
687
|
+
propEntries.push(`'${escapeStr(a.name)}': '${escapeStr(a.value)}'`);
|
|
607
688
|
} else {
|
|
608
|
-
propEntries.push(
|
|
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(
|
|
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]
|
|
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)
|
|
684
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
655
|
+
propEntries.push(`'${escapeStr(a.name)}': '${escapeStr(a.value)}'`);
|
|
575
656
|
} else {
|
|
576
|
-
propEntries.push(
|
|
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(
|
|
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]
|
|
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)
|
|
652
|
-
|
|
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
|
|
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
|
|
42
|
+
"@matthesketh/utopia-core": "0.2.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|