@b9g/crank 0.7.2 → 0.7.4

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/umd.js CHANGED
@@ -743,6 +743,7 @@
743
743
  tagName: getTagName(Portal),
744
744
  props: stripSpecialProps(ret.el.props),
745
745
  scope: undefined,
746
+ root,
746
747
  });
747
748
  // remember that typeof null === "object"
748
749
  if (typeof root === "object" && root !== null && children != null) {
@@ -765,32 +766,32 @@
765
766
  const schedulePromises = [];
766
767
  if (isPromiseLike(diff)) {
767
768
  return diff.then(() => {
768
- commit(adapter, ret, ret, ret.ctx, ret.scope, 0, schedulePromises, undefined);
769
+ commit(adapter, ret, ret, ret.ctx, ret.scope, root, 0, schedulePromises, undefined);
769
770
  if (schedulePromises.length > 0) {
770
771
  return Promise.all(schedulePromises).then(() => {
771
772
  if (typeof root !== "object" || root === null) {
772
- unmount(adapter, ret, ret.ctx, ret, false);
773
+ unmount(adapter, ret, ret.ctx, root, ret, false);
773
774
  }
774
775
  return adapter.read(unwrap(getChildValues(ret)));
775
776
  });
776
777
  }
777
778
  if (typeof root !== "object" || root === null) {
778
- unmount(adapter, ret, ret.ctx, ret, false);
779
+ unmount(adapter, ret, ret.ctx, root, ret, false);
779
780
  }
780
781
  return adapter.read(unwrap(getChildValues(ret)));
781
782
  });
782
783
  }
783
- commit(adapter, ret, ret, ret.ctx, ret.scope, 0, schedulePromises, undefined);
784
+ commit(adapter, ret, ret, ret.ctx, ret.scope, root, 0, schedulePromises, undefined);
784
785
  if (schedulePromises.length > 0) {
785
786
  return Promise.all(schedulePromises).then(() => {
786
787
  if (typeof root !== "object" || root === null) {
787
- unmount(adapter, ret, ret.ctx, ret, false);
788
+ unmount(adapter, ret, ret.ctx, root, ret, false);
788
789
  }
789
790
  return adapter.read(unwrap(getChildValues(ret)));
790
791
  });
791
792
  }
792
793
  if (typeof root !== "object" || root === null) {
793
- unmount(adapter, ret, ret.ctx, ret, false);
794
+ unmount(adapter, ret, ret.ctx, root, ret, false);
794
795
  }
795
796
  return adapter.read(unwrap(getChildValues(ret)));
796
797
  }
@@ -1040,11 +1041,12 @@
1040
1041
  tagName: getTagName(tag),
1041
1042
  props: el.props,
1042
1043
  scope,
1044
+ root,
1043
1045
  });
1044
1046
  }
1045
1047
  return diffChildren(adapter, root, ret, ctx, scope, ret, ret.el.props.children);
1046
1048
  }
1047
- function commit(adapter, host, ret, ctx, scope, index, schedulePromises, hydrationNodes) {
1049
+ function commit(adapter, host, ret, ctx, scope, root, index, schedulePromises, hydrationNodes) {
1048
1050
  if (getFlag(ret, IsCopied) && getFlag(ret, DidCommit)) {
1049
1051
  return getValue(ret);
1050
1052
  }
@@ -1077,19 +1079,19 @@
1077
1079
  }
1078
1080
  else {
1079
1081
  if (tag === Fragment) {
1080
- value = commitChildren(adapter, host, ctx, scope, ret, index, schedulePromises, hydrationNodes);
1082
+ value = commitChildren(adapter, host, ctx, scope, root, ret, index, schedulePromises, hydrationNodes);
1081
1083
  }
1082
1084
  else if (tag === Text) {
1083
- value = commitText(adapter, ret, el, scope, hydrationNodes);
1085
+ value = commitText(adapter, ret, el, scope, hydrationNodes, root);
1084
1086
  }
1085
1087
  else if (tag === Raw) {
1086
- value = commitRaw(adapter, host, ret, scope, hydrationNodes);
1088
+ value = commitRaw(adapter, host, ret, scope, hydrationNodes, root);
1087
1089
  }
1088
1090
  else {
1089
- value = commitHost(adapter, ret, ctx, schedulePromises, hydrationNodes);
1091
+ value = commitHost(adapter, ret, ctx, root, schedulePromises, hydrationNodes);
1090
1092
  }
1091
1093
  if (ret.fallback) {
1092
- unmount(adapter, host, ctx, ret.fallback, false);
1094
+ unmount(adapter, host, ctx, root, ret.fallback, false);
1093
1095
  ret.fallback = undefined;
1094
1096
  }
1095
1097
  }
@@ -1107,7 +1109,7 @@
1107
1109
  }
