@helvetia-design/core 0.0.6 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +10 -10
  2. package/components/ds-accordion.js +1 -1
  3. package/components/ds-app-footer.js +1 -1
  4. package/components/ds-app-navbar.js +1 -1
  5. package/components/ds-badge.js +1 -1
  6. package/components/ds-brand-icon.js +1 -1
  7. package/components/ds-button.js +1 -1
  8. package/components/ds-card-subtitle.js +1 -1
  9. package/components/ds-card-title.js +1 -1
  10. package/components/ds-carousel.js +1 -1
  11. package/components/ds-checkbox-group.js +1 -1
  12. package/components/ds-close.js +1 -1
  13. package/components/ds-content.js +1 -1
  14. package/components/ds-counter.js +1 -1
  15. package/components/ds-data-item.js +1 -1
  16. package/components/ds-datepicker.js +1 -1
  17. package/components/ds-divider.js +1 -1
  18. package/components/ds-drawer.js +1 -1
  19. package/components/ds-file-upload.js +1 -1
  20. package/components/ds-heading.js +1 -1
  21. package/components/ds-hint.js +1 -1
  22. package/components/ds-icon.js +1 -1
  23. package/components/ds-input.js +1 -1
  24. package/components/ds-item.js +1 -1
  25. package/components/ds-modal.js +1 -1
  26. package/components/ds-notification.js +1 -1
  27. package/components/ds-number-input.js +1 -1
  28. package/components/ds-pagination.js +1 -1
  29. package/components/ds-phone-input.js +1 -1
  30. package/components/ds-popup.js +1 -1
  31. package/components/ds-radio-group.js +1 -1
  32. package/components/ds-segment.js +1 -1
  33. package/components/ds-select.js +1 -1
  34. package/components/ds-slider.js +1 -1
  35. package/components/ds-snackbar.js +1 -1
  36. package/components/ds-stack.js +1 -1
  37. package/components/ds-step.js +1 -1
  38. package/components/ds-tabs.js +1 -1
  39. package/components/ds-tag.js +1 -1
  40. package/components/ds-textarea.js +1 -1
  41. package/components/ds-toast.js +1 -1
  42. package/components/ds-toggle.js +1 -1
  43. package/components/{p-BWrjPWjt.js → p-B0Z95Hj1.js} +1 -1
  44. package/components/p-B9El_sPq.js +1 -1
  45. package/components/p-BB4SRrRj.js +4 -0
  46. package/components/{p-B3VN_5Hp.js → p-BPtibja3.js} +1 -1
  47. package/components/{p-B1celIGd.js → p-C0OHfTQ0.js} +1 -1
  48. package/components/{p-BJ2D8QfA.js → p-CTSANrsO.js} +1 -1
  49. package/components/{p-DgLUmdPH.js → p-CjK_T5vp.js} +1 -1
  50. package/components/p-DBqTFl8O.js +4 -0
  51. package/components/p-DFgZk16R.js +6 -0
  52. package/components/{p-Ce1uMkAI.js → p-DO00xKgx.js} +1 -1
  53. package/components/{p-CJycEnj5.js → p-Dj9PaUM0.js} +1 -1
  54. package/components/{p-Bd5NhwlZ.js → p-DyfdPmyS.js} +1 -1
  55. package/components/p-V50XCJTN.js +4 -0
  56. package/components/{p-BJK2OSM1.js → p-hhVbp_AN.js} +1 -1
  57. package/components/{p-D6jvX9E6.js → p-qIRMq8Bl.js} +1 -1
  58. package/components/{p-CsftG7WI.js → p-xUHc9oRK.js} +1 -1
  59. package/dist/cjs/design-system.cjs.js +1 -1
  60. package/dist/cjs/ds-badge.cjs.entry.js +1 -1
  61. package/dist/cjs/ds-brand-icon_3.cjs.entry.js +17 -3
  62. package/dist/cjs/ds-card-subtitle.cjs.entry.js +3 -11
  63. package/dist/cjs/ds-card-title.cjs.entry.js +1 -1
  64. package/dist/cjs/ds-content_2.cjs.entry.js +9 -7
  65. package/dist/cjs/ds-counter.cjs.entry.js +4 -4
  66. package/dist/cjs/ds-datepicker.cjs.entry.js +1 -1
  67. package/dist/cjs/ds-divider_2.cjs.entry.js +3 -3
  68. package/dist/cjs/ds-file-upload.cjs.entry.js +1 -1
  69. package/dist/cjs/ds-heading.cjs.entry.js +5 -24
  70. package/dist/cjs/ds-icon_2.cjs.entry.js +2 -2
  71. package/dist/cjs/ds-input.cjs.entry.js +2 -2
  72. package/dist/cjs/ds-number-input.cjs.entry.js +1 -1
  73. package/dist/cjs/ds-phone-input.cjs.entry.js +1 -1
  74. package/dist/cjs/ds-select.cjs.entry.js +1 -1
  75. package/dist/cjs/ds-slider.cjs.entry.js +1 -1
  76. package/dist/cjs/ds-snackbar.cjs.entry.js +1 -1
  77. package/dist/cjs/ds-tag.cjs.entry.js +1 -1
  78. package/dist/cjs/ds-textarea.cjs.entry.js +1 -1
  79. package/dist/cjs/ds-toast.cjs.entry.js +1 -1
  80. package/dist/cjs/ds-toggle.cjs.entry.js +1 -1
  81. package/dist/cjs/{heading.interfaces-C-G-ZqUa.js → heading.interfaces-BGskP9PB.js} +0 -2
  82. package/dist/cjs/index-CyaxwkZQ.js +1 -1
  83. package/dist/cjs/loader.cjs.js +1 -1
  84. package/dist/cjs/{svg-Cs3pO28w.js → svg-CH7qWfV-.js} +272 -195
  85. package/dist/cjs/{t-shirt-DWHArrUC.js → t-shirt-DqirWTL2.js} +2 -6
  86. package/dist/collection/components/actions/button/button.js +36 -2
  87. package/dist/collection/components/actions/button/test/button.component.play.js +53 -0
  88. package/dist/collection/components/forms/counter/counter.host.css +1 -1
  89. package/dist/collection/components/forms/counter/counter.js +5 -3
  90. package/dist/collection/components/forms/datepicker/datepicker.host.css +1 -1
  91. package/dist/collection/components/forms/file-upload/file-upload.host.css +1 -1
  92. package/dist/collection/components/forms/input/input.host.css +1 -1
  93. package/dist/collection/components/forms/input/input.js +3 -1
  94. package/dist/collection/components/forms/number-input/number-input.host.css +1 -1
  95. package/dist/collection/components/forms/phone-input/phone-input.host.css +1 -1
  96. package/dist/collection/components/forms/select/select.host.css +1 -1
  97. package/dist/collection/components/forms/slider/slider.host.css +1 -1
  98. package/dist/collection/components/forms/textarea/textarea.host.css +1 -1
  99. package/dist/collection/components/forms/toggle/toggle.host.css +1 -1
  100. package/dist/collection/components/structure/card/card-subtitle/card-subtitle.js +5 -37
  101. package/dist/collection/components/structure/content/content.js +3 -3
  102. package/dist/collection/components/structure/divider/divider.js +3 -3
  103. package/dist/collection/components/structure/heading/heading.const.js +0 -8
  104. package/dist/collection/components/structure/heading/heading.host.css +1 -1
  105. package/dist/collection/components/structure/heading/heading.interfaces.js +0 -1
  106. package/dist/collection/components/structure/heading/heading.js +5 -42
  107. package/dist/collection/components/structure/heading/test/heading.a11y.play.js +3 -8
  108. package/dist/collection/components/structure/stack/stack.interfaces.js +2 -2
  109. package/dist/collection/components/structure/stack/stack.js +10 -8
  110. package/dist/collection/components/structure/stack/test/stack.component.play.js +27 -1
  111. package/dist/collection/foundation/normalize.component.play.js +39 -0
  112. package/dist/collection/global/constants/version.constant.js +1 -1
  113. package/dist/collection/utils/t-shirt.js +7 -6
  114. package/dist/design-system/design-system.esm.js +1 -1
  115. package/dist/design-system/{p-f67413bb28.entry.js → p-083f25371d.entry.js} +1 -1
  116. package/dist/design-system/{p-89418d345c.entry.js → p-1ca8c93650.entry.js} +1 -1
  117. package/dist/design-system/{p-6c9ebf73a3.entry.js → p-2b04230249.entry.js} +1 -1
  118. package/dist/design-system/p-3790705fde.entry.js +4 -0
  119. package/dist/design-system/{p-6b6fdc57c1.entry.js → p-4722d55123.entry.js} +1 -1
  120. package/dist/design-system/p-49ec36993d.entry.js +4 -0
  121. package/dist/design-system/{p-94a2067cb2.entry.js → p-68c0b49d0f.entry.js} +1 -1
  122. package/dist/design-system/{p-870e3c0138.entry.js → p-6d133bb1c4.entry.js} +1 -1
  123. package/dist/design-system/{p-65913aa877.entry.js → p-707ddadb07.entry.js} +1 -1
  124. package/dist/design-system/{p-a6709bd6a4.entry.js → p-812fca6694.entry.js} +1 -1
  125. package/dist/design-system/{p-1832a8b10c.entry.js → p-8d919b39a2.entry.js} +1 -1
  126. package/dist/design-system/p-9705c44121.entry.js +4 -0
  127. package/dist/design-system/{p-42bc0ecdea.entry.js → p-98a83e848d.entry.js} +1 -1
  128. package/dist/design-system/{p-B3VN_5Hp.js → p-BPtibja3.js} +1 -1
  129. package/dist/design-system/p-BZEWgXed.js +1 -1
  130. package/dist/design-system/p-CZXt-zTQ.js +4 -0
  131. package/dist/design-system/p-DFgZk16R.js +6 -0
  132. package/dist/design-system/{p-03f71b802d.entry.js → p-a2921f8048.entry.js} +1 -1
  133. package/dist/design-system/{p-68f1c98ef3.entry.js → p-a3755079c9.entry.js} +1 -1
  134. package/dist/design-system/{p-d5c0b5ddf3.entry.js → p-a99f6b63d3.entry.js} +1 -1
  135. package/dist/design-system/{p-76551271c7.entry.js → p-ac0d1f82c0.entry.js} +1 -1
  136. package/dist/design-system/{p-8c18e03816.entry.js → p-ba7800f8e6.entry.js} +1 -1
  137. package/dist/design-system/p-d2c33fa11f.entry.js +4 -0
  138. package/dist/design-system/{p-d12febf08e.entry.js → p-d85136052d.entry.js} +1 -1
  139. package/dist/design-system/{p-af756319ff.entry.js → p-f799b1412c.entry.js} +1 -1
  140. package/dist/esm/design-system.js +1 -1
  141. package/dist/esm/ds-badge.entry.js +1 -1
  142. package/dist/esm/ds-brand-icon_3.entry.js +17 -3
  143. package/dist/esm/ds-card-subtitle.entry.js +3 -11
  144. package/dist/esm/ds-card-title.entry.js +1 -1
  145. package/dist/esm/ds-content_2.entry.js +9 -7
  146. package/dist/esm/ds-counter.entry.js +4 -4
  147. package/dist/esm/ds-datepicker.entry.js +1 -1
  148. package/dist/esm/ds-divider_2.entry.js +3 -3
  149. package/dist/esm/ds-file-upload.entry.js +1 -1
  150. package/dist/esm/ds-heading.entry.js +5 -24
  151. package/dist/esm/ds-icon_2.entry.js +2 -2
  152. package/dist/esm/ds-input.entry.js +2 -2
  153. package/dist/esm/ds-number-input.entry.js +1 -1
  154. package/dist/esm/ds-phone-input.entry.js +1 -1
  155. package/dist/esm/ds-select.entry.js +1 -1
  156. package/dist/esm/ds-slider.entry.js +1 -1
  157. package/dist/esm/ds-snackbar.entry.js +1 -1
  158. package/dist/esm/ds-tag.entry.js +1 -1
  159. package/dist/esm/ds-textarea.entry.js +1 -1
  160. package/dist/esm/ds-toast.entry.js +1 -1
  161. package/dist/esm/ds-toggle.entry.js +1 -1
  162. package/dist/esm/{heading.interfaces-QyQjrb1P.js → heading.interfaces-CZXt-zTQ.js} +1 -2
  163. package/dist/esm/index-BZEWgXed.js +1 -1
  164. package/dist/esm/loader.js +1 -1
  165. package/dist/esm/{svg-BNUaCToz.js → svg-DFgZk16R.js} +272 -195
  166. package/dist/esm/{t-shirt-B3VN_5Hp.js → t-shirt-BPtibja3.js} +2 -6
  167. package/dist/types/components/actions/button/button.d.ts +4 -0
  168. package/dist/types/components/forms/counter/counter.d.ts +2 -0
  169. package/dist/types/components/forms/input/input.d.ts +2 -0
  170. package/dist/types/components/structure/card/card-subtitle/card-subtitle.d.ts +1 -6
  171. package/dist/types/components/structure/content/content.d.ts +1 -1
  172. package/dist/types/components/structure/divider/divider.d.ts +1 -1
  173. package/dist/types/components/structure/heading/heading.const.d.ts +0 -3
  174. package/dist/types/components/structure/heading/heading.d.ts +1 -6
  175. package/dist/types/components/structure/heading/heading.interfaces.d.ts +0 -2
  176. package/dist/types/components/structure/stack/stack.interfaces.d.ts +2 -2
  177. package/dist/types/components.d.ts +21 -28
  178. package/dist/types/foundation/normalize.component.play.d.ts +1 -0
  179. package/dist/types/utils/t-shirt.d.ts +5 -0
  180. package/docs/cem.json +50 -76
  181. package/docs/components.json +103 -134
  182. package/docs/html.html-data.json +25 -45
  183. package/hydrate/index.js +350 -283
  184. package/hydrate/index.mjs +350 -283
  185. package/package.json +6 -6
  186. package/sbom.cdx.json +9610 -9319
  187. package/components/p-B4PwKlaW.js +0 -4
  188. package/components/p-BNUaCToz.js +0 -6
  189. package/components/p-QyQjrb1P.js +0 -4
  190. package/components/p-VnJFXLI2.js +0 -4
  191. package/components/p-kwr2iIN4.js +0 -4
  192. package/dist/design-system/p-199e5794f9.entry.js +0 -4
  193. package/dist/design-system/p-97670520d2.entry.js +0 -4
  194. package/dist/design-system/p-BNUaCToz.js +0 -6
  195. package/dist/design-system/p-QyQjrb1P.js +0 -4
  196. package/dist/design-system/p-bee20202e4.entry.js +0 -4
  197. package/dist/design-system/p-d65c14d35e.entry.js +0 -4
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * (C) Helvetia Design System https://helvetia-design.vercel.app/ - Apache License 2.0
3
3
  */
