@ukic/web-components 3.28.0 → 3.30.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 (114) hide show
  1. package/components/ic-accordion.js +53 -38
  2. package/components/ic-accordion.js.map +1 -1
  3. package/components/ic-breadcrumb-group.js +16 -12
  4. package/components/ic-breadcrumb-group.js.map +1 -1
  5. package/components/ic-menu-item2.js +7 -2
  6. package/components/ic-menu-item2.js.map +1 -1
  7. package/components/ic-menu2.js +1393 -307
  8. package/components/ic-menu2.js.map +1 -1
  9. package/components/ic-navigation-button.js +8 -5
  10. package/components/ic-navigation-button.js.map +1 -1
  11. package/components/ic-pagination-bar.js.map +1 -1
  12. package/components/ic-pagination-item2.js.map +1 -1
  13. package/components/ic-pagination2.js.map +1 -1
  14. package/components/ic-popover-menu.js +17 -5
  15. package/components/ic-popover-menu.js.map +1 -1
  16. package/components/ic-radio-group.js.map +1 -1
  17. package/components/ic-skip-link.js.map +1 -1
  18. package/components/ic-status-tag.js.map +1 -1
  19. package/dist/cjs/core.cjs.js +1 -1
  20. package/dist/cjs/ic-accordion.cjs.entry.js +53 -38
  21. package/dist/cjs/ic-accordion.cjs.entry.js.map +1 -1
  22. package/dist/cjs/ic-breadcrumb-group.cjs.entry.js +16 -12
  23. package/dist/cjs/ic-breadcrumb-group.cjs.entry.js.map +1 -1
  24. package/dist/cjs/ic-button_3.cjs.entry.js.map +1 -1
  25. package/dist/cjs/ic-horizontal-scroll.cjs.entry.js.map +1 -1
  26. package/dist/cjs/ic-input-component-container_3.cjs.entry.js +1393 -307
  27. package/dist/cjs/ic-input-component-container_3.cjs.entry.js.map +1 -1
  28. package/dist/cjs/ic-menu-item.cjs.entry.js +5 -1
  29. package/dist/cjs/ic-menu-item.cjs.entry.js.map +1 -1
  30. package/dist/cjs/ic-navigation-button.cjs.entry.js +8 -5
  31. package/dist/cjs/ic-navigation-button.cjs.entry.js.map +1 -1
  32. package/dist/cjs/ic-popover-menu.cjs.entry.js +16 -5
  33. package/dist/cjs/ic-popover-menu.cjs.entry.js.map +1 -1
  34. package/dist/cjs/loader.cjs.js +1 -1
  35. package/dist/collection/components/ic-accordion/ic-accordion.js +53 -38
  36. package/dist/collection/components/ic-accordion/ic-accordion.js.map +1 -1
  37. package/dist/collection/components/ic-breadcrumb-group/ic-breadcrumb-group.js +17 -13
  38. package/dist/collection/components/ic-breadcrumb-group/ic-breadcrumb-group.js.map +1 -1
  39. package/dist/collection/components/ic-menu-item/ic-menu-item.css +9 -14
  40. package/dist/collection/components/ic-menu-item/ic-menu-item.js +24 -0
  41. package/dist/collection/components/ic-menu-item/ic-menu-item.js.map +1 -1
  42. package/dist/collection/components/ic-navigation-button/ic-navigation-button.js +8 -5
  43. package/dist/collection/components/ic-navigation-button/ic-navigation-button.js.map +1 -1
  44. package/dist/collection/components/ic-popover-menu/ic-popover-menu.js +36 -5
  45. package/dist/collection/components/ic-popover-menu/ic-popover-menu.js.map +1 -1
  46. package/dist/collection/components/ic-popover-menu/ic-popover-menu.stories.js +169 -1
  47. package/dist/components/ic-accordion.js +53 -38
  48. package/dist/components/ic-accordion.js.map +1 -1
  49. package/dist/components/ic-back-to-top.js.map +1 -1
  50. package/dist/components/ic-breadcrumb-group.js +16 -12
  51. package/dist/components/ic-breadcrumb-group.js.map +1 -1
  52. package/dist/components/ic-menu-item2.js +7 -2
  53. package/dist/components/ic-menu-item2.js.map +1 -1
  54. package/dist/components/ic-menu2.js +1393 -307
  55. package/dist/components/ic-menu2.js.map +1 -1
  56. package/dist/components/ic-navigation-button.js +8 -5
  57. package/dist/components/ic-navigation-button.js.map +1 -1
  58. package/dist/components/ic-popover-menu.js +17 -5
  59. package/dist/components/ic-popover-menu.js.map +1 -1
  60. package/dist/components/ic-radio-group.js.map +1 -1
  61. package/dist/components/ic-radio-option.js.map +1 -1
  62. package/dist/components/ic-side-navigation.js.map +1 -1
  63. package/dist/components/ic-skip-link.js.map +1 -1
  64. package/dist/components/ic-status-tag.js.map +1 -1
  65. package/dist/core/core.esm.js +1 -1
  66. package/dist/core/core.esm.js.map +1 -1
  67. package/dist/core/{p-9ced00a7.entry.js → p-41bbdc4c.entry.js} +2 -2
  68. package/dist/core/{p-9ced00a7.entry.js.map → p-41bbdc4c.entry.js.map} +1 -1
  69. package/dist/core/p-8fc5e9f9.entry.js.map +1 -1
  70. package/dist/core/p-98dee727.entry.js.map +1 -1
  71. package/dist/core/{p-fc4b7d2b.entry.js → p-9cf35c23.entry.js} +2 -2
  72. package/dist/core/p-9cf35c23.entry.js.map +1 -0
  73. package/dist/core/p-be8bbed8.entry.js +2 -0
  74. package/dist/core/p-be8bbed8.entry.js.map +1 -0
  75. package/dist/core/p-ccba84b7.entry.js +3 -0
  76. package/dist/core/p-ccba84b7.entry.js.map +1 -0
  77. package/dist/core/p-ef289935.entry.js +2 -0
  78. package/dist/core/p-ef289935.entry.js.map +1 -0
  79. package/dist/core/p-f8aa89ff.entry.js +2 -0
  80. package/dist/core/p-f8aa89ff.entry.js.map +1 -0
  81. package/dist/esm/core.js +1 -1
  82. package/dist/esm/ic-accordion.entry.js +53 -38
  83. package/dist/esm/ic-accordion.entry.js.map +1 -1
  84. package/dist/esm/ic-breadcrumb-group.entry.js +16 -12
  85. package/dist/esm/ic-breadcrumb-group.entry.js.map +1 -1
  86. package/dist/esm/ic-button_3.entry.js.map +1 -1
  87. package/dist/esm/ic-horizontal-scroll.entry.js.map +1 -1
  88. package/dist/esm/ic-input-component-container_3.entry.js +1393 -307
  89. package/dist/esm/ic-input-component-container_3.entry.js.map +1 -1
  90. package/dist/esm/ic-menu-item.entry.js +5 -1
  91. package/dist/esm/ic-menu-item.entry.js.map +1 -1
  92. package/dist/esm/ic-navigation-button.entry.js +8 -5
  93. package/dist/esm/ic-navigation-button.entry.js.map +1 -1
  94. package/dist/esm/ic-popover-menu.entry.js +16 -5
  95. package/dist/esm/ic-popover-menu.entry.js.map +1 -1
  96. package/dist/esm/loader.js +1 -1
  97. package/dist/types/components/ic-accordion/ic-accordion.d.ts +2 -0
  98. package/dist/types/components/ic-breadcrumb-group/ic-breadcrumb-group.d.ts +6 -1
  99. package/dist/types/components/ic-menu-item/ic-menu-item.d.ts +4 -0
  100. package/dist/types/components/ic-popover-menu/ic-popover-menu.d.ts +4 -0
  101. package/dist/types/components.d.ts +18 -2
  102. package/hydrate/index.js +1494 -369
  103. package/hydrate/index.mjs +1494 -369
  104. package/package.json +10 -6
  105. package/vscode-data.json +20 -1
  106. package/dist/core/p-2b1ce5b7.entry.js +0 -2
  107. package/dist/core/p-2b1ce5b7.entry.js.map +0 -1
  108. package/dist/core/p-56ab6fb7.entry.js +0 -2
  109. package/dist/core/p-56ab6fb7.entry.js.map +0 -1
  110. package/dist/core/p-d5fcd4f9.entry.js +0 -2
  111. package/dist/core/p-d5fcd4f9.entry.js.map +0 -1
  112. package/dist/core/p-fc4b7d2b.entry.js.map +0 -1
  113. package/dist/core/p-fdc4376e.entry.js +0 -3
  114. package/dist/core/p-fdc4376e.entry.js.map +0 -1
package/hydrate/index.js CHANGED
@@ -3866,16 +3866,9 @@ class Accordion {
3866
3866
  };
3867
3867
  // Set accordion animation
3868
3868
  this.setAccordionAnimation = (el, duration, property, delay) => {
3869
- if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
3870
- el.style.transitionDuration = `${duration}ms`;
3871
- el.style.transitionProperty = property;
3872
- el.style.transitionDelay = delay;
3873
- }
3874
- else {
3875
- el.style.transitionDuration = "0ms";
3876
- el.style.transitionProperty = "none";
3877
- el.style.transitionDelay = "0ms";
3878
- }
3869
+ el.style.transitionDuration = `${duration}ms`;
3870
+ el.style.transitionProperty = property;
3871
+ el.style.transitionTimingFunction = delay;
3879
3872
  };
