@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
@@ -3,7 +3,7 @@
3
3
  */
4
4
  'use strict';
5
5
 
6
- /*! @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 */
6
+ /*! @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 */
7
7
  function _arrayLikeToArray(r, a) {
8
8
  (null == a || a > r.length) && (a = r.length);
9
9
  for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
@@ -73,8 +73,12 @@ const arrayForEach = unapply(Array.prototype.forEach);
73
73
  const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
74
74
  const arrayPop = unapply(Array.prototype.pop);
75
75
  const arrayPush = unapply(Array.prototype.push);
76
+ const arraySlice = unapply(Array.prototype.slice);
76
77
  const arraySplice = unapply(Array.prototype.splice);
77
78
  const arrayIsArray = Array.isArray;
79
+ const setCreate = unconstruct(Set);
80
+ const setAdd = unapply(Set.prototype.add);
81
+ const setHas = unapply(Set.prototype.has);
78
82
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
79
83
  const stringToString = unapply(String.prototype.toString);
80
84
  const stringMatch = unapply(String.prototype.match);
@@ -219,6 +223,25 @@ function lookupGetter(object, prop) {
219
223
  }
220
224
  return fallbackValue;
221
225
  }
226
+ /**
227
+ * Like lookupGetter, but for accessor properties only, and returns null
228
+ * instead of a null-returning fallback when no getter exists on the chain.
229
+ * Lets a caller fall back to a direct property read in environments that
230
+ * lack the getter, where a fallback returning null would be indistinguishable
231
+ * from a legitimate null value (e.g. namespaceURI of a no-namespace element).
232
+ *
233
+ * @param object - The object (usually a prototype) to start the lookup at.
234
+ * @param prop - The accessor property name.
235
+ * @returns The unapplied getter, or null if none was found.
236
+ */
237
+ function lookupAccessor(object, prop) {
238
+ while (object !== null) {
239
+ const desc = getOwnPropertyDescriptor(object, prop);
240
+ if (desc) return desc.get ? unapply(desc.get) : null;
241
+ object = getPrototypeOf(object);
242
+ }
243
+ return null;
244
+ }
222
245
  function isRegex(value) {
223
246
  try {
224
247
  regExpTest(value, "");
@@ -896,7 +919,7 @@ const NODE_TYPE = {
896
919
  comment: 8,
897
920
  document: 9,
898
921
  documentFragment: 11};
899
- const LITERAL_TEXT_ELEMENT_NAMES = [
922
+ const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, [
900
923
  "style",
901
924
  "script",
902
925
  "xmp",
@@ -905,15 +928,7 @@ const LITERAL_TEXT_ELEMENT_NAMES = [
905
928
  "noframes",
906
929
  "plaintext",
907
930
  "noscript"
908
- ];
909
- const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
910
- const LITERAL_TEXT_CLOSE = function() {
911
- const map = {};
912
- arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, (name) => {
913
- map[name] = seal(new RegExp("</" + name + "(?=[\\t\\n\\f\\r />])", "i"));
914
- });
915
- return freeze(map);
916
- }();
931
+ ]));
917
932
  const getGlobal = function getGlobal() {
918
933
  return typeof window === "undefined" ? null : window;
919
934
  };