4
- /*! @license DOMPurify 3.4.16 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.16/LICENSE */
4
+ /*! @license DOMPurify 3.4.17 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.17/LICENSE */
5
5
  function _arrayLikeToArray(r, a) {
6
6
  (null == a || a > r.length) && (a = r.length);
7
7
  for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
@@ -71,8 +71,12 @@ const arrayForEach = unapply(Array.prototype.forEach);
71
71
  const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
72
72
  const arrayPop = unapply(Array.prototype.pop);
73
73
  const arrayPush = unapply(Array.prototype.push);
74
+ const arraySlice = unapply(Array.prototype.slice);
74
75
  const arraySplice = unapply(Array.prototype.splice);
75
76
  const arrayIsArray = Array.isArray;
77
+ const setCreate = unconstruct(Set);
78
+ const setAdd = unapply(Set.prototype.add);
79
+ const setHas = unapply(Set.prototype.has);
76
80
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
77
81
  const stringToString = unapply(String.prototype.toString);
78
82
  const stringMatch = unapply(String.prototype.match);
@@ -217,6 +221,25 @@ function lookupGetter(object, prop) {
217
221
  }
218
222
  return fallbackValue;
219
223
  }
224
+ /**
225
+ * Like lookupGetter, but for accessor properties only, and returns null
226
+ * instead of a null-returning fallback when no getter exists on the chain.
227
+ * Lets a caller fall back to a direct property read in environments that
228
+ * lack the getter, where a fallback returning null would be indistinguishable
229
+ * from a legitimate null value (e.g. namespaceURI of a no-namespace element).
230
+ *
231
+ * @param object - The object (usually a prototype) to start the lookup at.
232
+ * @param prop - The accessor property name.
233
+ * @returns The unapplied getter, or null if none was found.
234
+ */
235
+ function lookupAccessor(object, prop) {
236
+ while (object !== null) {
237
+ const desc = getOwnPropertyDescriptor(object, prop);
238
+ if (desc) return desc.get ? unapply(desc.get) : null;
239
+ object = getPrototypeOf(object);
240
+ }
241
+ return null;
242
+ }
220
243
  function isRegex(value) {
221
244
  try {
222
245
  regExpTest(value, "");
@@ -894,7 +917,7 @@ const NODE_TYPE = {
894
917
  comment: 8,
895
918
  document: 9,
896
919
  documentFragment: 11};
897
- const LITERAL_TEXT_ELEMENT_NAMES = [
920
+ const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, [
898
921
  "style",
899
922
  "script",
900
923
  "xmp",
@@ -903,15 +926,7 @@ const LITERAL_TEXT_ELEMENT_NAMES = [
903
926
  "noframes",
904
927
  "plaintext",
905
928
  "noscript"
906
- ];
907
- const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
908
- const LITERAL_TEXT_CLOSE = function() {
909
- const map = {};
910
- arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, (name) => {
911
- map[name] = seal(new RegExp("</" + name + "(?=[\\t\\n\\f\\r />])", "i"));
912
- });
913
- return freeze(map);
914
- }();
929
+ ]));
915
930
  const getGlobal = function getGlobal() {
916
931
  return typeof window === "undefined" ? null : window;
917
932
  };