3880
3873
  this.setExpandedContentStyle = (ev, expandedContent) => {
3881
3874
  if (ev.propertyName === "height" && expandedContent.clientHeight > 0) {
@@ -3889,30 +3882,52 @@ class Accordion {
3889
3882
  }
3890
3883
  };
3891
3884
  this.animateExpandedContent = () => {
3892
- if (this.expandedContentEl) {
3893
- const expandedContentEl = this.expandedContentEl;
3894
- const elementHeight = expandedContentEl.scrollHeight;
3895
- if (elementHeight > 0 && this.expanded) {
3896
- expandedContentEl.style.setProperty(this.CONTENT_VISIBILITY_PROPERTY, "visible");
3897
- expandedContentEl.style.height = `${elementHeight}px`;
3898
- this.setAccordionAnimation(expandedContentEl, "300", "height", "ease-out");
3899
- expandedContentEl.addEventListener("transitionend", (e) => {
3900
- this.setExpandedContentStyle(e, expandedContentEl);
3901
- });
3902
- }
3903
- else if (!this.expanded) {
3904
- const expandedContentEl = this.expandedContentEl;
3905
- expandedContentEl.style.height = `${expandedContentEl.scrollHeight}px`;
3906
- if (expandedContentEl.scrollHeight > 0 && !this.expanded) {
3907
- expandedContentEl.style.height = "0";
3908
- this.setAccordionAnimation(expandedContentEl, "300", "height", "ease-in");
3909
- expandedContentEl.classList.remove(EXPANDED_CONTENT_OPENED_CLASS);
3910
- }
3911
- expandedContentEl.addEventListener("transitionend", (e) => {
3912
- this.hideExpandedContent(e, expandedContentEl);
3913
- });
3914
- }
3885
+ const expandedContentEl = this.expandedContentEl;
3886
+ if (!expandedContentEl)
3887
+ return;
3888
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3889
+ if (this.expanded) {
3890
+ this.expandContent(expandedContentEl, prefersReducedMotion);
3891
+ return;
3915
3892
  }
3893
+ this.collapseContent(expandedContentEl, prefersReducedMotion);
3894
+ };
3895
+ this.expandContent = (expandedContentEl, prefersReducedMotion) => {
3896
+ const elementHeight = expandedContentEl.scrollHeight;
3897
+ if (elementHeight <= 0)
3898
+ return;
3899
+ expandedContentEl.style.setProperty(this.CONTENT_VISIBILITY_PROPERTY, "visible");
3900
+ if (prefersReducedMotion) {
3901
+ expandedContentEl.classList.add(EXPANDED_CONTENT_OPENED_CLASS);
3902
+ expandedContentEl.style.height = "auto";
3903
+ return;
3904
+ }
3905
+ expandedContentEl.style.height = `${elementHeight}px`;
3906
+ this.setAccordionAnimation(expandedContentEl, "300", "height", "ease-out");
3907
+ expandedContentEl.addEventListener("transitionend", (e) => {
3908
+ this.setExpandedContentStyle(e, expandedContentEl);
3909
+ }, { once: true });
3910
+ };
3911
+ this.collapseContent = (expandedContentEl, prefersReducedMotion) => {
3912
+ const elementHeight = expandedContentEl.scrollHeight;
3913
+ if (elementHeight <= 0)
3914
+ return;
3915
+ // Set the content panel as pixels from "auto" so it can be animated
3916
+ expandedContentEl.style.height = `${elementHeight}px`;
3917
+ if (prefersReducedMotion) {
3918
+ expandedContentEl.style.height = "0";
3919
+ expandedContentEl.classList.remove(EXPANDED_CONTENT_OPENED_CLASS);
3920
+ expandedContentEl.style.setProperty(this.CONTENT_VISIBILITY_PROPERTY, "hidden");
3921
+ return;
3922
+ }
3923
+ requestAnimationFrame(() => {
3924
+ expandedContentEl.style.height = "0";
3925
+ });
3926
+ this.setAccordionAnimation(expandedContentEl, "300", "height", "ease-in");
3927
+ expandedContentEl.classList.remove(EXPANDED_CONTENT_OPENED_CLASS);
3928
+ expandedContentEl.addEventListener("transitionend", (e) => {
3929
+ this.hideExpandedContent(e, expandedContentEl);
3930
+ }, { once: true });
3916
3931
  };
3917
3932
  }
3918
3933
  watchDisabledHandler() {
@@ -3949,19 +3964,19 @@ class Accordion {
3949
3964
  }
3950
3965
  render() {
3951
3966
  const { size, disabled, expanded, theme } = this;
3952
- return (hAsync(Host, { key: '58ea81b0011f31da3d2d872244720011cb9028c8', id: this.accordionId, class: {
3967
+ return (hAsync(Host, { key: '840baa7799dc4c48c232a7dc170937bb563900d5', id: this.accordionId, class: {
3953
3968
  ["ic-accordion-disabled"]: !!disabled,
3954
3969
  [`ic-theme-${theme}`]: theme !== "inherit",
3955
- }, "aria-disabled": disabled ? "true" : "false" }, hAsync("button", { key: '1a79e3d09955ed9bb07ddac08690ff157d98313d', ref: (el) => (this.accordionBtnHeading = el), id: `${this.accordionId}-button`, disabled: disabled, tabindex: disabled ? -1 : 0, class: {
3970
+ }, "aria-disabled": disabled ? "true" : "false" }, hAsync("button", { key: 'dce03cb5ba0f61a1d5b75ab1f648eaa970d5d78e', ref: (el) => (this.accordionBtnHeading = el), id: `${this.accordionId}-button`, disabled: disabled, tabindex: disabled ? -1 : 0, class: {
3956
3971
  [`${size}`]: true,
3957
3972
  ["section-button"]: true,
3958
3973
  ["section-button-open"]: !!expanded && !disabled,
3959
- }, "aria-expanded": `${expanded}`, "aria-controls": "expanded-content-area", onClick: this.toggleExpanded }, isSlotUsed(this.el, "icon") && (hAsync("div", { key: 'fc0e123d20dca395b2210f1ee14c7d89bb8e50ad', class: "icon-container" }, hAsync("slot", { key: 'c2185289998b60967ec0a0097a3aad8b4a88f07c', name: "icon" }))), hAsync("ic-typography", { key: '211d4d69f0468dbec907c6d32ec61adea8e6be61', variant: "subtitle-large", class: "section-header" }, isSlotUsed(this.el, "heading") ? (hAsync("slot", { name: "heading" })) : (this.heading)), hAsync("span", { key: 'e9f9ef3f67e85d33d0459c55d7307dcf522f0628', class: {
3974
+ }, "aria-expanded": `${expanded}`, "aria-controls": "expanded-content-area", onClick: this.toggleExpanded }, isSlotUsed(this.el, "icon") && (hAsync("div", { key: '3301b5e4526831e7e8a4400d775f55d99ec29d27', class: "icon-container" }, hAsync("slot", { key: 'aa49cdcd343009d6d5b7e68f5c1013cd7fcc7638', name: "icon" }))), hAsync("ic-typography", { key: 'da324db78e29a81f0c5bb7209f870b8832c25d67', variant: "subtitle-large", class: "section-header" }, isSlotUsed(this.el, "heading") ? (hAsync("slot", { name: "heading" })) : (this.heading)), hAsync("span", { key: '9eb1350c8ae3f9c940e1299a17b3b510ccb66ef6', class: {
3960
3975
  ["expand-chevron"]: true,
3961
3976
  ["content-expanded-chevron"]: !!expanded && !disabled,
3962
- }, "aria-hidden": "true", innerHTML: chevronIcon })), hAsync("div", { key: '1e74fb0c312fc767a7518cbfcce5f55a90fc22a9', class: {
3977
+ }, "aria-hidden": "true", innerHTML: chevronIcon })), hAsync("div", { key: '9b04e4a6df4def67189609215cd4849890900146', class: {
3963
3978
  ["expanded-content"]: true,
3964
- }, "aria-labelledby": `${this.accordionId}-button`, role: "region", "aria-hidden": `${!expanded}`, id: "expanded-content-area", ref: (el) => (this.expandedContentEl = el) }, hAsync("div", { key: '04ed5966177cce63233f64e4e2d01bcbecbe10af', class: "expanded-content-inner" }, this.message ? (hAsync("ic-typography", { variant: "body" }, this.message)) : (hAsync("slot", null))))));
3979
+ }, "aria-labelledby": `${this.accordionId}-button`, role: "region", "aria-hidden": `${!expanded}`, id: "expanded-content-area", ref: (el) => (this.expandedContentEl = el) }, hAsync("div", { key: '594f88da687634cbbad7e12d8df21a3dc7961a64', class: "expanded-content-inner" }, this.message ? (hAsync("ic-typography", { variant: "body" }, this.message)) : (hAsync("slot", null))))));
3965
3980
  }
3966
3981
  get el() { return getElement(this); }
3967
3982
  static get watchers() { return {
@@ -4958,6 +4973,8 @@ class BreadcrumbGroup {
4958
4973
  this.deviceSize = DEVICE_SIZES.XL;
4959
4974
  /**
4960
4975
  * If `true`, display only a single breadcrumb for the parent page with a back icon.
4976
+ * Requires at least two `ic-breadcrumb` children, as the displayed breadcrumb
4977
+ * shows the title of the parent (second-to-last) page.
4961
4978
  */
4962
4979
  this.backBreadcrumbOnly = false;
4963
4980
  /**
@@ -4990,12 +5007,10 @@ class BreadcrumbGroup {
4990
5007
  this.revertLastParentCollapsedBreadcrumb();
4991
5008
  }
4992
5009
  };
4993
- this.setBackBreadcrumbAttr = () => {
4994
- if (this.lastParentBreadcrumb) {
4995
- this.lastParentBreadcrumb.classList.add("show");
4996
- this.lastParentBreadcrumb.setAttribute(this.SHOW_BACK_ICON, "true");
4997
- }
4998
- };
5010
+ /**
5011
+ * Returns the second-to-last breadcrumb (the "parent" page), or `null`
5012
+ * if fewer than two breadcrumbs are present.
5013
+ */
4999
5014
  this.getLastParentBreadcrumb = () => {
5000
5015
  const allBreadcrumbs = this.el.querySelectorAll(this.IC_BREADCRUMB);
5001
5016
  return allBreadcrumbs.length > 1
@@ -5071,11 +5086,15 @@ class BreadcrumbGroup {
5071
5086
  };
5072
5087
  this.setLastParentCollapsedBackBreadcrumb = () => {
5073
5088
  this.lastParentBreadcrumb = this.getLastParentBreadcrumb();
5074
- this.setBackBreadcrumbAttr();
5075
- if (this.lastParentBreadcrumb) {
5076
- this.lastParentBreadcrumb.classList.remove("hide");
5077
- this.lastParentBreadcrumb.classList.add("show");
5089
+ if (this.lastParentBreadcrumb === null) {
5090
+ console.warn("ic-breadcrumb-group: backBreadcrumbOnly requires at least two " +
5091
+ "ic-breadcrumb children. The back breadcrumb displays the title " +
5092
+ "of the parent page, so a minimum of two breadcrumbs is needed.");
5093
+ return;
5078
5094
  }
5095
+ this.lastParentBreadcrumb.classList.add("show");
5096
+ this.lastParentBreadcrumb.classList.remove("hide");
5097
+ this.lastParentBreadcrumb.setAttribute(this.SHOW_BACK_ICON, "true");
5079
5098
  };
5080
5099
  this.revertLastParentCollapsedBreadcrumb = () => {
5081
5100
  if (this.lastParentBreadcrumb) {
@@ -5158,11 +5177,11 @@ class BreadcrumbGroup {
5158
5177
  }
5159
5178
  render() {
5160
5179
  const { backBreadcrumbOnly, collapsed, theme } = this;
5161
- return (hAsync(Host, { key: '6bee18e1a84707b5d493149d23094af6a0fb1be8', class: {
5180
+ return (hAsync(Host, { key: '6f0f099c548cb049c02a6338d7cdec696e75a82c', class: {
5162
5181
  "ic-breadcrumb-group-back": backBreadcrumbOnly,
5163
5182
  "ic-breadcrumb-group-collapsed": collapsed,
5164
5183
  [`ic-theme-${theme}`]: theme !== "inherit",
5165
- } }, hAsync("nav", { key: '144ad11cead96e18ae62eba8fc2dead82e188854', "aria-label": "breadcrumbs" }, hAsync("ol", { key: 'c101766f5e9b0e5d9a5aaebd5d0d8eda63ffc9ea' }, hAsync("slot", { key: 'c761cdbb01472d0d8c2550254063b59fa714f722' })))));
5184
+ } }, hAsync("nav", { key: '86b02604932d3d54d48383f85482f2241102f8c7', "aria-label": "breadcrumbs" }, hAsync("ol", { key: 'ffe25df4eee620a28a3e427e1d65c8cdbe0e2f84' }, hAsync("slot", { key: '818515360bb8783b6151a1651eb6319f4a533242' })))));
5166
5185
  }
5167
5186
  get el() { return getElement(this); }
5168
5187
  static get watchers() { return {
@@ -10628,28 +10647,69 @@ var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof win
10628
10647
 
10629
10648
  var purify = {exports: {}};
10630
10649
 
10631
- /*! @license DOMPurify 3.4.0 | (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.0/LICENSE */
10650
+ /*! @license DOMPurify 3.4.13 | (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.13/LICENSE */
10632
10651
 
10633
10652
  (function (module, exports) {
10634
10653
  (function (global, factory) {
10635
10654
  module.exports = factory() ;
10636
10655
  })(commonjsGlobal, (function () {
10637
- const {
10638
- entries,
10639
- setPrototypeOf,
10640
- isFrozen,
10641
- getPrototypeOf,
10642
- getOwnPropertyDescriptor
10643
- } = Object;
10644
- let {
10645
- freeze,
10646
- seal,
10647
- create
10648
- } = Object; // eslint-disable-line import/no-mutable-exports
10649
- let {
10650
- apply,
10651
- construct
10652
- } = typeof Reflect !== 'undefined' && Reflect;
10656
+ function _arrayLikeToArray(r, a) {
10657
+ (null == a || a > r.length) && (a = r.length);
10658
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
10659
+ return n;
10660
+ }
10661
+ function _arrayWithHoles(r) {
10662
+ if (Array.isArray(r)) return r;
10663
+ }
10664
+ function _iterableToArrayLimit(r, l) {
10665
+ var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
10666
+ if (null != t) {
10667
+ var e,
10668
+ n,
10669
+ i,
10670
+ u,
10671
+ a = [],
10672
+ f = true,
10673
+ o = false;
10674
+ try {
10675
+ if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
10676
+ } catch (r) {
10677
+ o = true, n = r;
10678
+ } finally {
10679
+ try {
10680
+ if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
10681
+ } finally {
10682
+ if (o) throw n;
10683
+ }
10684
+ }
10685
+ return a;
10686
+ }
10687
+ }
10688
+ function _nonIterableRest() {
10689
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
10690
+ }
10691
+ function _slicedToArray(r, e) {
10692
+ return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
10693
+ }
10694
+ function _unsupportedIterableToArray(r, a) {
10695
+ if (r) {
10696
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
10697
+ var t = {}.toString.call(r).slice(8, -1);
10698
+ return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
10699
+ }
10700
+ }
10701
+
10702
+ const entries = Object.entries,
10703
+ setPrototypeOf = Object.setPrototypeOf,
10704
+ isFrozen = Object.isFrozen,
10705
+ getPrototypeOf = Object.getPrototypeOf,
10706
+ getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
10707
+ let freeze = Object.freeze,
10708
+ seal = Object.seal,
10709
+ create = Object.create; // eslint-disable-line import/no-mutable-exports
10710
+ let _ref = typeof Reflect !== 'undefined' && Reflect,
10711
+ apply = _ref.apply,
10712
+ construct = _ref.construct;
10653
10713
  if (!freeze) {
10654
10714
  freeze = function freeze(x) {
10655
10715
  return x;
@@ -10681,13 +10741,19 @@ var purify = {exports: {}};
10681
10741
  const arrayPop = unapply(Array.prototype.pop);
10682
10742
  const arrayPush = unapply(Array.prototype.push);
10683
10743
  const arraySplice = unapply(Array.prototype.splice);
10744
+ const arrayIsArray = Array.isArray;
10684
10745
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
10685
10746
  const stringToString = unapply(String.prototype.toString);
10686
10747
  const stringMatch = unapply(String.prototype.match);
10687
10748
  const stringReplace = unapply(String.prototype.replace);
10688
10749
  const stringIndexOf = unapply(String.prototype.indexOf);
10689
10750
  const stringTrim = unapply(String.prototype.trim);
10751
+ const numberToString = unapply(Number.prototype.toString);
10752
+ const booleanToString = unapply(Boolean.prototype.toString);
10753
+ const bigintToString = typeof BigInt === 'undefined' ? null : unapply(BigInt.prototype.toString);
10754
+ const symbolToString = typeof Symbol === 'undefined' ? null : unapply(Symbol.prototype.toString);
10690
10755
  const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
10756
+ const objectToString = unapply(Object.prototype.toString);
10691
10757
  const regExpTest = unapply(RegExp.prototype.test);
10692
10758
  const typeErrorCreate = unconstruct(TypeError);
10693
10759
  /**
@@ -10737,6 +10803,9 @@ var purify = {exports: {}};
10737
10803
  // Prevent prototype setters from intercepting set as a this value.
10738
10804
  setPrototypeOf(set, null);
10739
10805
  }
10806
+ if (!arrayIsArray(array)) {
10807
+ return set;
10808
+ }
10740
10809
  let l = array.length;
10741
10810
  while (l--) {
10742
10811
  let element = array[l];
@@ -10777,10 +10846,13 @@ var purify = {exports: {}};
10777
10846
  */
10778
10847
  function clone(object) {
10779
10848
  const newObject = create(null);
10780
- for (const [property, value] of entries(object)) {
10849
+ for (const _ref2 of entries(object)) {
10850
+ var _ref3 = _slicedToArray(_ref2, 2);
10851
+ const property = _ref3[0];
10852
+ const value = _ref3[1];
10781
10853
  const isPropertyExist = objectHasOwnProperty(object, property);
10782
10854
  if (isPropertyExist) {
10783
- if (Array.isArray(value)) {
10855
+ if (arrayIsArray(value)) {
10784
10856
  newObject[property] = cleanArray(value);
10785
10857
  } else if (value && typeof value === 'object' && value.constructor === Object) {
10786
10858
  newObject[property] = clone(value);
@@ -10791,6 +10863,58 @@ var purify = {exports: {}};
10791
10863
  }
10792
10864
  return newObject;
10793
10865
  }
10866
+ /**
10867
+ * Convert non-node values into strings without depending on direct property access.
10868
+ *
10869
+ * @param value - The value to stringify.
10870
+ * @returns A string representation of the provided value.
10871
+ */
10872
+ function stringifyValue(value) {
10873
+ switch (typeof value) {
10874
+ case 'string':
10875
+ {
10876
+ return value;
10877
+ }
10878
+ case 'number':
10879
+ {
10880
+ return numberToString(value);
10881
+ }
10882
+ case 'boolean':
10883
+ {
10884
+ return booleanToString(value);
10885
+ }
10886
+ case 'bigint':
10887
+ {
10888
+ return bigintToString ? bigintToString(value) : '0';
10889
+ }
10890
+ case 'symbol':
10891
+ {
10892
+ return symbolToString ? symbolToString(value) : 'Symbol()';
10893
+ }
10894
+ case 'undefined':
10895
+ {
10896
+ return objectToString(value);
10897
+ }
10898
+ case 'function':
10899
+ case 'object':
10900
+ {
10901
+ if (value === null) {
10902
+ return objectToString(value);
10903
+ }
10904
+ const valueAsRecord = value;
10905
+ const valueToString = lookupGetter(valueAsRecord, 'toString');
10906
+ if (typeof valueToString === 'function') {
10907
+ const stringified = valueToString(valueAsRecord);
10908
+ return typeof stringified === 'string' ? stringified : objectToString(stringified);
10909
+ }
10910
+ return objectToString(value);
10911
+ }
10912
+ default:
10913
+ {
10914
+ return objectToString(value);
10915
+ }
10916
+ }
10917
+ }
10794
10918
  /**
10795
10919
  * This method automatically checks if the prop is function or getter and behaves accordingly.
10796
10920
  *
@@ -10816,6 +10940,14 @@ var purify = {exports: {}};
10816
10940
  }
10817
10941
  return fallbackValue;
10818
10942
  }
10943
+ function isRegex(value) {
10944
+ try {
10945
+ regExpTest(value, '');
10946
+ return true;
10947
+ } catch (_unused) {
10948
+ return false;
10949
+ }
10950
+ }
10819
10951
 
10820
10952
  const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
10821
10953
  const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'enterkeyhint', 'exportparts', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'inputmode', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'part', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);
@@ -10831,15 +10963,14 @@ var purify = {exports: {}};
10831
10963
  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
10832
10964
  const text = freeze(['#text']);
10833
10965
 
10834
- const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);
10835
- const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
10966
+ const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'command', 'commandfor', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns']);
10967
+ const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dominant-baseline', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-orientation', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
10836
10968
  const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lquote', 'lspace', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
10837
10969
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
10838
10970
 
10839
- // eslint-disable-next-line unicorn/better-regex
10840
- const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
10841
- const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
10842
- const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
10971
+ const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g);
10972
+ const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g);
10973
+ const TMPLIT_EXPR = seal(/\${[\w\W]*/g);
10843
10974
  const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
10844
10975
  const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
10845
10976
  const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
@@ -10849,30 +10980,31 @@ var purify = {exports: {}};
10849
10980
  );
10850
10981
  const DOCTYPE_NAME = seal(/^html$/i);
10851
10982
  const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
10983
+ // Markup-significant character probes used by _sanitizeElements.
10984
+ // Shared module-level instances are safe despite the sticky /g flags:
10985
+ // unapply() resets lastIndex for RegExp receivers before every call.
10986
+ const ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g);
10987
+ const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g);
10988
+ const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i);
10989
+ const SELF_CLOSING_TAG = seal(/\/>/i);
10852
10990
 
10853
- var EXPRESSIONS = /*#__PURE__*/Object.freeze({
10854
- __proto__: null,
10855
- ARIA_ATTR: ARIA_ATTR,
10856
- ATTR_WHITESPACE: ATTR_WHITESPACE,
10857
- CUSTOM_ELEMENT: CUSTOM_ELEMENT,
10858
- DATA_ATTR: DATA_ATTR,
10859
- DOCTYPE_NAME: DOCTYPE_NAME,
10860
- ERB_EXPR: ERB_EXPR,
10861
- IS_ALLOWED_URI: IS_ALLOWED_URI,
10862
- IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
10863
- MUSTACHE_EXPR: MUSTACHE_EXPR,
10864
- TMPLIT_EXPR: TMPLIT_EXPR
10865
- });
10866
-
10867
- /* eslint-disable @typescript-eslint/indent */
10868
10991
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
10869
10992
  const NODE_TYPE = {
10870
10993
  element: 1,
10994
+ attribute: 2,
10871
10995
  text: 3,
10996
+ cdataSection: 4,
10997
+ entityReference: 5,
10998
+ // Deprecated
10999
+ entityNode: 6,
10872
11000
  // Deprecated
10873
- progressingInstruction: 7,
11001
+ processingInstruction: 7,
10874
11002
  comment: 8,
10875
- document: 9};
11003
+ document: 9,
11004
+ documentType: 10,
11005
+ documentFragment: 11,
11006
+ notation: 12 // Deprecated
11007
+ };
10876
11008
  const getGlobal = function getGlobal() {
10877
11009
  return typeof window === 'undefined' ? null : window;
10878
11010
  };
@@ -10927,10 +11059,25 @@ var purify = {exports: {}};
10927
11059
  uponSanitizeShadowNode: []
10928
11060
  };
10929
11061
  };
11062
+ /**
11063
+ * Resolve a set-valued configuration option: a fresh set built from
11064
+ * cfg[key] when it is an own array property (seeded with a clone of
11065
+ * options.base when given, case-normalized via options.transform),
11066
+ * the fallback set otherwise.
11067
+ *
11068
+ * @param cfg the cloned, prototype-free configuration object
11069
+ * @param key the configuration property to read
11070
+ * @param fallback the set to use when the option is absent or not an array
11071
+ * @param options transform and optional base set to merge into
11072
+ * @returns the resolved set
11073
+ */
11074
+ const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
11075
+ return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
11076
+ };
10930
11077
  function createDOMPurify() {
10931
11078
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
10932
11079
  const DOMPurify = root => createDOMPurify(root);
10933
- DOMPurify.version = '3.4.0';
11080
+ DOMPurify.version = '3.4.13';
10934
11081
  DOMPurify.removed = [];
10935
11082
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
10936
11083
  // Not running in a browser, provide a factory function
@@ -10938,28 +11085,26 @@ var purify = {exports: {}};
10938
11085
  DOMPurify.isSupported = false;
10939
11086
  return DOMPurify;
10940
11087
  }
10941
- let {
10942
- document
10943
- } = window;
11088
+ let document = window.document;
10944
11089
  const originalDocument = document;
10945
11090
  const currentScript = originalDocument.currentScript;
10946
- const {
10947
- DocumentFragment,
10948
- HTMLTemplateElement,
10949
- Node,
10950
- Element,
10951
- NodeFilter,
10952
- NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
10953
- HTMLFormElement,
10954
- DOMParser,
10955
- trustedTypes
10956
- } = window;
11091
+ const HTMLTemplateElement = window.HTMLTemplateElement,
11092
+ Node = window.Node,
11093
+ Element = window.Element,
11094
+ NodeFilter = window.NodeFilter;
11095
+ const DOMParser = window.DOMParser,
11096
+ trustedTypes = window.trustedTypes;
10957
11097
  const ElementPrototype = Element.prototype;
10958
11098
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
10959
11099
  const remove = lookupGetter(ElementPrototype, 'remove');
10960
11100
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
10961
11101
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
10962
11102
  const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
11103
+ const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot');
11104
+ const getAttributes = lookupGetter(ElementPrototype, 'attributes');
11105
+ const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
11106
+ const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
11107
+ const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, 'ownerDocument') : null;
10963
11108
  // As per issue #47, the web-components registry is inherited by a
10964
11109
  // new document created via createHTMLDocument. As per the spec
10965
11110
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -10974,33 +11119,74 @@ var purify = {exports: {}};
10974
11119
  }
10975
11120
  let trustedTypesPolicy;
10976
11121
  let emptyHTML = '';
10977
- const {
10978
- implementation,
10979
- createNodeIterator,
10980
- createDocumentFragment,
10981
- getElementsByTagName
10982
- } = document;
10983
- const {
10984
- importNode
10985
- } = originalDocument;
11122
+ // The instance's own internal Trusted Types policy. Unlike a caller-supplied
11123
+ // `TRUSTED_TYPES_POLICY`, this is created at most once — Trusted Types throws
11124
+ // on duplicate policy names — and is the only policy allowed to persist
11125
+ // across configurations and survive `clearConfig()`.
11126
+ let defaultTrustedTypesPolicy;
11127
+ let defaultTrustedTypesPolicyResolved = false;
11128
+ // Tracks whether we are already inside a call to the configured Trusted Types
11129
+ // policy (`createHTML` or `createScriptURL`). If a supplied policy callback
11130
+ // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would
11131
+ // re-enter the policy and recurse until the stack overflows. We detect that
11132
+ // re-entry and throw a clear, actionable error instead. The guard is shared
11133
+ // across both callbacks, because either one re-entering `sanitize` triggers
11134
+ // the same unbounded recursion.
11135
+ let IN_TRUSTED_TYPES_POLICY = 0;
11136
+ const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy() {
11137
+ if (IN_TRUSTED_TYPES_POLICY > 0) {
11138
+ throw typeErrorCreate('A configured TRUSTED_TYPES_POLICY callback (createHTML or ' + 'createScriptURL) must not call DOMPurify.sanitize, as that causes ' + 'infinite recursion. Do not pass a policy whose callbacks wrap ' + 'DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted ' + 'Types" section of the README.');
11139
+ }
11140
+ };
11141
+ const _createTrustedHTML = function _createTrustedHTML(html) {
11142
+ _assertNotInTrustedTypesPolicy();
11143
+ IN_TRUSTED_TYPES_POLICY++;
11144
+ try {
11145
+ return trustedTypesPolicy.createHTML(html);
11146
+ } finally {
11147
+ IN_TRUSTED_TYPES_POLICY--;
11148
+ }
11149
+ };
11150
+ const _createTrustedScriptURL = function _createTrustedScriptURL(scriptUrl) {
11151
+ _assertNotInTrustedTypesPolicy();
11152
+ IN_TRUSTED_TYPES_POLICY++;
11153
+ try {
11154
+ return trustedTypesPolicy.createScriptURL(scriptUrl);
11155
+ } finally {
11156
+ IN_TRUSTED_TYPES_POLICY--;
11157
+ }
11158
+ };
11159
+ // Lazily resolve (and cache) the instance's internal default policy.
11160
+ // Resolution is attempted at most once: a successful `createPolicy` cannot be
11161
+ // repeated (Trusted Types throws on duplicate names), and a failed or
11162
+ // unsupported attempt must not be retried on every parse.
11163
+ const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy() {
11164
+ if (!defaultTrustedTypesPolicyResolved) {
11165
+ defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
11166
+ defaultTrustedTypesPolicyResolved = true;
11167
+ }
11168
+ return defaultTrustedTypesPolicy;
11169
+ };
11170
+ const _document = document,
11171
+ implementation = _document.implementation,
11172
+ createNodeIterator = _document.createNodeIterator,
11173
+ createDocumentFragment = _document.createDocumentFragment,
11174
+ getElementsByTagName = _document.getElementsByTagName;
11175
+ const importNode = originalDocument.importNode;
10986
11176
  let hooks = _createHooksMap();
10987
11177
  /**
10988
11178
  * Expose whether this browser supports running the full DOMPurify.
10989
11179
  */
10990
11180
  DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
10991
- const {
10992
- MUSTACHE_EXPR,
10993
- ERB_EXPR,
10994
- TMPLIT_EXPR,
10995
- DATA_ATTR,
10996
- ARIA_ATTR,
10997
- IS_SCRIPT_OR_DATA,
10998
- ATTR_WHITESPACE,
10999
- CUSTOM_ELEMENT
11000
- } = EXPRESSIONS;
11001
- let {
11002
- IS_ALLOWED_URI: IS_ALLOWED_URI$1
11003
- } = EXPRESSIONS;
11181
+ const MUSTACHE_EXPR$1 = MUSTACHE_EXPR,
11182
+ ERB_EXPR$1 = ERB_EXPR,
11183
+ TMPLIT_EXPR$1 = TMPLIT_EXPR,
11184
+ DATA_ATTR$1 = DATA_ATTR,
11185
+ ARIA_ATTR$1 = ARIA_ATTR,
11186
+ IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA,
11187
+ ATTR_WHITESPACE$1 = ATTR_WHITESPACE,
11188
+ CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT;
11189
+ let IS_ALLOWED_URI$1 = IS_ALLOWED_URI;
11004
11190
  /**
11005
11191
  * We consider the elements and attributes below to be safe. Ideally
11006
11192
  * don't add any new ones but feel free to remove unwanted ones.
@@ -11077,6 +11263,13 @@ var purify = {exports: {}};
11077
11263
  let WHOLE_DOCUMENT = false;
11078
11264
  /* Track whether config is already set on this instance of DOMPurify. */
11079
11265
  let SET_CONFIG = false;
11266
+ /* Pristine allowlist bindings captured at setConfig() time. On the
11267
+ * persistent-config path sanitize() restores the sets from these before
11268
+ * the per-walk hook clone-guard, so a hook's in-call widening cannot
11269
+ * carry across calls. Null until setConfig() is called; reset by
11270
+ * clearConfig(). */
11271
+ let SET_CONFIG_ALLOWED_TAGS = null;
11272
+ let SET_CONFIG_ALLOWED_ATTR = null;
11080
11273
  /* Decide if all elements (e.g. style, script) must be children of
11081
11274
  * document.body. By default, browsers might move them to document.head */
11082
11275
  let FORCE_BODY = false;
@@ -11119,7 +11312,17 @@ var purify = {exports: {}};
11119
11312
  let USE_PROFILES = {};
11120
11313
  /* Tags to ignore content of when KEEP_CONTENT is true */
11121
11314
  let FORBID_CONTENTS = null;
11122
- const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
11315
+ const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script',
11316
+ // <selectedcontent> mirrors the selected <option>'s subtree, cloned by
11317
+ // the UA (customizable <select>) — including any on* handlers — and the
11318
+ // engine re-mirrors synchronously whenever a removal changes which
11319
+ // option/selectedcontent is current, even inside DOMPurify's inert
11320
+ // DOMParser document. Hoisting its children on removal re-inserts a fresh
11321
+ // mirror target ahead of the walk, which the engine refills, looping
11322
+ // forever (DoS) and amplifying output. Dropping its content on removal
11323
+ // (rather than hoisting) breaks that cascade; the content is a duplicate
11324
+ // of the option, which is sanitized on its own. See campaign-3 F1/F6.
11325
+ 'selectedcontent', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
11123
11326
  /* Tags that are safe for data: URIs */
11124
11327
  let DATA_URI_TAGS = null;
11125
11328
  const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
@@ -11135,8 +11338,10 @@ var purify = {exports: {}};
11135
11338
  /* Allowed XHTML+XML namespaces */
11136
11339
  let ALLOWED_NAMESPACES = null;
11137
11340
  const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
11138
- let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
11139
- let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
11341
+ const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(['mi', 'mo', 'mn', 'ms', 'mtext']);
11342
+ let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
11343
+ const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
11344
+ let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
11140
11345
  // Certain elements are allowed in both SVG and HTML
11141
11346
  // namespace. We need to specify them explicitly
11142
11347
  // so that they don't get erroneously deleted from
@@ -11178,15 +11383,33 @@ var purify = {exports: {}};
11178
11383
  // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
11179
11384
  transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
11180
11385
  /* Set configuration parameters */
11181
- ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
11182
- ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
11183
- ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
11184
- URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
11185
- DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
11186
- FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
11187
- FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
11188
- FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
11189
- USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;
11386
+ ALLOWED_TAGS = _resolveSetOption(cfg, 'ALLOWED_TAGS', DEFAULT_ALLOWED_TAGS, {
11387
+ transform: transformCaseFunc
11388
+ });
11389
+ ALLOWED_ATTR = _resolveSetOption(cfg, 'ALLOWED_ATTR', DEFAULT_ALLOWED_ATTR, {
11390
+ transform: transformCaseFunc
11391
+ });
11392
+ ALLOWED_NAMESPACES = _resolveSetOption(cfg, 'ALLOWED_NAMESPACES', DEFAULT_ALLOWED_NAMESPACES, {
11393
+ transform: stringToString
11394
+ });
11395
+ URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, 'ADD_URI_SAFE_ATTR', DEFAULT_URI_SAFE_ATTRIBUTES, {
11396
+ transform: transformCaseFunc,
11397
+ base: DEFAULT_URI_SAFE_ATTRIBUTES
11398
+ });
11399
+ DATA_URI_TAGS = _resolveSetOption(cfg, 'ADD_DATA_URI_TAGS', DEFAULT_DATA_URI_TAGS, {
11400
+ transform: transformCaseFunc,
11401
+ base: DEFAULT_DATA_URI_TAGS
11402
+ });
11403
+ FORBID_CONTENTS = _resolveSetOption(cfg, 'FORBID_CONTENTS', DEFAULT_FORBID_CONTENTS, {
11404
+ transform: transformCaseFunc
11405
+ });
11406
+ FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
11407
+ transform: transformCaseFunc
11408
+ });
11409
+ FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
11410
+ transform: transformCaseFunc
11411
+ });
11412
+ USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
11190
11413
  ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
11191
11414
  ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
11192
11415
  ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
@@ -11202,20 +11425,22 @@ var purify = {exports: {}};
11202
11425
  SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
11203
11426
  KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
11204
11427
  IN_PLACE = cfg.IN_PLACE || false; // Default false
11205
- IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
11206
- NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
11207
- MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;
11208
- HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;
11209
- CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || create(null);
11210
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
11211
- CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
11212
- }
11213
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
11214
- CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
11215
- }
11216
- if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {
11217
- CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
11218
- }
11428
+ IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
11429
+ NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
11430
+ MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'MATHML_TEXT_INTEGRATION_POINTS') && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object' ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); // Default built-in map
11431
+ HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); // Default built-in map
11432
+ const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
11433
+ CUSTOM_ELEMENT_HANDLING = create(null);
11434
+ if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
11435
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; // Default undefined
11436
+ }
11437
+ if (objectHasOwnProperty(customElementHandling, 'attributeNameCheck') && isRegexOrFunction(customElementHandling.attributeNameCheck)) {
11438
+ CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck; // Default undefined
11439
+ }
11440
+ if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') {
11441
+ CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined
11442
+ }
11443
+ seal(CUSTOM_ELEMENT_HANDLING);
11219
11444
  if (SAFE_FOR_TEMPLATES) {
11220
11445
  ALLOW_DATA_ATTR = false;
11221
11446
  }