@@ -991,7 +1006,7 @@ const _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallbac
991
1006
  function createDOMPurify() {
992
1007
  let window = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
993
1008
  const DOMPurify = (root) => createDOMPurify(root);
994
- DOMPurify.version = "3.4.16";
1009
+ DOMPurify.version = "3.4.17";
995
1010
  DOMPurify.removed = [];
996
1011
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
997
1012
  DOMPurify.isSupported = false;
@@ -1020,6 +1035,10 @@ function createDOMPurify() {
1020
1035
  const _readNodeName = function _readNodeName(node) {
1021
1036
  return getNodeName ? getNodeName(node) : node.nodeName;
1022
1037
  };
1038
+ const getNamespaceURI = lookupAccessor(ElementPrototype, "namespaceURI");
1039
+ const _readNamespaceURI = function _readNamespaceURI(element) {
1040
+ return getNamespaceURI ? getNamespaceURI(element) : element.namespaceURI;
1041
+ };
1023
1042
  if (typeof HTMLTemplateElement === "function") {
1024
1043
  const template = document.createElement("template");
1025
1044
  if (template.content && template.content.ownerDocument) document = template.content.ownerDocument;
@@ -1140,6 +1159,7 @@ function createDOMPurify() {
1140
1159
  const SANITIZE_NAMED_PROPS_PREFIX = "user-content-";
1141
1160
  let KEEP_CONTENT = true;
1142
1161
  let IN_PLACE = false;
1162
+ let IN_PLACE_VISITED = null;
1143
1163
  let USE_PROFILES = {};
1144
1164
  let FORBID_CONTENTS = null;
1145
1165
  const DEFAULT_FORBID_CONTENTS = addToSet({}, [
@@ -1413,18 +1433,23 @@ function createDOMPurify() {
1413
1433
  * return. Return true otherwise.
1414
1434
  */
1415
1435
  const _checkValidNamespace = function _checkValidNamespace(element) {
1416
- let parent = getParentNode(element);
1417
- if (!parent || !parent.tagName) parent = {
1418
- namespaceURI: NAMESPACE,
1419
- tagName: "template"
1420
- };
1421
- const tagName = stringToLowerCase(element.tagName);
1422
- const parentTagName = stringToLowerCase(parent.tagName);
1423
- if (!ALLOWED_NAMESPACES[element.namespaceURI]) return false;
1424
- if (element.namespaceURI === SVG_NAMESPACE) return _checkSvgNamespace(tagName, parent, parentTagName);
1425
- if (element.namespaceURI === MATHML_NAMESPACE) return _checkMathMlNamespace(tagName, parent, parentTagName);
1426
- if (element.namespaceURI === HTML_NAMESPACE) return _checkHtmlNamespace(tagName, parent, parentTagName);
1427
- if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[element.namespaceURI]) return true;
1436
+ const elementNamespace = _readNamespaceURI(element);
1437
+ const tagName = stringToLowerCase(_readNodeName(element));
1438
+ const parentNode = getParentNode(element);
1439
+ let parent;
1440
+ let parentTagName;
1441
+ if (parentNode && _readNodeType(parentNode) === NODE_TYPE.element) {
1442
+ parent = { namespaceURI: _readNamespaceURI(parentNode) };
1443
+ parentTagName = stringToLowerCase(_readNodeName(parentNode));
1444
+ } else {
1445
+ parent = { namespaceURI: NAMESPACE };
1446
+ parentTagName = "template";
1447
+ }
1448
+ if (!ALLOWED_NAMESPACES[elementNamespace]) return false;
1449
+ if (elementNamespace === SVG_NAMESPACE) return _checkSvgNamespace(tagName, parent, parentTagName);
1450
+ if (elementNamespace === MATHML_NAMESPACE) return _checkMathMlNamespace(tagName, parent, parentTagName);
1451
+ if (elementNamespace === HTML_NAMESPACE) return _checkHtmlNamespace(tagName, parent, parentTagName);
1452
+ if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[elementNamespace]) return true;
1428
1453
  return false;
1429
1454
  };
1430
1455
  /**
@@ -1468,36 +1493,21 @@ function createDOMPurify() {
1468
1493
  }
1469
1494
  };
1470
1495
  /**
1471
- * _neutralizeRoot
1472
- *
1473
- * Fail-closed teardown of an in-place root after the sanitize walk aborts
1474
- * (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
1475
- * custom element's reaction detaches a node so `_forceRemove`'s deliberate
1476
- * parentless guard throws, or any other re-entrant engine mutation — would
1477
- * otherwise leave the caller's *live* tree half-sanitized, with everything
1478
- * after the abort point still carrying its handlers. There is no safe way
1479
- * to resume the walk (the tree mutated under us), so we strip the root bare:
1480
- * remove every child and every attribute, then let the caller's catch see
1481
- * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
1482
- * getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
1496
+ * Fail-closed teardown of an in-place root: strip every disallowed attribute
1497
+ * in the subtree first, so nothing armed is left behind, then remove all
1498
+ * children and the root's own attributes.
1483
1499
  *
1484
1500
  * @param root the in-place root to empty
1485
1501
  */
1486
1502
  const _neutralizeRoot = function _neutralizeRoot(root) {
1487
1503
  _neutralizeSubtree(root);
1488
1504
  const childNodes = getChildNodes(root);
1489
- if (childNodes) {
1490
- const snapshot = [];
1491
- arrayForEach(childNodes, (child) => {
1492
- arrayPush(snapshot, child);
1493
- });
1494
- arrayForEach(snapshot, (child) => {
1495
- try {
1496
- remove(child);
1497
- } catch (_) {}
1498
- });
1499
- }
1500
- const attributes = getAttributes(root);
1505
+ if (childNodes) arrayForEach(arraySlice(childNodes), (child) => {
1506
+ try {
1507
+ remove(child);
1508
+ } catch (_) {}
1509
+ });
1510
+ const attributes = _readNodeType(root) === NODE_TYPE.element ? getAttributes(root) : null;
1501
1511
  if (attributes) for (let i = attributes.length - 1; i >= 0; --i) {
1502
1512
  const attribute = attributes[i];
1503
1513
  const name = attribute && attribute.name;
@@ -1505,18 +1515,9 @@ function createDOMPurify() {
1505
1515
  }
1506
1516
  };
1507
1517
  /**
1508
- * _removeAttribute
1509
- *
1510
- * Name-based getAttributeNode()/removeAttribute() ASCII-lowercase their
1511
- * lookup key for HTML elements in an HTML document, so they silently miss an
1512
- * attribute whose stored qualified name still contains uppercase ASCII
1513
- * letters. That happens when the node came from a case-preserving source
1514
- * (an XML/XHTML document imported via importNode(), or createAttributeNS()),
1515
- * where e.g. `ONERROR` survives the walk: the policy check lowercases to
1516
- * `onerror` and rejects it, but `removeAttribute('ONERROR')` looks up
1517
- * `onerror` and finds nothing. Remove the exact Attr node instead, which is
1518
- * case- and namespace-exact, and fall back to name-based removal only when
1519
- * the caller could not supply the node.
1518
+ * Removes an attribute, preferably as its exact Attr node: name-based
1519
+ * removal lowercases the name in HTML documents and misses case-preserved
1520
+ * names, e.g. from imported XHTML.
1520
1521
  *
1521
1522
  * @param name an Attribute name
1522
1523
  * @param element a DOM node
@@ -1550,12 +1551,8 @@ function createDOMPurify() {
1550
1551
  }
1551
1552
  };
1552
1553
  /**
1553
- * _stripDisallowedAttributes
1554
- *
1555
- * Removes every attribute the active configuration does not allow from a
1556
- * single element, using the same allowlist as the main attribute pass (so
1557
- * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
1558
- * neutralise nodes that are being discarded from an in-place tree.
1554
+ * Removes every attribute the configuration does not allow. Used to
1555
+ * neutralize nodes discarded from an in-place tree.
1559
1556
  *
1560
1557
  * @param element the element to strip
1561
1558
  */
@@ -1570,77 +1567,158 @@ function createDOMPurify() {
1570
1567
  }
1571
1568
  };
1572
1569
  /**
1573
- * _neutralizeSubtree
1570
+ * Iterative depth-first walk, so deep trees cannot overflow the call stack,
1571
+ * reading through the cached getters, so clobbering cannot hide children.
1572
+ * When `visit` returns false, the node's children are skipped. With
1573
+ * `intoShadow`, open shadow roots are walked after their host's children.
1574
1574
  *
1575
- * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT
1576
- * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary,
1577
- * namespace, comment, processing-instruction and KEEP_CONTENT:false removals
1578
- * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
1579
- * those dropped nodes are detached from the caller's LIVE tree but a
1580
- * handler-bearing original among them (an `<img onerror>`/`<video>` that was
1581
- * loading) keeps its queued resource event, which fires in page scope after
1582
- * sanitize returns. This walks a removed subtree and strips every attribute
1583
- * the active configuration does not allow — so `on*` handlers are cancelled
1584
- * through the SAME allowlist that governs kept nodes, not a separate `/^on/`
1585
- * blocklist. Run synchronously before sanitize returns, i.e. before any
1586
- * queued event can fire. Hook-free by design: these nodes leave the output,
1587
- * so firing attribute hooks for them would be surprising. Clobber-safe reads;
1588
- * a doomed clobbered node may shadow `removeAttribute` (its own attributes are
1589
- * irrelevant — it is discarded — while its non-clobbered descendants, e.g.
1590
- * the `<img>`, are reached and scrubbed).
1591
- *
1592
- * @param root the root of a removed subtree to neutralise
1575
+ * @param root where to start
1576
+ * @param visit called for every node
1577
+ * @param intoShadow whether to enter open shadow roots
1593
1578
  */
1594
- const _neutralizeSubtree = function _neutralizeSubtree(root) {
1579
+ const _walkTree = function _walkTree(root, visit) {
1580
+ let intoShadow = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false;
1595
1581
  const stack = [root];
1596
1582
  while (stack.length > 0) {
1597
1583
  const node = stack.pop();
1598
- if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
1584
+ if (visit(node) === false) continue;
1585
+ if (intoShadow && _readNodeType(node) === NODE_TYPE.element) {
1586
+ const shadowRoot = getShadowRoot(node);
1587
+ if (_isDocumentFragment(shadowRoot)) stack.push(shadowRoot);
1588
+ }
1599
1589
  const childNodes = getChildNodes(node);
1600
1590
  if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
1601
1591
  }
1602
1592
  };
1603
1593
  /**
1604
- * _neutralizePatchLinkage
1594
+ * Strips disallowed attributes from every element of a discarded subtree,
1595
+ * so a handler that is already queued (a loading image) cannot fire.
1605
1596
  *
1606
- * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
1607
- * hardening, https://github.com/WICG/declarative-partial-updates).
1597
+ * @param root the root of a removed subtree
1598
+ */
1599
+ const _neutralizeSubtree = function _neutralizeSubtree(root) {
1600
+ _walkTree(root, (node) => {
1601
+ if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
1602
+ });
1603
+ };
1604
+ /**
1605
+ * All nodes under `root`, root first, including open shadow roots. Template
1606
+ * content is not entered, it is inert.
1608
1607
  *
1609
- * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
1610
- * markers (PIs / markup comments) node-by-node, in document order, AS it
1611
- * reaches each node. On a live in-place root that leaves a window: from the
1612
- * moment the root is connected until the walk arrives at a given node, that
1613
- * node's linkage is live. A patch applied on connection/stream can fire as
1614
- * a microtask during the walk and inject or teleport an unsanitized DOM
1615
- * range into a region the iterator has already passed and will not revisit,
1616
- * so the post-return "tree is sanitized" contract is violated. Sweep the
1617
- * whole tree once up front and sever every linkage before the walk begins,
1618
- * closing that window.
1608
+ * @param root the in-place root
1609
+ * @return the nodes
1610
+ */
1611
+ const _collectLiveNodes = function _collectLiveNodes(root) {
1612
+ const nodes = [];
1613
+ _walkTree(root, (node) => {
1614
+ nodes.push(node);
1615
+ }, true);
1616
+ return nodes;
1617
+ };
1618
+ /**
1619
+ * Strips disallowed attributes from every element that was under the
1620
+ * in-place root when the call started and no longer is, whatever removed
1621
+ * or moved it.
1619
1622
  *
1620
- * This CANNOT undo a patch that already fired before sanitize ran — that is
1621
- * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
1622
- * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
1623
- * to group with the rest of the declarative-partial-updates handling and
1624
- * stay overridable, consistent with the codebase.
1623
+ * @param root the in-place root
1624
+ * @param before the root's nodes, collected before the walk
1625
+ */
1626
+ const _neutralizeEscapedElements = function _neutralizeEscapedElements(root, before) {
1627
+ const reachable = setCreate();
1628
+ arrayForEach(_collectLiveNodes(root), (node) => {
1629
+ if (_readNodeType(node) === NODE_TYPE.element) setAdd(reachable, node);
1630
+ });
1631
+ arrayForEach(before, (node) => {
1632
+ if (_readNodeType(node) === NODE_TYPE.element && !setHas(reachable, node)) _stripDisallowedAttributes(node);
1633
+ });
1634
+ };
1635
+ /**
1636
+ * Neutralizes everything that left the in-place tree during the call: what
1637
+ * DOMPurify removed, and what hooks or custom elements removed or moved out.
1625
1638
  *
1626
- * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
1627
- * clobbered root cannot defeat the sweep of its non-clobbered descendants.
1639
+ * @param root the in-place root
1640
+ * @param before the root's nodes, collected before the walk
1641
+ * @return true if the root itself was removed
1642
+ */
1643
+ const _neutralizeDiscarded = function _neutralizeDiscarded(root, before) {
1644
+ let rootRemoved = false;
1645
+ arrayForEach(DOMPurify.removed, (entry) => {
1646
+ if (entry.element) {
1647
+ rootRemoved = rootRemoved || entry.element === root;
1648
+ _neutralizeSubtree(entry.element);
1649
+ }
1650
+ });
1651
+ _neutralizeEscapedElements(root, before);
1652
+ return rootRemoved;
1653
+ };
1654
+ const MAX_UNVISITED_ROUNDS = 8;
1655
+ /**
1656
+ * The walk only moves forward, so a node moved to a position it already
1657
+ * passed is never visited. Give every original node that is still in the
1658
+ * tree but unvisited the normal treatment, hooks included. Nodes created
1659
+ * during the call are left alone. Fails closed if the tree keeps changing.
1628
1660
  *
1629
- * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
1630
- * .html Q1): this mirrors the existing policy of keeping `for` on
1631
- * <label>/<output>. If the shipping feature can drive a patch through a
1632
- * surviving `for`-on-label/output + `id` pair, this pre-pass and the
1633
- * attribute check at _isBasicCustomElement's caller must additionally drop
1634
- * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
1661
+ * @param root the in-place root
1662
+ * @param before the root's nodes, collected before the walk
1663
+ */
1664
+ const _sanitizeUnvisitedNodes = function _sanitizeUnvisitedNodes(root, before) {
1665
+ const visited = IN_PLACE_VISITED;
1666
+ let original = null;
1667
+ const getOriginal = () => {
1668
+ if (!original) {
1669
+ original = setCreate();
1670
+ arrayForEach(before, (beforeNode) => {
1671
+ setAdd(original, beforeNode);
1672
+ });
1673
+ }
1674
+ return original;
1675
+ };
1676
+ for (let round = 0;; round++) {
1677
+ const live = _collectLiveNodes(root);
1678
+ const containers = [root];
1679
+ let pending = false;
1680
+ arrayForEach(live, (node) => {
1681
+ if (_readNodeType(node) === NODE_TYPE.documentFragment) arrayPush(containers, node);
1682
+ else if (!pending && !setHas(visited, node)) pending = setHas(getOriginal(), node);
1683
+ });
1684
+ if (!pending) return;
1685
+ if (round >= MAX_UNVISITED_ROUNDS) throw typeErrorCreate("the tree kept changing during in-place sanitization; refusing to return it");
1686
+ arrayForEach(containers, (container, index) => {
1687
+ _sanitizeUnvisitedIn(container, index > 0, root, getOriginal());
1688
+ });
1689
+ }
1690
+ };
1691
+ /**
1692
+ * One _sanitizeUnvisitedNodes pass over the root or an open shadow root.
1635
1693
  *
1636
- * @param root the in-place root to sweep
1694
+ * @param container the root or an open shadow root
1695
+ * @param inShadow whether container is a shadow root
1696
+ * @param root the in-place root
1697
+ * @param original the root's nodes collected before the walk, as a set
1637
1698
  */
1699
+ const _sanitizeUnvisitedIn = function _sanitizeUnvisitedIn(container, inShadow, root, original) {
1700
+ const iterator = _createNodeIterator(container);
1701
+ let node;
1702
+ while (node = iterator.nextNode()) {
1703
+ if (!setHas(original, node) || setHas(IN_PLACE_VISITED, node)) continue;
1704
+ if (inShadow) _executeHooks(hooks.uponSanitizeShadowNode, node, null);
1705
+ _sanitizeElements(node, root);
1706
+ _sanitizeAttributes(node, root);
1707
+ if (_isDocumentFragment(node.content)) _sanitizeShadowDOM2(node.content);
1708
+ }
1709
+ };
1638
1710
  /**
1639
- * Central policy for declarative-partial-updates patch-linkage attributes,
1640
- * shared by the _neutralizePatchLinkage pre-pass and _isValidAttribute so
1641
- * the two sites cannot drift: `patchsrc` always links, `for` links
1642
- * everywhere except on <label>/<output>, and the whole policy is gated on
1643
- * SAFE_FOR_XML (see the rationale block in _isValidAttribute).
1711
+ * Records that _sanitizeElements processed `node`. No-op outside IN_PLACE.
1712
+ *
1713
+ * @param node the node being processed
1714
+ */
1715
+ const _markVisited = function _markVisited(node) {
1716
+ if (IN_PLACE_VISITED) setAdd(IN_PLACE_VISITED, node);
1717
+ };
1718
+ /**
1719
+ * Patch-linkage attributes (declarative partial updates): `patchsrc`
1720
+ * always, `for` everywhere except on <label> and <output>. Gated on
1721
+ * SAFE_FOR_XML. Shared by the IN_PLACE pre-pass and _isValidAttribute.
1644
1722
  *
1645
1723
  * @param lcName the transformCaseFunc'd attribute name
1646
1724
  * @param lcTag the transformCaseFunc'd tag name of the carrying element
@@ -1651,17 +1729,26 @@ function createDOMPurify() {
1651
1729
  if (lcName === "patchsrc") return true;
1652
1730
  return lcName === "for" && lcTag !== "label" && lcTag !== "output";
1653
1731
  };
1732
+ /**
1733
+ * IN_PLACE pre-pass: sever all patch linkage before the walk, so no patch
1734
+ * can fire mid-walk into a part of the tree the walker already passed.
1735
+ * Removes range markers (processing instructions, comments with markup)
1736
+ * and strips linkage attributes. It cannot undo a patch that already fired.
1737
+ *
1738
+ * TODO: if the shipped feature can patch through `for` on <label>/<output>,
1739
+ * drop that pair here too (see test/declarative-patch-probe.html).
1740
+ *
1741
+ * @param root the in-place root to sweep
1742
+ */
1654
1743
  const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
1655
1744
  if (!SAFE_FOR_XML) return;
1656
- const stack = [root];
1657
- while (stack.length > 0) {
1658
- const node = stack.pop();
1745
+ _walkTree(root, (node) => {
1659
1746
  const nodeType = _readNodeType(node);
1660
1747
  if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
1661
1748
  try {
1662
1749
  remove(node);
1663
1750
  } catch (_) {}
1664
- continue;
1751
+ return false;
1665
1752
  }
1666
1753
  if (nodeType === NODE_TYPE.element) {
1667
1754
  const element = node;
@@ -1671,9 +1758,8 @@ function createDOMPurify() {
1671
1758
  if (element.hasAttribute && element.hasAttribute("for") && _isPatchLinkageAttribute("for", lcTag)) element.removeAttribute("for");
1672
1759
  } catch (_) {}
1673
1760
  }
1674
- const childNodes = getChildNodes(node);
1675
- if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
1676
- }
1761
+ return true;
1762
+ });
1677
1763
  };
1678
1764
  /**
1679
1765
  * _initDocument
@@ -1750,6 +1836,27 @@ function createDOMPurify() {
1750
1836
  */
1751
1837
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1752
1838
  var _node$querySelectorAl;
1839
+ _scrubOwnCharacterData(node);
1840
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, "template");
1841
+ if (templates) arrayForEach(templates, (tmpl) => {
1842
+ if (_isDocumentFragment(tmpl.content)) _scrubTemplateExpressions2(tmpl.content);
1843
+ });
1844
+ };
1845
+ /**
1846
+ * _scrubOwnCharacterData
1847
+ *
1848
+ * Non-recursive core of _scrubTemplateExpressions: normalize `node` and
1849
+ * strip template expressions from its own character data only. Does not
1850
+ * enter <template>.content or attached shadow roots (NodeIterator never
1851
+ * does). Used by _sanitizeShadowDOM, where every nested template content
1852
+ * and shadow root is already walked - and therefore scrubbed - by its own
1853
+ * recursive _sanitizeShadowDOM call; recursing here as well would rescrub
1854
+ * each nested fragment once per ancestor, which is quadratic in nesting
1855
+ * depth.
1856
+ *
1857
+ * @param node The root whose own character data should be scrubbed.
1858
+ */
1859
+ const _scrubOwnCharacterData = function _scrubOwnCharacterData(node) {
1753
1860
  node.normalize();
1754
1861
  const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
1755
1862
  const walker = createNodeIterator.call(doc || node, node, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
@@ -1758,10 +1865,6 @@ function createDOMPurify() {
1758
1865
  currentNode.data = _stripTemplateExpressions(currentNode.data);
1759
1866
  currentNode = walker.nextNode();
1760
1867
  }
1761
- const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, "template");
1762
- if (templates) arrayForEach(templates, (tmpl) => {
1763
- if (_isDocumentFragment(tmpl.content)) _scrubTemplateExpressions2(tmpl.content);
1764
- });
1765
1868
  };
1766
1869
  /**
1767
1870
  * _isClobbered
@@ -1837,7 +1940,7 @@ function createDOMPurify() {
1837
1940
  */
1838
1941
  const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
1839
1942
  if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) return true;
1840
- 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;
1943
+ 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;
1841
1944
  if (currentNode.nodeType === NODE_TYPE.processingInstruction) return true;
1842
1945
  if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) return true;