@@ -989,7 +1004,7 @@ const _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallbac
989
1004
  function createDOMPurify() {
990
1005
  let window = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
991
1006
  const DOMPurify = (root) => createDOMPurify(root);
992
- DOMPurify.version = "3.4.16";
1007
+ DOMPurify.version = "3.4.17";
993
1008
  DOMPurify.removed = [];
994
1009
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
995
1010
  DOMPurify.isSupported = false;
@@ -1018,6 +1033,10 @@ function createDOMPurify() {
1018
1033
  const _readNodeName = function _readNodeName(node) {
1019
1034
  return getNodeName ? getNodeName(node) : node.nodeName;
1020
1035
  };
1036
+ const getNamespaceURI = lookupAccessor(ElementPrototype, "namespaceURI");
1037
+ const _readNamespaceURI = function _readNamespaceURI(element) {
1038
+ return getNamespaceURI ? getNamespaceURI(element) : element.namespaceURI;
1039
+ };
1021
1040
  if (typeof HTMLTemplateElement === "function") {
1022
1041
  const template = document.createElement("template");
1023
1042
  if (template.content && template.content.ownerDocument) document = template.content.ownerDocument;
@@ -1138,6 +1157,7 @@ function createDOMPurify() {
1138
1157
  const SANITIZE_NAMED_PROPS_PREFIX = "user-content-";
1139
1158
  let KEEP_CONTENT = true;
1140
1159
  let IN_PLACE = false;
1160
+ let IN_PLACE_VISITED = null;
1141
1161
  let USE_PROFILES = {};
1142
1162
  let FORBID_CONTENTS = null;
1143
1163
  const DEFAULT_FORBID_CONTENTS = addToSet({}, [
@@ -1411,18 +1431,23 @@ function createDOMPurify() {
1411
1431
  * return. Return true otherwise.
1412
1432
  */
1413
1433
  const _checkValidNamespace = function _checkValidNamespace(element) {
1414
- let parent = getParentNode(element);
1415
- if (!parent || !parent.tagName) parent = {
1416
- namespaceURI: NAMESPACE,
1417
- tagName: "template"
1418
- };
1419
- const tagName = stringToLowerCase(element.tagName);
1420
- const parentTagName = stringToLowerCase(parent.tagName);
1421
- if (!ALLOWED_NAMESPACES[element.namespaceURI]) return false;
1422
- if (element.namespaceURI === SVG_NAMESPACE) return _checkSvgNamespace(tagName, parent, parentTagName);
1423
- if (element.namespaceURI === MATHML_NAMESPACE) return _checkMathMlNamespace(tagName, parent, parentTagName);
1424
- if (element.namespaceURI === HTML_NAMESPACE) return _checkHtmlNamespace(tagName, parent, parentTagName);
1425
- if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[element.namespaceURI]) return true;
1434
+ const elementNamespace = _readNamespaceURI(element);
1435
+ const tagName = stringToLowerCase(_readNodeName(element));
1436
+ const parentNode = getParentNode(element);
1437
+ let parent;
1438
+ let parentTagName;
1439
+ if (parentNode && _readNodeType(parentNode) === NODE_TYPE.element) {
1440
+ parent = { namespaceURI: _readNamespaceURI(parentNode) };
1441
+ parentTagName = stringToLowerCase(_readNodeName(parentNode));
1442
+ } else {
1443
+ parent = { namespaceURI: NAMESPACE };
1444
+ parentTagName = "template";
1445
+ }
1446
+ if (!ALLOWED_NAMESPACES[elementNamespace]) return false;
1447
+ if (elementNamespace === SVG_NAMESPACE) return _checkSvgNamespace(tagName, parent, parentTagName);
1448
+ if (elementNamespace === MATHML_NAMESPACE) return _checkMathMlNamespace(tagName, parent, parentTagName);
1449
+ if (elementNamespace === HTML_NAMESPACE) return _checkHtmlNamespace(tagName, parent, parentTagName);
1450
+ if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[elementNamespace]) return true;
1426
1451
  return false;
1427
1452
  };
1428
1453
  /**
@@ -1466,36 +1491,21 @@ function createDOMPurify() {
1466
1491
  }
1467
1492
  };
1468
1493
  /**
1469
- * _neutralizeRoot
1470
- *
1471
- * Fail-closed teardown of an in-place root after the sanitize walk aborts
1472
- * (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
1473
- * custom element's reaction detaches a node so `_forceRemove`'s deliberate
1474
- * parentless guard throws, or any other re-entrant engine mutation — would
1475
- * otherwise leave the caller's *live* tree half-sanitized, with everything
1476
- * after the abort point still carrying its handlers. There is no safe way
1477
- * to resume the walk (the tree mutated under us), so we strip the root bare:
1478
- * remove every child and every attribute, then let the caller's catch see
1479
- * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
1480
- * getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
1494
+ * Fail-closed teardown of an in-place root: strip every disallowed attribute
1495
+ * in the subtree first, so nothing armed is left behind, then remove all
1496
+ * children and the root's own attributes.
1481
1497
  *
1482
1498
  * @param root the in-place root to empty
1483
1499
  */
1484
1500
  const _neutralizeRoot = function _neutralizeRoot(root) {
1485
1501
  _neutralizeSubtree(root);
1486
1502
  const childNodes = getChildNodes(root);
1487
- if (childNodes) {
1488
- const snapshot = [];
1489
- arrayForEach(childNodes, (child) => {
1490
- arrayPush(snapshot, child);
1491
- });
1492
- arrayForEach(snapshot, (child) => {
1493
- try {
1494
- remove(child);
1495
- } catch (_) {}
1496
- });
1497
- }
1498
- const attributes = getAttributes(root);
1503
+ if (childNodes) arrayForEach(arraySlice(childNodes), (child) => {
1504
+ try {
1505
+ remove(child);
1506
+ } catch (_) {}
1507
+ });
1508
+ const attributes = _readNodeType(root) === NODE_TYPE.element ? getAttributes(root) : null;
1499
1509
  if (attributes) for (let i = attributes.length - 1; i >= 0; --i) {
1500
1510
  const attribute = attributes[i];
1501
1511
  const name = attribute && attribute.name;
@@ -1503,18 +1513,9 @@ function createDOMPurify() {
1503
1513
  }
1504
1514
  };
1505
1515
  /**
1506
- * _removeAttribute
1507
- *
1508
- * Name-based getAttributeNode()/removeAttribute() ASCII-lowercase their
1509
- * lookup key for HTML elements in an HTML document, so they silently miss an
1510
- * attribute whose stored qualified name still contains uppercase ASCII
1511
- * letters. That happens when the node came from a case-preserving source
1512
- * (an XML/XHTML document imported via importNode(), or createAttributeNS()),
1513
- * where e.g. `ONERROR` survives the walk: the policy check lowercases to
1514
- * `onerror` and rejects it, but `removeAttribute('ONERROR')` looks up
1515
- * `onerror` and finds nothing. Remove the exact Attr node instead, which is
1516
- * case- and namespace-exact, and fall back to name-based removal only when
1517
- * the caller could not supply the node.
1516
+ * Removes an attribute, preferably as its exact Attr node: name-based
1517
+ * removal lowercases the name in HTML documents and misses case-preserved
1518
+ * names, e.g. from imported XHTML.
1518
1519
  *
1519
1520
  * @param name an Attribute name
1520
1521
  * @param element a DOM node
@@ -1548,12 +1549,8 @@ function createDOMPurify() {
1548
1549
  }
1549
1550
  };
1550
1551
  /**
1551
- * _stripDisallowedAttributes
1552
- *
1553
- * Removes every attribute the active configuration does not allow from a
1554
- * single element, using the same allowlist as the main attribute pass (so
1555
- * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
1556
- * neutralise nodes that are being discarded from an in-place tree.
1552
+ * Removes every attribute the configuration does not allow. Used to
1553
+ * neutralize nodes discarded from an in-place tree.
1557
1554
  *
1558
1555
  * @param element the element to strip
1559
1556
  */
@@ -1568,77 +1565,158 @@ function createDOMPurify() {
1568
1565
  }
1569
1566
  };
1570
1567
  /**
1571
- * _neutralizeSubtree
1568
+ * Iterative depth-first walk, so deep trees cannot overflow the call stack,
1569
+ * reading through the cached getters, so clobbering cannot hide children.
1570
+ * When `visit` returns false, the node's children are skipped. With
1571
+ * `intoShadow`, open shadow roots are walked after their host's children.
1572
1572
  *
1573
- * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT
1574
- * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary,
1575
- * namespace, comment, processing-instruction and KEEP_CONTENT:false removals
1576
- * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
1577
- * those dropped nodes are detached from the caller's LIVE tree but a
1578
- * handler-bearing original among them (an `<img onerror>`/`<video>` that was
1579
- * loading) keeps its queued resource event, which fires in page scope after
1580
- * sanitize returns. This walks a removed subtree and strips every attribute
1581
- * the active configuration does not allow — so `on*` handlers are cancelled
1582
- * through the SAME allowlist that governs kept nodes, not a separate `/^on/`
1583
- * blocklist. Run synchronously before sanitize returns, i.e. before any
1584
- * queued event can fire. Hook-free by design: these nodes leave the output,
1585
- * so firing attribute hooks for them would be surprising. Clobber-safe reads;
1586
- * a doomed clobbered node may shadow `removeAttribute` (its own attributes are
1587
- * irrelevant — it is discarded — while its non-clobbered descendants, e.g.
1588
- * the `<img>`, are reached and scrubbed).
1589
- *
1590
- * @param root the root of a removed subtree to neutralise
1573
+ * @param root where to start
1574
+ * @param visit called for every node
1575
+ * @param intoShadow whether to enter open shadow roots
1591
1576
  */
1592
- const _neutralizeSubtree = function _neutralizeSubtree(root) {
1577
+ const _walkTree = function _walkTree(root, visit) {
1578
+ let intoShadow = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false;
1593
1579
  const stack = [root];
1594
1580
  while (stack.length > 0) {
1595
1581
  const node = stack.pop();
1596
- if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
1582
+ if (visit(node) === false) continue;
1583
+ if (intoShadow && _readNodeType(node) === NODE_TYPE.element) {
1584
+ const shadowRoot = getShadowRoot(node);
1585
+ if (_isDocumentFragment(shadowRoot)) stack.push(shadowRoot);
1586
+ }
1597
1587
  const childNodes = getChildNodes(node);
1598
1588
  if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
1599
1589
  }
1600
1590
  };
1601
1591
  /**
1602
- * _neutralizePatchLinkage
1592
+ * Strips disallowed attributes from every element of a discarded subtree,
1593
+ * so a handler that is already queued (a loading image) cannot fire.
1603
1594
  *
1604
- * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
1605
- * hardening, https://github.com/WICG/declarative-partial-updates).
1595
+ * @param root the root of a removed subtree
1596
+ */
1597
+ const _neutralizeSubtree = function _neutralizeSubtree(root) {
1598
+ _walkTree(root, (node) => {
1599
+ if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
1600
+ });
1601
+ };
1602
+ /**
1603
+ * All nodes under `root`, root first, including open shadow roots. Template
1604
+ * content is not entered, it is inert.
1606
1605
  *
1607
- * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
1608
- * markers (PIs / markup comments) node-by-node, in document order, AS it
1609
- * reaches each node. On a live in-place root that leaves a window: from the
1610
- * moment the root is connected until the walk arrives at a given node, that
1611
- * node's linkage is live. A patch applied on connection/stream can fire as
1612
- * a microtask during the walk and inject or teleport an unsanitized DOM
1613
- * range into a region the iterator has already passed and will not revisit,
1614
- * so the post-return "tree is sanitized" contract is violated. Sweep the
1615
- * whole tree once up front and sever every linkage before the walk begins,
1616
- * closing that window.
1606
+ * @param root the in-place root
1607
+ * @return the nodes
1608
+ */
1609
+ const _collectLiveNodes = function _collectLiveNodes(root) {
1610
+ const nodes = [];
1611
+ _walkTree(root, (node) => {
1612
+ nodes.push(node);
1613
+ }, true);
1614
+ return nodes;
1615
+ };
1616
+ /**
1617
+ * Strips disallowed attributes from every element that was under the
1618
+ * in-place root when the call started and no longer is, whatever removed
1619
+ * or moved it.
1617
1620
  *
1618
- * This CANNOT undo a patch that already fired before sanitize ran — that is
1619
- * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
1620
- * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
1621
- * to group with the rest of the declarative-partial-updates handling and
1622
- * stay overridable, consistent with the codebase.
1621
+ * @param root the in-place root
1622
+ * @param before the root's nodes, collected before the walk
1623
+ */
1624
+ const _neutralizeEscapedElements = function _neutralizeEscapedElements(root, before) {
1625
+ const reachable = setCreate();
1626
+ arrayForEach(_collectLiveNodes(root), (node) => {
1627
+ if (_readNodeType(node) === NODE_TYPE.element) setAdd(reachable, node);
1628
+ });
1629
+ arrayForEach(before, (node) => {
1630
+ if (_readNodeType(node) === NODE_TYPE.element && !setHas(reachable, node)) _stripDisallowedAttributes(node);
1631
+ });
1632
+ };
1633
+ /**
1634
+ * Neutralizes everything that left the in-place tree during the call: what
1635
+ * DOMPurify removed, and what hooks or custom elements removed or moved out.
1623
1636
  *
1624
- * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
1625
- * clobbered root cannot defeat the sweep of its non-clobbered descendants.
1637
+ * @param root the in-place root
1638
+ * @param before the root's nodes, collected before the walk
1639
+ * @return true if the root itself was removed
1640
+ */
1641
+ const _neutralizeDiscarded = function _neutralizeDiscarded(root, before) {
1642
+ let rootRemoved = false;
1643
+ arrayForEach(DOMPurify.removed, (entry) => {
1644
+ if (entry.element) {
1645
+ rootRemoved = rootRemoved || entry.element === root;
1646
+ _neutralizeSubtree(entry.element);
1647
+ }
1648
+ });
1649
+ _neutralizeEscapedElements(root, before);
1650
+ return rootRemoved;
1651
+ };
1652
+ const MAX_UNVISITED_ROUNDS = 8;
1653
+ /**
1654
+ * The walk only moves forward, so a node moved to a position it already
1655
+ * passed is never visited. Give every original node that is still in the
1656
+ * tree but unvisited the normal treatment, hooks included. Nodes created
1657
+ * during the call are left alone. Fails closed if the tree keeps changing.
1626
1658
  *
1627
- * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
1628
- * .html Q1): this mirrors the existing policy of keeping `for` on
1629
- * <label>/<output>. If the shipping feature can drive a patch through a
1630
- * surviving `for`-on-label/output + `id` pair, this pre-pass and the
1631
- * attribute check at _isBasicCustomElement's caller must additionally drop
1632
- * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
1659
+ * @param root the in-place root
1660
+ * @param before the root's nodes, collected before the walk
1661
+ */
1662
+ const _sanitizeUnvisitedNodes = function _sanitizeUnvisitedNodes(root, before) {
1663
+ const visited = IN_PLACE_VISITED;
1664
+ let original = null;
1665
+ const getOriginal = () => {
1666
+ if (!original) {
1667
+ original = setCreate();
1668
+ arrayForEach(before, (beforeNode) => {
1669
+ setAdd(original, beforeNode);
1670
+ });
1671
+ }
1672
+ return original;
1673
+ };
1674
+ for (let round = 0;; round++) {
1675
+ const live = _collectLiveNodes(root);
1676
+ const containers = [root];
1677
+ let pending = false;
1678
+ arrayForEach(live, (node) => {
1679
+ if (_readNodeType(node) === NODE_TYPE.documentFragment) arrayPush(containers, node);
1680
+ else if (!pending && !setHas(visited, node)) pending = setHas(getOriginal(), node);
1681
+ });
1682
+ if (!pending) return;
1683
+ if (round >= MAX_UNVISITED_ROUNDS) throw typeErrorCreate("the tree kept changing during in-place sanitization; refusing to return it");
1684
+ arrayForEach(containers, (container, index) => {
1685
+ _sanitizeUnvisitedIn(container, index > 0, root, getOriginal());
1686
+ });
1687
+ }
1688
+ };
1689
+ /**
1690
+ * One _sanitizeUnvisitedNodes pass over the root or an open shadow root.
1633
1691
  *
1634
- * @param root the in-place root to sweep
1692
+ * @param container the root or an open shadow root
1693
+ * @param inShadow whether container is a shadow root
1694
+ * @param root the in-place root
1695
+ * @param original the root's nodes collected before the walk, as a set
1635
1696
  */
1697
+ const _sanitizeUnvisitedIn = function _sanitizeUnvisitedIn(container, inShadow, root, original) {
1698
+ const iterator = _createNodeIterator(container);
1699
+ let node;
1700
+ while (node = iterator.nextNode()) {
1701
+ if (!setHas(original, node) || setHas(IN_PLACE_VISITED, node)) continue;
1702
+ if (inShadow) _executeHooks(hooks.uponSanitizeShadowNode, node, null);
1703
+ _sanitizeElements(node, root);
1704
+ _sanitizeAttributes(node, root);
1705
+ if (_isDocumentFragment(node.content)) _sanitizeShadowDOM2(node.content);
1706
+ }
1707
+ };
1636
1708
  /**
1637
- * Central policy for declarative-partial-updates patch-linkage attributes,
1638
- * shared by the _neutralizePatchLinkage pre-pass and _isValidAttribute so
1639
- * the two sites cannot drift: `patchsrc` always links, `for` links
1640
- * everywhere except on <label>/<output>, and the whole policy is gated on
1641
- * SAFE_FOR_XML (see the rationale block in _isValidAttribute).
1709
+ * Records that _sanitizeElements processed `node`. No-op outside IN_PLACE.
1710
+ *
1711
+ * @param node the node being processed
1712
+ */
1713
+ const _markVisited = function _markVisited(node) {
1714
+ if (IN_PLACE_VISITED) setAdd(IN_PLACE_VISITED, node);
1715
+ };
1716
+ /**
1717
+ * Patch-linkage attributes (declarative partial updates): `patchsrc`
1718
+ * always, `for` everywhere except on <label> and <output>. Gated on
1719
+ * SAFE_FOR_XML. Shared by the IN_PLACE pre-pass and _isValidAttribute.
1642
1720
  *
1643
1721
  * @param lcName the transformCaseFunc'd attribute name
1644
1722
  * @param lcTag the transformCaseFunc'd tag name of the carrying element
@@ -1649,17 +1727,26 @@ function createDOMPurify() {
1649
1727
  if (lcName === "patchsrc") return true;
1650
1728
  return lcName === "for" && lcTag !== "label" && lcTag !== "output";
1651
1729
  };
1730
+ /**
1731
+ * IN_PLACE pre-pass: sever all patch linkage before the walk, so no patch
1732
+ * can fire mid-walk into a part of the tree the walker already passed.
1733
+ * Removes range markers (processing instructions, comments with markup)
1734
+ * and strips linkage attributes. It cannot undo a patch that already fired.
1735
+ *
1736
+ * TODO: if the shipped feature can patch through `for` on <label>/<output>,
1737
+ * drop that pair here too (see test/declarative-patch-probe.html).
1738
+ *
1739
+ * @param root the in-place root to sweep
1740
+ */
1652
1741
  const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
1653
1742
  if (!SAFE_FOR_XML) return;
1654
- const stack = [root];
1655
- while (stack.length > 0) {
1656
- const node = stack.pop();
1743
+ _walkTree(root, (node) => {
1657
1744
  const nodeType = _readNodeType(node);
1658
1745
  if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
1659
1746
  try {
1660
1747
  remove(node);
1661
1748
  } catch (_) {}
1662
- continue;
1749
+ return false;
1663
1750
  }
1664
1751
  if (nodeType === NODE_TYPE.element) {
1665
1752
  const element = node;
@@ -1669,9 +1756,8 @@ function createDOMPurify() {
1669
1756
  if (element.hasAttribute && element.hasAttribute("for") && _isPatchLinkageAttribute("for", lcTag)) element.removeAttribute("for");
1670
1757
  } catch (_) {}
1671
1758
  }
1672
- const childNodes = getChildNodes(node);
1673
- if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
1674
- }
1759
+ return true;
1760
+ });
1675
1761
  };
1676
1762
  /**
1677
1763
  * _initDocument
@@ -1748,6 +1834,27 @@ function createDOMPurify() {
1748
1834
  */
1749
1835
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1750
1836
  var _node$querySelectorAl;
1837
+ _scrubOwnCharacterData(node);
1838
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, "template");
1839
+ if (templates) arrayForEach(templates, (tmpl) => {
1840
+ if (_isDocumentFragment(tmpl.content)) _scrubTemplateExpressions2(tmpl.content);
1841
+ });
1842
+ };
1843
+ /**
1844
+ * _scrubOwnCharacterData
1845
+ *
1846
+ * Non-recursive core of _scrubTemplateExpressions: normalize `node` and
1847
+ * strip template expressions from its own character data only. Does not
1848
+ * enter <template>.content or attached shadow roots (NodeIterator never
1849
+ * does). Used by _sanitizeShadowDOM, where every nested template content
1850
+ * and shadow root is already walked - and therefore scrubbed - by its own
1851
+ * recursive _sanitizeShadowDOM call; recursing here as well would rescrub
1852
+ * each nested fragment once per ancestor, which is quadratic in nesting
1853
+ * depth.
1854
+ *
1855
+ * @param node The root whose own character data should be scrubbed.
1856
+ */
1857
+ const _scrubOwnCharacterData = function _scrubOwnCharacterData(node) {
1751
1858
  node.normalize();
1752
1859
  const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
1753
1860
  const walker = createNodeIterator.call(doc || node, node, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
@@ -1756,10 +1863,6 @@ function createDOMPurify() {
1756
1863
  currentNode.data = _stripTemplateExpressions(currentNode.data);
1757
1864
  currentNode = walker.nextNode();
1758
1865
  }
1759
- const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, "template");
1760
- if (templates) arrayForEach(templates, (tmpl) => {
1761
- if (_isDocumentFragment(tmpl.content)) _scrubTemplateExpressions2(tmpl.content);
1762
- });
1763
1866
  };
1764
1867
  /**
1765
1868
  * _isClobbered
@@ -1835,7 +1938,7 @@ function createDOMPurify() {
1835
1938
  */
1836
1939
  const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
1837
1940
  if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) return true;
1838
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && LITERAL_TEXT_ELEMENTS[tagName] && (_isNode(currentNode.firstElementChild) || typeof currentNode.textContent === "string" && regExpTest(LITERAL_TEXT_CLOSE[tagName], currentNode.textContent))) return true;
1941
+ if (SAFE_FOR_XML && LITERAL_TEXT_ELEMENTS[tagName] && _readNodeType(currentNode) === NODE_TYPE.element && _readNamespaceURI(currentNode) === HTML_NAMESPACE && (_isNode(currentNode.firstElementChild) || typeof currentNode.textContent === "string" && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent))) return true;
1839
1942
  if (currentNode.nodeType === NODE_TYPE.processingInstruction) return true;