@@ -11251,36 +11476,36 @@ var purify = {exports: {}};
11251
11476
  EXTRA_ELEMENT_HANDLING.tagCheck = null;
11252
11477
  EXTRA_ELEMENT_HANDLING.attributeCheck = null;
11253
11478
  /* Merge configuration parameters */
11254
- if (cfg.ADD_TAGS) {
11479
+ if (objectHasOwnProperty(cfg, 'ADD_TAGS')) {
11255
11480
  if (typeof cfg.ADD_TAGS === 'function') {
11256
11481
  EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;
11257
- } else {
11482
+ } else if (arrayIsArray(cfg.ADD_TAGS)) {
11258
11483
  if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
11259
11484
  ALLOWED_TAGS = clone(ALLOWED_TAGS);
11260
11485
  }
11261
11486
  addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
11262
11487
  }
11263
11488
  }
11264
- if (cfg.ADD_ATTR) {
11489
+ if (objectHasOwnProperty(cfg, 'ADD_ATTR')) {
11265
11490
  if (typeof cfg.ADD_ATTR === 'function') {
11266
11491
  EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;
11267
- } else {
11492
+ } else if (arrayIsArray(cfg.ADD_ATTR)) {
11268
11493
  if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
11269
11494
  ALLOWED_ATTR = clone(ALLOWED_ATTR);
11270
11495
  }
11271
11496
  addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
11272
11497
  }
11273
11498
  }
11274
- if (cfg.ADD_URI_SAFE_ATTR) {
11499
+ if (objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) {
11275
11500
  addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
11276
11501
  }
11277
- if (cfg.FORBID_CONTENTS) {
11502
+ if (objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS)) {
11278
11503
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
11279
11504
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
11280
11505
  }
11281
11506
  addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
11282
11507
  }
11283
- if (cfg.ADD_FORBID_CONTENTS) {
11508
+ if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
11284
11509
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
11285
11510
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
11286
11511
  }
@@ -11299,6 +11524,13 @@ var purify = {exports: {}};
11299
11524
  addToSet(ALLOWED_TAGS, ['tbody']);
11300
11525
  delete FORBID_TAGS.tbody;
11301
11526
  }
11527
+ // Re-derive the active Trusted Types policy from this configuration on
11528
+ // every parse. The active policy must never be sticky closure state that
11529
+ // outlives the config that set it: a caller-supplied policy left in place
11530
+ // after `clearConfig()` — or after a later call that supplied none, or
11531
+ // `TRUSTED_TYPES_POLICY: null` — could sign a subsequent "default"
11532
+ // `RETURN_TRUSTED_TYPE` result with a foreign, possibly unsafe policy.
11533
+ // See GHSA-vxr8-fq34-vvx9.
11302
11534
  if (cfg.TRUSTED_TYPES_POLICY) {
11303
11535
  if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
11304
11536
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
@@ -11306,18 +11538,45 @@ var purify = {exports: {}};
11306
11538
  if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
11307
11539
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
11308
11540
  }
11309
- // Overwrite existing TrustedTypes policy.
11541
+ // A caller-supplied policy applies to this configuration only.
11542
+ const previousTrustedTypesPolicy = trustedTypesPolicy;
11310
11543
  trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
11311
- // Sign local variables required by `sanitize`.
11312
- emptyHTML = trustedTypesPolicy.createHTML('');
11544
+ // Sign local variables required by `sanitize`. If the supplied policy's
11545
+ // `createHTML` is circular (i.e. it calls `DOMPurify.sanitize`), this
11546
+ // throws via the re-entrancy guard. Restore the previous policy first so
11547
+ // the instance is not left in a poisoned state. See #1422.
11548
+ try {
11549
+ emptyHTML = _createTrustedHTML('');
11550
+ } catch (error) {
11551
+ trustedTypesPolicy = previousTrustedTypesPolicy;
11552
+ throw error;
11553
+ }
11554
+ } else if (cfg.TRUSTED_TYPES_POLICY === null) {
11555
+ // Explicit opt-out for this call: perform no Trusted Types signing and
11556
+ // create nothing (so a strict `trusted-types` CSP that disallows a
11557
+ // `dompurify` policy can still call `sanitize` from inside its own
11558
+ // policy — see #1422). Resetting to `undefined` rather than a sticky
11559
+ // `null` also drops any previously retained caller policy, so it cannot
11560
+ // resurface on a later call, while still allowing the next config-less
11561
+ // call to restore the internal default policy. See GHSA-vxr8-fq34-vvx9.
11562
+ trustedTypesPolicy = undefined;
11563
+ emptyHTML = '';
11313
11564
  } else {
11314
- // Uninitialized policy, attempt to initialize the internal dompurify policy.
11565
+ // No policy supplied: keep the currently active policy if one is set — a
11566
+ // previously supplied policy is intentionally sticky across config-less
11567
+ // calls — otherwise fall back to the instance's own internal policy,
11568
+ // created at most once. (A policy supplied for a *single* call still
11569
+ // lingers by design; what must not linger is a policy whose configuration
11570
+ // has been torn down via `clearConfig()`, which restores the default.)
11315
11571
  if (trustedTypesPolicy === undefined) {
11316
- trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
11572
+ trustedTypesPolicy = _getDefaultTrustedTypesPolicy();
11317
11573
  }
11318
- // If creating the internal policy succeeded sign internal variables.
11319
- if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
11320
- emptyHTML = trustedTypesPolicy.createHTML('');
11574
+ // Sign internal variables only when a policy is active. A falsy policy
11575
+ // (Trusted Types unsupported, creation failed, or an explicit opt-out)
11576
+ // leaves `emptyHTML` as a plain string, so we never call `.createHTML` on
11577
+ // a non-policy and throw. See #1422.
11578
+ if (trustedTypesPolicy && typeof emptyHTML === 'string') {
11579
+ emptyHTML = _createTrustedHTML('');
11321
11580
  }
11322
11581
  }
11323
11582
  // Prevent further manipulation of configuration.
@@ -11332,6 +11591,77 @@ var purify = {exports: {}};
11332
11591
  * correctly. */
11333
11592
  const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
11334
11593
  const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