1108
1110
  return value;
1109
1111
  }
1110
- function commitChildren(adapter, host, ctx, scope, parent, index, schedulePromises, hydrationNodes) {
1112
+ function commitChildren(adapter, host, ctx, scope, root, parent, index, schedulePromises, hydrationNodes) {
1111
1113
  let values = [];
1112
1114
  for (let i = 0, children = wrap(parent.children); i < children.length; i++) {
1113
1115
  let child = children[i];
@@ -1130,6 +1132,7 @@
1130
1132
  node,
1131
1133
  parentNode: host.value,
1132
1134
  isNested: false,
1135
+ root,
1133
1136
  });
1134
1137
  }
1135
1138
  }
@@ -1189,7 +1192,7 @@
1189
1192
  schedulePromises.push(safeRace(schedulePromises1));
1190
1193
  }
1191
1194
  if (child) {
1192
- const value = commit(adapter, host, child, ctx, scope, index, schedulePromises, hydrationNodes);
1195
+ const value = commit(adapter, host, child, ctx, scope, root, index, schedulePromises, hydrationNodes);
1193
1196
  if (Array.isArray(value)) {
1194
1197
  for (let j = 0; j < value.length; j++) {
1195
1198
  values.push(value[j]);
@@ -1205,7 +1208,7 @@
1205
1208
  if (parent.graveyard) {
1206
1209
  for (let i = 0; i < parent.graveyard.length; i++) {
1207
1210
  const child = parent.graveyard[i];
1208
- unmount(adapter, host, ctx, child, false);
1211
+ unmount(adapter, host, ctx, root, child, false);
1209
1212
  }
1210
1213
  parent.graveyard = undefined;
1211
1214
  }
@@ -1216,17 +1219,18 @@
1216
1219
  }
1217
1220
  return values;
1218
1221
  }
1219
- function commitText(adapter, ret, el, scope, hydrationNodes) {
1222
+ function commitText(adapter, ret, el, scope, hydrationNodes, root) {
1220
1223
  const value = adapter.text({
1221
1224
  value: el.props.value,
1222
1225
  scope,
1223
1226
  oldNode: ret.value,
1224
1227
  hydrationNodes,
1228
+ root,
1225
1229
  });
1226
1230
  ret.value = value;
1227
1231
  return value;
1228
1232
  }
1229
- function commitRaw(adapter, host, ret, scope, hydrationNodes) {
1233
+ function commitRaw(adapter, host, ret, scope, hydrationNodes, root) {
1230
1234
  if (!ret.oldProps || ret.oldProps.value !== ret.el.props.value) {
1231
1235
  const oldNodes = wrap(ret.value);
1232
1236
  for (let i = 0; i < oldNodes.length; i++) {
@@ -1235,18 +1239,20 @@
1235
1239
  node: oldNode,
1236
1240
  parentNode: host.value,
1237
1241
  isNested: false,
1242
+ root,
1238
1243
  });
1239
1244
  }
1240
1245
  ret.value = adapter.raw({
1241
1246
  value: ret.el.props.value,
1242
1247
  scope,
1243
1248
  hydrationNodes,
1249
+ root,
1244
1250
  });
1245
1251
  }
1246
1252
  ret.oldProps = stripSpecialProps(ret.el.props);
1247
1253
  return ret.value;
1248
1254
  }
1249
- function commitHost(adapter, ret, ctx, schedulePromises, hydrationNodes) {
1255
+ function commitHost(adapter, ret, ctx, root, schedulePromises, hydrationNodes) {
1250
1256
  if (getFlag(ret, IsCopied) && getFlag(ret, DidCommit)) {
1251
1257
  return getValue(ret);
1252
1258
  }
@@ -1299,6 +1305,7 @@
1299
1305
  node,
1300
1306
  props,
1301
1307
  scope,
1308
+ root,
1302
1309
  });
1303
1310
  if (childHydrationNodes) {
1304
1311
  for (let i = 0; i < childHydrationNodes.length; i++) {
@@ -1306,6 +1313,7 @@
1306
1313
  node: childHydrationNodes[i],
1307
1314
  parentNode: node,
1308
1315
  isNested: false,
1316
+ root,
1309
1317
  });
1310
1318
  }