1840
1943
  if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) return true;
1841
1944
  return false;
@@ -1879,7 +1982,7 @@ function createDOMPurify() {
1879
1982
  */
1880
1983
  const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
1881
1984
  if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
1882
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1985
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName] && !FORBID_CONTENTS[stringToLowerCase(tagName)]) {
1883
1986
  const parentNode = getParentNode(currentNode);
1884
1987
  const childNodes = getChildNodes(currentNode);
1885
1988
  if (childNodes && parentNode) {
@@ -1912,35 +2015,13 @@ function createDOMPurify() {
1912
2015
  return set === defaultSet || set === setConfigSet ? clone(set) : set;
1913
2016
  };
1914
2017
  /**
1915
- * Shared guard for a node that a hook has detached from the walk tree,
1916
- * used after each element-hook site in _sanitizeElements. Detaching is a
1917
- * long-standing user pattern (issue #469; draw.io-style foreignObject
1918
- * filtering). Per the cached, unclobberable parentNode getter the node is
1919
- * genuinely out of the tree, so it can reach neither the serialized
1920
- * output nor an IN_PLACE live tree; treat it as removed and stop
1921
- * processing it. Without this guard, the unsafe-node / namespace checks
1922
- * would call _forceRemove on a parentless node and hit the REPORT-3
1923
- * fail-closed throw — which exists for nodes DOMPurify wants gone but
1924
- * *cannot* detach (clobbered / parentless roots), the opposite of a node
1925
- * that is already safely gone. The walk root is exempt: a detached
1926
- * IN_PLACE root is legitimate input and must still be fully sanitized,
1927
- * and a kill-decision on it must keep hitting the REPORT-3 throw.
1928
- *
1929
- * Nodes detached by hooks stay the hook's responsibility for placement:
1930
- * they are not recorded in DOMPurify.removed, so the post-walk IN_PLACE
1931
- * pass (which iterates DOMPurify.removed) does not reach them. But a
1932
- * hook-detached subtree can still hold a queued resource-event handler -
1933
- * e.g. an <img onload> that began loading when the caller built the live
1934
- * tree - which fires in page scope after sanitize returns even though the
1935
- * handler never reached the returned tree. That is the audit-5 F1 hazard,
1936
- * and the documented node.remove() hook pattern walks straight into it.
1937
- * So on the IN_PLACE path we neutralize the detached subtree inline,
1938
- * stripping its non-allow-listed attributes before returning, exactly as
1939
- * the post-walk pass does for _forceRemove'd subtrees.
2018
+ * A hook may have detached the node it was called for. If so, treat it as
2019
+ * removed and stop processing it; the walk root is exempt. On the IN_PLACE
2020
+ * path the detached subtree is neutralized right away.
1940
2021
  *
1941
2022
  * @param currentNode the node a hook may have detached
1942
2023
  * @param root the current walk root
1943
- * @return true if the node is detached and now handled, false otherwise
2024
+ * @return true if the node was detached
1944
2025
  */
1945
2026
  const _handleHookDetachedNode = function _handleHookDetachedNode(currentNode, root) {
1946
2027
  if (currentNode === root || getParentNode(currentNode) !== null) return false;
@@ -1957,6 +2038,7 @@ function createDOMPurify() {
1957
2038
  * @return true if node was killed, false if left alive
1958
2039
  */
1959
2040
  const _sanitizeElements = function _sanitizeElements(currentNode, root) {
2041
+ _markVisited(currentNode);
1960
2042
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1961
2043
  if (_handleHookDetachedNode(currentNode, root)) return true;
1962
2044
  if (_isClobbered(currentNode)) {
@@ -2100,6 +2182,22 @@ function createDOMPurify() {
2100
2182
  }
2101
2183
  };
2102
2184
  /**
2185
+ * _isUnsafeAttributeValue
2186
+ *
2187
+ * Value-based attribute checks that the attribute walk applies before any
2188
+ * hook can force-keep an attribute, shared with the public
2189
+ * isValidAttribute() so that it never approves a value sanitize() would
2190
+ * strip.
2191
+ *
2192
+ * @param name the attribute name as found on the node (case preserved)
2193
+ * @param value the attribute value
2194
+ * @returns true if the attribute must be removed because of its value
2195
+ */
2196
+ const _isUnsafeAttributeValue = function _isUnsafeAttributeValue(name, value) {
2197
+ if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) return true;
2198
+ return stringToLowerCase(name) === "attributename" && stringMatch(value, "href") !== null;
2199
+ };
2200
+ /**
2103
2201
  * _sanitizeAttributes
2104
2202
  *
2105
2203
  * @protect attributes
@@ -2143,11 +2241,7 @@ function createDOMPurify() {
2143
2241
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
2144
2242
  recreatedNamedProp = true;
2145
2243
  }
2146
- if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
2147
- _removeAttribute(name, currentNode, attr);
2148
- continue;
2149
- }
2150
- if (lcName === "attributename" && stringMatch(value, "href")) {
2244
+ if (_isUnsafeAttributeValue(name, value)) {
2151
2245
  _removeAttribute(name, currentNode, attr);
2152
2246
  continue;
2153
2247
  }
@@ -2195,24 +2289,13 @@ function createDOMPurify() {
2195
2289
  }
2196
2290
  }
2197
2291
  }
2292
+ if (SAFE_FOR_TEMPLATES) _scrubOwnCharacterData(fragment);
2198
2293
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
2199
2294
  };
2200
2295
  /**
2201
- * _sanitizeAttachedShadowRoots
2202
- *
2203
- * Walks `root` and feeds every attached shadow root we encounter into
2204
- * the existing _sanitizeShadowDOM pipeline. The default node iterator
2205
- * does not descend into shadow trees, so nodes inside an attached
2206
- * shadow root would otherwise be skipped entirely.
2207
- *
2208
- * Two real input paths put attached shadow roots in front of us:
2209
- * 1. IN_PLACE on a DOM node that already has shadow roots attached.
2210
- * 2. DOM-node input where importNode(dirty, true) deep-clones the
2211
- * shadow root because it was created with `clonable: true`.
2212
- *
2213
- * This pass runs once, up front, so the main iteration loop (and the
2214
- * existing _sanitizeShadowDOM template-content recursion) stay
2215
- * untouched — string-input paths are not affected.
2296
+ * Sanitizes every attached shadow root under `root`, since the node
2297
+ * iterator does not enter shadow trees. Needed for IN_PLACE input and for
2298
+ * DOM input whose clonable shadow roots were copied by importNode().
2216
2299
  *
2217
2300
  * @param root the subtree root to walk for attached shadow roots
2218
2301
  */
@@ -2277,6 +2360,8 @@ function createDOMPurify() {
2277
2360
  if (hooks.uponSanitizeAttribute.length > 0) ALLOWED_ATTR = clone(ALLOWED_ATTR);
2278
2361
  DOMPurify.removed = [];
2279
2362
  const inPlace = IN_PLACE && typeof dirty !== "string" && _isNode(dirty);
2363
+ let inPlaceNodes = [];
2364
+ const outerVisited = IN_PLACE_VISITED;
2280
2365
  if (inPlace) {
2281
2366
  _neutralizePatchLinkage(dirty);
2282
2367
  const nn = _readNodeName(dirty);
@@ -2291,12 +2376,8 @@ function createDOMPurify() {
2291
2376
  _neutralizeRoot(dirty);
2292
2377
  throw typeErrorCreate("root node is clobbered and cannot be sanitized in-place");
2293
2378
  }
2294
- try {
2295
- _sanitizeAttachedShadowRoots(dirty);
2296
- } catch (error) {
2297
- _neutralizeRoot(dirty);
2298
- throw error;
2299
- }
2379
+ inPlaceNodes = _collectLiveNodes(dirty);
2380
+ IN_PLACE_VISITED = setCreate();
2300
2381
  } else if (_isNode(dirty)) {
2301
2382
  body = _initDocument("<!---->");
2302
2383
  importedNode = body.ownerDocument.importNode(dirty, true);
@@ -2312,30 +2393,25 @@ function createDOMPurify() {
2312
2393
  if (body && FORCE_BODY) _forceRemove(body.firstChild);
2313
2394
  const walkRoot = inPlace ? dirty : body;
2314
2395
  try {
2396
+ if (inPlace) _sanitizeAttachedShadowRoots(dirty);
2315
2397
  const nodeIterator = _createNodeIterator(walkRoot);
2316
2398
  while (currentNode = nodeIterator.nextNode()) {
2317
2399
  _sanitizeElements(currentNode, walkRoot);
2318
2400
  _sanitizeAttributes(currentNode, walkRoot);
2319
2401
  if (_isDocumentFragment(currentNode.content)) _sanitizeShadowDOM2(currentNode.content);
2320
2402
  }
2403
+ if (inPlace) _sanitizeUnvisitedNodes(dirty, inPlaceNodes);
2321
2404
  } catch (error) {
2322
2405
  if (inPlace) {
2323
2406
  _neutralizeRoot(dirty);
2324
- arrayForEach(DOMPurify.removed, (entry) => {
2325
- if (entry.element) _neutralizeSubtree(entry.element);
2326
- });
2407
+ _neutralizeDiscarded(dirty, inPlaceNodes);
2327
2408
  }
2328
2409
  throw error;
2410
+ } finally {
2411
+ IN_PLACE_VISITED = outerVisited;
2329
2412
  }
2330
2413
  if (inPlace) {
2331
- let rootWasRemoved = false;
2332
- arrayForEach(DOMPurify.removed, (entry) => {
2333
- if (entry.element) {
2334
- if (entry.element === dirty) rootWasRemoved = true;
2335
- _neutralizeSubtree(entry.element);
2336
- }
2337
- });
2338
- if (rootWasRemoved) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
2414
+ if (_neutralizeDiscarded(dirty, inPlaceNodes)) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
2339
2415
  if (SAFE_FOR_TEMPLATES) _scrubTemplateExpressions2(dirty);
2340
2416
  return dirty;
2341
2417
  }
@@ -2372,6 +2448,7 @@ function createDOMPurify() {
2372
2448
  if (!CONFIG) _parseConfig({});
2373
2449
  const lcTag = transformCaseFunc(tag);
2374
2450
  const lcName = transformCaseFunc(attr);
2451
+ if (typeof value === "string" && (_isUnsafeAttributeValue(attr, value) || !ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value))) return false;
2375
2452
  return _isValidAttribute(lcTag, lcName, value);
2376
2453
  };
2377
2454
  DOMPurify.addHook = function(entryPoint, hookFunction) {