1843
1946
  return false;
@@ -1881,7 +1984,7 @@ function createDOMPurify() {
1881
1984
  */
1882
1985
  const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
1883
1986
  if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
1884
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1987
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName] && !FORBID_CONTENTS[stringToLowerCase(tagName)]) {
1885
1988
  const parentNode = getParentNode(currentNode);
1886
1989
  const childNodes = getChildNodes(currentNode);
1887
1990
  if (childNodes && parentNode) {
@@ -1914,35 +2017,13 @@ function createDOMPurify() {
1914
2017
  return set === defaultSet || set === setConfigSet ? clone(set) : set;
1915
2018
  };
1916
2019
  /**
1917
- * Shared guard for a node that a hook has detached from the walk tree,
1918
- * used after each element-hook site in _sanitizeElements. Detaching is a
1919
- * long-standing user pattern (issue #469; draw.io-style foreignObject
1920
- * filtering). Per the cached, unclobberable parentNode getter the node is
1921
- * genuinely out of the tree, so it can reach neither the serialized
1922
- * output nor an IN_PLACE live tree; treat it as removed and stop
1923
- * processing it. Without this guard, the unsafe-node / namespace checks
1924
- * would call _forceRemove on a parentless node and hit the REPORT-3
1925
- * fail-closed throw — which exists for nodes DOMPurify wants gone but
1926
- * *cannot* detach (clobbered / parentless roots), the opposite of a node
1927
- * that is already safely gone. The walk root is exempt: a detached
1928
- * IN_PLACE root is legitimate input and must still be fully sanitized,
1929
- * and a kill-decision on it must keep hitting the REPORT-3 throw.
1930
- *
1931
- * Nodes detached by hooks stay the hook's responsibility for placement:
1932
- * they are not recorded in DOMPurify.removed, so the post-walk IN_PLACE
1933
- * pass (which iterates DOMPurify.removed) does not reach them. But a
1934
- * hook-detached subtree can still hold a queued resource-event handler -
1935
- * e.g. an <img onload> that began loading when the caller built the live
1936
- * tree - which fires in page scope after sanitize returns even though the
1937
- * handler never reached the returned tree. That is the audit-5 F1 hazard,
1938
- * and the documented node.remove() hook pattern walks straight into it.
1939
- * So on the IN_PLACE path we neutralize the detached subtree inline,
1940
- * stripping its non-allow-listed attributes before returning, exactly as
1941
- * the post-walk pass does for _forceRemove'd subtrees.
2020
+ * A hook may have detached the node it was called for. If so, treat it as
2021
+ * removed and stop processing it; the walk root is exempt. On the IN_PLACE
2022
+ * path the detached subtree is neutralized right away.
1942
2023
  *
1943
2024
  * @param currentNode the node a hook may have detached
1944
2025
  * @param root the current walk root
1945
- * @return true if the node is detached and now handled, false otherwise
2026
+ * @return true if the node was detached
1946
2027
  */
1947
2028
  const _handleHookDetachedNode = function _handleHookDetachedNode(currentNode, root) {
1948
2029
  if (currentNode === root || getParentNode(currentNode) !== null) return false;
@@ -1959,6 +2040,7 @@ function createDOMPurify() {
1959
2040
  * @return true if node was killed, false if left alive
1960
2041
  */
1961
2042
  const _sanitizeElements = function _sanitizeElements(currentNode, root) {
2043
+ _markVisited(currentNode);
1962
2044
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1963
2045
  if (_handleHookDetachedNode(currentNode, root)) return true;
1964
2046
  if (_isClobbered(currentNode)) {
@@ -2102,6 +2184,22 @@ function createDOMPurify() {
2102
2184
  }
2103
2185
  };
2104
2186
  /**
2187
+ * _isUnsafeAttributeValue
2188
+ *
2189
+ * Value-based attribute checks that the attribute walk applies before any
2190
+ * hook can force-keep an attribute, shared with the public
2191
+ * isValidAttribute() so that it never approves a value sanitize() would
2192
+ * strip.
2193
+ *
2194
+ * @param name the attribute name as found on the node (case preserved)
2195
+ * @param value the attribute value
2196
+ * @returns true if the attribute must be removed because of its value
2197
+ */
2198
+ const _isUnsafeAttributeValue = function _isUnsafeAttributeValue(name, value) {
2199
+ if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) return true;
2200
+ return stringToLowerCase(name) === "attributename" && stringMatch(value, "href") !== null;
2201
+ };
2202
+ /**
2105
2203
  * _sanitizeAttributes
2106
2204
  *
2107
2205
  * @protect attributes
@@ -2145,11 +2243,7 @@ function createDOMPurify() {
2145
2243
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
2146
2244
  recreatedNamedProp = true;
2147
2245
  }
2148
- if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
2149
- _removeAttribute(name, currentNode, attr);
2150
- continue;
2151
- }
2152
- if (lcName === "attributename" && stringMatch(value, "href")) {
2246
+ if (_isUnsafeAttributeValue(name, value)) {
2153
2247
  _removeAttribute(name, currentNode, attr);
2154
2248
  continue;
2155
2249
  }
@@ -2197,24 +2291,13 @@ function createDOMPurify() {
2197
2291
  }
2198
2292
  }
2199
2293
  }
2294
+ if (SAFE_FOR_TEMPLATES) _scrubOwnCharacterData(fragment);
2200
2295
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
2201
2296
  };
2202
2297
  /**
2203
- * _sanitizeAttachedShadowRoots
2204
- *
2205
- * Walks `root` and feeds every attached shadow root we encounter into
2206
- * the existing _sanitizeShadowDOM pipeline. The default node iterator
2207
- * does not descend into shadow trees, so nodes inside an attached
2208
- * shadow root would otherwise be skipped entirely.
2209
- *
2210
- * Two real input paths put attached shadow roots in front of us:
2211
- * 1. IN_PLACE on a DOM node that already has shadow roots attached.
2212
- * 2. DOM-node input where importNode(dirty, true) deep-clones the
2213
- * shadow root because it was created with `clonable: true`.
2214
- *
2215
- * This pass runs once, up front, so the main iteration loop (and the
2216
- * existing _sanitizeShadowDOM template-content recursion) stay
2217
- * untouched — string-input paths are not affected.
2298
+ * Sanitizes every attached shadow root under `root`, since the node
2299
+ * iterator does not enter shadow trees. Needed for IN_PLACE input and for
2300
+ * DOM input whose clonable shadow roots were copied by importNode().
2218
2301
  *
2219
2302
  * @param root the subtree root to walk for attached shadow roots
2220
2303
  */
@@ -2279,6 +2362,8 @@ function createDOMPurify() {
2279
2362
  if (hooks.uponSanitizeAttribute.length > 0) ALLOWED_ATTR = clone(ALLOWED_ATTR);
2280
2363
  DOMPurify.removed = [];
2281
2364
  const inPlace = IN_PLACE && typeof dirty !== "string" && _isNode(dirty);
2365
+ let inPlaceNodes = [];
2366
+ const outerVisited = IN_PLACE_VISITED;
2282
2367
  if (inPlace) {
2283
2368
  _neutralizePatchLinkage(dirty);
2284
2369
  const nn = _readNodeName(dirty);
@@ -2293,12 +2378,8 @@ function createDOMPurify() {
2293
2378
  _neutralizeRoot(dirty);
2294
2379
  throw typeErrorCreate("root node is clobbered and cannot be sanitized in-place");
2295
2380
  }
2296
- try {
2297
- _sanitizeAttachedShadowRoots(dirty);
2298
- } catch (error) {
2299
- _neutralizeRoot(dirty);
2300
- throw error;
2301
- }
2381
+ inPlaceNodes = _collectLiveNodes(dirty);
2382
+ IN_PLACE_VISITED = setCreate();
2302
2383
  } else if (_isNode(dirty)) {
2303
2384
  body = _initDocument("<!---->");
2304
2385
  importedNode = body.ownerDocument.importNode(dirty, true);
@@ -2314,30 +2395,25 @@ function createDOMPurify() {
2314
2395
  if (body && FORCE_BODY) _forceRemove(body.firstChild);
2315
2396
  const walkRoot = inPlace ? dirty : body;
2316
2397
  try {
2398
+ if (inPlace) _sanitizeAttachedShadowRoots(dirty);
2317
2399
  const nodeIterator = _createNodeIterator(walkRoot);
2318
2400
  while (currentNode = nodeIterator.nextNode()) {
2319
2401
  _sanitizeElements(currentNode, walkRoot);
2320
2402
  _sanitizeAttributes(currentNode, walkRoot);
2321
2403
  if (_isDocumentFragment(currentNode.content)) _sanitizeShadowDOM2(currentNode.content);
2322
2404
  }
2405
+ if (inPlace) _sanitizeUnvisitedNodes(dirty, inPlaceNodes);
2323
2406
  } catch (error) {
2324
2407
  if (inPlace) {
2325
2408
  _neutralizeRoot(dirty);
2326
- arrayForEach(DOMPurify.removed, (entry) => {
2327
- if (entry.element) _neutralizeSubtree(entry.element);
2328
- });
2409
+ _neutralizeDiscarded(dirty, inPlaceNodes);
2329
2410
  }
2330
2411
  throw error;
2412
+ } finally {
2413
+ IN_PLACE_VISITED = outerVisited;
2331
2414
  }
2332
2415
  if (inPlace) {
2333
- let rootWasRemoved = false;
2334
- arrayForEach(DOMPurify.removed, (entry) => {
2335
- if (entry.element) {
2336
- if (entry.element === dirty) rootWasRemoved = true;
2337
- _neutralizeSubtree(entry.element);
2338
- }
2339
- });
2340
- if (rootWasRemoved) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
2416
+ if (_neutralizeDiscarded(dirty, inPlaceNodes)) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
2341
2417
  if (SAFE_FOR_TEMPLATES) _scrubTemplateExpressions2(dirty);
2342
2418
  return dirty;
2343
2419
  }
@@ -2374,6 +2450,7 @@ function createDOMPurify() {
2374
2450
  if (!CONFIG) _parseConfig({});
2375
2451
  const lcTag = transformCaseFunc(tag);
2376
2452
  const lcName = transformCaseFunc(attr);
2453
+ if (typeof value === "string" && (_isUnsafeAttributeValue(attr, value) || !ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value))) return false;
2377
2454
  return _isValidAttribute(lcTag, lcName, value);
2378
2455
  };
2379
2456
  DOMPurify.addHook = function(entryPoint, hookFunction) {