1311
1319
  }
@@ -1334,6 +1342,7 @@
1334
1342
  node: nextChild,
1335
1343
  props,
1336
1344
  scope,
1345
+ root,
1337
1346
  });
1338
1347
  if (childHydrationNodes) {
1339
1348
  node = nextChild;
@@ -1342,6 +1351,7 @@
1342
1351
  node: childHydrationNodes[i],
1343
1352
  parentNode: node,
1344
1353
  isNested: false,
1354
+ root,
1345
1355
  });
1346
1356
  }
1347
1357
  }
@@ -1356,6 +1366,7 @@
1356
1366
  tagName: getTagName(tag),
1357
1367
  props,
1358
1368
  scope,
1369
+ root,
1359
1370
  });
1360
1371
  }
1361
1372
  ret.value = node;
@@ -1369,13 +1380,14 @@
1369
1380
  props,
1370
1381
  oldProps,
1371
1382
  scope,
1383
+ root,
1372
1384
  copyProps,
1373
1385
  isHydrating: !!childHydrationNodes,
1374
1386
  quietProps,
1375
1387
  });
1376
1388
  }
1377
1389
  if (!copyChildren) {
1378
- const children = commitChildren(adapter, ret, ctx, scope, ret, 0, schedulePromises, hydrationMetaProp && !hydrationMetaProp.includes("children")
1390
+ const children = commitChildren(adapter, ret, ctx, scope, tag === Portal ? node : root, ret, 0, schedulePromises, hydrationMetaProp && !hydrationMetaProp.includes("children")
1379
1391
  ? undefined
1380
1392
  : childHydrationNodes);
1381
1393
  adapter.arrange({
@@ -1385,6 +1397,7 @@
1385
1397
  props,
1386
1398
  children,
1387
1399
  oldProps,
1400
+ root,
1388
1401
  });
1389
1402
  }
1390
1403
  ret.oldProps = props;
@@ -1485,10 +1498,10 @@
1485
1498
  }
1486
1499
  }
1487
1500
  }
1488
- function unmount(adapter, host, ctx, ret, isNested) {
1501
+ function unmount(adapter, host, ctx, root, ret, isNested) {
1489
1502
  // TODO: set the IsUnmounted flag consistently for all retainers
1490
1503
  if (ret.fallback) {
1491
- unmount(adapter, host, ctx, ret.fallback, isNested);
1504
+ unmount(adapter, host, ctx, root, ret.fallback, isNested);
1492
1505
  ret.fallback = undefined;
1493
1506
  }
1494
1507
  if (getFlag(ret, IsResurrecting)) {
@@ -1499,7 +1512,7 @@
1499
1512
  const lingerers = ret.lingerers[i];
1500
1513
  if (lingerers) {
1501
1514
  for (const lingerer of lingerers) {
1502
- unmount(adapter, host, ctx, lingerer, isNested);
1515
+ unmount(adapter, host, ctx, root, lingerer, isNested);
1503
1516
  }
1504
1517
  }
1505
1518
  }
@@ -1509,16 +1522,16 @@
1509
1522
  unmountComponent(ret.ctx, isNested);
1510
1523
  }
1511
1524
  else if (ret.el.tag === Fragment) {
1512
- unmountChildren(adapter, host, ctx, ret, isNested);
1525
+ unmountChildren(adapter, host, ctx, root, ret, isNested);
1513
1526
  }
1514
1527
  else if (ret.el.tag === Portal) {
1515
- unmountChildren(adapter, ret, ctx, ret, false);
1528
+ unmountChildren(adapter, ret, ctx, ret.value, ret, false);
1516
1529
  if (ret.value != null) {
1517
1530
  adapter.finalize(ret.value);
1518
1531
  }
1519
1532
  }
1520
1533
  else {
1521
- unmountChildren(adapter, ret, ctx, ret, true);
1534
+ unmountChildren(adapter, ret, ctx, root, ret, true);
1522
1535
  if (getFlag(ret, DidCommit)) {
1523
1536
  if (ctx) {
1524
1537
  // Remove the value from every context which shares the same host.
@@ -1528,22 +1541,23 @@
1528
1541
  node: ret.value,
1529
1542
  parentNode: host.value,
1530
1543
  isNested,
1544
+ root,
1531
1545
  });
1532
1546
  }
1533
1547
  }
