@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 +210 -34
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +210 -34
- package/package.json +2 -2
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.
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
42
|
+
"@matthesketh/utopia-core": "0.3.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|