@matthesketh/utopia-compiler 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -245,7 +245,7 @@ var TemplateParser = class {
245
245
  if (this.lookingAt("{{")) {
246
246
  flush();
247
247
  this.pos += 2;
248
- const endIdx = this.source.indexOf("}}", this.pos);
248
+ const endIdx = this.findInterpolationEnd(this.pos);
249
249
  if (endIdx === -1) throw this.error("Unterminated interpolation {{ }}");
250
250
  const expression = this.source.slice(this.pos, endIdx).trim();
251
251
  nodes.push({ type: 3 /* Interpolation */, expression });
@@ -257,6 +257,39 @@ var TemplateParser = class {
257
257
  flush();
258
258
  return nodes;
259
259
  }
260
+ // ---- Interpolation end finder -------------------------------------------
261
+ /**
262
+ * Find the position of the closing `}}` for an interpolation expression,
263
+ * respecting JavaScript string literals so that `}}` inside quotes is not
264
+ * treated as the end delimiter.
265
+ *
266
+ * Handles single-quoted, double-quoted, and template literal strings,
267
+ * including escaped characters within them.
268
+ */
269
+ findInterpolationEnd(start) {
270
+ let pos = start;
271
+ let inSingle = false;
272
+ let inDouble = false;
273
+ let inBacktick = false;
274
+ while (pos < this.source.length - 1) {
275
+ const ch = this.source[pos];
276
+ if ((inSingle || inDouble || inBacktick) && ch === "\\") {
277
+ pos += 2;
278
+ continue;
279
+ }
280
+ if (!inDouble && !inBacktick && ch === "'") {
281
+ inSingle = !inSingle;
282
+ } else if (!inSingle && !inBacktick && ch === '"') {
283
+ inDouble = !inDouble;
284
+ } else if (!inSingle && !inDouble && ch === "`") {
285
+ inBacktick = !inBacktick;
286
+ } else if (!inSingle && !inDouble && !inBacktick && ch === "}" && this.source[pos + 1] === "}") {
287
+ return pos;
288
+ }
289
+ pos++;
290
+ }
291
+ return -1;
292
+ }
260
293
  // ---- Low-level helpers --------------------------------------------------
261
294
  readTagName() {
262
295
  const start = this.pos;
@@ -412,7 +445,7 @@ var CodeGenerator = class {
412
445
 
413
446
  ` : "";
414
447
  const fnBody = this.code.map((l) => ` ${l}`).join("\n");
415
- const moduleCode = `${importLine}function __render() {
448
+ const moduleCode = `${importLine}function __render(_ctx) {
416
449
  ${fnBody}
417
450
  }
418
451
  `;
@@ -441,6 +474,9 @@ ${fnBody}
441
474
  if (forDir) {
442
475
  return this.genFor(node, forDir, scope);
443
476
  }
477
+ if (node.tag === "slot") {
478
+ return this.genSlot(node);
479
+ }
444
480
  if (isComponentTag(node.tag)) {
445
481
  return this.genComponent(node, scope);
446
482
  }
@@ -678,6 +714,30 @@ ${fnBody}
678
714
  this.emit(line);
679
715
  }
680
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
+ }
681
741
  // ---- Component generation -----------------------------------------------
682
742
  genComponent(node, scope) {
683
743
  const compVar = this.freshVar();
@@ -696,7 +756,42 @@ ${fnBody}
696
756
  }
697
757
  const propsStr = propEntries.length > 0 ? `{ ${propEntries.join(", ")} }` : "{}";
698
758
  this.helpers.add("createComponent");
699
- this.emit(`const ${compVar} = createComponent(${node.tag}, ${propsStr})`);
759
+ const substantiveChildren = node.children.filter(
760
+ (c) => c.type === 1 /* Element */ || c.type === 3 /* Interpolation */ || c.type === 2 /* Text */ && c.content.trim() !== ""
761
+ );
762
+ if (substantiveChildren.length > 0) {
763
+ const slotFnVar = this.freshVar();
764
+ const savedCode = this.code;
765
+ this.code = [];
766
+ if (substantiveChildren.length === 1 && substantiveChildren[0].type === 1 /* Element */) {
767
+ const innerVar = this.genNode(substantiveChildren[0], scope);
768
+ this.emit(`return ${innerVar}`);
769
+ } else {
770
+ this.helpers.add("createElement");
771
+ this.helpers.add("appendChild");
772
+ const fragVar = this.freshVar();
773
+ this.emit(`const ${fragVar} = createElement('div')`);
774
+ for (const child of node.children) {
775
+ const childVar = this.genNode(child, scope);
776
+ if (childVar) {
777
+ this.emit(`appendChild(${fragVar}, ${childVar})`);
778
+ }
779
+ }
780
+ this.emit(`return ${fragVar}`);
781
+ }
782
+ const slotLines = [...this.code];
783
+ this.code = savedCode;
784
+ this.emit(`const ${slotFnVar} = () => {`);
785
+ for (const line of slotLines) {
786
+ this.emit(` ${line}`);
787
+ }
788
+ this.emit(`}`);
789
+ this.emit(
790
+ `const ${compVar} = createComponent(${node.tag}, ${propsStr}, { default: ${slotFnVar} })`
791
+ );
792
+ } else {
793
+ this.emit(`const ${compVar} = createComponent(${node.tag}, ${propsStr})`);
794
+ }
700
795
  return compVar;
701
796
  }
702
797
  // ---- Expression resolution ----------------------------------------------
package/dist/index.js CHANGED
@@ -213,7 +213,7 @@ var TemplateParser = class {
213
213
  if (this.lookingAt("{{")) {
214
214
  flush();
215
215
  this.pos += 2;
216
- const endIdx = this.source.indexOf("}}", this.pos);
216
+ const endIdx = this.findInterpolationEnd(this.pos);
217
217
  if (endIdx === -1) throw this.error("Unterminated interpolation {{ }}");
218
218
  const expression = this.source.slice(this.pos, endIdx).trim();
219
219
  nodes.push({ type: 3 /* Interpolation */, expression });
@@ -225,6 +225,39 @@ var TemplateParser = class {
225
225
  flush();
226
226
  return nodes;
227
227
  }
228
+ // ---- Interpolation end finder -------------------------------------------
229
+ /**
230
+ * Find the position of the closing `}}` for an interpolation expression,
231
+ * respecting JavaScript string literals so that `}}` inside quotes is not
232
+ * treated as the end delimiter.
233
+ *
234
+ * Handles single-quoted, double-quoted, and template literal strings,
235
+ * including escaped characters within them.
236
+ */
237
+ findInterpolationEnd(start) {
238
+ let pos = start;
239
+ let inSingle = false;
240
+ let inDouble = false;
241
+ let inBacktick = false;
242
+ while (pos < this.source.length - 1) {
243
+ const ch = this.source[pos];
244
+ if ((inSingle || inDouble || inBacktick) && ch === "\\") {
245
+ pos += 2;
246
+ continue;
247
+ }
248
+ if (!inDouble && !inBacktick && ch === "'") {
249
+ inSingle = !inSingle;
250
+ } else if (!inSingle && !inBacktick && ch === '"') {
251
+ inDouble = !inDouble;
252
+ } else if (!inSingle && !inDouble && ch === "`") {
253
+ inBacktick = !inBacktick;
254
+ } else if (!inSingle && !inDouble && !inBacktick && ch === "}" && this.source[pos + 1] === "}") {
255
+ return pos;
256
+ }
257
+ pos++;
258
+ }
259
+ return -1;
260
+ }
228
261
  // ---- Low-level helpers --------------------------------------------------
229
262
  readTagName() {
230
263
  const start = this.pos;
@@ -380,7 +413,7 @@ var CodeGenerator = class {
380
413
 
381
414
  ` : "";
382
415
  const fnBody = this.code.map((l) => ` ${l}`).join("\n");
383
- const moduleCode = `${importLine}function __render() {
416
+ const moduleCode = `${importLine}function __render(_ctx) {
384
417
  ${fnBody}
385
418
  }
386
419
  `;
@@ -409,6 +442,9 @@ ${fnBody}
409
442
  if (forDir) {
410
443
  return this.genFor(node, forDir, scope);
411
444
  }
445
+ if (node.tag === "slot") {
446
+ return this.genSlot(node);
447
+ }
412
448
  if (isComponentTag(node.tag)) {
413
449
  return this.genComponent(node, scope);
414
450
  }
@@ -646,6 +682,30 @@ ${fnBody}
646
682
  this.emit(line);
647
683
  }
648
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
+ }
649
709
  // ---- Component generation -----------------------------------------------
650
710
  genComponent(node, scope) {
651
711
  const compVar = this.freshVar();
@@ -664,7 +724,42 @@ ${fnBody}
664
724
  }
665
725
  const propsStr = propEntries.length > 0 ? `{ ${propEntries.join(", ")} }` : "{}";
666
726
  this.helpers.add("createComponent");
667
- this.emit(`const ${compVar} = createComponent(${node.tag}, ${propsStr})`);
727
+ const substantiveChildren = node.children.filter(
728
+ (c) => c.type === 1 /* Element */ || c.type === 3 /* Interpolation */ || c.type === 2 /* Text */ && c.content.trim() !== ""
729
+ );
730
+ if (substantiveChildren.length > 0) {
731
+ const slotFnVar = this.freshVar();
732
+ const savedCode = this.code;
733
+ this.code = [];
734
+ if (substantiveChildren.length === 1 && substantiveChildren[0].type === 1 /* Element */) {
735
+ const innerVar = this.genNode(substantiveChildren[0], scope);
736
+ this.emit(`return ${innerVar}`);
737
+ } else {
738
+ this.helpers.add("createElement");
739
+ this.helpers.add("appendChild");
740
+ const fragVar = this.freshVar();
741
+ this.emit(`const ${fragVar} = createElement('div')`);
742
+ for (const child of node.children) {
743
+ const childVar = this.genNode(child, scope);
744
+ if (childVar) {
745
+ this.emit(`appendChild(${fragVar}, ${childVar})`);
746
+ }
747
+ }
748
+ this.emit(`return ${fragVar}`);
749
+ }
750
+ const slotLines = [...this.code];
751
+ this.code = savedCode;
752
+ this.emit(`const ${slotFnVar} = () => {`);
753
+ for (const line of slotLines) {
754
+ this.emit(` ${line}`);
755
+ }
756
+ this.emit(`}`);
757
+ this.emit(
758
+ `const ${compVar} = createComponent(${node.tag}, ${propsStr}, { default: ${slotFnVar} })`
759
+ );
760
+ } else {
761
+ this.emit(`const ${compVar} = createComponent(${node.tag}, ${propsStr})`);
762
+ }
668
763
  return compVar;
669
764
  }
670
765
  // ---- Expression resolution ----------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthesketh/utopia-compiler",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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.2.0"
42
+ "@matthesketh/utopia-core": "0.3.1"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsup src/index.ts --format esm,cjs --dts",