11594
+ /**
11595
+ * Namespace rules for an element in the SVG namespace.
11596
+ *
11597
+ * @param tagName the element's lowercase tag name
11598
+ * @param parent the (possibly simulated) parent node
11599
+ * @param parentTagName the parent's lowercase tag name
11600
+ * @returns true if a spec-compliant parser could produce this element
11601
+ */
11602
+ const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) {
11603
+ // The only way to switch from HTML namespace to SVG
11604
+ // is via <svg>. If it happens via any other tag, then
11605
+ // it should be killed.
11606
+ if (parent.namespaceURI === HTML_NAMESPACE) {
11607
+ return tagName === 'svg';
11608
+ }
11609
+ // The only way to switch from MathML to SVG is via <svg>
11610
+ // if the parent is either <annotation-xml> or a MathML
11611
+ // text integration point.
11612
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
11613
+ return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
11614
+ }
11615
+ // We only allow elements that are defined in SVG
11616
+ // spec. All others are disallowed in SVG namespace.
11617
+ return Boolean(ALL_SVG_TAGS[tagName]);
11618
+ };
11619
+ /**
11620
+ * Namespace rules for an element in the MathML namespace.
11621
+ *
11622
+ * @param tagName the element's lowercase tag name
11623
+ * @param parent the (possibly simulated) parent node
11624
+ * @param parentTagName the parent's lowercase tag name
11625
+ * @returns true if a spec-compliant parser could produce this element
11626
+ */
11627
+ const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) {
11628
+ // The only way to switch from HTML namespace to MathML
11629
+ // is via <math>. If it happens via any other tag, then
11630
+ // it should be killed.
11631
+ if (parent.namespaceURI === HTML_NAMESPACE) {
11632
+ return tagName === 'math';
11633
+ }
11634
+ // The only way to switch from SVG to MathML is via
11635
+ // <math> and HTML integration points
11636
+ if (parent.namespaceURI === SVG_NAMESPACE) {
11637
+ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
11638
+ }
11639
+ // We only allow elements that are defined in MathML
11640
+ // spec. All others are disallowed in MathML namespace.
11641
+ return Boolean(ALL_MATHML_TAGS[tagName]);
11642
+ };
11643
+ /**
11644
+ * Namespace rules for an element in the HTML namespace.
11645
+ *
11646
+ * @param tagName the element's lowercase tag name
11647
+ * @param parent the (possibly simulated) parent node
11648
+ * @param parentTagName the parent's lowercase tag name
11649
+ * @returns true if a spec-compliant parser could produce this element
11650
+ */
11651
+ const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) {
11652
+ // The only way to switch from SVG to HTML is via
11653
+ // HTML integration points, and from MathML to HTML
11654
+ // is via MathML text integration points
11655
+ if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
11656
+ return false;
11657
+ }
11658
+ if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
11659
+ return false;
11660
+ }
11661
+ // We disallow tags that are specific for MathML
11662
+ // or SVG and should never appear in HTML namespace
11663
+ return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
11664
+ };
11335
11665
  /**
11336
11666
  * @param element a DOM element whose namespace is being checked
11337
11667
  * @returns Return false if the element has a
@@ -11354,51 +11684,13 @@ var purify = {exports: {}};
11354
11684
  return false;
11355
11685
  }
11356
11686
  if (element.namespaceURI === SVG_NAMESPACE) {
11357
- // The only way to switch from HTML namespace to SVG
11358
- // is via <svg>. If it happens via any other tag, then
11359
- // it should be killed.
11360
- if (parent.namespaceURI === HTML_NAMESPACE) {
11361
- return tagName === 'svg';
11362
- }
11363
- // The only way to switch from MathML to SVG is via`
11364
- // svg if parent is either <annotation-xml> or MathML
11365
- // text integration points.
11366
- if (parent.namespaceURI === MATHML_NAMESPACE) {
11367
- return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
11368
- }
11369
- // We only allow elements that are defined in SVG
11370
- // spec. All others are disallowed in SVG namespace.
11371
- return Boolean(ALL_SVG_TAGS[tagName]);
11687
+ return _checkSvgNamespace(tagName, parent, parentTagName);
11372
11688
  }
11373
11689
  if (element.namespaceURI === MATHML_NAMESPACE) {
11374
- // The only way to switch from HTML namespace to MathML
11375
- // is via <math>. If it happens via any other tag, then
11376
- // it should be killed.
11377
- if (parent.namespaceURI === HTML_NAMESPACE) {
11378
- return tagName === 'math';
11379
- }
11380
- // The only way to switch from SVG to MathML is via
11381
- // <math> and HTML integration points
11382
- if (parent.namespaceURI === SVG_NAMESPACE) {
11383
- return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
11384
- }
11385
- // We only allow elements that are defined in MathML
11386
- // spec. All others are disallowed in MathML namespace.
11387
- return Boolean(ALL_MATHML_TAGS[tagName]);
11690
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
11388
11691
  }
11389
11692
  if (element.namespaceURI === HTML_NAMESPACE) {
11390
- // The only way to switch from SVG to HTML is via
11391
- // HTML integration points, and from MathML to HTML
11392
- // is via MathML text integration points
11393
- if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
11394
- return false;
11395
- }
11396
- if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
11397
- return false;
11398
- }
11399
- // We disallow tags that are specific for MathML
11400
- // or SVG and should never appear in HTML namespace
11401
- return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
11693
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
11402
11694
  }
11403
11695
  // For XHTML and XML documents that support custom namespaces
11404
11696
  if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
@@ -11423,7 +11715,81 @@ var purify = {exports: {}};
11423
11715
  // eslint-disable-next-line unicorn/prefer-dom-node-remove
11424
11716
  getParentNode(node).removeChild(node);
11425
11717
  } catch (_) {
11718
+ /* The normal detach failed — this is reached for a parentless node
11719
+ (getParentNode() is null, so .removeChild throws). Element.prototype
11720
+ .remove() is itself a spec no-op on a parentless node, so a recorded
11721
+ "removal" would otherwise hand the caller back an intact,
11722
+ payload-bearing node (e.g. a detached IN_PLACE root the mXSS canary or
11723
+ the style-with-element-child rule decided to kill). Fail closed by
11724
+ throwing — exactly as a clobbered root does at the IN_PLACE entry —
11725
+ rather than trying to "neutralize" the node via its own methods.
11726
+ Neutralizing would mean calling getAttributeNames()/removeAttribute()
11727
+ on the node, both of which a <form> root can clobber via a named child
11728
+ (and _isClobbered does not even probe getAttributeNames), so the
11729
+ neutralize step could itself be silently defeated, leaving the payload
11730
+ intact. A throw touches only the cached, clobber-safe remove() and
11731
+ getParentNode(). Generalizes GHSA-r47g-fvhr-h676 (clobbered-form root)
11732
+ to every root-kill reason. REPORT-3.
11733
+ This lives inside the catch, so it never fires for a normally-removed
11734
+ in-tree node: those have a parent, removeChild() succeeds, and the
11735
+ catch is not entered. Only a kept (parentless) root reaches here. */
11426
11736
  remove(node);
11737
+ if (!getParentNode(node)) {
11738
+ throw typeErrorCreate('a node selected for removal could not be detached from its tree ' + 'and cannot be safely returned; refusing to sanitize in place');
11739
+ }
11740
+ }
11741
+ };
11742
+ /**
11743
+ * _neutralizeRoot
11744
+ *
11745
+ * Fail-closed teardown of an in-place root after the sanitize walk aborts
11746
+ * (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
11747
+ * custom element's reaction detaches a node so `_forceRemove`'s deliberate
11748
+ * parentless guard throws, or any other re-entrant engine mutation — would
11749
+ * otherwise leave the caller's *live* tree half-sanitized, with everything
11750
+ * after the abort point still carrying its handlers. There is no safe way
11751
+ * to resume the walk (the tree mutated under us), so we strip the root bare:
11752
+ * remove every child and every attribute, then let the caller's catch see
11753
+ * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
11754
+ * getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
11755
+ *
11756
+ * @param root the in-place root to empty
11757
+ */
11758
+ const _neutralizeRoot = function _neutralizeRoot(root) {
11759
+ /* Strip every disallowed attribute (on* handlers included) off the whole
11760
+ subtree BEFORE detaching anything. Detaching first would hand back
11761
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
11762
+ whose queued resource event still fires in page scope after we throw.
11763
+ Clobber-safe reads; a doomed clobbered node's own attributes are
11764
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
11765
+ _neutralizeSubtree(root);
11766
+ const childNodes = getChildNodes(root);
11767
+ if (childNodes) {
11768
+ const snapshot = [];
11769
+ arrayForEach(childNodes, child => {
11770
+ arrayPush(snapshot, child);
11771
+ });
11772
+ arrayForEach(snapshot, child => {
11773
+ try {
11774
+ remove(child);
11775
+ } catch (_) {
11776
+ /* Best-effort teardown; a still-attached child is handled below */
11777
+ }
11778
+ });
11779
+ }
11780
+ const attributes = getAttributes(root);
11781
+ if (attributes) {
11782
+ for (let i = attributes.length - 1; i >= 0; --i) {
11783
+ const attribute = attributes[i];
11784
+ const name = attribute && attribute.name;
11785
+ if (typeof name === 'string') {
11786
+ try {
11787
+ root.removeAttribute(name);
11788
+ } catch (_) {
11789
+ /* Clobbered removeAttribute — ignore (fail-closed best effort) */
11790
+ }
11791
+ }
11792
+ }
11427
11793
  }
11428
11794
  };
11429
11795
  /**
@@ -11458,6 +11824,148 @@ var purify = {exports: {}};
11458
11824
  }
11459
11825
  }
11460
11826
  };
11827
+ /**
11828
+ * _stripDisallowedAttributes
11829
+ *
11830
+ * Removes every attribute the active configuration does not allow from a
11831
+ * single element, using the same allowlist as the main attribute pass (so
11832
+ * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
11833
+ * neutralise nodes that are being discarded from an in-place tree.
11834
+ *
11835
+ * @param element the element to strip
11836
+ */
11837
+ const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) {
11838
+ const attributes = getAttributes(element);
11839
+ if (!attributes) {
11840
+ return;
11841
+ }
11842
+ for (let i = attributes.length - 1; i >= 0; --i) {
11843
+ const attribute = attributes[i];
11844
+ const name = attribute && attribute.name;
11845
+ if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) {
11846
+ continue;
11847
+ }
11848
+ try {
11849
+ element.removeAttribute(name);
11850
+ } catch (_) {
11851
+ /* Clobbered removeAttribute on a doomed node — ignore */
11852
+ }
11853
+ }
11854
+ };
11855
+ /**
11856
+ * _neutralizeSubtree
11857
+ *
11858
+ * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT
11859
+ * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary,
11860
+ * namespace, comment, processing-instruction and KEEP_CONTENT:false removals
11861
+ * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
11862
+ * those dropped nodes are detached from the caller's LIVE tree but a
11863
+ * handler-bearing original among them (an `<img onerror>`/`<video>` that was
11864
+ * loading) keeps its queued resource event, which fires in page scope after
11865
+ * sanitize returns. This walks a removed subtree and strips every attribute
11866
+ * the active configuration does not allow — so `on*` handlers are cancelled
11867
+ * through the SAME allowlist that governs kept nodes, not a separate `/^on/`
11868
+ * blocklist. Run synchronously before sanitize returns, i.e. before any
11869
+ * queued event can fire. Hook-free by design: these nodes leave the output,
11870
+ * so firing attribute hooks for them would be surprising. Clobber-safe reads;
11871
+ * a doomed clobbered node may shadow `removeAttribute` (its own attributes are
11872
+ * irrelevant — it is discarded — while its non-clobbered descendants, e.g.
11873
+ * the `<img>`, are reached and scrubbed).
11874
+ *
11875
+ * @param root the root of a removed subtree to neutralise
11876
+ */
11877
+ const _neutralizeSubtree = function _neutralizeSubtree(root) {
11878
+ const stack = [root];
11879
+ while (stack.length > 0) {
11880
+ const node = stack.pop();
11881
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
11882
+ if (nodeType === NODE_TYPE.element) {
11883
+ _stripDisallowedAttributes(node);
11884
+ }
11885
+ const childNodes = getChildNodes(node);
11886
+ if (childNodes) {
11887
+ for (let i = childNodes.length - 1; i >= 0; --i) {
11888
+ stack.push(childNodes[i]);
11889
+ }
11890
+ }
11891
+ }
11892
+ };
11893
+ /**
11894
+ * _neutralizePatchLinkage
11895
+ *
11896
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
11897
+ * hardening, https://github.com/WICG/declarative-partial-updates).
11898
+ *
11899
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
11900
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
11901
+ * reaches each node. On a live in-place root that leaves a window: from the
11902
+ * moment the root is connected until the walk arrives at a given node, that
11903
+ * node's linkage is live. A patch applied on connection/stream can fire as
11904
+ * a microtask during the walk and inject or teleport an unsanitized DOM
11905
+ * range into a region the iterator has already passed and will not revisit,
11906
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
11907
+ * whole tree once up front and sever every linkage before the walk begins,
11908
+ * closing that window.
11909
+ *
11910
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
11911
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
11912
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
11913
+ * to group with the rest of the declarative-partial-updates handling and
11914
+ * stay overridable, consistent with the codebase.
11915
+ *
11916
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
11917
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
11918
+ *
11919
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
11920
+ * .html Q1): this mirrors the existing policy of keeping `for` on
11921
+ * <label>/<output>. If the shipping feature can drive a patch through a
11922
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
11923
+ * attribute check at _isBasicCustomElement's caller must additionally drop
11924
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
11925
+ *
11926
+ * @param root the in-place root to sweep
11927
+ */
11928
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
11929
+ if (!SAFE_FOR_XML) {
11930
+ return;
11931
+ }
11932
+ const stack = [root];
11933
+ while (stack.length > 0) {
11934
+ const node = stack.pop();
11935
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
11936
+ /* Remove range markers (the target side of a patch linkage): every
11937
+ processing instruction, and any markup-bearing comment. */
11938
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
11939
+ try {
11940
+ remove(node);
11941
+ } catch (_) {
11942
+ /* Best-effort */
11943
+ }
11944
+ continue;
11945
+ }
11946
+ /* Strip patch-source attributes (the source side) off elements. */
11947
+ if (nodeType === NODE_TYPE.element) {
11948
+ const element = node;
11949
+ const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
11950
+ try {
11951
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
11952
+ element.removeAttribute('patchsrc');
11953
+ }
11954
+ if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
11955
+ element.removeAttribute('for');
11956
+ }
11957
+ } catch (_) {
11958
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
11959
+ }
11960
+ }
11961
+ const childNodes = getChildNodes(node);
11962
+ if (childNodes) {
11963
+ for (let i = childNodes.length - 1; i >= 0; --i) {
11964
+ stack.push(childNodes[i]);
11965
+ }
11966
+ }
11967
+ }
11968
+ };
11461
11969
  /**
11462
11970
  * _initDocument
11463
11971
  *
@@ -11479,7 +11987,7 @@ var purify = {exports: {}};
11479
11987
  // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
11480
11988
  dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
11481
11989
  }
11482
- const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
11990
+ const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty;
11483
11991
  /*
11484
11992
  * Use the DOMParser API by default, fallback later if needs be
11485
11993
  * DOMParser not work for svg when has multiple root element.
@@ -11515,33 +12023,297 @@ var purify = {exports: {}};
11515
12023
  * @return The created NodeIterator
11516
12024
  */