1534
1548
  }
1535
- function unmountChildren(adapter, host, ctx, ret, isNested) {
1549
+ function unmountChildren(adapter, host, ctx, root, ret, isNested) {
1536
1550
  if (ret.graveyard) {
1537
1551
  for (let i = 0; i < ret.graveyard.length; i++) {
1538
1552
  const child = ret.graveyard[i];
1539
- unmount(adapter, host, ctx, child, isNested);
1553
+ unmount(adapter, host, ctx, root, child, isNested);
1540
1554
  }
1541
1555
  ret.graveyard = undefined;
1542
1556
  }
1543
1557
  for (let i = 0, children = wrap(ret.children); i < children.length; i++) {
1544
1558
  const child = children[i];
1545
1559
  if (typeof child === "object") {
1546
- unmount(adapter, host, ctx, child, isNested);
1560
+ unmount(adapter, host, ctx, root, child, isNested);
1547
1561
  }
1548
1562
  }
1549
1563
  }
@@ -2337,7 +2351,7 @@
2337
2351
  });
2338
2352
  return getValue(ctx.ret);
2339
2353
  }
2340
- const values = commitChildren(ctx.adapter, ctx.host, ctx, ctx.scope, ctx.ret, ctx.index, schedulePromises, hydrationNodes);
2354
+ const values = commitChildren(ctx.adapter, ctx.host, ctx, ctx.scope, ctx.root, ctx.ret, ctx.index, schedulePromises, hydrationNodes);
2341
2355
  if (getFlag(ctx.ret, IsUnmounted)) {
2342
2356
  return;
2343
2357
  }
@@ -2361,7 +2375,7 @@
2361
2375
  setFlag(ctx.ret, IsScheduling, wasScheduling);
2362
2376
  propagateComponent(ctx);
2363
2377
  if (ctx.ret.fallback) {
2364
- unmount(ctx.adapter, ctx.host, ctx.parent, ctx.ret.fallback, false);
2378
+ unmount(ctx.adapter, ctx.host, ctx.parent, ctx.root, ctx.ret.fallback, false);
2365
2379
  }
2366
2380
  ctx.ret.fallback = undefined;
2367
2381
  });
@@ -2387,7 +2401,7 @@
2387
2401
  propagateComponent(ctx);
2388
2402
  }
2389
2403
  if (ctx.ret.fallback) {
2390
- unmount(ctx.adapter, ctx.host, ctx.parent, ctx.ret.fallback, false);
2404
+ unmount(ctx.adapter, ctx.host, ctx.parent, ctx.root, ctx.ret.fallback, false);
2391
2405
  }
2392
2406
  ctx.ret.fallback = undefined;
2393
2407
  setFlag(ctx.ret, IsUpdating, false);
@@ -2453,6 +2467,7 @@
2453
2467
  props,
2454
2468
  oldProps: props,
2455
2469
  children: hostChildren,
2470
+ root: ctx.root,
2456
2471
  });
2457
2472
  flush(ctx.adapter, ctx.root, ctx);
2458
2473
  }
@@ -2507,7 +2522,7 @@
2507
2522
  ctx.schedule = undefined;
2508
2523
  }
2509
2524
  clearEventListeners(ctx.ctx);
