@lightspeed-js/core 0.4.5 → 0.4.6

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
@@ -1133,6 +1133,30 @@ function isPlain(value) {
1133
1133
  return proto === Object.prototype || proto === null;
1134
1134
  }
1135
1135
  // src/runtime/dom/ForBlock.ts
1136
+ var TAG_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1137
+ var REFUSED_TAGS = new Set([
1138
+ "script",
1139
+ "style",
1140
+ "template",
1141
+ "slot",
1142
+ "iframe",
1143
+ "object",
1144
+ "embed"
1145
+ ]);
1146
+ function retag(source, name) {
1147
+ const doc = source.ownerDocument;
1148
+ const el = source.namespaceURI ? doc.createElementNS(source.namespaceURI, name) : doc.createElement(name);
1149
+ for (const attr of Array.from(source.attributes)) {
1150
+ if (attr.namespaceURI)
1151
+ el.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
1152
+ else
1153
+ el.setAttribute(attr.name, attr.value);
1154
+ }
1155
+ for (const child of Array.from(source.childNodes)) {
1156
+ el.appendChild(child.cloneNode(true));
1157
+ }
1158
+ return el;
1159
+ }
1136
1160
  function toIterable(value) {
1137
1161
  if (value == null)
1138
1162
  return [];
@@ -1174,12 +1198,13 @@ class ForBlock {
1174
1198
  renderer;
1175
1199
  conditional;
1176
1200
  itemsExpr;
1201
+ tagged;
1177
1202
  rows = new Map;
1178
1203
  order = [];
1179
1204
  pendingItems = null;
1180
1205
  running = false;
1181
1206
  warnedDuplicates = false;
1182
- constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "") {
1207
+ constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "", tagged = false) {
1183
1208
  this.owner = owner;
1184
1209
  this.anchor = anchor;
1185
1210
  this.templates = templates;
@@ -1190,22 +1215,55 @@ class ForBlock {
1190
1215
  this.renderer = renderer;
1191
1216
  this.conditional = conditional;
1192
1217
  this.itemsExpr = itemsExpr;
1218
+ this.tagged = tagged;
1193
1219
  }
1220
+ retagged = new Map;
1221
+ warnedTag = false;
1194
1222
  toItems(value) {
1195
1223
  if (this.conditional) {
1196
1224
  return Number.isInteger(value) && value >= 0 ? [value] : [];
1197
1225
  }
1226
+ if (this.tagged)
1227
+ return [this.tagFor(value)];
1198
1228
  return toIterable(value);
1199
1229
  }
1200
1230
  templateFor(item) {
1231
+ if (this.tagged) {
1232
+ const source = this.templates[0];
1233
+ if (item === source.localName)
1234
+ return source;
1235
+ let made = this.retagged.get(item);
1236
+ if (!made) {
1237
+ made = retag(source, item);
1238
+ this.retagged.set(item, made);
1239
+ }
1240
+ return made;
1241
+ }
1201
1242
  return this.conditional ? this.templates[item] : this.templates[0];
1202
1243
  }
1244
+ tagFor(value) {
1245
+ const written = this.templates[0].localName;
1246
+ if (value == null || value === "")
1247
+ return written;
1248
+ const name = typeof value === "string" ? value.trim().toLowerCase() : null;
1249
+ if (name && TAG_NAME.test(name) && !REFUSED_TAGS.has(name))
1250
+ return name;
1251
+ if (!this.warnedTag) {
1252
+ this.warnedTag = true;
1253
+ warn(`$tag="${this.itemsExpr}" gave ${JSON.stringify(value)}, which is not an element it can make - rendering <${written}>.`);
1254
+ }
1255
+ return written;
1256
+ }
1203
1257
  schedule(items) {
1204
1258
  this.pendingItems = items;
1205
1259
  if (this.running)
1206
1260
  return;
1207
1261
  this.running = true;
1208
- this.drain();
1262
+ this.draining = this.drain();
1263
+ }
1264
+ draining = null;
1265
+ settled() {
1266
+ return this.draining ?? Promise.resolve();
1209
1267
  }
1210
1268
  async drain() {
1211
1269
  try {
package/dist/index.js CHANGED
@@ -1040,6 +1040,30 @@ class Row {
1040
1040
  this.locals[this.locals.length - 1] = value;
1041
1041
  }
1042
1042
  }
1043
+ var TAG_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1044
+ var REFUSED_TAGS = new Set([
1045
+ "script",
1046
+ "style",
1047
+ "template",
1048
+ "slot",
1049
+ "iframe",
1050
+ "object",
1051
+ "embed"
1052
+ ]);
1053
+ function retag(source, name) {
1054
+ const doc = source.ownerDocument;
1055
+ const el = source.namespaceURI ? doc.createElementNS(source.namespaceURI, name) : doc.createElement(name);
1056
+ for (const attr of Array.from(source.attributes)) {
1057
+ if (attr.namespaceURI)
1058
+ el.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
1059
+ else
1060
+ el.setAttribute(attr.name, attr.value);
1061
+ }
1062
+ for (const child of Array.from(source.childNodes)) {
1063
+ el.appendChild(child.cloneNode(true));
1064
+ }
1065
+ return el;
1066
+ }
1043
1067
  function toIterable(value) {
1044
1068
  if (value == null)
1045
1069
  return [];
@@ -1081,12 +1105,13 @@ class ForBlock {
1081
1105
  renderer;
1082
1106
  conditional;
1083
1107
  itemsExpr;
1108
+ tagged;
1084
1109
  rows = new Map;
1085
1110
  order = [];
1086
1111
  pendingItems = null;
1087
1112
  running = false;
1088
1113
  warnedDuplicates = false;
1089
- constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "") {
1114
+ constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "", tagged = false) {
1090
1115
  this.owner = owner;
1091
1116
  this.anchor = anchor;
1092
1117
  this.templates = templates;
@@ -1097,22 +1122,55 @@ class ForBlock {
1097
1122
  this.renderer = renderer;
1098
1123
  this.conditional = conditional;
1099
1124
  this.itemsExpr = itemsExpr;
1125
+ this.tagged = tagged;
1100
1126
  }
1127
+ retagged = new Map;
1128
+ warnedTag = false;
1101
1129
  toItems(value) {
1102
1130
  if (this.conditional) {
1103
1131
  return Number.isInteger(value) && value >= 0 ? [value] : [];
1104
1132
  }
1133
+ if (this.tagged)
1134
+ return [this.tagFor(value)];
1105
1135
  return toIterable(value);
1106
1136
  }
1107
1137
  templateFor(item) {
1138
+ if (this.tagged) {
1139
+ const source = this.templates[0];
1140
+ if (item === source.localName)
1141
+ return source;
1142
+ let made = this.retagged.get(item);
1143
+ if (!made) {
1144
+ made = retag(source, item);
1145
+ this.retagged.set(item, made);
1146
+ }
1147
+ return made;
1148
+ }
1108
1149
  return this.conditional ? this.templates[item] : this.templates[0];
1109
1150
  }
1151
+ tagFor(value) {
1152
+ const written = this.templates[0].localName;
1153
+ if (value == null || value === "")
1154
+ return written;
1155
+ const name = typeof value === "string" ? value.trim().toLowerCase() : null;
1156
+ if (name && TAG_NAME.test(name) && !REFUSED_TAGS.has(name))
1157
+ return name;
1158
+ if (!this.warnedTag) {
1159
+ this.warnedTag = true;
1160
+ warn(`$tag="${this.itemsExpr}" gave ${JSON.stringify(value)}, which is not an element it can make - rendering <${written}>.`);
1161
+ }
1162
+ return written;
1163
+ }
1110
1164
  schedule(items) {
1111
1165
  this.pendingItems = items;
1112
1166
  if (this.running)
1113
1167
  return;
1114
1168
  this.running = true;
1115
- this.drain();
1169
+ this.draining = this.drain();
1170
+ }
1171
+ draining = null;
1172
+ settled() {
1173
+ return this.draining ?? Promise.resolve();
1116
1174
  }
1117
1175
  async drain() {
1118
1176
  try {
@@ -1466,6 +1524,15 @@ class Mounter {
1466
1524
  this.createBlock(el, context, "if");
1467
1525
  return;
1468
1526
  }
1527
+ if (el.hasAttribute("data-bind-tag")) {
1528
+ if (el.tagName === "TEMPLATE") {
1529
+ warn("$tag on a <template> means nothing - it renders its contents, not itself.", el);
1530
+ el.removeAttribute("data-bind-tag");
1531
+ } else {
1532
+ this.createBlock(el, context, "tag");
1533
+ return;
1534
+ }
1535
+ }
1469
1536
  if (el.hasAttribute("data-bind-else") || el.hasAttribute("data-bind-else-if")) {
1470
1537
  warn("$else / $else-if must directly follow a $if or $else-if sibling", el);
1471
1538
  el.remove();
@@ -1611,6 +1678,13 @@ class Mounter {
1611
1678
  i += consumed.templates - 1;
1612
1679
  continue;
1613
1680
  }
1681
+ if (el.hasAttribute("data-bind-tag") && el.tagName !== "TEMPLATE") {
1682
+ const consumed = this.adoptBlock(el, found, d, context, "tag");
1683
+ if (!consumed)
1684
+ return false;
1685
+ d += consumed.dom;
1686
+ continue;
1687
+ }
1614
1688
  if (el.tagName === "SLOT") {
1615
1689
  if (!found[d] || !isSlotStart(found[d])) {
1616
1690
  if (!this.adoptNodes(Array.from(el.childNodes), found.slice(d), context, markers)) {
@@ -1746,18 +1820,19 @@ class Mounter {
1746
1820
  const rawExpression = el.getAttribute(`data-bind-${kind}`) ?? "";
1747
1821
  const branches = conditional ? this.gatherBranches(el) : [];
1748
1822
  const expression = conditional ? composeBranchExpression(branches) : rawExpression;
1749
- const parsed = conditional ? { alias: null, items: expression } : parseForExpression(rawExpression);
1823
+ const parsed = conditional || kind === "tag" ? { alias: null, items: expression } : parseForExpression(rawExpression);
1750
1824
  if (!parsed) {
1751
1825
  warn(`Could not parse $for="${expression}" - expected "item in items"`);
1752
1826
  el.remove();
1753
1827
  return null;
1754
1828
  }
1755
- if (!conditional && el.hasAttribute("data-bind-if")) {
1829
+ if (kind === "for" && el.hasAttribute("data-bind-if")) {
1756
1830
  warn(`$for and $if on the same element are not supported (${rawExpression}). Wrap one in an element.`);
1757
1831
  }
1758
- const keyExpr = conditional ? null : el.getAttribute("data-bind-key");
1832
+ const keyExpr = kind === "for" ? el.getAttribute("data-bind-key") : null;
1759
1833
  el.removeAttribute(`data-bind-${kind}`);
1760
- el.removeAttribute("data-bind-key");
1834
+ if (kind === "for")
1835
+ el.removeAttribute("data-bind-key");
1761
1836
  let anchor;
1762
1837
  if (existingAnchor) {
1763
1838
  anchor = existingAnchor;
@@ -1772,7 +1847,7 @@ class Mounter {
1772
1847
  const compiled = this.bindings.compileExpression(keyExpr, params);
1773
1848
  return compiled(context.owner.state, context.owner.props, this.cellsProxy(), this.bindings.pendingProbe(), ...locals);
1774
1849
  } : null;
1775
- const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items);
1850
+ const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items, kind === "tag");
1776
1851
  context.holder.addBlock(block);
1777
1852
  context.holder.addBinding(this.bindings.createForBinding(block, parsed.items, context.params));
1778
1853
  return block;
@@ -1860,9 +1935,10 @@ class Mounter {
1860
1935
  if (this.ssg) {
1861
1936
  fragment.insertBefore(document.createComment(ROW_MARK), fragment.firstChild);
1862
1937
  }
1863
- row.nodes = Array.from(fragment.childNodes);
1864
1938
  this.bindings.updateRow(block.owner, row);
1865
1939
  row.instances.forEach((child) => this.bindings.updateTree(child));
1940
+ await Promise.all(row.blocks.map((nested) => nested.settled()));
1941
+ row.nodes = Array.from(fragment.childNodes);
1866
1942
  return row;
1867
1943
  }
1868
1944
  connected() {
@@ -2268,7 +2344,7 @@ function isBlockAnchor(node) {
2268
2344
  if (node.nodeType !== 8)
2269
2345
  return false;
2270
2346
  const data = node.data.trim();
2271
- return data.startsWith("for:") || data.startsWith("if:");
2347
+ return data.startsWith("for:") || data.startsWith("if:") || data.startsWith("tag:");
2272
2348
  }
2273
2349
  function isInsignificant(node) {
2274
2350
  return node.nodeType === 3 && !(node.textContent ?? "").trim();
package/dist/ssg.js CHANGED
@@ -1059,6 +1059,30 @@ class Row {
1059
1059
  this.locals[this.locals.length - 1] = value;
1060
1060
  }
1061
1061
  }
1062
+ var TAG_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1063
+ var REFUSED_TAGS = new Set([
1064
+ "script",
1065
+ "style",
1066
+ "template",
1067
+ "slot",
1068
+ "iframe",
1069
+ "object",
1070
+ "embed"
1071
+ ]);
1072
+ function retag(source, name) {
1073
+ const doc = source.ownerDocument;
1074
+ const el = source.namespaceURI ? doc.createElementNS(source.namespaceURI, name) : doc.createElement(name);
1075
+ for (const attr of Array.from(source.attributes)) {
1076
+ if (attr.namespaceURI)
1077
+ el.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
1078
+ else
1079
+ el.setAttribute(attr.name, attr.value);
1080
+ }
1081
+ for (const child of Array.from(source.childNodes)) {
1082
+ el.appendChild(child.cloneNode(true));
1083
+ }
1084
+ return el;
1085
+ }
1062
1086
  function toIterable(value) {
1063
1087
  if (value == null)
1064
1088
  return [];
@@ -1100,12 +1124,13 @@ class ForBlock {
1100
1124
  renderer;
1101
1125
  conditional;
1102
1126
  itemsExpr;
1127
+ tagged;
1103
1128
  rows = new Map;
1104
1129
  order = [];
1105
1130
  pendingItems = null;
1106
1131
  running = false;
1107
1132
  warnedDuplicates = false;
1108
- constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "") {
1133
+ constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "", tagged = false) {
1109
1134
  this.owner = owner;
1110
1135
  this.anchor = anchor;
1111
1136
  this.templates = templates;
@@ -1116,22 +1141,55 @@ class ForBlock {
1116
1141
  this.renderer = renderer;
1117
1142
  this.conditional = conditional;
1118
1143
  this.itemsExpr = itemsExpr;
1144
+ this.tagged = tagged;
1119
1145
  }
1146
+ retagged = new Map;
1147
+ warnedTag = false;
1120
1148
  toItems(value) {
1121
1149
  if (this.conditional) {
1122
1150
  return Number.isInteger(value) && value >= 0 ? [value] : [];
1123
1151
  }
1152
+ if (this.tagged)
1153
+ return [this.tagFor(value)];
1124
1154
  return toIterable(value);
1125
1155
  }
1126
1156
  templateFor(item) {
1157
+ if (this.tagged) {
1158
+ const source = this.templates[0];
1159
+ if (item === source.localName)
1160
+ return source;
1161
+ let made = this.retagged.get(item);
1162
+ if (!made) {
1163
+ made = retag(source, item);
1164
+ this.retagged.set(item, made);
1165
+ }
1166
+ return made;
1167
+ }
1127
1168
  return this.conditional ? this.templates[item] : this.templates[0];
1128
1169
  }
1170
+ tagFor(value) {
1171
+ const written = this.templates[0].localName;
1172
+ if (value == null || value === "")
1173
+ return written;
1174
+ const name = typeof value === "string" ? value.trim().toLowerCase() : null;
1175
+ if (name && TAG_NAME.test(name) && !REFUSED_TAGS.has(name))
1176
+ return name;
1177
+ if (!this.warnedTag) {
1178
+ this.warnedTag = true;
1179
+ warn(`$tag="${this.itemsExpr}" gave ${JSON.stringify(value)}, which is not an element it can make - rendering <${written}>.`);
1180
+ }
1181
+ return written;
1182
+ }
1129
1183
  schedule(items) {
1130
1184
  this.pendingItems = items;
1131
1185
  if (this.running)
1132
1186
  return;
1133
1187
  this.running = true;
1134
- this.drain();
1188
+ this.draining = this.drain();
1189
+ }
1190
+ draining = null;
1191
+ settled() {
1192
+ return this.draining ?? Promise.resolve();
1135
1193
  }
1136
1194
  async drain() {
1137
1195
  try {
@@ -1489,6 +1547,15 @@ class Mounter {
1489
1547
  this.createBlock(el, context, "if");
1490
1548
  return;
1491
1549
  }
1550
+ if (el.hasAttribute("data-bind-tag")) {
1551
+ if (el.tagName === "TEMPLATE") {
1552
+ warn("$tag on a <template> means nothing - it renders its contents, not itself.", el);
1553
+ el.removeAttribute("data-bind-tag");
1554
+ } else {
1555
+ this.createBlock(el, context, "tag");
1556
+ return;
1557
+ }
1558
+ }
1492
1559
  if (el.hasAttribute("data-bind-else") || el.hasAttribute("data-bind-else-if")) {
1493
1560
  warn("$else / $else-if must directly follow a $if or $else-if sibling", el);
1494
1561
  el.remove();
@@ -1634,6 +1701,13 @@ class Mounter {
1634
1701
  i += consumed.templates - 1;
1635
1702
  continue;
1636
1703
  }
1704
+ if (el.hasAttribute("data-bind-tag") && el.tagName !== "TEMPLATE") {
1705
+ const consumed = this.adoptBlock(el, found, d, context, "tag");
1706
+ if (!consumed)
1707
+ return false;
1708
+ d += consumed.dom;
1709
+ continue;
1710
+ }
1637
1711
  if (el.tagName === "SLOT") {
1638
1712
  if (!found[d] || !isSlotStart(found[d])) {
1639
1713
  if (!this.adoptNodes(Array.from(el.childNodes), found.slice(d), context, markers)) {
@@ -1769,18 +1843,19 @@ class Mounter {
1769
1843
  const rawExpression = el.getAttribute(`data-bind-${kind}`) ?? "";
1770
1844
  const branches = conditional ? this.gatherBranches(el) : [];
1771
1845
  const expression = conditional ? composeBranchExpression(branches) : rawExpression;
1772
- const parsed = conditional ? { alias: null, items: expression } : parseForExpression(rawExpression);
1846
+ const parsed = conditional || kind === "tag" ? { alias: null, items: expression } : parseForExpression(rawExpression);
1773
1847
  if (!parsed) {
1774
1848
  warn(`Could not parse $for="${expression}" - expected "item in items"`);
1775
1849
  el.remove();
1776
1850
  return null;
1777
1851
  }
1778
- if (!conditional && el.hasAttribute("data-bind-if")) {
1852
+ if (kind === "for" && el.hasAttribute("data-bind-if")) {
1779
1853
  warn(`$for and $if on the same element are not supported (${rawExpression}). Wrap one in an element.`);
1780
1854
  }
1781
- const keyExpr = conditional ? null : el.getAttribute("data-bind-key");
1855
+ const keyExpr = kind === "for" ? el.getAttribute("data-bind-key") : null;
1782
1856
  el.removeAttribute(`data-bind-${kind}`);
1783
- el.removeAttribute("data-bind-key");
1857
+ if (kind === "for")
1858
+ el.removeAttribute("data-bind-key");
1784
1859
  let anchor;
1785
1860
  if (existingAnchor) {
1786
1861
  anchor = existingAnchor;
@@ -1795,7 +1870,7 @@ class Mounter {
1795
1870
  const compiled = this.bindings.compileExpression(keyExpr, params);
1796
1871
  return compiled(context.owner.state, context.owner.props, this.cellsProxy(), this.bindings.pendingProbe(), ...locals);
1797
1872
  } : null;
1798
- const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items);
1873
+ const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items, kind === "tag");
1799
1874
  context.holder.addBlock(block);
1800
1875
  context.holder.addBinding(this.bindings.createForBinding(block, parsed.items, context.params));
1801
1876
  return block;
@@ -1883,9 +1958,10 @@ class Mounter {
1883
1958
  if (this.ssg) {
1884
1959
  fragment.insertBefore(document.createComment(ROW_MARK), fragment.firstChild);
1885
1960
  }
1886
- row.nodes = Array.from(fragment.childNodes);
1887
1961
  this.bindings.updateRow(block.owner, row);
1888
1962
  row.instances.forEach((child) => this.bindings.updateTree(child));
1963
+ await Promise.all(row.blocks.map((nested) => nested.settled()));
1964
+ row.nodes = Array.from(fragment.childNodes);
1889
1965
  return row;
1890
1966
  }
1891
1967
  connected() {
@@ -2291,7 +2367,7 @@ function isBlockAnchor(node) {
2291
2367
  if (node.nodeType !== 8)
2292
2368
  return false;
2293
2369
  const data = node.data.trim();
2294
- return data.startsWith("for:") || data.startsWith("if:");
2370
+ return data.startsWith("for:") || data.startsWith("if:") || data.startsWith("tag:");
2295
2371
  }
2296
2372
  function isInsignificant(node) {
2297
2373
  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 {};
package/dist/vite.js CHANGED
@@ -490,6 +490,30 @@ class Row {
490
490
  this.locals[this.locals.length - 1] = value;
491
491
  }
492
492
  }
493
+ var TAG_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
494
+ var REFUSED_TAGS = new Set([
495
+ "script",
496
+ "style",
497
+ "template",
498
+ "slot",
499
+ "iframe",
500
+ "object",
501
+ "embed"
502
+ ]);
503
+ function retag(source, name) {
504
+ const doc = source.ownerDocument;
505
+ const el = source.namespaceURI ? doc.createElementNS(source.namespaceURI, name) : doc.createElement(name);
506
+ for (const attr of Array.from(source.attributes)) {
507
+ if (attr.namespaceURI)
508
+ el.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
509
+ else
510
+ el.setAttribute(attr.name, attr.value);
511
+ }
512
+ for (const child of Array.from(source.childNodes)) {
513
+ el.appendChild(child.cloneNode(true));
514
+ }
515
+ return el;
516
+ }
493
517
  function toIterable(value) {
494
518
  if (value == null)
495
519
  return [];
@@ -531,12 +555,13 @@ class ForBlock {
531
555
  renderer;
532
556
  conditional;
533
557
  itemsExpr;
558
+ tagged;
534
559
  rows = new Map;
535
560
  order = [];
536
561
  pendingItems = null;
537
562
  running = false;
538
563
  warnedDuplicates = false;
539
- constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "") {
564
+ constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "", tagged = false) {
540
565
  this.owner = owner;
541
566
  this.anchor = anchor;
542
567
  this.templates = templates;
@@ -547,22 +572,55 @@ class ForBlock {
547
572
  this.renderer = renderer;
548
573
  this.conditional = conditional;
549
574
  this.itemsExpr = itemsExpr;
575
+ this.tagged = tagged;
550
576
  }
577
+ retagged = new Map;
578
+ warnedTag = false;
551
579
  toItems(value) {
552
580
  if (this.conditional) {
553
581
  return Number.isInteger(value) && value >= 0 ? [value] : [];
554
582
  }
583
+ if (this.tagged)
584
+ return [this.tagFor(value)];
555
585
  return toIterable(value);
556
586
  }
557
587
  templateFor(item) {
588
+ if (this.tagged) {
589
+ const source = this.templates[0];
590
+ if (item === source.localName)
591
+ return source;
592
+ let made = this.retagged.get(item);
593
+ if (!made) {
594
+ made = retag(source, item);
595
+ this.retagged.set(item, made);
596
+ }
597
+ return made;
598
+ }
558
599
  return this.conditional ? this.templates[item] : this.templates[0];
559
600
  }
601
+ tagFor(value) {
602
+ const written = this.templates[0].localName;
603
+ if (value == null || value === "")
604
+ return written;
605
+ const name = typeof value === "string" ? value.trim().toLowerCase() : null;
606
+ if (name && TAG_NAME.test(name) && !REFUSED_TAGS.has(name))
607
+ return name;
608
+ if (!this.warnedTag) {
609
+ this.warnedTag = true;
610
+ warn(`$tag="${this.itemsExpr}" gave ${JSON.stringify(value)}, which is not an element it can make - rendering <${written}>.`);
611
+ }
612
+ return written;
613
+ }
560
614
  schedule(items) {
561
615
  this.pendingItems = items;
562
616
  if (this.running)
563
617
  return;
564
618
  this.running = true;
565
- this.drain();
619
+ this.draining = this.drain();
620
+ }
621
+ draining = null;
622
+ settled() {
623
+ return this.draining ?? Promise.resolve();
566
624
  }
567
625
  async drain() {
568
626
  try {
@@ -920,6 +978,15 @@ class Mounter {
920
978
  this.createBlock(el, context, "if");
921
979
  return;
922
980
  }
981
+ if (el.hasAttribute("data-bind-tag")) {
982
+ if (el.tagName === "TEMPLATE") {
983
+ warn("$tag on a <template> means nothing - it renders its contents, not itself.", el);
984
+ el.removeAttribute("data-bind-tag");
985
+ } else {
986
+ this.createBlock(el, context, "tag");
987
+ return;
988
+ }
989
+ }
923
990
  if (el.hasAttribute("data-bind-else") || el.hasAttribute("data-bind-else-if")) {
924
991
  warn("$else / $else-if must directly follow a $if or $else-if sibling", el);
925
992
  el.remove();
@@ -1065,6 +1132,13 @@ class Mounter {
1065
1132
  i += consumed.templates - 1;
1066
1133
  continue;
1067
1134
  }
1135
+ if (el.hasAttribute("data-bind-tag") && el.tagName !== "TEMPLATE") {
1136
+ const consumed = this.adoptBlock(el, found, d, context, "tag");
1137
+ if (!consumed)
1138
+ return false;
1139
+ d += consumed.dom;
1140
+ continue;
1141
+ }
1068
1142
  if (el.tagName === "SLOT") {
1069
1143
  if (!found[d] || !isSlotStart(found[d])) {
1070
1144
  if (!this.adoptNodes(Array.from(el.childNodes), found.slice(d), context, markers)) {
@@ -1200,18 +1274,19 @@ class Mounter {
1200
1274
  const rawExpression = el.getAttribute(`data-bind-${kind}`) ?? "";
1201
1275
  const branches = conditional ? this.gatherBranches(el) : [];
1202
1276
  const expression = conditional ? composeBranchExpression(branches) : rawExpression;
1203
- const parsed = conditional ? { alias: null, items: expression } : parseForExpression(rawExpression);
1277
+ const parsed = conditional || kind === "tag" ? { alias: null, items: expression } : parseForExpression(rawExpression);
1204
1278
  if (!parsed) {
1205
1279
  warn(`Could not parse $for="${expression}" - expected "item in items"`);
1206
1280
  el.remove();
1207
1281
  return null;
1208
1282
  }
1209
- if (!conditional && el.hasAttribute("data-bind-if")) {
1283
+ if (kind === "for" && el.hasAttribute("data-bind-if")) {
1210
1284
  warn(`$for and $if on the same element are not supported (${rawExpression}). Wrap one in an element.`);
1211
1285
  }
1212
- const keyExpr = conditional ? null : el.getAttribute("data-bind-key");
1286
+ const keyExpr = kind === "for" ? el.getAttribute("data-bind-key") : null;
1213
1287
  el.removeAttribute(`data-bind-${kind}`);
1214
- el.removeAttribute("data-bind-key");
1288
+ if (kind === "for")
1289
+ el.removeAttribute("data-bind-key");
1215
1290
  let anchor;
1216
1291
  if (existingAnchor) {
1217
1292
  anchor = existingAnchor;
@@ -1226,7 +1301,7 @@ class Mounter {
1226
1301
  const compiled = this.bindings.compileExpression(keyExpr, params);
1227
1302
  return compiled(context.owner.state, context.owner.props, this.cellsProxy(), this.bindings.pendingProbe(), ...locals);
1228
1303
  } : null;
1229
- const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items);
1304
+ const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items, kind === "tag");
1230
1305
  context.holder.addBlock(block);
1231
1306
  context.holder.addBinding(this.bindings.createForBinding(block, parsed.items, context.params));
1232
1307
  return block;
@@ -1314,9 +1389,10 @@ class Mounter {
1314
1389
  if (this.ssg) {
1315
1390
  fragment.insertBefore(document.createComment(ROW_MARK), fragment.firstChild);
1316
1391
  }
1317
- row.nodes = Array.from(fragment.childNodes);
1318
1392
  this.bindings.updateRow(block.owner, row);
1319
1393
  row.instances.forEach((child) => this.bindings.updateTree(child));
1394
+ await Promise.all(row.blocks.map((nested) => nested.settled()));
1395
+ row.nodes = Array.from(fragment.childNodes);
1320
1396
  return row;
1321
1397
  }
1322
1398
  connected() {
@@ -1722,7 +1798,7 @@ function isBlockAnchor(node) {
1722
1798
  if (node.nodeType !== 8)
1723
1799
  return false;
1724
1800
  const data = node.data.trim();
1725
- return data.startsWith("for:") || data.startsWith("if:");
1801
+ return data.startsWith("for:") || data.startsWith("if:") || data.startsWith("tag:");
1726
1802
  }
1727
1803
  function isInsignificant(node) {
1728
1804
  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.6",
4
4
  "description": "Lightspeed: a compile-first framework for islands of reactivity in static HTML.",
5
5
  "keywords": [
6
6
  "lightspeed",