11517
12025
  const _createNodeIterator = function _createNodeIterator(root) {
11518
- return createNodeIterator.call(root.ownerDocument || root, root,
12026
+ /* Read ownerDocument through the cached Node.prototype getter, never the
12027
+ direct property. HTMLFormElement has [LegacyOverrideBuiltIns], so a
12028
+ clobbering child (<input name="ownerDocument"> or a form-associated
12029
+ external input) shadows the prototype getter and makes a direct read
12030
+ return that <input>. createNodeIterator.call(<input>, ...) then throws
12031
+ "Illegal invocation", and on the IN_PLACE path that throw lands before
12032
+ the walk's fail-closed barrier - leaving the caller's live tree, with
12033
+ any already-armed handler in it, un-neutralized. The cached getter
12034
+ returns the real Document regardless of the clobber. */
12035
+ const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
12036
+ return createNodeIterator.call(doc || root, root,
11519
12037
  // eslint-disable-next-line no-bitwise
11520
12038
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
11521
12039
  };
12040
+ /**
12041
+ * Replace template expression syntax (mustache, ERB, template
12042
+ * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
12043
+ * sites. Order matters: mustache, then ERB, then template literal.
12044
+ *
12045
+ * @param value the string to scrub
12046
+ * @returns the scrubbed string
12047
+ */
12048
+ const _stripTemplateExpressions = function _stripTemplateExpressions(value) {
12049
+ value = stringReplace(value, MUSTACHE_EXPR$1, ' ');
12050
+ value = stringReplace(value, ERB_EXPR$1, ' ');
12051
+ value = stringReplace(value, TMPLIT_EXPR$1, ' ');
12052
+ return value;
12053
+ };
12054
+ /**
12055
+ * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
12056
+ * character data of an element subtree. Used as the final safety net for
12057
+ * SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions
12058
+ * which only form after text-node normalization (e.g. fragments split across
12059
+ * stripped elements) cannot survive into a template-evaluating framework.
12060
+ *
12061
+ * Walks text/comment/CDATA/processing-instruction nodes and mutates `.data`
12062
+ * in place rather than round-tripping through innerHTML. This preserves
12063
+ * descendant node references (important for IN_PLACE callers), avoids a
12064
+ * serialize/reparse cycle, and reads literal character data — which means
12065
+ * `<%...%>` in text content matches the ERB regex against its real bytes
12066
+ * instead of the HTML-entity-escaped form innerHTML would produce.
12067
+ *
12068
+ * Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for
12069
+ * attributes is performed during the per-node `_sanitizeAttributes` pass.
12070
+ *
12071
+ * @param node The root element whose character data should be scrubbed.
12072
+ */
12073
+ const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
12074
+ var _node$querySelectorAl;
12075
+ node.normalize();
12076
+ /* Clobber-safe ownerDocument read, same reasoning as _createNodeIterator:
12077
+ under SAFE_FOR_TEMPLATES this runs on the live IN_PLACE root, which may
12078
+ carry a form-named-getter override of ownerDocument. */
12079
+ const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
12080
+ const walker = createNodeIterator.call(doc || node, node,
12081
+ // eslint-disable-next-line no-bitwise
12082
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
12083
+ let currentNode = walker.nextNode();
12084
+ while (currentNode) {
12085
+ currentNode.data = _stripTemplateExpressions(currentNode.data);
12086
+ currentNode = walker.nextNode();
12087
+ }
12088
+ // NodeIterator does not descend into <template>.content per the DOM spec,
12089
+ // so we must explicitly recurse into each template's content fragment,
12090
+ // mirroring the approach used by _sanitizeShadowDOM.
12091
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, 'template');
12092
+ if (templates) {
12093
+ arrayForEach(templates, tmpl => {
12094
+ if (_isDocumentFragment(tmpl.content)) {
12095
+ _scrubTemplateExpressions2(tmpl.content);
12096
+ }
12097
+ });
12098
+ }
12099
+ };
11522
12100
  /**
11523
12101
  * _isClobbered
11524
12102
  *
12103
+ * Detect DOM-clobbering on HTMLFormElement nodes. Form is the only HTML
12104
+ * interface with [LegacyOverrideBuiltIns]; a descendant element with a
12105
+ * `name` attribute matching a prototype property shadows that property
12106
+ * on direct reads. We use this check at the IN_PLACE entry-point and
12107
+ * during attribute sanitization to refuse clobbered forms.
12108
+ *
11525
12109
  * @param element element to check for clobbering attacks
11526
12110
  * @return true if clobbered, false if safe
11527
12111
  */
11528
12112
  const _isClobbered = function _isClobbered(element) {
11529
- return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function');
12113
+ // Realm-independent tag-name probe. If we can't determine the tag
12114
+ // name at all, we can't reason about clobbering — return false
12115
+ // (the caller's other defences still apply).
12116
+ const realTagName = getNodeName ? getNodeName(element) : null;
12117
+ if (typeof realTagName !== 'string') {
12118
+ return false;
12119
+ }
12120
+ if (transformCaseFunc(realTagName) !== 'form') {
12121
+ return false;
12122
+ }
12123
+ return typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' ||
12124
+ // Realm-safe NamedNodeMap detection: equality against the cached
12125
+ // prototype getter. Clobbered .attributes (e.g. <input name="attributes">)
12126
+ // makes the direct read diverge from the cached read; a clean form
12127
+ // (same-realm OR foreign-realm) has both reads pointing at the same
12128
+ // canonical NamedNodeMap.
12129
+ element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' ||
12130
+ // NodeType clobbering probe. Cached Node.prototype.nodeType getter
12131
+ // returns the integer 1 for any Element regardless of realm; direct
12132
+ // read on a clobbered form (e.g. <input name="nodeType">) returns
12133
+ // the named child element. Cheap addition — nodeType is read from
12134
+ // an internal slot, no serialization cost — and removes a residual
12135
+ // clobbering surface used by several mXSS / PI / comment branches
12136
+ // in _sanitizeElements that compare currentNode.nodeType directly.
12137
+ element.nodeType !== getNodeType(element) ||
12138
+ // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named
12139
+ // "childNodes" shadows the prototype getter. Direct reads of
12140
+ // form.childNodes from a clobbered form return the named child
12141
+ // instead of the real NodeList, so any walk that reads it directly
12142
+ // skips the form's real children. Compare the direct read to the
12143
+ // cached Node.prototype getter — when the form's named-property
12144
+ // getter intercepts the read, the two values differ and we flag
12145
+ // the form. This catches every clobbering child type (input,
12146
+ // select, etc.) regardless of whether the named child happens to
12147
+ // carry a numeric .length, which a typeof-based probe would miss
12148
+ // (e.g. HTMLSelectElement.length is a defined unsigned-long).
12149
+ element.childNodes !== getChildNodes(element);
11530
12150
  };
11531
12151
  /**
11532
- * Checks whether the given object is a DOM node.
12152
+ * Checks whether the given value is a DocumentFragment from any realm.
12153
+ *
12154
+ * The realm-independent replacement reads `nodeType` through the cached
12155
+ * Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE
12156
+ * constant (11). nodeType is a numeric value resolved from the node's
12157
+ * internal slot, identical across realms for the same kind of node.
12158
+ *
12159
+ * @param value object to check
12160
+ * @return true if value is a DocumentFragment-shaped node from any realm
12161
+ */
12162
+ const _isDocumentFragment = function _isDocumentFragment(value) {
12163
+ if (!getNodeType || typeof value !== 'object' || value === null) {
12164
+ return false;
12165
+ }
12166
+ try {
12167
+ return getNodeType(value) === NODE_TYPE.documentFragment;
12168
+ } catch (_) {
12169
+ return false;
12170
+ }
12171
+ };
12172
+ /**
12173
+ * Checks whether the given object is a DOM node, including nodes that
12174
+ * originate from a different window/realm (e.g. an iframe's
12175
+ * contentDocument). The previous `value instanceof Node` check was
12176
+ * realm-bound: nodes from a different window failed it, causing
12177
+ * sanitize() to silently stringify them and reset IN_PLACE to false,
12178
+ * returning the original node unsanitized. See GHSA-4w3q-35jp-p934.
11533
12179
  *
11534
12180
  * @param value object to check whether it's a DOM node
11535
- * @return true is object is a DOM node
12181
+ * @return true if value is a DOM node from any realm
11536
12182
  */
11537
12183
  const _isNode = function _isNode(value) {
11538
- return typeof Node === 'function' && value instanceof Node;
12184
+ if (!getNodeType || typeof value !== 'object' || value === null) {
12185
+ return false;
12186
+ }
12187
+ try {
12188
+ return typeof getNodeType(value) === 'number';
12189
+ } catch (_) {
12190
+ return false;
12191
+ }
11539
12192
  };
11540
12193
  function _executeHooks(hooks, currentNode, data) {
12194
+ if (hooks.length === 0) {
12195
+ return;
12196
+ }
11541
12197
  arrayForEach(hooks, hook => {
11542
12198
  hook.call(DOMPurify, currentNode, data, CONFIG);
11543
12199
  });
11544
12200
  }
12201
+ /**
12202
+ * Structural-threat checks that condemn a node regardless of the
12203
+ * allowlists: mXSS via namespace confusion, risky CSS construction,
12204
+ * processing instructions, markup-bearing comments. Pure predicate;
12205
+ * the caller removes. Check order is load-bearing.
12206
+ *
12207
+ * @param currentNode the node to inspect
12208
+ * @param tagName the node's transformCaseFunc'd tag name
12209
+ * @return true if the node must be removed
12210
+ */
12211
+ const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
12212
+ /* Detect mXSS attempts abusing namespace confusion */
12213
+ if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
12214
+ return true;
12215
+ }
12216
+ /* Remove risky CSS construction leading to mXSS */
12217
+ if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
12218
+ return true;
12219
+ }
12220
+ /* Remove any occurrence of processing instructions */
12221
+ if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
12222
+ return true;
12223
+ }
12224
+ /* Remove any kind of possibly harmful comments */
12225
+ if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) {
12226
+ return true;
12227
+ }
12228
+ return false;
12229
+ };
12230
+ /**
12231
+ * Handle a node whose tag is forbidden or not allowlisted: keep
12232
+ * allowed custom elements (false return exits _sanitizeElements
12233
+ * early - the namespace and fallback-tag removal checks are
12234
+ * intentionally skipped for kept custom elements), else hoist
12235
+ * content per KEEP_CONTENT and remove.
12236
+ *
12237
+ * A kept custom element is the ONLY case in which this function
12238
+ * returns false, so the caller uses that return value to run the
12239
+ * afterSanitizeElements hook on the kept element and keep the
12240
+ * element-hook lifecycle consistent with normal allowlisted
12241
+ * elements (GHSA-c2j3-45gr-mqc4).
12242
+ *
12243
+ * @param currentNode the disallowed node
12244
+ * @param tagName the node's transformCaseFunc'd tag name
12245
+ * @return true if the node was removed, false if kept
12246
+ */
12247
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
12248
+ /* Check if we have a custom element to handle */
12249
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
12250
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
12251
+ return false;
12252
+ }
12253
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
12254
+ return false;
12255
+ }
12256
+ }
12257
+ /* Keep content except for bad-listed elements.
12258
+ Use the cached prototype getters exclusively — the previous code
12259
+ had `|| currentNode.parentNode` / `|| currentNode.childNodes`
12260
+ fallbacks, but the cached getters always return the canonical
12261
+ value (or null for a real parent-less node), so the fallback
12262
+ path was dead in safe cases and a clobbering surface in unsafe
12263
+ ones. Falsy cached results stay falsy; the `if (childNodes &&
12264
+ parentNode)` check already gates correctly. */
12265
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
12266
+ const parentNode = getParentNode(currentNode);
12267
+ const childNodes = getChildNodes(currentNode);
12268
+ if (childNodes && parentNode) {
12269
+ const childCount = childNodes.length;
12270
+ /* Hoist by moving each child up one level rather than deep-cloning
12271
+ it. Moving transfers every descendant exactly once, so a chain of
12272
+ nested disallowed elements costs O(n) instead of the O(n^2) that
12273
+ re-cloning the shrinking subtree at each level produced; it also
12274
+ empties the removed original, so `DOMPurify.removed` no longer
12275
+ pins whole subtrees. Moving preserves the in-place guarantee too:
12276
+ an original carrying already-queued resource events (`<img
12277
+ onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
12278
+ relocated and sanitised rather than left detached but still armed.
12279
+ The sole case that must clone is removing the walk root itself.
12280
+ The result is serialised from the root's subtree, so a restrictive
12281
+ ALLOWED_TAGS that strips the root (`body` on the string path) must
12282
+ leave the content inside it, which only cloning does. In IN_PLACE
12283
+ the root is pre-validated as an allowed tag and so is never removed
12284
+ here, so that path always takes the move branch.
12285
+ `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
12286
+ valid whether we move (drops the trailing entry) or clone (leaves
12287
+ the list intact). */
12288
+ for (let i = childCount - 1; i >= 0; --i) {
12289
+ const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
12290
+ parentNode.insertBefore(hoisted, getNextSibling(currentNode));
12291
+ }
12292
+ }
12293
+ }
12294
+ _forceRemove(currentNode);
12295
+ return true;
12296
+ };
12297
+ /**
12298
+ * Fork a hook-mutable allowlist off its shared binding the first time a
12299
+ * (possibly lazily-installed) uponSanitize* hook is about to see it, so the
12300
+ * hook cannot widen the per-instance default or the setConfig binding by
12301
+ * reference and leak past the call. Returns the set unchanged once it is
12302
+ * already call-local, so repeated calls across elements are idempotent.
12303
+ *
12304
+ * @param hookList the uponSanitize* hook array for this event
12305
+ * @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding
12306
+ * @param defaultSet the per-instance DEFAULT_ALLOWED_* constant
12307
+ * @param setConfigSet the captured setConfig() binding, or null
12308
+ * @return a call-local clone if a hook is present and set is still shared,
12309
+ * else set unchanged
12310
+ */
12311
+ const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) {
12312
+ if (hookList.length === 0) {
12313
+ return set;
12314
+ }
12315
+ return set === defaultSet || set === setConfigSet ? clone(set) : set;
12316
+ };
11545
12317
  /**
11546
12318
  * _sanitizeElements
11547
12319
  *
@@ -11551,86 +12323,112 @@ var purify = {exports: {}};
11551
12323
  * @param currentNode to check for permission to exist
11552
12324
  * @return true if node was killed, false if left alive
11553
12325
  */
11554
- const _sanitizeElements = function _sanitizeElements(currentNode) {
11555
- let content = null;
12326
+ // eslint-disable-next-line complexity
12327
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
11556
12328
  /* Execute a hook if present */
11557
12329
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
12330
+ /* A hook may have detached the node - treat it as removed (see the
12331
+ detached-node comment after the uponSanitizeElement hook below). On
12332
+ the IN_PLACE path, neutralize the detached subtree first so a queued
12333
+ resource handler on it cannot fire in page scope after we return. */
12334
+ if (currentNode !== root && getParentNode(currentNode) === null) {
12335
+ if (IN_PLACE) {
12336
+ _neutralizeSubtree(currentNode);
12337
+ }
12338
+ return true;
12339
+ }
11558
12340
  /* Check if element is clobbered or can clobber */
11559
12341
  if (_isClobbered(currentNode)) {
11560
12342
  _forceRemove(currentNode);
11561
12343
  return true;
11562
12344
  }
11563
12345
  /* Now let's check the element's type and name */
11564
- const tagName = transformCaseFunc(currentNode.nodeName);
12346
+ const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
12347
+ /* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement
12348
+ hook may have been installed after that guard sampled the hook arrays
12349
+ (e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still
12350
+ aliasing a shared binding that a widening hook would mutate by
12351
+ reference. Fork it before exposing it to the hook. */
12352
+ ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
11565
12353
  /* Execute a hook if present */
11566
12354
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
11567
12355
  tagName,
11568
12356
  allowedTags: ALLOWED_TAGS
11569
12357
  });
11570
- /* Detect mXSS attempts abusing namespace confusion */
11571
- if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
11572
- _forceRemove(currentNode);
11573
- return true;
11574
- }
11575
- /* Remove risky CSS construction leading to mXSS */
11576
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
11577
- _forceRemove(currentNode);
11578
- return true;
11579
- }
11580
- /* Remove any occurrence of processing instructions */
11581
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
11582
- _forceRemove(currentNode);
12358
+ /* A hook may have detached the node from the tree — a long-standing
12359
+ user pattern (issue #469; draw.io-style foreignObject filtering).
12360
+ Per the cached, unclobberable parentNode getter the node is
12361
+ genuinely out of the tree, so it can reach neither the serialized
12362
+ output nor an IN_PLACE live tree; treat it as removed and stop
12363
+ processing it. Without this guard, the unsafe-node / namespace
12364
+ checks below would call _forceRemove on a parentless node and hit
12365
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
12366
+ wants gone but *cannot* detach (clobbered / parentless roots), the
12367
+ opposite of a node that is already safely gone. The walk root is
12368
+ exempt: a detached IN_PLACE root is legitimate input and must still
12369
+ be fully sanitized, and a kill-decision on it must keep hitting the
12370
+ REPORT-3 throw. Nodes detached by hooks stay the hook's
12371
+ responsibility for placement: they are not recorded in
12372
+ DOMPurify.removed, so the post-walk IN_PLACE pass (which iterates
12373
+ DOMPurify.removed) does not reach them. But a hook-detached subtree
12374
+ can still hold a queued resource-event handler - e.g. an <img onload>
12375
+ that began loading when the caller built the live tree - which fires
12376
+ in page scope after sanitize returns even though the handler never
12377
+ reached the returned tree. That is the audit-5 F1 hazard, and the
12378
+ documented node.remove() hook pattern walks straight into it. So on
12379
+ the IN_PLACE path we neutralize the detached subtree inline here,
12380
+ stripping its non-allow-listed attributes before returning, exactly
12381
+ as the post-walk pass does for _forceRemove'd subtrees. */
12382
+ if (currentNode !== root && getParentNode(currentNode) === null) {
12383
+ if (IN_PLACE) {
12384
+ _neutralizeSubtree(currentNode);
12385
+ }
11583
12386
  return true;
11584
12387
  }
11585
- /* Remove any kind of possibly harmful comments */
11586
- if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
12388
+ /* Remove mXSS vectors, processing instructions and risky comments */
12389
+ if (_isUnsafeNode(currentNode, tagName)) {
11587
12390
  _forceRemove(currentNode);
11588
12391
  return true;
11589
12392
  }
11590
12393
  /* Remove element if anything forbids its presence */
11591
12394
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
11592
- /* Check if we have a custom element to handle */
11593
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
11594
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
11595
- return false;
11596
- }
11597
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
11598
- return false;
11599
- }
11600
- }
11601
- /* Keep content except for bad-listed elements */
11602
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
11603
- const parentNode = getParentNode(currentNode) || currentNode.parentNode;
11604
- const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
11605
- if (childNodes && parentNode) {
11606
- const childCount = childNodes.length;
11607
- for (let i = childCount - 1; i >= 0; --i) {
11608
- const childClone = cloneNode(childNodes[i], true);
11609
- childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
11610
- parentNode.insertBefore(childClone, getNextSibling(currentNode));
11611
- }
11612
- }
11613
- }
11614
- _forceRemove(currentNode);
11615
- return true;
11616
- }
11617
- /* Check whether element has a valid namespace */
11618
- if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
12395
+ const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
12396
+ /* A false return means the node is a custom element kept via
12397
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
12398
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
12399
+ element-hook lifecycle matches normal allowlisted elements: a
12400
+ security policy applied in this hook (e.g. stripping an attribute
12401
+ from every surviving element) must not silently skip kept custom
12402
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
12403
+ tail below - the hook runs, then the walker's subsequent
12404
+ _sanitizeAttributes pass sanitizes the element's attributes. The
12405
+ deliberately skipped namespace and fallback-tag removal checks stay
12406
+ skipped; they are removal decisions, not the hook contract. */
12407
+ if (removed === false) {
12408
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
12409
+ }
12410
+ return removed;
12411
+ }
12412
+ /* Check whether element has a valid namespace.
12413
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
12414
+ nodeType getter rather than `instanceof Element`, which is realm-
12415
+ bound and short-circuits to false for any node minted in a different
12416
+ realm — letting a foreign-realm element with a forbidden namespace
12417
+ slip past the namespace check entirely. */
12418
+ const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
12419
+ if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
11619
12420
  _forceRemove(currentNode);
11620
12421
  return true;
11621
12422
  }
11622
12423
  /* Make sure that older browsers don't get fallback-tag mXSS */
11623
- if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
12424
+ if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) {
11624
12425
  _forceRemove(currentNode);
11625
12426
  return true;
11626
12427
  }
11627
12428
  /* Sanitize element content to be template-safe */
11628
12429
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
11629
12430
  /* Get the element's text content */