2510
- unmountChildren(ctx.adapter, ctx.host, ctx, ctx.ret, isNested);
2525
+ unmountChildren(ctx.adapter, ctx.host, ctx, ctx.root, ctx.ret, isNested);
2511
2526
  if (didLinger) {
2512
2527
  // If we lingered, we call finalize to ensure rendering is finalized
2513
2528
  if (ctx.root != null) {
@@ -2728,6 +2743,15 @@
2728
2743
 
2729
2744
  const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
2730
2745
  const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
2746
+ function getRootDocument(root) {
2747
+ if (root && root.ownerDocument) {
2748
+ return root.ownerDocument;
2749
+ }
2750
+ if (root && root.nodeType === Node.DOCUMENT_NODE) {
2751
+ return root;
2752
+ }
2753
+ return document;
2754
+ }
2731
2755
  function isWritableProperty(element, name) {
2732
2756
  // walk up the object's prototype chain to find the owner
2733
2757
  let propOwner = element;
@@ -2758,11 +2782,18 @@
2758
2782
  else if (expectedValue === true || expectedValue === "") {
2759
2783
  console.warn(`Expected "${showName}" to be ${expectedValue === true ? "present" : '""'} but found ${String(actualValue)} while hydrating:`, element);
2760
2784
  }
2761
- else if (typeof window !== "undefined" &&
2762
- window.location &&
2763
- new URL(expectedValue, window.location.origin).href ===
2764
- new URL(actualValue, window.location.origin).href) ;
2765
2785
  else {
2786
+ // Check if this is a URL mismatch that's actually just resolution
2787
+ const win = element.ownerDocument.defaultView;
2788
+ if (win && win.location) {
2789
+ const origin = win.location.origin;
2790
+ if (new URL(expectedValue, origin).href ===
2791
+ new URL(actualValue, origin).href) {
2792
+ // attrs which are URLs will often be resolved to their full
2793
+ // href in the DOM, so we squash these errors
2794
+ return;
2795
+ }
2796
+ }
2766
2797
  console.warn(`Expected "${showName}" to be "${String(expectedValue)}" but found ${String(actualValue)} while hydrating:`, element);
2767
2798
  }
2768
2799
  }
@@ -2783,7 +2814,7 @@
2783
2814
  }
2784
2815
  return props.xmlns || xmlns;
2785
2816
  },
