@lightspeed-js/core 0.4.5 → 0.4.7

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/compile.js CHANGED
@@ -1019,7 +1019,7 @@ class BindingEvaluator {
1019
1019
  binding.el.innerHTML = value == null ? "" : String(value);
1020
1020
  break;
1021
1021
  case "attr": {
1022
- const attr = binding.attr;
1022
+ const attr = svgAttributeName(binding.el, binding.attr);
1023
1023
  const aria = attr.startsWith("aria-");
1024
1024
  if (BOOLEAN_ATTRIBUTES.has(attr) && !aria) {
1025
1025
  if (value)
@@ -1132,7 +1132,95 @@ function isPlain(value) {
1132
1132
  const proto = Object.getPrototypeOf(value);
1133
1133
  return proto === Object.prototype || proto === null;
1134
1134
  }
1135
+ var SVG_NS = "http://www.w3.org/2000/svg";
1136
+ var SVG_ATTRIBUTES = new Map([
1137
+ "attributeName",
1138
+ "attributeType",
1139
+ "baseFrequency",
1140
+ "baseProfile",
1141
+ "calcMode",
1142
+ "clipPathUnits",
1143
+ "diffuseConstant",
1144
+ "edgeMode",
1145
+ "filterUnits",
1146
+ "glyphRef",
1147
+ "gradientTransform",
1148
+ "gradientUnits",
1149
+ "kernelMatrix",
1150
+ "kernelUnitLength",
1151
+ "keyPoints",
1152
+ "keySplines",
1153
+ "keyTimes",
1154
+ "lengthAdjust",
1155
+ "limitingConeAngle",
1156
+ "markerHeight",
1157
+ "markerUnits",
1158
+ "markerWidth",
1159
+ "maskContentUnits",
1160
+ "maskUnits",
1161
+ "numOctaves",
1162
+ "pathLength",
1163
+ "patternContentUnits",
1164
+ "patternTransform",
1165
+ "patternUnits",
1166
+ "pointsAtX",
1167
+ "pointsAtY",
1168
+ "pointsAtZ",
1169
+ "preserveAlpha",
1170
+ "preserveAspectRatio",
1171
+ "primitiveUnits",
1172
+ "refX",
1173
+ "refY",
1174
+ "repeatCount",
1175
+ "repeatDur",
1176
+ "requiredExtensions",
1177
+ "requiredFeatures",
1178
+ "specularConstant",
1179
+ "specularExponent",
1180
+ "spreadMethod",
1181
+ "startOffset",
1182
+ "stdDeviation",
1183
+ "stitchTiles",
1184
+ "surfaceScale",
1185
+ "systemLanguage",
1186
+ "tableValues",
1187
+ "targetX",
1188
+ "targetY",
1189
+ "textLength",
1190
+ "viewBox",
1191
+ "viewTarget",
1192
+ "xChannelSelector",
1193
+ "yChannelSelector",
1194
+ "zoomAndPan"
1195
+ ].map((name) => [name.toLowerCase(), name]));
1196
+ function svgAttributeName(el, attr) {
1197
+ return el.namespaceURI === SVG_NS ? SVG_ATTRIBUTES.get(attr) ?? attr : attr;
1198
+ }
1135
1199
  // src/runtime/dom/ForBlock.ts
1200
+ var TAG_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1201
+ var REFUSED_TAGS = new Set([
1202
+ "script",
1203
+ "style",
1204
+ "template",
1205
+ "slot",
1206
+ "iframe",
1207
+ "object",
1208
+ "embed"
1209
+ ]);
1210
+ function retag(source, name) {
1211
+ const doc = source.ownerDocument;
1212
+ const el = source.namespaceURI ? doc.createElementNS(source.namespaceURI, name) : doc.createElement(name);
1213
+ for (const attr of Array.from(source.attributes)) {
1214
+ if (attr.namespaceURI)
1215
+ el.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
1216
+ else
1217
+ el.setAttribute(attr.name, attr.value);
1218
+ }
1219
+ for (const child of Array.from(source.childNodes)) {
1220
+ el.appendChild(child.cloneNode(true));
1221
+ }
1222
+ return el;
1223
+ }
1136
1224
  function toIterable(value) {
1137
1225
  if (value == null)
1138
1226
  return [];
@@ -1174,12 +1262,13 @@ class ForBlock {
1174
1262
  renderer;
1175
1263
  conditional;
1176
1264
  itemsExpr;
1265
+ tagged;
1177
1266
  rows = new Map;
1178
1267
  order = [];
1179
1268
  pendingItems = null;
1180
1269
  running = false;
1181
1270
  warnedDuplicates = false;
1182
- constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "") {
1271
+ constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "", tagged = false) {
1183
1272
  this.owner = owner;
1184
1273
  this.anchor = anchor;
1185
1274
  this.templates = templates;
@@ -1190,22 +1279,55 @@ class ForBlock {
1190
1279
  this.renderer = renderer;
1191
1280
  this.conditional = conditional;
1192
1281
  this.itemsExpr = itemsExpr;
1282
+ this.tagged = tagged;
1193
1283
  }
1284
+ retagged = new Map;
1285
+ warnedTag = false;
1194
1286
  toItems(value) {
1195
1287
  if (this.conditional) {
1196
1288
  return Number.isInteger(value) && value >= 0 ? [value] : [];
1197
1289
  }
1290
+ if (this.tagged)
1291
+ return [this.tagFor(value)];
1198
1292
  return toIterable(value);
1199
1293
  }
1200
1294
  templateFor(item) {
1295
+ if (this.tagged) {
1296
+ const source = this.templates[0];
1297
+ if (item === source.localName)
1298
+ return source;
1299
+ let made = this.retagged.get(item);
1300
+ if (!made) {
1301
+ made = retag(source, item);
1302
+ this.retagged.set(item, made);
1303
+ }
1304
+ return made;
1305
+ }
1201
1306
  return this.conditional ? this.templates[item] : this.templates[0];
1202
1307
  }
1308
+ tagFor(value) {
1309
+ const written = this.templates[0].localName;
1310
+ if (value == null || value === "")
1311
+ return written;
1312
+ const name = typeof value === "string" ? value.trim().toLowerCase() : null;
1313
+ if (name && TAG_NAME.test(name) && !REFUSED_TAGS.has(name))
1314
+ return name;
1315
+ if (!this.warnedTag) {
1316
+ this.warnedTag = true;
1317
+ warn(`$tag="${this.itemsExpr}" gave ${JSON.stringify(value)}, which is not an element it can make - rendering <${written}>.`);
1318
+ }
1319
+ return written;
1320
+ }
1203
1321
  schedule(items) {
1204
1322
  this.pendingItems = items;
1205
1323
  if (this.running)
1206
1324
  return;
1207
1325
  this.running = true;
1208
- this.drain();
1326
+ this.draining = this.drain();
1327
+ }
1328
+ draining = null;
1329
+ settled() {
1330
+ return this.draining ?? Promise.resolve();
1209
1331
  }
1210
1332
  async drain() {
1211
1333
  try {
package/dist/index.js CHANGED
@@ -591,7 +591,7 @@ class BindingEvaluator {
591
591
  binding.el.innerHTML = value == null ? "" : String(value);
592
592
  break;
593
593
  case "attr": {
594
- const attr = binding.attr;
594
+ const attr = svgAttributeName(binding.el, binding.attr);
595
595
  const aria = attr.startsWith("aria-");
596
596
  if (BOOLEAN_ATTRIBUTES.has(attr) && !aria) {
597
597
  if (value)
@@ -704,6 +704,70 @@ function isPlain(value) {
704
704
  const proto = Object.getPrototypeOf(value);
705
705
  return proto === Object.prototype || proto === null;
706
706
  }
707
+ var SVG_NS = "http://www.w3.org/2000/svg";
708
+ var SVG_ATTRIBUTES = new Map([
709
+ "attributeName",
710
+ "attributeType",
711
+ "baseFrequency",
712
+ "baseProfile",
713
+ "calcMode",
714
+ "clipPathUnits",
715
+ "diffuseConstant",
716
+ "edgeMode",
717
+ "filterUnits",
718
+ "glyphRef",
719
+ "gradientTransform",
720
+ "gradientUnits",
721
+ "kernelMatrix",
722
+ "kernelUnitLength",
723
+ "keyPoints",
724
+ "keySplines",
725
+ "keyTimes",
726
+ "lengthAdjust",
727
+ "limitingConeAngle",
728
+ "markerHeight",
729
+ "markerUnits",
730
+ "markerWidth",
731
+ "maskContentUnits",
732
+ "maskUnits",
733
+ "numOctaves",
734
+ "pathLength",
735
+ "patternContentUnits",
736
+ "patternTransform",
737
+ "patternUnits",
738
+ "pointsAtX",
739
+ "pointsAtY",
740
+ "pointsAtZ",
741
+ "preserveAlpha",
742
+ "preserveAspectRatio",
743
+ "primitiveUnits",
744
+ "refX",
745
+ "refY",
746
+ "repeatCount",
747
+ "repeatDur",
748
+ "requiredExtensions",
749
+ "requiredFeatures",
750
+ "specularConstant",
751
+ "specularExponent",
752
+ "spreadMethod",
753
+ "startOffset",
754
+ "stdDeviation",
755
+ "stitchTiles",
756
+ "surfaceScale",
757
+ "systemLanguage",
758
+ "tableValues",
759
+ "targetX",
760
+ "targetY",
761
+ "textLength",
762
+ "viewBox",
763
+ "viewTarget",
764
+ "xChannelSelector",
765
+ "yChannelSelector",
766
+ "zoomAndPan"
767
+ ].map((name) => [name.toLowerCase(), name]));
768
+ function svgAttributeName(el, attr) {
769
+ return el.namespaceURI === SVG_NS ? SVG_ATTRIBUTES.get(attr) ?? attr : attr;
770
+ }
707
771
  // src/runtime/events/EventBinder.ts
708
772
  class EventBinder {
709
773
  bind(el, eventType, handlerName, owner, cleanupTarget = owner) {
@@ -1040,6 +1104,30 @@ class Row {
1040
1104
  this.locals[this.locals.length - 1] = value;
1041
1105
  }
1042
1106
  }
1107
+ var TAG_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1108
+ var REFUSED_TAGS = new Set([
1109
+ "script",
1110
+ "style",
1111
+ "template",
1112
+ "slot",
1113
+ "iframe",
1114
+ "object",
1115
+ "embed"
1116
+ ]);
1117
+ function retag(source, name) {
1118
+ const doc = source.ownerDocument;
1119
+ const el = source.namespaceURI ? doc.createElementNS(source.namespaceURI, name) : doc.createElement(name);
1120
+ for (const attr of Array.from(source.attributes)) {
1121
+ if (attr.namespaceURI)
1122
+ el.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
1123
+ else
1124
+ el.setAttribute(attr.name, attr.value);
1125
+ }
1126
+ for (const child of Array.from(source.childNodes)) {
1127
+ el.appendChild(child.cloneNode(true));
1128
+ }
1129
+ return el;
1130
+ }
1043
1131
  function toIterable(value) {
1044
1132
  if (value == null)
1045
1133
  return [];
@@ -1081,12 +1169,13 @@ class ForBlock {
1081
1169
  renderer;
1082
1170
  conditional;
1083
1171
  itemsExpr;
1172
+ tagged;
1084
1173
  rows = new Map;
1085
1174
  order = [];
1086
1175
  pendingItems = null;
1087
1176
  running = false;
1088
1177
  warnedDuplicates = false;
1089
- constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "") {
1178
+ constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "", tagged = false) {
1090
1179
  this.owner = owner;
1091
1180
  this.anchor = anchor;
1092
1181
  this.templates = templates;
@@ -1097,22 +1186,55 @@ class ForBlock {
1097
1186
  this.renderer = renderer;
1098
1187
  this.conditional = conditional;
1099
1188
  this.itemsExpr = itemsExpr;
1189
+ this.tagged = tagged;
1100
1190
  }
1191
+ retagged = new Map;
1192
+ warnedTag = false;
1101
1193
  toItems(value) {
1102
1194
  if (this.conditional) {
1103
1195
  return Number.isInteger(value) && value >= 0 ? [value] : [];
1104
1196
  }
1197
+ if (this.tagged)
1198
+ return [this.tagFor(value)];
1105
1199
  return toIterable(value);
1106
1200
  }
1107
1201
  templateFor(item) {
1202
+ if (this.tagged) {
1203
+ const source = this.templates[0];
1204
+ if (item === source.localName)
1205
+ return source;
1206
+ let made = this.retagged.get(item);
1207
+ if (!made) {
1208
+ made = retag(source, item);
1209
+ this.retagged.set(item, made);
1210
+ }
1211
+ return made;
1212
+ }
1108
1213
  return this.conditional ? this.templates[item] : this.templates[0];
1109
1214
  }
1215
+ tagFor(value) {
1216
+ const written = this.templates[0].localName;
1217
+ if (value == null || value === "")
1218
+ return written;
1219
+ const name = typeof value === "string" ? value.trim().toLowerCase() : null;
1220
+ if (name && TAG_NAME.test(name) && !REFUSED_TAGS.has(name))
1221
+ return name;
1222
+ if (!this.warnedTag) {
1223
+ this.warnedTag = true;
1224
+ warn(`$tag="${this.itemsExpr}" gave ${JSON.stringify(value)}, which is not an element it can make - rendering <${written}>.`);
1225
+ }
1226
+ return written;
1227
+ }
1110
1228
  schedule(items) {
1111
1229
  this.pendingItems = items;
1112
1230
  if (this.running)
1113
1231
  return;
1114
1232
  this.running = true;
1115
- this.drain();
1233
+ this.draining = this.drain();
1234
+ }
1235
+ draining = null;
1236
+ settled() {
1237
+ return this.draining ?? Promise.resolve();
1116
1238
  }
1117
1239
  async drain() {
1118
1240
  try {
@@ -1466,6 +1588,15 @@ class Mounter {
1466
1588
  this.createBlock(el, context, "if");
1467
1589
  return;
1468
1590
  }
1591
+ if (el.hasAttribute("data-bind-tag")) {
1592
+ if (el.tagName === "TEMPLATE") {
1593
+ warn("$tag on a <template> means nothing - it renders its contents, not itself.", el);
1594
+ el.removeAttribute("data-bind-tag");
1595
+ } else {
1596
+ this.createBlock(el, context, "tag");
1597
+ return;
1598
+ }
1599
+ }
1469
1600
  if (el.hasAttribute("data-bind-else") || el.hasAttribute("data-bind-else-if")) {
1470
1601
  warn("$else / $else-if must directly follow a $if or $else-if sibling", el);
1471
1602
  el.remove();
@@ -1611,12 +1742,22 @@ class Mounter {
1611
1742
  i += consumed.templates - 1;
1612
1743
  continue;
1613
1744
  }
1745
+ if (el.hasAttribute("data-bind-tag") && el.tagName !== "TEMPLATE") {
1746
+ const consumed = this.adoptBlock(el, found, d, context, "tag");
1747
+ if (!consumed)
1748
+ return false;
1749
+ d += consumed.dom;
1750
+ continue;
1751
+ }
1614
1752
  if (el.tagName === "SLOT") {
1615
1753
  if (!found[d] || !isSlotStart(found[d])) {
1616
- if (!this.adoptNodes(Array.from(el.childNodes), found.slice(d), context, markers)) {
1754
+ const fallback = significant(Array.from(el.childNodes));
1755
+ const countable = fallback.every((n) => n.nodeType === 3 || n.nodeType === 1 && !isBlockTemplate(n) && n.tagName !== "SLOT");
1756
+ const share = countable ? found.slice(d, d + fallback.length) : found.slice(d);
1757
+ if (!this.adoptNodes(fallback, share, context, markers)) {
1617
1758
  return false;
1618
1759
  }
1619
- d = found.length;
1760
+ d += share.length;
1620
1761
  continue;
1621
1762
  }
1622
1763
  const name = found[d].data.slice(SLOT_MARK.length);
@@ -1746,18 +1887,19 @@ class Mounter {
1746
1887
  const rawExpression = el.getAttribute(`data-bind-${kind}`) ?? "";
1747
1888
  const branches = conditional ? this.gatherBranches(el) : [];
1748
1889
  const expression = conditional ? composeBranchExpression(branches) : rawExpression;
1749
- const parsed = conditional ? { alias: null, items: expression } : parseForExpression(rawExpression);
1890
+ const parsed = conditional || kind === "tag" ? { alias: null, items: expression } : parseForExpression(rawExpression);
1750
1891
  if (!parsed) {
1751
1892
  warn(`Could not parse $for="${expression}" - expected "item in items"`);
1752
1893
  el.remove();
1753
1894
  return null;
1754
1895
  }
1755
- if (!conditional && el.hasAttribute("data-bind-if")) {
1896
+ if (kind === "for" && el.hasAttribute("data-bind-if")) {
1756
1897
  warn(`$for and $if on the same element are not supported (${rawExpression}). Wrap one in an element.`);
1757
1898
  }
1758
- const keyExpr = conditional ? null : el.getAttribute("data-bind-key");
1899
+ const keyExpr = kind === "for" ? el.getAttribute("data-bind-key") : null;
1759
1900
  el.removeAttribute(`data-bind-${kind}`);
1760
- el.removeAttribute("data-bind-key");
1901
+ if (kind === "for")
1902
+ el.removeAttribute("data-bind-key");
1761
1903
  let anchor;
1762
1904
  if (existingAnchor) {
1763
1905
  anchor = existingAnchor;
@@ -1772,7 +1914,7 @@ class Mounter {
1772
1914
  const compiled = this.bindings.compileExpression(keyExpr, params);
1773
1915
  return compiled(context.owner.state, context.owner.props, this.cellsProxy(), this.bindings.pendingProbe(), ...locals);
1774
1916
  } : null;
1775
- const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items);
1917
+ const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items, kind === "tag");
1776
1918
  context.holder.addBlock(block);
1777
1919
  context.holder.addBinding(this.bindings.createForBinding(block, parsed.items, context.params));
1778
1920
  return block;
@@ -1860,9 +2002,10 @@ class Mounter {
1860
2002
  if (this.ssg) {
1861
2003
  fragment.insertBefore(document.createComment(ROW_MARK), fragment.firstChild);
1862
2004
  }
1863
- row.nodes = Array.from(fragment.childNodes);
1864
2005
  this.bindings.updateRow(block.owner, row);
1865
2006
  row.instances.forEach((child) => this.bindings.updateTree(child));
2007
+ await Promise.all(row.blocks.map((nested) => nested.settled()));
2008
+ row.nodes = Array.from(fragment.childNodes);
1866
2009
  return row;
1867
2010
  }
1868
2011
  connected() {
@@ -2268,7 +2411,10 @@ function isBlockAnchor(node) {
2268
2411
  if (node.nodeType !== 8)
2269
2412
  return false;
2270
2413
  const data = node.data.trim();
2271
- return data.startsWith("for:") || data.startsWith("if:");
2414
+ return data.startsWith("for:") || data.startsWith("if:") || data.startsWith("tag:");
2415
+ }
2416
+ function isBlockTemplate(el) {
2417
+ return el.hasAttribute("data-bind-for") || el.hasAttribute("data-bind-if") || el.hasAttribute("data-bind-tag");
2272
2418
  }
2273
2419
  function isInsignificant(node) {
2274
2420
  return node.nodeType === 3 && !(node.textContent ?? "").trim();
package/dist/ssg.js CHANGED
@@ -610,7 +610,7 @@ class BindingEvaluator {
610
610
  binding.el.innerHTML = value == null ? "" : String(value);
611
611
  break;
612
612
  case "attr": {
613
- const attr = binding.attr;
613
+ const attr = svgAttributeName(binding.el, binding.attr);
614
614
  const aria = attr.startsWith("aria-");
615
615
  if (BOOLEAN_ATTRIBUTES.has(attr) && !aria) {
616
616
  if (value)
@@ -723,6 +723,70 @@ function isPlain(value) {
723
723
  const proto = Object.getPrototypeOf(value);
724
724
  return proto === Object.prototype || proto === null;
725
725
  }
726
+ var SVG_NS = "http://www.w3.org/2000/svg";
727
+ var SVG_ATTRIBUTES = new Map([
728
+ "attributeName",
729
+ "attributeType",
730
+ "baseFrequency",
731
+ "baseProfile",
732
+ "calcMode",
733
+ "clipPathUnits",
734
+ "diffuseConstant",
735
+ "edgeMode",
736
+ "filterUnits",
737
+ "glyphRef",
738
+ "gradientTransform",
739
+ "gradientUnits",
740
+ "kernelMatrix",
741
+ "kernelUnitLength",
742
+ "keyPoints",
743
+ "keySplines",
744
+ "keyTimes",
745
+ "lengthAdjust",
746
+ "limitingConeAngle",
747
+ "markerHeight",
748
+ "markerUnits",
749
+ "markerWidth",
750
+ "maskContentUnits",
751
+ "maskUnits",
752
+ "numOctaves",
753
+ "pathLength",
754
+ "patternContentUnits",
755
+ "patternTransform",
756
+ "patternUnits",
757
+ "pointsAtX",
758
+ "pointsAtY",
759
+ "pointsAtZ",
760
+ "preserveAlpha",
761
+ "preserveAspectRatio",
762
+ "primitiveUnits",
763
+ "refX",
764
+ "refY",
765
+ "repeatCount",
766
+ "repeatDur",
767
+ "requiredExtensions",
768
+ "requiredFeatures",
769
+ "specularConstant",
770
+ "specularExponent",
771
+ "spreadMethod",
772
+ "startOffset",
773
+ "stdDeviation",
774
+ "stitchTiles",
775
+ "surfaceScale",
776
+ "systemLanguage",
777
+ "tableValues",
778
+ "targetX",
779
+ "targetY",
780
+ "textLength",
781
+ "viewBox",
782
+ "viewTarget",
783
+ "xChannelSelector",
784
+ "yChannelSelector",
785
+ "zoomAndPan"
786
+ ].map((name) => [name.toLowerCase(), name]));
787
+ function svgAttributeName(el, attr) {
788
+ return el.namespaceURI === SVG_NS ? SVG_ATTRIBUTES.get(attr) ?? attr : attr;
789
+ }
726
790
  // src/runtime/events/EventBinder.ts
727
791
  class EventBinder {
728
792
  bind(el, eventType, handlerName, owner, cleanupTarget = owner) {
@@ -1059,6 +1123,30 @@ class Row {
1059
1123
  this.locals[this.locals.length - 1] = value;
1060
1124
  }
1061
1125
  }
1126
+ var TAG_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1127
+ var REFUSED_TAGS = new Set([
1128
+ "script",
1129
+ "style",
1130
+ "template",
1131
+ "slot",
1132
+ "iframe",
1133
+ "object",
1134
+ "embed"
1135
+ ]);
1136
+ function retag(source, name) {
1137
+ const doc = source.ownerDocument;
1138
+ const el = source.namespaceURI ? doc.createElementNS(source.namespaceURI, name) : doc.createElement(name);
1139
+ for (const attr of Array.from(source.attributes)) {
1140
+ if (attr.namespaceURI)
1141
+ el.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
1142
+ else
1143
+ el.setAttribute(attr.name, attr.value);
1144
+ }
1145
+ for (const child of Array.from(source.childNodes)) {
1146
+ el.appendChild(child.cloneNode(true));
1147
+ }
1148
+ return el;
1149
+ }
1062
1150
  function toIterable(value) {
1063
1151
  if (value == null)
1064
1152
  return [];
@@ -1100,12 +1188,13 @@ class ForBlock {
1100
1188
  renderer;
1101
1189
  conditional;
1102
1190
  itemsExpr;
1191
+ tagged;
1103
1192
  rows = new Map;
1104
1193
  order = [];
1105
1194
  pendingItems = null;
1106
1195
  running = false;
1107
1196
  warnedDuplicates = false;
1108
- constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "") {
1197
+ constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "", tagged = false) {
1109
1198
  this.owner = owner;
1110
1199
  this.anchor = anchor;
1111
1200
  this.templates = templates;
@@ -1116,22 +1205,55 @@ class ForBlock {
1116
1205
  this.renderer = renderer;
1117
1206
  this.conditional = conditional;
1118
1207
  this.itemsExpr = itemsExpr;
1208
+ this.tagged = tagged;
1119
1209
  }
1210
+ retagged = new Map;
1211
+ warnedTag = false;
1120
1212
  toItems(value) {
1121
1213
  if (this.conditional) {
1122
1214
  return Number.isInteger(value) && value >= 0 ? [value] : [];
1123
1215
  }
1216
+ if (this.tagged)
1217
+ return [this.tagFor(value)];
1124
1218
  return toIterable(value);
1125
1219
  }
1126
1220
  templateFor(item) {
1221
+ if (this.tagged) {
1222
+ const source = this.templates[0];
1223
+ if (item === source.localName)
1224
+ return source;
1225
+ let made = this.retagged.get(item);
1226
+ if (!made) {
1227
+ made = retag(source, item);
1228
+ this.retagged.set(item, made);
1229
+ }
1230
+ return made;
1231
+ }
1127
1232
  return this.conditional ? this.templates[item] : this.templates[0];
1128
1233
  }
1234
+ tagFor(value) {
1235
+ const written = this.templates[0].localName;
1236
+ if (value == null || value === "")
1237
+ return written;
1238
+ const name = typeof value === "string" ? value.trim().toLowerCase() : null;
1239
+ if (name && TAG_NAME.test(name) && !REFUSED_TAGS.has(name))
1240
+ return name;
1241
+ if (!this.warnedTag) {
1242
+ this.warnedTag = true;
1243
+ warn(`$tag="${this.itemsExpr}" gave ${JSON.stringify(value)}, which is not an element it can make - rendering <${written}>.`);
1244
+ }
1245
+ return written;
1246
+ }
1129
1247
  schedule(items) {
1130
1248
  this.pendingItems = items;
1131
1249
  if (this.running)
1132
1250
  return;
1133
1251
  this.running = true;
1134
- this.drain();
1252
+ this.draining = this.drain();
1253
+ }
1254
+ draining = null;
1255
+ settled() {
1256
+ return this.draining ?? Promise.resolve();
1135
1257
  }
1136
1258
  async drain() {
1137
1259
  try {
@@ -1489,6 +1611,15 @@ class Mounter {
1489
1611
  this.createBlock(el, context, "if");
1490
1612
  return;
1491
1613
  }
1614
+ if (el.hasAttribute("data-bind-tag")) {
1615
+ if (el.tagName === "TEMPLATE") {
1616
+ warn("$tag on a <template> means nothing - it renders its contents, not itself.", el);
1617
+ el.removeAttribute("data-bind-tag");
1618
+ } else {
1619
+ this.createBlock(el, context, "tag");
1620
+ return;
1621
+ }
1622
+ }
1492
1623
  if (el.hasAttribute("data-bind-else") || el.hasAttribute("data-bind-else-if")) {
1493
1624
  warn("$else / $else-if must directly follow a $if or $else-if sibling", el);
1494
1625
  el.remove();
@@ -1634,12 +1765,22 @@ class Mounter {
1634
1765
  i += consumed.templates - 1;
1635
1766
  continue;
1636
1767
  }
1768
+ if (el.hasAttribute("data-bind-tag") && el.tagName !== "TEMPLATE") {
1769
+ const consumed = this.adoptBlock(el, found, d, context, "tag");
1770
+ if (!consumed)
1771
+ return false;
1772
+ d += consumed.dom;
1773
+ continue;
1774
+ }
1637
1775
  if (el.tagName === "SLOT") {
1638
1776
  if (!found[d] || !isSlotStart(found[d])) {
1639
- if (!this.adoptNodes(Array.from(el.childNodes), found.slice(d), context, markers)) {
1777
+ const fallback = significant(Array.from(el.childNodes));
1778
+ const countable = fallback.every((n) => n.nodeType === 3 || n.nodeType === 1 && !isBlockTemplate(n) && n.tagName !== "SLOT");
1779
+ const share = countable ? found.slice(d, d + fallback.length) : found.slice(d);
1780
+ if (!this.adoptNodes(fallback, share, context, markers)) {
1640
1781
  return false;
1641
1782
  }
1642
- d = found.length;
1783
+ d += share.length;
1643
1784
  continue;
1644
1785
  }
1645
1786
  const name = found[d].data.slice(SLOT_MARK.length);
@@ -1769,18 +1910,19 @@ class Mounter {
1769
1910
  const rawExpression = el.getAttribute(`data-bind-${kind}`) ?? "";
1770
1911
  const branches = conditional ? this.gatherBranches(el) : [];
1771
1912
  const expression = conditional ? composeBranchExpression(branches) : rawExpression;
1772
- const parsed = conditional ? { alias: null, items: expression } : parseForExpression(rawExpression);
1913
+ const parsed = conditional || kind === "tag" ? { alias: null, items: expression } : parseForExpression(rawExpression);
1773
1914
  if (!parsed) {
1774
1915
  warn(`Could not parse $for="${expression}" - expected "item in items"`);
1775
1916
  el.remove();
1776
1917
  return null;
1777
1918
  }
1778
- if (!conditional && el.hasAttribute("data-bind-if")) {
1919
+ if (kind === "for" && el.hasAttribute("data-bind-if")) {
1779
1920
  warn(`$for and $if on the same element are not supported (${rawExpression}). Wrap one in an element.`);
1780
1921
  }
1781
- const keyExpr = conditional ? null : el.getAttribute("data-bind-key");
1922
+ const keyExpr = kind === "for" ? el.getAttribute("data-bind-key") : null;
1782
1923
  el.removeAttribute(`data-bind-${kind}`);
1783
- el.removeAttribute("data-bind-key");
1924
+ if (kind === "for")
1925
+ el.removeAttribute("data-bind-key");
1784
1926
  let anchor;
1785
1927
  if (existingAnchor) {
1786
1928
  anchor = existingAnchor;
@@ -1795,7 +1937,7 @@ class Mounter {
1795
1937
  const compiled = this.bindings.compileExpression(keyExpr, params);
1796
1938
  return compiled(context.owner.state, context.owner.props, this.cellsProxy(), this.bindings.pendingProbe(), ...locals);
1797
1939
  } : null;
1798
- const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items);
1940
+ const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items, kind === "tag");
1799
1941
  context.holder.addBlock(block);
1800
1942
  context.holder.addBinding(this.bindings.createForBinding(block, parsed.items, context.params));
1801
1943
  return block;
@@ -1883,9 +2025,10 @@ class Mounter {
1883
2025
  if (this.ssg) {
1884
2026
  fragment.insertBefore(document.createComment(ROW_MARK), fragment.firstChild);
1885
2027
  }
1886
- row.nodes = Array.from(fragment.childNodes);
1887
2028
  this.bindings.updateRow(block.owner, row);
1888
2029
  row.instances.forEach((child) => this.bindings.updateTree(child));
2030
+ await Promise.all(row.blocks.map((nested) => nested.settled()));
2031
+ row.nodes = Array.from(fragment.childNodes);
1889
2032
  return row;
1890
2033
  }
1891
2034
  connected() {
@@ -2291,7 +2434,10 @@ function isBlockAnchor(node) {
2291
2434
  if (node.nodeType !== 8)
2292
2435
  return false;
2293
2436
  const data = node.data.trim();
2294
- return data.startsWith("for:") || data.startsWith("if:");
2437
+ return data.startsWith("for:") || data.startsWith("if:") || data.startsWith("tag:");
2438
+ }
2439
+ function isBlockTemplate(el) {
2440
+ return el.hasAttribute("data-bind-for") || el.hasAttribute("data-bind-if") || el.hasAttribute("data-bind-tag");
2295
2441
  }
2296
2442
  function isInsignificant(node) {
2297
2443
  return node.nodeType === 3 && !(node.textContent ?? "").trim();
@@ -73,6 +73,8 @@ export declare class ForBlock {
73
73
  conditional: boolean;
74
74
  /** The collection expression, kept so adoption can re-evaluate it. */
75
75
  itemsExpr: string;
76
+ /** `$tag`: one row, of the element the expression names (ADR-063). */
77
+ tagged: boolean;
76
78
  rows: Map<string, Row>;
77
79
  order: string[];
78
80
  private pendingItems;
@@ -93,7 +95,12 @@ export declare class ForBlock {
93
95
  /** `$if`: zero or one row, driven by a condition rather than a list. */
94
96
  conditional?: boolean,
95
97
  /** The collection expression, kept so adoption can re-evaluate it. */
96
- itemsExpr?: string);
98
+ itemsExpr?: string,
99
+ /** `$tag`: one row, of the element the expression names (ADR-063). */
100
+ tagged?: boolean);
101
+ /** `$tag`'s copies of the template under other names, made once each. */
102
+ private retagged;
103
+ private warnedTag;
97
104
  /**
98
105
  * Turn a bound expression into the rows it implies.
99
106
  *
@@ -103,6 +110,8 @@ export declare class ForBlock {
103
110
  toItems(value: any): any[];
104
111
  /** The markup a row should clone. */
105
112
  templateFor(item: any): Element;
113
+ /** The element `$tag` evaluated to, or the one as written. */
114
+ private tagFor;
106
115
  /**
107
116
  * Queue a reconcile.
108
117
  *
@@ -111,6 +120,14 @@ export declare class ForBlock {
111
120
  * matters, so it replaces any queued one and the loop runs until settled.
112
121
  */
113
122
  schedule(items: any[]): void;
123
+ private draining;
124
+ /**
125
+ * Resolves once every queued reconcile has run.
126
+ *
127
+ * A row built inside another block's new row waits for this before that row
128
+ * counts its nodes, or the rows placed meanwhile are left out of it.
129
+ */
130
+ settled(): Promise<void>;
114
131
  private drain;
115
132
  /** Public so adoption can match rendered rows to the current data. */
116
133
  keysFor(items: any[]): string[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/dist/vite.js CHANGED
@@ -349,7 +349,7 @@ class BindingEvaluator {
349
349
  binding.el.innerHTML = value == null ? "" : String(value);
350
350
  break;
351
351
  case "attr": {
352
- const attr = binding.attr;
352
+ const attr = svgAttributeName(binding.el, binding.attr);
353
353
  const aria = attr.startsWith("aria-");
354
354
  if (BOOLEAN_ATTRIBUTES.has(attr) && !aria) {
355
355
  if (value)
@@ -462,6 +462,70 @@ function isPlain(value) {
462
462
  const proto = Object.getPrototypeOf(value);
463
463
  return proto === Object.prototype || proto === null;
464
464
  }
465
+ var SVG_NS = "http://www.w3.org/2000/svg";
466
+ var SVG_ATTRIBUTES = new Map([
467
+ "attributeName",
468
+ "attributeType",
469
+ "baseFrequency",
470
+ "baseProfile",
471
+ "calcMode",
472
+ "clipPathUnits",
473
+ "diffuseConstant",
474
+ "edgeMode",
475
+ "filterUnits",
476
+ "glyphRef",
477
+ "gradientTransform",
478
+ "gradientUnits",
479
+ "kernelMatrix",
480
+ "kernelUnitLength",
481
+ "keyPoints",
482
+ "keySplines",
483
+ "keyTimes",
484
+ "lengthAdjust",
485
+ "limitingConeAngle",
486
+ "markerHeight",
487
+ "markerUnits",
488
+ "markerWidth",
489
+ "maskContentUnits",
490
+ "maskUnits",
491
+ "numOctaves",
492
+ "pathLength",
493
+ "patternContentUnits",
494
+ "patternTransform",
495
+ "patternUnits",
496
+ "pointsAtX",
497
+ "pointsAtY",
498
+ "pointsAtZ",
499
+ "preserveAlpha",
500
+ "preserveAspectRatio",
501
+ "primitiveUnits",
502
+ "refX",
503
+ "refY",
504
+ "repeatCount",
505
+ "repeatDur",
506
+ "requiredExtensions",
507
+ "requiredFeatures",
508
+ "specularConstant",
509
+ "specularExponent",
510
+ "spreadMethod",
511
+ "startOffset",
512
+ "stdDeviation",
513
+ "stitchTiles",
514
+ "surfaceScale",
515
+ "systemLanguage",
516
+ "tableValues",
517
+ "targetX",
518
+ "targetY",
519
+ "textLength",
520
+ "viewBox",
521
+ "viewTarget",
522
+ "xChannelSelector",
523
+ "yChannelSelector",
524
+ "zoomAndPan"
525
+ ].map((name) => [name.toLowerCase(), name]));
526
+ function svgAttributeName(el, attr) {
527
+ return el.namespaceURI === SVG_NS ? SVG_ATTRIBUTES.get(attr) ?? attr : attr;
528
+ }
465
529
  // src/runtime/dom/ForBlock.ts
466
530
  class Row {
467
531
  locals;
@@ -490,6 +554,30 @@ class Row {
490
554
  this.locals[this.locals.length - 1] = value;
491
555
  }
492
556
  }
557
+ var TAG_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
558
+ var REFUSED_TAGS = new Set([
559
+ "script",
560
+ "style",
561
+ "template",
562
+ "slot",
563
+ "iframe",
564
+ "object",
565
+ "embed"
566
+ ]);
567
+ function retag(source, name) {
568
+ const doc = source.ownerDocument;
569
+ const el = source.namespaceURI ? doc.createElementNS(source.namespaceURI, name) : doc.createElement(name);
570
+ for (const attr of Array.from(source.attributes)) {
571
+ if (attr.namespaceURI)
572
+ el.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
573
+ else
574
+ el.setAttribute(attr.name, attr.value);
575
+ }
576
+ for (const child of Array.from(source.childNodes)) {
577
+ el.appendChild(child.cloneNode(true));
578
+ }
579
+ return el;
580
+ }
493
581
  function toIterable(value) {
494
582
  if (value == null)
495
583
  return [];
@@ -531,12 +619,13 @@ class ForBlock {
531
619
  renderer;
532
620
  conditional;
533
621
  itemsExpr;
622
+ tagged;
534
623
  rows = new Map;
535
624
  order = [];
536
625
  pendingItems = null;
537
626
  running = false;
538
627
  warnedDuplicates = false;
539
- constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "") {
628
+ constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "", tagged = false) {
540
629
  this.owner = owner;
541
630
  this.anchor = anchor;
542
631
  this.templates = templates;
@@ -547,22 +636,55 @@ class ForBlock {
547
636
  this.renderer = renderer;
548
637
  this.conditional = conditional;
549
638
  this.itemsExpr = itemsExpr;
639
+ this.tagged = tagged;
550
640
  }
641
+ retagged = new Map;
642
+ warnedTag = false;
551
643
  toItems(value) {
552
644
  if (this.conditional) {
553
645
  return Number.isInteger(value) && value >= 0 ? [value] : [];
554
646
  }
647
+ if (this.tagged)
648
+ return [this.tagFor(value)];
555
649
  return toIterable(value);
556
650
  }
557
651
  templateFor(item) {
652
+ if (this.tagged) {
653
+ const source = this.templates[0];
654
+ if (item === source.localName)
655
+ return source;
656
+ let made = this.retagged.get(item);
657
+ if (!made) {
658
+ made = retag(source, item);
659
+ this.retagged.set(item, made);
660
+ }
661
+ return made;
662
+ }
558
663
  return this.conditional ? this.templates[item] : this.templates[0];
559
664
  }
665
+ tagFor(value) {
666
+ const written = this.templates[0].localName;
667
+ if (value == null || value === "")
668
+ return written;
669
+ const name = typeof value === "string" ? value.trim().toLowerCase() : null;
670
+ if (name && TAG_NAME.test(name) && !REFUSED_TAGS.has(name))
671
+ return name;
672
+ if (!this.warnedTag) {
673
+ this.warnedTag = true;
674
+ warn(`$tag="${this.itemsExpr}" gave ${JSON.stringify(value)}, which is not an element it can make - rendering <${written}>.`);
675
+ }
676
+ return written;
677
+ }
560
678
  schedule(items) {
561
679
  this.pendingItems = items;
562
680
  if (this.running)
563
681
  return;
564
682
  this.running = true;
565
- this.drain();
683
+ this.draining = this.drain();
684
+ }
685
+ draining = null;
686
+ settled() {
687
+ return this.draining ?? Promise.resolve();
566
688
  }
567
689
  async drain() {
568
690
  try {
@@ -920,6 +1042,15 @@ class Mounter {
920
1042
  this.createBlock(el, context, "if");
921
1043
  return;
922
1044
  }
1045
+ if (el.hasAttribute("data-bind-tag")) {
1046
+ if (el.tagName === "TEMPLATE") {
1047
+ warn("$tag on a <template> means nothing - it renders its contents, not itself.", el);
1048
+ el.removeAttribute("data-bind-tag");
1049
+ } else {
1050
+ this.createBlock(el, context, "tag");
1051
+ return;
1052
+ }
1053
+ }
923
1054
  if (el.hasAttribute("data-bind-else") || el.hasAttribute("data-bind-else-if")) {
924
1055
  warn("$else / $else-if must directly follow a $if or $else-if sibling", el);
925
1056
  el.remove();
@@ -1065,12 +1196,22 @@ class Mounter {
1065
1196
  i += consumed.templates - 1;
1066
1197
  continue;
1067
1198
  }
1199
+ if (el.hasAttribute("data-bind-tag") && el.tagName !== "TEMPLATE") {
1200
+ const consumed = this.adoptBlock(el, found, d, context, "tag");
1201
+ if (!consumed)
1202
+ return false;
1203
+ d += consumed.dom;
1204
+ continue;
1205
+ }
1068
1206
  if (el.tagName === "SLOT") {
1069
1207
  if (!found[d] || !isSlotStart(found[d])) {
1070
- if (!this.adoptNodes(Array.from(el.childNodes), found.slice(d), context, markers)) {
1208
+ const fallback = significant(Array.from(el.childNodes));
1209
+ const countable = fallback.every((n) => n.nodeType === 3 || n.nodeType === 1 && !isBlockTemplate(n) && n.tagName !== "SLOT");
1210
+ const share = countable ? found.slice(d, d + fallback.length) : found.slice(d);
1211
+ if (!this.adoptNodes(fallback, share, context, markers)) {
1071
1212
  return false;
1072
1213
  }
1073
- d = found.length;
1214
+ d += share.length;
1074
1215
  continue;
1075
1216
  }
1076
1217
  const name = found[d].data.slice(SLOT_MARK.length);
@@ -1200,18 +1341,19 @@ class Mounter {
1200
1341
  const rawExpression = el.getAttribute(`data-bind-${kind}`) ?? "";
1201
1342
  const branches = conditional ? this.gatherBranches(el) : [];
1202
1343
  const expression = conditional ? composeBranchExpression(branches) : rawExpression;
1203
- const parsed = conditional ? { alias: null, items: expression } : parseForExpression(rawExpression);
1344
+ const parsed = conditional || kind === "tag" ? { alias: null, items: expression } : parseForExpression(rawExpression);
1204
1345
  if (!parsed) {
1205
1346
  warn(`Could not parse $for="${expression}" - expected "item in items"`);
1206
1347
  el.remove();
1207
1348
  return null;
1208
1349
  }
1209
- if (!conditional && el.hasAttribute("data-bind-if")) {
1350
+ if (kind === "for" && el.hasAttribute("data-bind-if")) {
1210
1351
  warn(`$for and $if on the same element are not supported (${rawExpression}). Wrap one in an element.`);
1211
1352
  }
1212
- const keyExpr = conditional ? null : el.getAttribute("data-bind-key");
1353
+ const keyExpr = kind === "for" ? el.getAttribute("data-bind-key") : null;
1213
1354
  el.removeAttribute(`data-bind-${kind}`);
1214
- el.removeAttribute("data-bind-key");
1355
+ if (kind === "for")
1356
+ el.removeAttribute("data-bind-key");
1215
1357
  let anchor;
1216
1358
  if (existingAnchor) {
1217
1359
  anchor = existingAnchor;
@@ -1226,7 +1368,7 @@ class Mounter {
1226
1368
  const compiled = this.bindings.compileExpression(keyExpr, params);
1227
1369
  return compiled(context.owner.state, context.owner.props, this.cellsProxy(), this.bindings.pendingProbe(), ...locals);
1228
1370
  } : null;
1229
- const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items);
1371
+ const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items, kind === "tag");
1230
1372
  context.holder.addBlock(block);
1231
1373
  context.holder.addBinding(this.bindings.createForBinding(block, parsed.items, context.params));
1232
1374
  return block;
@@ -1314,9 +1456,10 @@ class Mounter {
1314
1456
  if (this.ssg) {
1315
1457
  fragment.insertBefore(document.createComment(ROW_MARK), fragment.firstChild);
1316
1458
  }
1317
- row.nodes = Array.from(fragment.childNodes);
1318
1459
  this.bindings.updateRow(block.owner, row);
1319
1460
  row.instances.forEach((child) => this.bindings.updateTree(child));
1461
+ await Promise.all(row.blocks.map((nested) => nested.settled()));
1462
+ row.nodes = Array.from(fragment.childNodes);
1320
1463
  return row;
1321
1464
  }
1322
1465
  connected() {
@@ -1722,7 +1865,10 @@ function isBlockAnchor(node) {
1722
1865
  if (node.nodeType !== 8)
1723
1866
  return false;
1724
1867
  const data = node.data.trim();
1725
- return data.startsWith("for:") || data.startsWith("if:");
1868
+ return data.startsWith("for:") || data.startsWith("if:") || data.startsWith("tag:");
1869
+ }
1870
+ function isBlockTemplate(el) {
1871
+ return el.hasAttribute("data-bind-for") || el.hasAttribute("data-bind-if") || el.hasAttribute("data-bind-tag");
1726
1872
  }
1727
1873
  function isInsignificant(node) {
1728
1874
  return node.nodeType === 3 && !(node.textContent ?? "").trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightspeed-js/core",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Lightspeed: a compile-first framework for islands of reactivity in static HTML.",
5
5
  "keywords": [
6
6
  "lightspeed",