11630
- content = currentNode.textContent;
11631
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
11632
- content = stringReplace(content, expr, ' ');
11633
- });
12431
+ const content = _stripTemplateExpressions(currentNode.textContent);
11634
12432
  if (currentNode.textContent !== content) {
11635
12433
  arrayPush(DOMPurify.removed, {
11636
12434
  element: currentNode.cloneNode()
@@ -11656,15 +12454,43 @@ var purify = {exports: {}};
11656
12454
  if (FORBID_ATTR[lcName]) {
11657
12455
  return false;
11658
12456
  }
12457
+ /* Reject declarative-partial-updates patch-linkage attributes
12458
+ (https://github.com/WICG/declarative-partial-updates).
12459
+ Empirical note (Chrome 150, verified — see
12460
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
12461
+ sanitization. For the string path it fires during sanitize()'s own
12462
+ parse, so the walk sees and sanitizes the fully materialized expanded
12463
+ tree — teleports into MathML/SVG integration points included; a
12464
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
12465
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
12466
+ Either way DOMPurify is NOT blind to the patch.
12467
+ This removal is therefore defense-in-depth rather than the sole barrier:
12468
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
12469
+ in the caller's context, and keeps behaviour deterministic if a future
12470
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
12471
+ anywhere else (notably <template for>) it links the element to a patch
12472
+ target and teleports or removes an arbitrary DOM range by id/marker name.
12473
+ `patchsrc` fetches remote markup and is treated as a script-loading
12474
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
12475
+ other structural-threat checks and stays overridable, consistent with
12476
+ the rest of the codebase. PI range markers are already removed by
12477
+ _isUnsafeNode. */
12478
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
12479
+ return false;
12480
+ }
12481
+ if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
12482
+ return false;
12483
+ }
11659
12484
  /* Make sure attribute cannot clobber */
11660
12485
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
11661
12486
  return false;
11662
12487
  }
12488
+ const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
11663
12489
  /* Allow valid data-* attributes: At least one character after "-"
11664
12490
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
11665
12491
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
11666
12492
  We don't need to check the value; it's always URI safe. */
11667
- if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
12493
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted) {
11668
12494
  if (
11669
12495
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
11670
12496
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -11676,11 +12502,15 @@ var purify = {exports: {}};
11676
12502
  return false;
11677
12503
  }
11678
12504
  /* Check value is safe. First, is attr inert? If so, is safe */
11679
- } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) {
12505
+ } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; else if (value) {
11680
12506
  return false;
11681
12507
  } else ;
11682
12508
  return true;
11683
12509
  };
12510
+ /* Names the HTML spec reserves from valid-custom-element-name; these must
12511
+ * never be treated as basic custom elements even when a permissive
12512
+ * CUSTOM_ELEMENT_HANDLING.tagNameCheck is configured. */
12513
+ const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, ['annotation-xml', 'color-profile', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'missing-glyph']);
11684
12514
  /**
11685
12515
  * _isBasicCustomElement
11686
12516
  * checks if at least one dash is included in tagName, and it's not the first char
@@ -11690,7 +12520,64 @@ var purify = {exports: {}};
11690
12520
  * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
11691
12521
  */
11692
12522
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
11693
- return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);
12523
+ return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
12524
+ };
12525
+ /**
12526
+ * Wrap an attribute value in the matching Trusted Types object when
12527
+ * the active policy requires it. Namespaced attributes pass through
12528
+ * unchanged (no TT support yet, see
12529
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
12530
+ *
12531
+ * @param lcTag lowercase tag name of the containing element
12532
+ * @param lcName lowercase attribute name
12533
+ * @param namespaceURI the attribute's namespace, if any
12534
+ * @param value the attribute value to wrap
12535
+ * @return the value, wrapped when Trusted Types demand it
12536
+ */
12537
+ const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) {
12538
+ if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function' && !namespaceURI) {
12539
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
12540
+ case 'TrustedHTML':
12541
+ {
12542
+ return _createTrustedHTML(value);
12543
+ }
12544
+ case 'TrustedScriptURL':
12545
+ {
12546
+ return _createTrustedScriptURL(value);
12547
+ }
12548
+ }
12549
+ }
12550
+ return value;
12551
+ };
12552
+ /**
12553
+ * Write a modified attribute value back onto the element. On
12554
+ * success, re-probe for clobbering introduced by the new value and
12555
+ * remove the element when found; otherwise pop the removal entry
12556
+ * recorded by the earlier _removeAttribute (long-standing pairing
12557
+ * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
12558
+ * failure, remove the attribute instead.
12559
+ *
12560
+ * @param currentNode the element carrying the attribute
12561
+ * @param name the attribute name as present on the element
12562
+ * @param namespaceURI the attribute's namespace, if any
12563
+ * @param value the new attribute value
12564
+ */
12565
+ const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
12566
+ try {
12567
+ if (namespaceURI) {
12568
+ currentNode.setAttributeNS(namespaceURI, name, value);
12569
+ } else {
12570
+ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
12571
+ currentNode.setAttribute(name, value);
12572
+ }
12573
+ if (_isClobbered(currentNode)) {
12574
+ _forceRemove(currentNode);
12575
+ } else {
12576
+ arrayPop(DOMPurify.removed);
12577
+ }
12578
+ } catch (_) {
12579
+ _removeAttribute(name, currentNode);
12580
+ }
11694
12581
  };
11695
12582
  /**
11696
12583
  * _sanitizeAttributes
@@ -11705,13 +12592,14 @@ var purify = {exports: {}};
11705
12592
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
11706
12593
  /* Execute a hook if present */
11707
12594
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
11708
- const {
11709
- attributes
11710
- } = currentNode;
12595
+ const attributes = currentNode.attributes;
11711
12596
  /* Check if we have attributes; if not we might have a text node */
11712
12597
  if (!attributes || _isClobbered(currentNode)) {
11713
12598
  return;
11714
12599
  }
12600
+ /* Same lazy-install guard as uponSanitizeElement (see there): fork the
12601
+ attribute allowlist off its shared binding before a hook can see it. */
12602
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
11715
12603
  const hookEvent = {
11716
12604
  attrName: '',
11717
12605
  attrValue: '',
@@ -11720,14 +12608,13 @@ var purify = {exports: {}};
11720
12608
  forceKeepAttr: undefined
11721
12609
  };
11722
12610
  let l = attributes.length;
12611
+ const lcTag = transformCaseFunc(currentNode.nodeName);
11723
12612
  /* Go backwards over all attributes; safely remove bad ones */
11724
12613
  while (l--) {
11725
12614
  const attr = attributes[l];
11726
- const {
11727
- name,
11728
- namespaceURI,
11729
- value: attrValue
11730
- } = attr;
12615
+ const name = attr.name,
12616
+ namespaceURI = attr.namespaceURI,
12617
+ attrValue = attr.value;
11731
12618
  const lcName = transformCaseFunc(name);
11732
12619
  const initValue = attrValue;
11733
12620
  let value = name === 'value' ? initValue : stringTrim(initValue);
@@ -11741,12 +12628,14 @@ var purify = {exports: {}};
11741
12628
  /* Full DOM Clobbering protection via namespace isolation,
11742
12629
  * Prefix id and name attributes with `user-content-`
11743
12630
  */
11744
- if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
12631
+ if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
11745
12632
  // Remove the attribute with this value
11746
12633
  _removeAttribute(name, currentNode);
11747
12634
  // Prefix the value and later re-create the attribute with the sanitized value
11748
12635
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
11749
12636
  }
12637
+ // Else: already prefixed, leave the attribute alone — the prefix is
12638
+ // itself the clobbering protection, and re-applying it is incorrect.
11750
12639
  /* Work around a security issue with comments inside attributes */
11751
12640
  if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
11752
12641
  _removeAttribute(name, currentNode);
@@ -11757,7 +12646,7 @@ var purify = {exports: {}};
11757
12646
  _removeAttribute(name, currentNode);
11758
12647
  continue;
11759
12648
  }
11760
- /* Did the hooks approve of the attribute? */
12649
+ /* Did the hooks force-keep the attribute? */
11761
12650
  if (hookEvent.forceKeepAttr) {
11762
12651
  continue;
11763
12652
  }
@@ -11767,56 +12656,24 @@ var purify = {exports: {}};
11767
12656
  continue;
11768
12657
  }
11769
12658
  /* Work around a security issue in jQuery 3.0 */
11770
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
12659
+ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
11771
12660
  _removeAttribute(name, currentNode);
11772
12661
  continue;
11773
12662
  }
11774
12663
  /* Sanitize attribute content to be template-safe */
11775
12664
  if (SAFE_FOR_TEMPLATES) {
11776
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
11777
- value = stringReplace(value, expr, ' ');
11778
- });
12665
+ value = _stripTemplateExpressions(value);
11779
12666
  }
11780
12667
  /* Is `value` valid for this attribute? */
11781
- const lcTag = transformCaseFunc(currentNode.nodeName);
11782
12668
  if (!_isValidAttribute(lcTag, lcName, value)) {
11783
12669
  _removeAttribute(name, currentNode);
11784
12670
  continue;
11785
12671
  }
11786
12672
  /* Handle attributes that require Trusted Types */
11787
- if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
11788
- if (namespaceURI) ; else {
11789
- switch (trustedTypes.getAttributeType(lcTag, lcName)) {
11790
- case 'TrustedHTML':
11791
- {
11792
- value = trustedTypesPolicy.createHTML(value);
11793
- break;
11794
- }
11795
- case 'TrustedScriptURL':
11796
- {
11797
- value = trustedTypesPolicy.createScriptURL(value);
11798
- break;
11799
- }
11800
- }
11801
- }
11802
- }
12673
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
11803
12674
  /* Handle invalid data-* attribute set by try-catching it */
11804
12675
  if (value !== initValue) {
11805
- try {
11806
- if (namespaceURI) {
11807
- currentNode.setAttributeNS(namespaceURI, name, value);
11808
- } else {
11809
- /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
11810
- currentNode.setAttribute(name, value);
11811
- }
11812
- if (_isClobbered(currentNode)) {
11813
- _forceRemove(currentNode);
11814
- } else {
11815
- arrayPop(DOMPurify.removed);
11816
- }
11817
- } catch (_) {
11818
- _removeAttribute(name, currentNode);
11819
- }
12676
+ _setAttributeValue(currentNode, name, namespaceURI, value);
11820
12677
  }
11821
12678
  }
11822
12679
  /* Execute a hook if present */
@@ -11836,17 +12693,136 @@ var purify = {exports: {}};
11836
12693
  /* Execute a hook if present */
11837
12694
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
11838
12695
  /* Sanitize tags and elements */
11839
- _sanitizeElements(shadowNode);
12696
+ _sanitizeElements(shadowNode, fragment);
11840
12697
  /* Check attributes next */
11841
12698
  _sanitizeAttributes(shadowNode);
11842
- /* Deep shadow DOM detected */
11843
- if (shadowNode.content instanceof DocumentFragment) {
12699
+ /* Deep shadow DOM detected.
12700
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType against the
12701
+ DOCUMENT_FRAGMENT_NODE constant rather than instanceof, so we
12702
+ recurse into <template>.content from foreign realms too. */
12703
+ if (_isDocumentFragment(shadowNode.content)) {
11844
12704
  _sanitizeShadowDOM2(shadowNode.content);
11845
12705
  }
12706
+ /* An element iterated here may itself host an attached
12707
+ shadow root. The default NodeIterator does not enter shadow
12708
+ trees, so a shadow root nested inside template.content was
12709
+ previously reached by no walk at all (the pre-pass at
12710
+ _sanitizeAttachedShadowRoots descends via childNodes, which
12711
+ doesn't enter template.content; the template-content recursion
12712
+ above iterates the content but never inspected shadowRoot).
12713
+ Walk it explicitly. The nodeType guard avoids reading
12714
+ shadowRoot off text / comment / CDATA / PI nodes that the
12715
+ iterator also surfaces. */
12716
+ const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
12717
+ if (shadowNodeType === NODE_TYPE.element) {
12718
+ const innerSr = getShadowRoot(shadowNode);
12719
+ if (_isDocumentFragment(innerSr)) {
12720
+ _sanitizeAttachedShadowRoots(innerSr);
12721
+ _sanitizeShadowDOM2(innerSr);
12722
+ }
12723
+ }
11846
12724
  }
11847
12725
  /* Execute a hook if present */
11848
12726
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
11849
12727
  };
12728
+ /**
12729
+ * _sanitizeAttachedShadowRoots
12730
+ *
12731
+ * Walks `root` and feeds every attached shadow root we encounter into
12732
+ * the existing _sanitizeShadowDOM pipeline. The default node iterator
12733
+ * does not descend into shadow trees, so nodes inside an attached
12734
+ * shadow root would otherwise be skipped entirely.
12735
+ *
12736
+ * Two real input paths put attached shadow roots in front of us:
12737
+ * 1. IN_PLACE on a DOM node that already has shadow roots attached.
12738
+ * 2. DOM-node input where importNode(dirty, true) deep-clones the
12739
+ * shadow root because it was created with `clonable: true`.
12740
+ *
12741
+ * This pass runs once, up front, so the main iteration loop (and the
12742
+ * existing _sanitizeShadowDOM template-content recursion) stay
12743
+ * untouched — string-input paths are not affected.
12744
+ *
12745
+ * @param root the subtree root to walk for attached shadow roots
12746
+ */
12747
+ const _sanitizeAttachedShadowRoots = function _sanitizeAttachedShadowRoots(root) {
12748
+ /* Iterative (explicit stack) rather than per-child recursion. DOM APIs
12749
+ impose no depth cap, so an attacker-shaped tree (JSON/CRDT/editor data
12750
+ built straight into the DOM — the IN_PLACE surface) deeper than the JS
12751
+ call-stack budget would otherwise overflow native recursion here and
12752
+ throw at the IN_PLACE entry pre-pass, before a single node is
12753
+ sanitized, leaving the caller's live tree untouched (fail-open). See
12754
+ campaign-3 F4. A heap stack keeps depth off the call stack.
12755
+ Each work item is either a node to descend into, or a deferred
12756
+ `_sanitizeShadowDOM` for an already-walked shadow root. The deferred
12757
+ form preserves the original post-order discipline: a shadow root's
12758
+ nested shadow roots are discovered before the outer shadow is
12759
+ sanitized (which may remove hosts). Pushes are in reverse of the
12760
+ desired processing order (LIFO): template content, then children, then
12761
+ the shadow-sanitize, then the shadow walk — so the order matches the
12762
+ previous recursion exactly. */
12763
+ const stack = [{
12764
+ node: root,
12765
+ shadow: null
12766
+ }];
12767
+ while (stack.length > 0) {
12768
+ const item = stack.pop();
12769
+ /* Deferred shadow-DOM sanitisation: runs after its subtree was walked. */
12770
+ if (item.shadow) {
12771
+ _sanitizeShadowDOM2(item.shadow);
12772
+ continue;
12773
+ }
12774
+ const node = item.node;
12775
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
12776
+ const isElement = nodeType === NODE_TYPE.element;
12777
+ /* (pushed last → processed first) Children, snapshotted in reverse so
12778
+ the first child is processed first. Snapshotting matters because a
12779
+ hook may detach siblings mid-walk. */
12780
+ const childNodes = getChildNodes(node);
12781
+ if (childNodes) {
12782
+ for (let i = childNodes.length - 1; i >= 0; --i) {
12783
+ stack.push({
12784
+ node: childNodes[i],
12785
+ shadow: null
12786
+ });
12787
+ }
12788
+ }
12789
+ /* (pushed before children → processed after them, matching the old
12790
+ "template content last" order) When the node is a <template>,
12791
+ descend into its content. */
12792
+ if (isElement) {
12793
+ const rootName = getNodeName ? getNodeName(node) : null;
12794
+ if (typeof rootName === 'string' && transformCaseFunc(rootName) === 'template') {
12795
+ const content = node.content;
12796
+ if (_isDocumentFragment(content)) {
12797
+ stack.push({
12798
+ node: content,
12799
+ shadow: null
12800
+ });
12801
+ }
12802
+ }
12803
+ }
12804
+ /* Shadow root (processed first): walk its subtree, then sanitise it.
12805
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
12806
+ rather than `instanceof DocumentFragment`, which is realm-bound and
12807
+ silently skipped foreign-realm shadow roots (e.g.
12808
+ iframe.contentDocument attachShadow). */
12809
+ if (isElement) {
12810
+ const sr = getShadowRoot(node);
12811
+ if (_isDocumentFragment(sr)) {
12812
+ /* Push the deferred sanitise first so it pops after the shadow
12813
+ walk we push next, i.e. nested shadow roots are discovered
12814
+ before this one is sanitised. */
12815
+ stack.push({
12816
+ node: null,
12817
+ shadow: sr
12818
+ }, {
12819
+ node: sr,
12820
+ shadow: null
12821
+ });
12822
+ }
12823
+ }
12824
+ }
12825
+ };
11850
12826
  // eslint-disable-next-line complexity
11851
12827
  DOMPurify.sanitize = function (dirty) {
11852
12828
  let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -11863,13 +12839,9 @@ var purify = {exports: {}};
11863
12839
  }
11864
12840
  /* Stringify, in case dirty is an object */
11865
12841
  if (typeof dirty !== 'string' && !_isNode(dirty)) {
11866
- if (typeof dirty.toString === 'function') {
11867
- dirty = dirty.toString();
11868
- if (typeof dirty !== 'string') {
11869
- throw typeErrorCreate('dirty is not a string, aborting');
11870
- }
11871
- } else {
11872
- throw typeErrorCreate('toString is not a function');
12842
+ dirty = stringifyValue(dirty);
12843
+ if (typeof dirty !== 'string') {
12844
+ throw typeErrorCreate('dirty is not a string, aborting');
11873
12845
  }
11874
12846
  }
11875
12847
  /* Return dirty HTML if DOMPurify cannot run */
@@ -11877,24 +12849,90 @@ var purify = {exports: {}};
11877
12849
  return dirty;
11878
12850
  }
11879
12851
  /* Assign config vars */
11880
- if (!SET_CONFIG) {
12852
+ if (SET_CONFIG) {
12853
+ /* Persistent setConfig() path: _parseConfig is skipped, so the sets are
12854
+ * not re-derived per call. Restore them from the pristine bindings
12855
+ * captured at setConfig() time so a previous call's hook clone (mutated
12856
+ * below) does not carry over. */
12857
+ ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
12858
+ ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
12859
+ } else {
11881
12860
  _parseConfig(cfg);
11882
12861
  }
12862
+ /* Clone the hook-mutable allowlists before the walk whenever an
12863
+ * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
12864
+ * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
12865
+ * a hook that widens them would otherwise mutate the shared set
12866
+ * permanently: across later calls and across every element. Cloning per
12867
+ * walk keeps documented in-call widening working while scoping it to the
12868
+ * call. A single guard for both config paths - the per-call path rebinds
12869
+ * the sets in _parseConfig each call, the persistent path restores them
12870
+ * from the captured bindings just above - so the two cannot diverge. */
12871
+ if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) {
12872
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
12873
+ }
12874
+ if (hooks.uponSanitizeAttribute.length > 0) {
12875
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
12876
+ }
11883
12877
  /* Clean up removed elements */