2786
- create({ tag, tagName, scope: xmlns, }) {
2817
+ create({ tag, tagName, scope: xmlns, root, }) {
2787
2818
  if (typeof tag !== "string") {
2788
2819
  throw new Error(`Unknown tag: ${tagName}`);
2789
2820
  }
@@ -2793,18 +2824,18 @@
2793
2824
  else if (tag.toLowerCase() === "math") {
2794
2825
  xmlns = MATHML_NAMESPACE;
2795
2826
  }
2796
- return xmlns
2797
- ? document.createElementNS(xmlns, tag)
2798
- : document.createElement(tag);
2827
+ const doc = getRootDocument(root);
2828
+ return xmlns ? doc.createElementNS(xmlns, tag) : doc.createElement(tag);
2799
2829
  },
2800
- adopt({ tag, tagName, node, }) {
2830
+ adopt({ tag, tagName, node, root, }) {
2801
2831
  if (typeof tag !== "string" && tag !== Portal) {
2802
2832
  throw new Error(`Unknown tag: ${tagName}`);
2803
2833
  }
2804
- if (node === document.body ||
2805
- node === document.head ||
2806
- node === document.documentElement ||
2807
- node === document) {
2834
+ const doc = getRootDocument(root);
2835
+ if (node === doc.body ||
2836
+ node === doc.head ||
2837
+ node === doc.documentElement ||
2838
+ node === doc) {
2808
2839
  console.warn(`Hydrating ${node.nodeName.toLowerCase()} is discouraged as it is destructive and may remove unknown nodes.`);
2809
2840
  }
2810
2841
  if (node == null ||
@@ -2962,19 +2993,28 @@
2962
2993
  const hydratingClassName = isHydrating
2963
2994
  ? element.getAttribute("class")
2964
2995
  : undefined;
2965
- for (const className in { ...oldValue, ...value }) {
2966
- const classValue = value && value[className];
2967
- if (classValue) {
2968
- element.classList.add(className);
2969
- if (hydratingClasses && hydratingClasses.has(className)) {
2970
- hydratingClasses.delete(className);
2971
- }
2972
- else if (isHydrating) {
2973
- shouldIssueWarning = true;
2974
- }
2996
+ const allClassNames = { ...oldValue, ...value };
2997
+ // Two passes: removes first, then adds. This ensures that
2998
+ // overlapping classes in different keys are handled correctly.
2999
+ // e.g. {"a b": false, "b c": true} should result in "b c"
3000
+ for (const classNames in allClassNames) {
3001
+ if (!(value && value[classNames])) {
3002
+ const classes = classNames.split(/\s+/).filter(Boolean);
3003
+ element.classList.remove(...classes);
2975
3004
  }
2976
- else {
2977
- element.classList.remove(className);
3005
+ }
3006
+ for (const classNames in allClassNames) {
3007
+ if (value && value[classNames]) {
3008
+ const classes = classNames.split(/\s+/).filter(Boolean);
3009
+ element.classList.add(...classes);
3010
+ for (const className of classes) {
3011
+ if (hydratingClasses && hydratingClasses.has(className)) {
3012
+ hydratingClasses.delete(className);
3013
+ }
3014
+ else if (isHydrating) {
3015
+ shouldIssueWarning = true;
3016
+ }
3017
+ }
2978
3018
  }
2979
3019
  }
2980
3020
  if (shouldIssueWarning ||
@@ -3023,7 +3063,22 @@
3023
3063
  !(typeof value === "string" &&
3024
3064
  typeof element[name] === "boolean") &&
3025
3065
  isWritableProperty(element, name)) {
3026
- if (element[name] !== value || oldValue === undefined) {
3066
+ // For URL properties like src and href, the DOM property returns the
3067
+ // resolved absolute URL. We need to resolve the prop value the same way
3068
+ // to compare correctly.
3069
+ let domValue = element[name];
3070
+ let propValue = value;
3071
+ if ((name === "src" || name === "href") &&
3072
+ typeof value === "string" &&
3073
+ typeof domValue === "string") {
3074
+ try {
3075
+ propValue = new URL(value, element.baseURI).href;
3076
+ }
3077
+ catch {
3078
+ // Invalid URL, use original value for comparison
3079
+ }
3080
+ }
3081
+ if (propValue !== domValue || oldValue === undefined) {
3027
3082
  if (isHydrating &&
3028
3083
  typeof element[name] === "string" &&
3029
3084
  element[name] !== value) {
@@ -3086,7 +3141,8 @@
3086
3141
  parentNode.removeChild(node);
3087
3142
  }
3088
3143
  },
3089
- text({ value, oldNode, hydrationNodes, }) {
3144
+ text({ value, oldNode, hydrationNodes, root, }) {
3145
+ const doc = getRootDocument(root);
3090
3146
  if (hydrationNodes != null) {
3091
3147
  let node = hydrationNodes.shift();
3092
3148
  if (!node || node.nodeType !== Node.TEXT_NODE) {
@@ -3100,7 +3156,7 @@
3100
3156
  // the text node is longer than the expected text, so we
3101
3157
  // reuse the existing text node, but truncate it and unshift the rest
3102
3158
  node.data = value;
3103
- hydrationNodes.unshift(document.createTextNode(textData.slice(value.length)));
3159
+ hydrationNodes.unshift(doc.createTextNode(textData.slice(value.length)));
3104
3160
  return node;
3105
3161
  }
3106
3162
  }
@@ -3118,16 +3174,17 @@
3118
3174
  }
3119
3175
  return oldNode;
3120
3176
  }
3121
- return document.createTextNode(value);
3177
+ return doc.createTextNode(value);
3122
3178
  },
3123
- raw({ value, scope: xmlns, hydrationNodes, }) {
3179
+ raw({ value, scope: xmlns, hydrationNodes, root, }) {
3124
3180
  let nodes;
3125
3181
  if (typeof value === "string") {
3182
+ const doc = getRootDocument(root);
3126
3183
  const el = xmlns == null
3127
- ? document.createElement("div")
3184
+ ? doc.createElement("div")
3128
3185
  : xmlns === SVG_NAMESPACE
3129
- ? document.createElementNS(xmlns, "svg")
3130
- : document.createElementNS(xmlns, "math");
3186
+ ? doc.createElementNS(xmlns, "svg")
3187
+ : doc.createElementNS(xmlns, "math");
3131
3188
  el.innerHTML = value;
3132
3189
  nodes = Array.from(el.childNodes);
3133
3190
  }
@@ -3255,6 +3312,20 @@
3255
3312
  }
3256
3313
  attrs.push(`class="${escape(value)}"`);
3257
3314
  }
3315
+ else if (name === "class") {
3316
+ if (typeof value === "string") {
3317
+ attrs.push(`class="${escape(value)}"`);
3318
+ }
3319
+ else if (typeof value === "object" && value !== null) {
3320
+ // class={{"foo bar": true, "baz": false}} syntax
3321
+ const classes = Object.keys(value)
3322
+ .filter((k) => value[k])
3323
+ .join(" ");
3324
+ if (classes) {
3325
+ attrs.push(`class="${escape(classes)}"`);
3326
+ }
3327
+ }
3328
+ }
3258
3329
  else {
3259
3330
  if (name.startsWith("attr:")) {
3260
3331
  name = name.slice("attr:".length);