11884
12878
  DOMPurify.removed = [];
11885
- /* Check if dirty is correctly typed for IN_PLACE */
11886
- if (typeof dirty === 'string') {
11887
- IN_PLACE = false;
11888
- }
11889
- if (IN_PLACE) {
11890
- /* Do some early pre-sanitization to avoid unsafe root nodes */
11891
- if (dirty.nodeName) {
11892
- const tagName = transformCaseFunc(dirty.nodeName);
12879
+ /* Resolve IN_PLACE for this call without mutating persistent config.
12880
+ Writing the IN_PLACE closure variable here leaks under setConfig(),
12881
+ where _parseConfig is skipped on later calls: a single string call would
12882
+ disable in-place mode for every subsequent node call, returning a
12883
+ sanitized copy while leaving the caller's node — which in-place callers
12884
+ keep using and whose return value they ignore — unsanitized. REPORT-2. */
12885
+ const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
12886
+ if (inPlace) {
12887
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
12888
+ linkage across the live tree BEFORE the walk, so no patch can fire
12889
+ mid-walk and inject into an already-processed region. Runs first, so
12890
+ it also covers the forbidden/clobbered roots that throw below. */
12891
+ _neutralizePatchLinkage(dirty);
12892
+ /* Do some early pre-sanitization to avoid unsafe root nodes.
12893
+ Read nodeName through the cached prototype getter — a clobbering
12894
+ child named "nodeName" on the form root would otherwise shadow
12895
+ the property and let this check skip the root-allowlist
12896
+ validation entirely. */
12897
+ const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
12898
+ if (typeof nn === 'string') {
12899
+ const tagName = transformCaseFunc(nn);
11893
12900
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
12901
+ /* Fail closed on a live root: neutralize handlers/children before
12902
+ throwing, exactly as the mid-walk abort path does. */
12903
+ _neutralizeRoot(dirty);
11894
12904
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
11895
12905
  }
11896
12906
  }
11897
- } else if (dirty instanceof Node) {
12907
+ /* Pre-flight the root through _isClobbered. The iterator-driven
12908
+ removal path can not detach a parent-less root: _forceRemove
12909
+ falls through to Element.prototype.remove(), which per spec
12910
+ is a no-op on a node with no parent. A clobbered root would
12911
+ then survive the main loop with its attributes uninspected,
12912
+ because _sanitizeAttributes early-returns on _isClobbered. The
12913
+ result would be an attacker-controlled form, complete with any
12914
+ event-handler attributes the caller passed in, handed back to
12915
+ the application unsanitized. Refuse to sanitize such a root
12916
+ the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
12917
+ if (_isClobbered(dirty)) {
12918
+ /* Fail closed on a live clobbered root before throwing.
12919
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
12920
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
12921
+ _neutralizeRoot(dirty);
12922
+ throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
12923
+ }
12924
+ /* Sanitize attached shadow roots before the main iterator runs.
12925
+ The iterator does not descend into shadow trees. Same fail-closed
12926
+ barrier as the main walk (campaign-3 F2): a custom-element reaction
12927
+ inside a shadow root could abort this pre-pass before the walk runs,
12928
+ which would otherwise leave the entire live tree unsanitized. */
12929
+ try {
12930
+ _sanitizeAttachedShadowRoots(dirty);
12931
+ } catch (error) {
12932
+ _neutralizeRoot(dirty);
12933
+ throw error;
12934
+ }
12935
+ } else if (_isNode(dirty)) {
11898
12936
  /* If dirty is a DOM element, append to an empty document to avoid
11899
12937
  elements being stripped by the parser */
11900
12938
  body = _initDocument('<!---->');
@@ -11908,12 +12946,18 @@ var purify = {exports: {}};
11908
12946
  // eslint-disable-next-line unicorn/prefer-dom-node-append
11909
12947
  body.appendChild(importedNode);
11910
12948
  }
12949
+ /* Clonable shadow roots are deep-cloned by importNode(); sanitize
12950
+ them before the main iterator runs, since the iterator does not
12951
+ descend into shadow trees. The walk routes every read through a
12952
+ cached prototype getter so clobbering descendants on a form root
12953
+ cannot hide a shadow host from this pass. */
12954
+ _sanitizeAttachedShadowRoots(importedNode);
11911
12955
  } else {
11912
12956
  /* Exit directly if we have nothing to do */
11913
12957
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
11914
12958
  // eslint-disable-next-line unicorn/prefer-includes
11915
12959
  dirty.indexOf('<') === -1) {
11916
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
12960
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
11917
12961
  }
11918
12962
  /* Initialize the document to work on */
11919
12963
  body = _initDocument(dirty);
@@ -11927,31 +12971,71 @@ var purify = {exports: {}};
11927
12971
  _forceRemove(body.firstChild);
11928
12972
  }
11929
12973
  /* Get node iterator */
11930
- const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
11931
- /* Now start iterating over the created document */
11932
- while (currentNode = nodeIterator.nextNode()) {
11933
- /* Sanitize tags and elements */
11934
- _sanitizeElements(currentNode);
11935
- /* Check attributes next */
11936
- _sanitizeAttributes(currentNode);
11937
- /* Shadow DOM detected, sanitize it */
11938
- if (currentNode.content instanceof DocumentFragment) {
11939
- _sanitizeShadowDOM2(currentNode.content);
12974
+ const walkRoot = inPlace ? dirty : body;
12975
+ /* Now start iterating over the created document.
12976
+ The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
12977
+ engine/custom-element mutation can detach a node mid-walk so
12978
+ `_forceRemove`'s parentless guard throws, aborting the loop. Without the
12979
+ barrier the caller's in-place tree would be left half-sanitized with the
12980
+ unvisited tail still armed. _createNodeIterator itself is inside the
12981
+ barrier too: constructing the iterator dereferences the root's document,
12982
+ and any failure there (e.g. an exotic/clobbered root) must still fail
12983
+ closed rather than skip the neutralize. On any throw we fail closed -
12984
+ strip the in-place root bare - then rethrow so the existing throw
12985
+ contract is preserved. (String/DOM-copy paths never return the partial
12986
+ body, so the propagating throw is already fail-closed there.) */
12987
+ try {
12988
+ const nodeIterator = _createNodeIterator(walkRoot);
12989
+ while (currentNode = nodeIterator.nextNode()) {
12990
+ /* Sanitize tags and elements */
12991
+ _sanitizeElements(currentNode, walkRoot);
12992
+ /* Check attributes next */
12993
+ _sanitizeAttributes(currentNode);
12994
+ /* Shadow DOM detected, sanitize it.
12995
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
12996
+ instead of instanceof, so foreign-realm <template>.content is
12997
+ walked correctly. */
12998
+ if (_isDocumentFragment(currentNode.content)) {
12999
+ _sanitizeShadowDOM2(currentNode.content);
13000
+ }
13001
+ }
13002
+ } catch (error) {
13003
+ if (inPlace) {
13004
+ _neutralizeRoot(dirty);
13005
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
13006
+ detached from the root, so _neutralizeRoot's subtree pass does not
13007
+ reach them. Defuse them too, mirroring the success-path loop below. */
13008
+ arrayForEach(DOMPurify.removed, entry => {
13009
+ if (entry.element) {
13010
+ _neutralizeSubtree(entry.element);
13011
+ }
13012
+ });
11940
13013
  }
13014
+ throw error;
11941
13015
  }
11942
13016
  /* If we sanitized `dirty` in-place, return it. */
11943
- if (IN_PLACE) {
13017
+ if (inPlace) {
13018
+ /* Fail-closed completion of the audit-5 F1 fix: every node removed from
13019
+ the caller's live tree is detached but may still hold a queued
13020
+ resource-event handler that fires in page scope after we return. The
13021
+ move-hoist covers only disallowed-tag KEEP_CONTENT removals; strip the
13022
+ non-allow-listed attributes off every other removed subtree (clobber,
13023
+ mXSS, namespace, comments, KEEP_CONTENT:false, …) so those handlers are
13024
+ cancelled before any event can fire. Runs synchronously, pre-return. */
13025
+ arrayForEach(DOMPurify.removed, entry => {
13026
+ if (entry.element) {
13027
+ _neutralizeSubtree(entry.element);
13028
+ }
13029
+ });
13030
+ if (SAFE_FOR_TEMPLATES) {
13031
+ _scrubTemplateExpressions2(dirty);
13032
+ }
11944
13033
  return dirty;
11945
13034
  }
11946
13035
  /* Return sanitized string or DOM */
11947
13036
  if (RETURN_DOM) {
11948
13037
  if (SAFE_FOR_TEMPLATES) {
11949
- body.normalize();
11950
- let html = body.innerHTML;
11951
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
11952
- html = stringReplace(html, expr, ' ');
11953
- });
11954
- body.innerHTML = html;
13038
+ _scrubTemplateExpressions2(body);
11955
13039
  }
11956
13040
  if (RETURN_DOM_FRAGMENT) {
11957
13041
  returnNode = createDocumentFragment.call(body.ownerDocument);
@@ -11981,20 +13065,28 @@ var purify = {exports: {}};
11981
13065
  }
11982
13066
  /* Sanitize final string template-safe */
11983
13067
  if (SAFE_FOR_TEMPLATES) {
11984
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
11985
- serializedHTML = stringReplace(serializedHTML, expr, ' ');
11986
- });
13068
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
11987
13069
  }
11988
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
13070
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
11989
13071
  };
11990
13072
  DOMPurify.setConfig = function () {
11991
13073
  let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
11992
13074
  _parseConfig(cfg);
11993
13075
  SET_CONFIG = true;
13076
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
13077
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
11994
13078
  };
11995
13079
  DOMPurify.clearConfig = function () {
11996
13080
  CONFIG = null;
11997
13081
  SET_CONFIG = false;
13082
+ SET_CONFIG_ALLOWED_TAGS = null;
13083
+ SET_CONFIG_ALLOWED_ATTR = null;
13084
+ // Drop any caller-supplied Trusted Types policy so it cannot poison later
13085
+ // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
13086
+ // never recreated — Trusted Types throws on duplicate names) is restored by
13087
+ // the next `_parseConfig`. See GHSA-vxr8-fq34-vvx9.
13088
+ trustedTypesPolicy = defaultTrustedTypesPolicy;
13089
+ emptyHTML = '';
11998
13090
  };
11999
13091
  DOMPurify.isValidAttribute = function (tag, attr, value) {
12000
13092
  /* Initialize shared config vars if necessary. */
@@ -12009,9 +13101,19 @@ var purify = {exports: {}};
12009
13101
  if (typeof hookFunction !== 'function') {
12010
13102
  return;
12011
13103
  }
13104
+ /* Reject unknown entry points. Without this, a non-hook key (e.g.
13105
+ * '__proto__') indexes off the prototype chain rather than a real
13106
+ * hook array, and arrayPush then writes to Object.prototype. Guard
13107
+ * with an own-property check against the known hook names. */
13108
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
13109
+ return;
13110
+ }
12012
13111
  arrayPush(hooks[entryPoint], hookFunction);
12013
13112
  };
12014
13113
  DOMPurify.removeHook = function (entryPoint, hookFunction) {
13114
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
13115
+ return undefined;
13116
+ }
12015
13117
  if (hookFunction !== undefined) {
12016
13118
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
12017
13119
  return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
@@ -12019,6 +13121,9 @@ var purify = {exports: {}};
12019
13121
  return arrayPop(hooks[entryPoint]);
12020
13122
  };
12021
13123
  DOMPurify.removeHooks = function (entryPoint) {
13124
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
13125
+ return;
13126
+ }
12022
13127
  hooks[entryPoint] = [];
12023
13128
  };
12024
13129
  DOMPurify.removeAllHooks = function () {
@@ -13198,7 +14303,7 @@ class MenuGroup {
13198
14303
  }; }
13199
14304
  }
13200
14305
 
13201
- const icMenuItemCss = "/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%;}body{margin:0}main{display:block}h1{font-size:2em;margin:0.67em 0}hr{box-sizing:content-box;height:0;overflow:visible;}pre{font-family:monospace, monospace;font-size:1em;}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em;}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0;}button,input{overflow:visible}button,select{text-transform:none}button,[type=\"button\"],[type=\"reset\"],[type=\"submit\"]{-webkit-appearance:button}button::-moz-focus-inner,[type=\"button\"]::-moz-focus-inner,[type=\"reset\"]::-moz-focus-inner,[type=\"submit\"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=\"button\"]:-moz-focusring,[type=\"reset\"]:-moz-focusring,[type=\"submit\"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal;}progress{vertical-align:baseline}textarea{overflow:auto}[type=\"checkbox\"],[type=\"radio\"]{box-sizing:border-box;padding:0;}[type=\"number\"]::-webkit-inner-spin-button,[type=\"number\"]::-webkit-outer-spin-button{height:auto}[type=\"search\"]{-webkit-appearance:textfield;outline-offset:-2px;}[type=\"search\"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit;}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font-style:inherit;vertical-align:baseline}:host ::part(button){color:var(--ic-popover-menu-item-check);--ic-typography-color:var(--ic-popover-menu-item-check);padding:calc(var(--ic-space-xxxs) / 2);display:flex;align-items:flex-start;gap:var(--ic-space-xs);border-radius:0;min-height:2.5rem;height:auto}.focus-border{display:flex;align-items:flex-start;gap:var(--ic-space-xs);border-radius:var(--ic-border-radius);margin:calc(var(--ic-space-xxxs) / 2);padding:var(--ic-space-xxs);border:var(--ic-space-xxxs) dashed transparent;--icon-height:auto}:host(.ic-menu-item-disabled) ::part(button){color:var(--ic-popover-menu-item-check-disabled) !important;--ic-typography-color:var(--ic-popover-menu-item-check-disabled) !important;pointer-events:none}:host(.ic-menu-item-disabled) .menu-item-label,:host(.ic-menu-item-disabled) .shortcut,:host(.ic-menu-item-disabled) .menu-item-description{--ic-typography-color:var(--ic-popover-menu-item-label-disabled) !important}:host(.ic-menu-item-variant-destructive) ::part(button){color:var(--ic-popover-menu-destructive-item-icon);--ic-typography-color:var(--ic-popover-menu-destructive-item-label)}:host ::part(button):hover{background-color:var(--ic-popover-background-hover)}:host ::part(button):focus-visible,:host ::part(button):focus{color:var(--ic-popover-menu-item-icon-focused);--ic-typography-color:purple;background-color:var(--ic-popover-background-focus);box-shadow:none}:host(.ic-menu-item-disabled) ::part(button):focus-visible,:host(.ic-menu-item-disabled) ::part(button):focus{color:var(--ic-popover-menu-item-icon-disabled) !important;--ic-typography-color:orange !important}:host(.ic-menu-item-variant-destructive) ::part(button):focus-visible,:host(.ic-menu-item-variant-destructive) ::part(button):focus{background-color:var(--ic-popover-destructive-background-focus)}ic-button:focus-within .menu-item-label,ic-button:focus-within .shortcut,ic-button:focus-within .menu-item-description{--ic-typography-color:var(--ic-popover-menu-item-label-focused)}:host(.ic-menu-item-disabled) ic-button:focus-within .focus-border{border-color:var(--ic-popover-menu-border)}:host(.ic-menu-item-disabled) ic-button:focus-within .menu-item-label,:host(.ic-menu-item-disabled) ic-button:focus-within .shortcut,:host(.ic-menu-item-disabled) ic-button:focus-within .menu-item-description{--ic-typography-color:var(--ic-popover-menu-item-label-disabled) !important}:host ::part(button):active{background-color:var(--ic-popover-background-pressed);color:var(--ic-popover-menu-item-label) !important;--ic-typography-color:var(--ic-popover-menu-item-label) !important}:host(.ic-menu-item-variant-destructive) ::part(button):active{background-color:var(--ic-popover-background-pressed);color:var(--ic-popover-menu-destructive-item-label) !important;--ic-typography-color:var(\n --ic-popover-menu-destructive-item-label\n ) !important}.menu-item-info{display:flex;flex-direction:column;flex:1;align-items:baseline;white-space:pre-line;text-align:left}.menu-labels{display:flex;align-items:baseline;gap:var(--ic-space-xs)}.menu-item-description{--ic-typography-color:var(--ic-popover-menu-item-description)}.shortcut{--ic-typography-color:var(--ic-popover-menu-item-shortcut);display:flex;flex:min-content}:host(.ic-menu-item-variant-destructive) .menu-item-label{text-transform:uppercase;font-weight:600;letter-spacing:0.025em}ic-button:active .menu-item-label{--ic-typography-color:var(--ic-popover-menu-item-label)}ic-button:active .menu-item-description{--ic-typography-color:var(--ic-popover-menu-item-description)}ic-button:active .shortcut{--ic-typography-color:var(--ic-popover-menu-item-shortcut)}:host(.ic-menu-item-variant-destructive) ic-button:active .menu-item-label{--ic-typography-color:var(\n --ic-popover-menu-destructive-item-label\n ) !important}.icon,.check-icon,.submenu-icon,.icon ::slotted(svg),.check-icon svg,.submenu-icon svg{width:var(--ic-space-lg);height:var(--ic-space-lg)}.check-icon.hide{visibility:hidden}@media (forced-colors: active){:host ::part(button){border:canvas}:host ::part(button):focus-visible{outline:var(--ic-space-xxxs) solid transparent}.focus-border{border-color:canvas}:host(.ic-menu-item-disabled) ic-button:focus-within .focus-border{border-color:GrayText}:host(.ic-menu-item-disabled) .menu-item-info,:host(.ic-menu-item-disabled) .menu-item-label,:host(.ic-menu-item-disabled) .shortcut,:host(.ic-menu-item-disabled) .menu-item-description,:host(.ic-menu-item-disabled) ic-button:focus-within .menu-item-info,:host(.ic-menu-item-disabled) ic-button:focus-within .menu-item-label,:host(.ic-menu-item-disabled) ic-button:focus-within .shortcut,:host(.ic-menu-item-disabled) ic-button:focus-within .menu-item-description{--ic-typography-color:GrayText !important}:host(.ic-menu-item-disabled) ::part(button):focus-visible,:host(.ic-menu-item-disabled) ::part(button):focus{outline-color:GrayText !important}}";
14306
+ const icMenuItemCss = "/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%;}body{margin:0}main{display:block}h1{font-size:2em;margin:0.67em 0}hr{box-sizing:content-box;height:0;overflow:visible;}pre{font-family:monospace, monospace;font-size:1em;}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em;}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0;}button,input{overflow:visible}button,select{text-transform:none}button,[type=\"button\"],[type=\"reset\"],[type=\"submit\"]{-webkit-appearance:button}button::-moz-focus-inner,[type=\"button\"]::-moz-focus-inner,[type=\"reset\"]::-moz-focus-inner,[type=\"submit\"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=\"button\"]:-moz-focusring,[type=\"reset\"]:-moz-focusring,[type=\"submit\"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal;}progress{vertical-align:baseline}textarea{overflow:auto}[type=\"checkbox\"],[type=\"radio\"]{box-sizing:border-box;padding:0;}[type=\"number\"]::-webkit-inner-spin-button,[type=\"number\"]::-webkit-outer-spin-button{height:auto}[type=\"search\"]{-webkit-appearance:textfield;outline-offset:-2px;}[type=\"search\"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit;}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font-style:inherit;vertical-align:baseline}:host ::part(button){color:var(--ic-popover-menu-item-check);--ic-typography-color:var(--ic-popover-menu-item-check);padding:calc(var(--ic-space-xxxs) / 2);display:flex;align-items:flex-start;gap:var(--ic-space-xs);border-radius:0;min-height:2.5rem;height:auto}.focus-border{display:flex;align-items:flex-start;gap:var(--ic-space-xs);border-radius:var(--ic-border-radius);margin:calc(var(--ic-space-xxxs) / 2);padding:var(--ic-space-xxs);border:var(--ic-space-xxxs) dashed transparent;--icon-height:auto}:host(.ic-menu-item-disabled) ::part(button){color:var(--ic-popover-menu-item-check-disabled) !important;--ic-typography-color:var(--ic-popover-menu-item-check-disabled) !important;pointer-events:none}:host(.ic-menu-item-disabled) .menu-item-label,:host(.ic-menu-item-disabled) .shortcut,:host(.ic-menu-item-disabled) .menu-item-description{--ic-typography-color:var(--ic-popover-menu-item-label-disabled) !important}:host(.ic-menu-item-variant-destructive) ::part(button){color:var(--ic-popover-menu-destructive-item-icon);--ic-typography-color:var(--ic-popover-menu-destructive-item-label)}:host(.ic-menu-item-variant-destructive) ::part(button):focus{background-color:var(--ic-popover-destructive-background-focus)}:host ::part(button):hover{background-color:var(--ic-popover-background-hover)}:host ::part(button):focus{box-shadow:none}:host ::part(button):focus,:host ::part(button):focus-visible{color:var(--ic-popover-menu-item-icon-focused);--ic-typography-color:var(--ic-popover-menu-item-label-focused);background-color:var(--ic-popover-background-focus)}ic-button:focus-within .menu-item-label,ic-button:focus-within .shortcut,ic-button:focus-within .menu-item-description{--ic-typography-color:var(--ic-popover-menu-item-label-focused)}:host(.ic-menu-item-disabled) ic-button:focus-within .focus-border{border-color:var(--ic-popover-menu-border)}:host(.ic-menu-item-disabled) ic-button:focus-within .menu-item-label,:host(.ic-menu-item-disabled) ic-button:focus-within .shortcut,:host(.ic-menu-item-disabled) ic-button:focus-within .menu-item-description{--ic-typography-color:var(--ic-popover-menu-item-label-disabled) !important}:host ::part(button):active{background-color:var(--ic-popover-background-pressed);color:var(--ic-popover-menu-item-label) !important;--ic-typography-color:var(--ic-popover-menu-item-label) !important}:host(.ic-menu-item-variant-destructive) ::part(button):active{background-color:var(--ic-popover-background-pressed);color:var(--ic-popover-menu-destructive-item-label) !important;--ic-typography-color:var(\n --ic-popover-menu-destructive-item-label\n ) !important}.menu-item-info{display:flex;flex-direction:column;flex:1;align-items:baseline;white-space:pre-line;text-align:left}.menu-labels{display:flex;align-items:baseline;gap:var(--ic-space-xs)}.menu-item-description{--ic-typography-color:var(--ic-popover-menu-item-description)}.shortcut{--ic-typography-color:var(--ic-popover-menu-item-shortcut);display:flex;flex:min-content}:host(.ic-menu-item-variant-destructive) .menu-item-label{text-transform:uppercase;font-weight:600;letter-spacing:0.025em}ic-button:active .menu-item-label{--ic-typography-color:var(--ic-popover-menu-item-label)}ic-button:active .menu-item-description{--ic-typography-color:var(--ic-popover-menu-item-description)}ic-button:active .shortcut{--ic-typography-color:var(--ic-popover-menu-item-shortcut)}:host(.ic-menu-item-variant-destructive) ic-button:active .menu-item-label{--ic-typography-color:var(\n --ic-popover-menu-destructive-item-label\n ) !important}.icon,.check-icon,.submenu-icon,.icon ::slotted(svg),.check-icon svg,.submenu-icon svg{width:var(--ic-space-lg);height:var(--ic-space-lg)}.check-icon.hide{visibility:hidden}@media (forced-colors: active){:host ::part(button){border:canvas}:host ::part(button):focus-visible{outline:var(--ic-space-xxxs) solid transparent}.focus-border{border-color:canvas}:host(.ic-menu-item-disabled) ic-button:focus-within .focus-border{border-color:GrayText}:host(.ic-menu-item-disabled) .menu-item-info,:host(.ic-menu-item-disabled) .menu-item-label,:host(.ic-menu-item-disabled) .shortcut,:host(.ic-menu-item-disabled) .menu-item-description,:host(.ic-menu-item-disabled) ic-button:focus-within .menu-item-info,:host(.ic-menu-item-disabled) ic-button:focus-within .menu-item-label,:host(.ic-menu-item-disabled) ic-button:focus-within .shortcut,:host(.ic-menu-item-disabled) ic-button:focus-within .menu-item-description{--ic-typography-color:GrayText !important}:host(.ic-menu-item-disabled) ::part(button):focus-visible,:host(.ic-menu-item-disabled) ::part(button):focus{outline-color:GrayText !important}}";
13202
14307
  var IcMenuItemStyle0 = icMenuItemCss;
13203
14308
 
13204
14309
  /**
@@ -13223,6 +14328,10 @@ class MenuItem {
13223
14328
  * The variant of the menu item.
13224
14329
  */
13225
14330
  this.variant = "default";
14331
+ /**
14332
+ * If `true`, the menu will close when this menu item is clicked.
14333
+ */
14334
+ this.closeMenuOnSelect = true;
13226
14335
  this.handleClick = (e) => {
13227
14336
  if (isPropDefined(this.submenuTriggerFor)) {
13228
14337
  this.triggerPopoverMenuInstance.emit();
@@ -13323,7 +14432,8 @@ class MenuItem {
13323
14432
  "rel": [1],
13324
14433
  "submenuTriggerFor": [1, "submenu-trigger-for"],
13325
14434
  "target": [1],
13326
- "variant": [1537]
14435
+ "variant": [1537],
14436
+ "closeMenuOnSelect": [4, "close-menu-on-select"]
13327
14437
  },
13328
14438
  "$listeners$": [[2, "click", "handleHostClick"]],
13329
14439
  "$lazyBundleId$": "-",
@@ -13365,11 +14475,14 @@ class NavigationButton {
13365
14475
  */
13366
14476
  this.hostMutationCallback = (mutationList) => {
13367
14477
  mutationList.forEach(({ attributeName }) => {
13368
- if (attributeName) {
14478
+ if (attributeName && MUTABLE_ATTRIBUTES$2.includes(attributeName)) {
13369
14479
  const attribute = this.el.getAttribute(attributeName);
13370
- if (attribute && MUTABLE_ATTRIBUTES$2.includes(attributeName)) {
14480
+ if (attribute) {
13371
14481
  this.inheritedAttributes[attributeName] = attribute;
13372
14482
  }
14483
+ else {
14484
+ delete this.inheritedAttributes[attributeName];
14485
+ }
13373
14486
  }
13374
14487
  });
13375
14488
  };
@@ -13433,11 +14546,11 @@ class NavigationButton {
13433
14546
  target,
13434
14547
  theme,
13435
14548
  };
13436
- return (hAsync(Host, { key: '538ea349b1f0791b0cd68898380fe4cfd41b5496', class: {
14549
+ return (hAsync(Host, { key: 'dd30f288d728c8e8dffd4108d05e0a02b782d9ea', class: {
13437
14550
  "in-side-menu": isMenuMode,
13438
14551
  [`ic-theme-${theme}`]: theme !== "inherit",
13439
14552
  dark: parentThemeDark,
13440
- } }, hAsync("ic-button", Object.assign({ key: 'afee63c46529276cd2b77ef6d7c1c69b47a7ae15', ref: (el) => (this.buttonEl = el), class: { "popout-menu-button": isMenuMode }, "aria-label": !isMenuMode ? label : null, disableTooltip: isMenuMode, fullWidth: isMenuMode, monochrome: !isMenuMode, size: isMenuMode ? "medium" : "large", variant: isMenuMode ? "tertiary" : "icon-tertiary" }, buttonProps, inheritedAttributes), isMenuMode ? label : "", hAsync("slot", { key: '8c2a89dfc8f974017cf5384355ce0f881218d038', slot: "left-icon", name: "icon" }), isSlotUsed(this.el, "badge") && hAsync("slot", { key: '7fab4237f3f6745159d584df008e82214ecd3322', name: "badge" }))));
14553
+ } }, hAsync("ic-button", Object.assign({ key: 'b59e4538abe478aa21617a1ce3347668c3859e40', ref: (el) => (this.buttonEl = el), class: { "popout-menu-button": isMenuMode }, "aria-label": !isMenuMode ? label : null, disableTooltip: isMenuMode, fullWidth: isMenuMode, monochrome: !isMenuMode, size: isMenuMode ? "medium" : "large", variant: isMenuMode ? "tertiary" : "icon-tertiary" }, buttonProps, inheritedAttributes), isMenuMode ? label : "", hAsync("slot", { key: '4ca3226dd11288f30f595d8b0fa8484838b527c4', slot: "left-icon", name: "icon" }), isSlotUsed(this.el, "badge") && hAsync("slot", { key: '89408c559f0b62b9f92686bbc14a5312f8e6c2c6', name: "badge" }))));
13441
14554
  }
13442
14555
  static get delegatesFocus() { return true; }
13443
14556
  get el() { return getElement(this); }
@@ -15209,6 +16322,10 @@ class PopoverMenu {
15209
16322
  * If `true`, the popover menu will be displayed.
15210
16323
  */
15211
16324
  this.open = undefined;
16325
+ /**
16326
+ * If `always`, the popover menu will close when any menu item is clicked. If `toggle`, the popover menu will close when a non-toggle menu item is clicked. If `never`, the popover menu will not close when any menu item is clicked.
16327
+ */
16328
+ this.closeOnItemSelect = "always";
15212
16329
  this.setButtonFocus = () => {
15213
16330
  var _a;
15214
16331
  (_a = this.popoverMenuEls[this.currentFocus]) === null || _a === void 0 ? void 0 : _a.focus();
@@ -15382,6 +16499,13 @@ class PopoverMenu {
15382
16499
  }
15383
16500
  handleMenuItemClick(ev) {
15384
16501
  if (!ev.detail.submenuTriggerFor && ev.detail.label !== "Back") {
16502
+ if (this.closeOnItemSelect === "never" ||
16503
+ (ev.detail.variant === "toggle" &&
16504
+ this.closeOnItemSelect === "toggle") ||
16505
+ ev.detail.closeMenuOnSelect === false) {
16506
+ this.currentFocus = this.popoverMenuEls.findIndex((menuItem) => menuItem === ev.detail);
16507
+ return;
16508
+ }
15385
16509
  this.closeMenu(false, ev.detail);
15386
16510
  }
15387
16511
  }
@@ -15477,19 +16601,19 @@ class PopoverMenu {
15477
16601
  this.popperProps = props;
15478
16602
  }
15479
16603
  render() {
15480
- return (hAsync(Host, { key: '7f6ecb546f399d0b1b5da98ede2923e196314f54', class: {
16604
+ return (hAsync(Host, { key: 'b664e4a0944016c88fe19232ea7b1cf2fa65cbd9', class: {
15481
16605
  [`ic-theme-${this.theme}`]: this.theme !== "inherit",
15482
- } }, isSlotUsed(this.el, "anchor") && hAsync("slot", { key: '70e80e8ce605af1076d48399bd19ef337fba4e69', name: "anchor" }), hAsync("div", { key: 'd5bae4e0dc2318a501855154eb6f8f904eba823e', class: {
16606
+ } }, isSlotUsed(this.el, "anchor") && hAsync("slot", { key: '978425aac1a58c1f19ad59e8ebe9869492fe8a5d', name: "anchor" }), hAsync("div", { key: 'c36a21a4f9913deebed1c418ce4c85995fa49054', class: {
15483
16607
  ["ic-popover-menu"]: true,
15484
16608
  ["ic-popover-menu-open"]: !!this.open,
15485
- } }, hAsync("div", { key: '552017695fedbac167dfba7972a93ec8fded05ed', id: this.parentPopover === undefined
16609
+ } }, hAsync("div", { key: '5e54d43f8699270ea59ee2d408d099e73b51a5e0', id: this.parentPopover === undefined
15486
16610
  ? `ic-popover-submenu-${this.submenuId}`
15487
16611
  : "", class: {
15488
16612
  menu: true,
15489
- } }, hAsync("span", { key: 'd7acdf61f5003080acfb2fcfb906ade2ea029285', class: {
16613
+ } }, hAsync("span", { key: '50b901cdd6ce1c6e51839cdacb61034f45972a3a', class: {
15490
16614
  "opening-from-parent": this.openingFromParent,
15491
16615
  "opening-from-child": this.openingFromChild,
15492
- } }, isPropDefined(this.submenuId) && (hAsync("span", { key: '25b80b6353fb4e169c7af7738a5c14ce7284431c' }, hAsync("span", { key: '2241c3237bf2c032768b13560e1d2fc64d65491b', role: "menu" }, hAsync("ic-menu-item", { key: '814abfe87aa94ed2d6422b0e8c8186f77415370b', class: "ic-popover-submenu-back-button", ref: (el) => (this.backButton = el), label: "Back", onClick: this.handleBackButtonClick, id: `ic-popover-submenu-back-button-${this.submenuLevel}` }, hAsync("svg", { key: '5489c92487057e827a2ddc0b7ae3117ead6914ac', slot: "icon", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", class: "submenu-back-icon" }, hAsync("path", { key: 'ad3b776b8cd8272231f98fa5e3826ceace0c81a2', d: "M20 11H7.83L13.42 5.41L12 4L4 12L12 20L13.41 18.59L7.83 13H20V11Z", fill: "currentColor" })))), hAsync("ic-typography", { key: 'd78a1a238211a3676b5f0cbc2f0f0565914915c8', variant: "subtitle-small", class: "parent-label" }, this.parentLabel))), hAsync("div", { key: '9e4590864d5595d7bcdd13a7a193f6ad5b3f3f5c', class: "menu-body", "aria-label": this.getMenuAriaLabel(), role: "menu" }, hAsync("slot", { key: 'c2a730872a8782847df15906996d290d2d0b23a5' })))))));
16616
+ } }, isPropDefined(this.submenuId) && (hAsync("span", { key: 'b152354aff6888ea5dd18850fb7ce40f3e7d975c' }, hAsync("span", { key: 'b7f43d96c7980573f740ff5d6495aa5416b6b89c', role: "menu" }, hAsync("ic-menu-item", { key: '62eb52181ef3b9c647573202a90940f3f90b2b1f', class: "ic-popover-submenu-back-button", ref: (el) => (this.backButton = el), label: "Back", onClick: this.handleBackButtonClick, id: `ic-popover-submenu-back-button-${this.submenuLevel}` }, hAsync("svg", { key: 'ea5fe2b6bedb7a97c146e9bf19f0c4538bcd7cc4', slot: "icon", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", class: "submenu-back-icon" }, hAsync("path", { key: '57795916b6c3026915b4b166647512bc7b2571e0', d: "M20 11H7.83L13.42 5.41L12 4L4 12L12 20L13.41 18.59L7.83 13H20V11Z", fill: "currentColor" })))), hAsync("ic-typography", { key: 'c6b1a275a79f431eb9c32b8b811302c2bc7e86a2', variant: "subtitle-small", class: "parent-label" }, this.parentLabel))), hAsync("div", { key: 'a227dd549ab5bc60aad95d62d4ab8a13fdd4e46f', class: "menu-body", "aria-label": this.getMenuAriaLabel(), role: "menu" }, hAsync("slot", { key: '5f991353095b3d406ff76d101be9c72dd1ff26b7' })))))));
15493
16617
  }
15494
16618
  static get delegatesFocus() { return true; }
15495
16619
  get el() { return getElement(this); }
@@ -15509,6 +16633,7 @@ class PopoverMenu {
15509
16633
  "submenuLevel": [2, "submenu-level"],
15510
16634
  "theme": [1],
15511
16635
  "open": [1540],
16636
+ "closeOnItemSelect": [1, "close-on-item-select"],
15512
16637
  "openingFromChild": [32],
15513
16638
  "openingFromParent": [32],
15514
16639
  "popperProps": [32],