@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.mjs CHANGED
@@ -3862,16 +3862,9 @@ class Accordion {
3862
3862
  };
3863
3863
  // Set accordion animation
3864
3864
  this.setAccordionAnimation = (el, duration, property, delay) => {
3865
- if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
3866
- el.style.transitionDuration = `${duration}ms`;
3867
- el.style.transitionProperty = property;
3868
- el.style.transitionDelay = delay;
3869
- }
3870
- else {
3871
- el.style.transitionDuration = "0ms";
3872
- el.style.transitionProperty = "none";
3873
- el.style.transitionDelay = "0ms";
3874
- }
3865
+ el.style.transitionDuration = `${duration}ms`;
3866
+ el.style.transitionProperty = property;
3867
+ el.style.transitionTimingFunction = delay;
3875
3868
  };
3876
3869
  this.setExpandedContentStyle = (ev, expandedContent) => {
3877
3870
  if (ev.propertyName === "height" && expandedContent.clientHeight > 0) {
@@ -3885,30 +3878,52 @@ class Accordion {
3885
3878
  }
3886
3879
  };
3887
3880
  this.animateExpandedContent = () => {
3888
- if (this.expandedContentEl) {
3889
- const expandedContentEl = this.expandedContentEl;
3890
- const elementHeight = expandedContentEl.scrollHeight;
3891
- if (elementHeight > 0 && this.expanded) {
3892
- expandedContentEl.style.setProperty(this.CONTENT_VISIBILITY_PROPERTY, "visible");
3893
- expandedContentEl.style.height = `${elementHeight}px`;
3894
- this.setAccordionAnimation(expandedContentEl, "300", "height", "ease-out");
3895
- expandedContentEl.addEventListener("transitionend", (e) => {
3896
- this.setExpandedContentStyle(e, expandedContentEl);
3897
- });
3898
- }
3899
- else if (!this.expanded) {
3900
- const expandedContentEl = this.expandedContentEl;
3901
- expandedContentEl.style.height = `${expandedContentEl.scrollHeight}px`;
3902
- if (expandedContentEl.scrollHeight > 0 && !this.expanded) {
3903
- expandedContentEl.style.height = "0";
3904
- this.setAccordionAnimation(expandedContentEl, "300", "height", "ease-in");
3905
- expandedContentEl.classList.remove(EXPANDED_CONTENT_OPENED_CLASS);
3906
- }
3907
- expandedContentEl.addEventListener("transitionend", (e) => {
3908
- this.hideExpandedContent(e, expandedContentEl);
3909
- });
3910
- }
3881
+ const expandedContentEl = this.expandedContentEl;
3882
+ if (!expandedContentEl)
3883
+ return;
3884
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3885
+ if (this.expanded) {
3886
+ this.expandContent(expandedContentEl, prefersReducedMotion);
3887
+ return;
3911
3888
  }
3889
+ this.collapseContent(expandedContentEl, prefersReducedMotion);
3890
+ };
3891
+ this.expandContent = (expandedContentEl, prefersReducedMotion) => {
3892
+ const elementHeight = expandedContentEl.scrollHeight;
3893
+ if (elementHeight <= 0)
3894
+ return;
3895
+ expandedContentEl.style.setProperty(this.CONTENT_VISIBILITY_PROPERTY, "visible");
3896
+ if (prefersReducedMotion) {
3897
+ expandedContentEl.classList.add(EXPANDED_CONTENT_OPENED_CLASS);
3898
+ expandedContentEl.style.height = "auto";
3899
+ return;
3900
+ }
3901
+ expandedContentEl.style.height = `${elementHeight}px`;
3902
+ this.setAccordionAnimation(expandedContentEl, "300", "height", "ease-out");
3903
+ expandedContentEl.addEventListener("transitionend", (e) => {
3904
+ this.setExpandedContentStyle(e, expandedContentEl);
3905
+ }, { once: true });
3906
+ };
3907
+ this.collapseContent = (expandedContentEl, prefersReducedMotion) => {
3908
+ const elementHeight = expandedContentEl.scrollHeight;
3909
+ if (elementHeight <= 0)
3910
+ return;
3911
+ // Set the content panel as pixels from "auto" so it can be animated
3912
+ expandedContentEl.style.height = `${elementHeight}px`;
3913
+ if (prefersReducedMotion) {
3914
+ expandedContentEl.style.height = "0";
3915
+ expandedContentEl.classList.remove(EXPANDED_CONTENT_OPENED_CLASS);
3916
+ expandedContentEl.style.setProperty(this.CONTENT_VISIBILITY_PROPERTY, "hidden");
3917
+ return;
3918
+ }
3919
+ requestAnimationFrame(() => {
3920
+ expandedContentEl.style.height = "0";
3921
+ });
3922
+ this.setAccordionAnimation(expandedContentEl, "300", "height", "ease-in");
3923
+ expandedContentEl.classList.remove(EXPANDED_CONTENT_OPENED_CLASS);
3924
+ expandedContentEl.addEventListener("transitionend", (e) => {
3925
+ this.hideExpandedContent(e, expandedContentEl);
3926
+ }, { once: true });
3912
3927
  };
3913
3928
  }
3914
3929
  watchDisabledHandler() {
@@ -3945,19 +3960,19 @@ class Accordion {
3945
3960
  }
3946
3961
  render() {
3947
3962
  const { size, disabled, expanded, theme } = this;
3948
- return (hAsync(Host, { key: '58ea81b0011f31da3d2d872244720011cb9028c8', id: this.accordionId, class: {
3963
+ return (hAsync(Host, { key: '840baa7799dc4c48c232a7dc170937bb563900d5', id: this.accordionId, class: {
3949
3964
  ["ic-accordion-disabled"]: !!disabled,
3950
3965
  [`ic-theme-${theme}`]: theme !== "inherit",
3951
- }, "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: {
3966
+ }, "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: {
3952
3967
  [`${size}`]: true,
3953
3968
  ["section-button"]: true,
3954
3969
  ["section-button-open"]: !!expanded && !disabled,
3955
- }, "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: {
3970
+ }, "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: {
3956
3971
  ["expand-chevron"]: true,
3957
3972
  ["content-expanded-chevron"]: !!expanded && !disabled,
3958
- }, "aria-hidden": "true", innerHTML: chevronIcon })), hAsync("div", { key: '1e74fb0c312fc767a7518cbfcce5f55a90fc22a9', class: {
3973
+ }, "aria-hidden": "true", innerHTML: chevronIcon })), hAsync("div", { key: '9b04e4a6df4def67189609215cd4849890900146', class: {
3959
3974
  ["expanded-content"]: true,
3960
- }, "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))))));
3975
+ }, "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))))));
3961
3976
  }
3962
3977
  get el() { return getElement(this); }
3963
3978
  static get watchers() { return {
@@ -4954,6 +4969,8 @@ class BreadcrumbGroup {
4954
4969
  this.deviceSize = DEVICE_SIZES.XL;
4955
4970
  /**
4956
4971
  * If `true`, display only a single breadcrumb for the parent page with a back icon.
4972
+ * Requires at least two `ic-breadcrumb` children, as the displayed breadcrumb
4973
+ * shows the title of the parent (second-to-last) page.
4957
4974
  */
4958
4975
  this.backBreadcrumbOnly = false;
4959
4976
  /**
@@ -4986,12 +5003,10 @@ class BreadcrumbGroup {
4986
5003
  this.revertLastParentCollapsedBreadcrumb();
4987
5004
  }
4988
5005
  };
4989
- this.setBackBreadcrumbAttr = () => {
4990
- if (this.lastParentBreadcrumb) {
4991
- this.lastParentBreadcrumb.classList.add("show");
4992
- this.lastParentBreadcrumb.setAttribute(this.SHOW_BACK_ICON, "true");
4993
- }
4994
- };
5006
+ /**
5007
+ * Returns the second-to-last breadcrumb (the "parent" page), or `null`
5008
+ * if fewer than two breadcrumbs are present.
5009
+ */
4995
5010
  this.getLastParentBreadcrumb = () => {
4996
5011
  const allBreadcrumbs = this.el.querySelectorAll(this.IC_BREADCRUMB);
4997
5012
  return allBreadcrumbs.length > 1
@@ -5067,11 +5082,15 @@ class BreadcrumbGroup {
5067
5082
  };
5068
5083
  this.setLastParentCollapsedBackBreadcrumb = () => {
5069
5084
  this.lastParentBreadcrumb = this.getLastParentBreadcrumb();
5070
- this.setBackBreadcrumbAttr();
5071
- if (this.lastParentBreadcrumb) {
5072
- this.lastParentBreadcrumb.classList.remove("hide");
5073
- this.lastParentBreadcrumb.classList.add("show");
5085
+ if (this.lastParentBreadcrumb === null) {
5086
+ console.warn("ic-breadcrumb-group: backBreadcrumbOnly requires at least two " +
5087
+ "ic-breadcrumb children. The back breadcrumb displays the title " +
5088
+ "of the parent page, so a minimum of two breadcrumbs is needed.");
5089
+ return;
5074
5090
  }
5091
+ this.lastParentBreadcrumb.classList.add("show");
5092
+ this.lastParentBreadcrumb.classList.remove("hide");
5093
+ this.lastParentBreadcrumb.setAttribute(this.SHOW_BACK_ICON, "true");
5075
5094
  };
5076
5095
  this.revertLastParentCollapsedBreadcrumb = () => {
5077
5096
  if (this.lastParentBreadcrumb) {
@@ -5154,11 +5173,11 @@ class BreadcrumbGroup {
5154
5173
  }
5155
5174
  render() {
5156
5175
  const { backBreadcrumbOnly, collapsed, theme } = this;
5157
- return (hAsync(Host, { key: '6bee18e1a84707b5d493149d23094af6a0fb1be8', class: {
5176
+ return (hAsync(Host, { key: '6f0f099c548cb049c02a6338d7cdec696e75a82c', class: {
5158
5177
  "ic-breadcrumb-group-back": backBreadcrumbOnly,
5159
5178
  "ic-breadcrumb-group-collapsed": collapsed,
5160
5179
  [`ic-theme-${theme}`]: theme !== "inherit",
5161
- } }, hAsync("nav", { key: '144ad11cead96e18ae62eba8fc2dead82e188854', "aria-label": "breadcrumbs" }, hAsync("ol", { key: 'c101766f5e9b0e5d9a5aaebd5d0d8eda63ffc9ea' }, hAsync("slot", { key: 'c761cdbb01472d0d8c2550254063b59fa714f722' })))));
5180
+ } }, hAsync("nav", { key: '86b02604932d3d54d48383f85482f2241102f8c7', "aria-label": "breadcrumbs" }, hAsync("ol", { key: 'ffe25df4eee620a28a3e427e1d65c8cdbe0e2f84' }, hAsync("slot", { key: '818515360bb8783b6151a1651eb6319f4a533242' })))));
5162
5181
  }
5163
5182
  get el() { return getElement(this); }
5164
5183
  static get watchers() { return {
@@ -10624,28 +10643,69 @@ var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof win
10624
10643
 
10625
10644
  var purify = {exports: {}};
10626
10645
 
10627
- /*! @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 */
10646
+ /*! @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 */
10628
10647
 
10629
10648
  (function (module, exports) {
10630
10649
  (function (global, factory) {
10631
10650
  module.exports = factory() ;
10632
10651
  })(commonjsGlobal, (function () {
10633
- const {
10634
- entries,
10635
- setPrototypeOf,
10636
- isFrozen,
10637
- getPrototypeOf,
10638
- getOwnPropertyDescriptor
10639
- } = Object;
10640
- let {
10641
- freeze,
10642
- seal,
10643
- create
10644
- } = Object; // eslint-disable-line import/no-mutable-exports
10645
- let {
10646
- apply,
10647
- construct
10648
- } = typeof Reflect !== 'undefined' && Reflect;
10652
+ function _arrayLikeToArray(r, a) {
10653
+ (null == a || a > r.length) && (a = r.length);
10654
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
10655
+ return n;
10656
+ }
10657
+ function _arrayWithHoles(r) {
10658
+ if (Array.isArray(r)) return r;
10659
+ }
10660
+ function _iterableToArrayLimit(r, l) {
10661
+ var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
10662
+ if (null != t) {
10663
+ var e,
10664
+ n,
10665
+ i,
10666
+ u,
10667
+ a = [],
10668
+ f = true,
10669
+ o = false;
10670
+ try {
10671
+ 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);
10672
+ } catch (r) {
10673
+ o = true, n = r;
10674
+ } finally {
10675
+ try {
10676
+ if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
10677
+ } finally {
10678
+ if (o) throw n;
10679
+ }
10680
+ }
10681
+ return a;
10682
+ }
10683
+ }
10684
+ function _nonIterableRest() {
10685
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
10686
+ }
10687
+ function _slicedToArray(r, e) {
10688
+ return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
10689
+ }
10690
+ function _unsupportedIterableToArray(r, a) {
10691
+ if (r) {
10692
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
10693
+ var t = {}.toString.call(r).slice(8, -1);
10694
+ 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;
10695
+ }
10696
+ }
10697
+
10698
+ const entries = Object.entries,
10699
+ setPrototypeOf = Object.setPrototypeOf,
10700
+ isFrozen = Object.isFrozen,
10701
+ getPrototypeOf = Object.getPrototypeOf,
10702
+ getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
10703
+ let freeze = Object.freeze,
10704
+ seal = Object.seal,
10705
+ create = Object.create; // eslint-disable-line import/no-mutable-exports
10706
+ let _ref = typeof Reflect !== 'undefined' && Reflect,
10707
+ apply = _ref.apply,
10708
+ construct = _ref.construct;
10649
10709
  if (!freeze) {
10650
10710
  freeze = function freeze(x) {
10651
10711
  return x;
@@ -10677,13 +10737,19 @@ var purify = {exports: {}};
10677
10737
  const arrayPop = unapply(Array.prototype.pop);
10678
10738
  const arrayPush = unapply(Array.prototype.push);
10679
10739
  const arraySplice = unapply(Array.prototype.splice);
10740
+ const arrayIsArray = Array.isArray;
10680
10741
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
10681
10742
  const stringToString = unapply(String.prototype.toString);
10682
10743
  const stringMatch = unapply(String.prototype.match);
10683
10744
  const stringReplace = unapply(String.prototype.replace);
10684
10745
  const stringIndexOf = unapply(String.prototype.indexOf);
10685
10746
  const stringTrim = unapply(String.prototype.trim);
10747
+ const numberToString = unapply(Number.prototype.toString);
10748
+ const booleanToString = unapply(Boolean.prototype.toString);
10749
+ const bigintToString = typeof BigInt === 'undefined' ? null : unapply(BigInt.prototype.toString);
10750
+ const symbolToString = typeof Symbol === 'undefined' ? null : unapply(Symbol.prototype.toString);
10686
10751
  const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
10752
+ const objectToString = unapply(Object.prototype.toString);
10687
10753
  const regExpTest = unapply(RegExp.prototype.test);
10688
10754
  const typeErrorCreate = unconstruct(TypeError);
10689
10755
  /**
@@ -10733,6 +10799,9 @@ var purify = {exports: {}};
10733
10799
  // Prevent prototype setters from intercepting set as a this value.
10734
10800
  setPrototypeOf(set, null);
10735
10801
  }
10802
+ if (!arrayIsArray(array)) {
10803
+ return set;
10804
+ }
10736
10805
  let l = array.length;
10737
10806
  while (l--) {
10738
10807
  let element = array[l];
@@ -10773,10 +10842,13 @@ var purify = {exports: {}};
10773
10842
  */
10774
10843
  function clone(object) {
10775
10844
  const newObject = create(null);
10776
- for (const [property, value] of entries(object)) {
10845
+ for (const _ref2 of entries(object)) {
10846
+ var _ref3 = _slicedToArray(_ref2, 2);
10847
+ const property = _ref3[0];
10848
+ const value = _ref3[1];
10777
10849
  const isPropertyExist = objectHasOwnProperty(object, property);
10778
10850
  if (isPropertyExist) {
10779
- if (Array.isArray(value)) {
10851
+ if (arrayIsArray(value)) {
10780
10852
  newObject[property] = cleanArray(value);
10781
10853
  } else if (value && typeof value === 'object' && value.constructor === Object) {
10782
10854
  newObject[property] = clone(value);
@@ -10787,6 +10859,58 @@ var purify = {exports: {}};
10787
10859
  }
10788
10860
  return newObject;
10789
10861
  }
10862
+ /**
10863
+ * Convert non-node values into strings without depending on direct property access.
10864
+ *
10865
+ * @param value - The value to stringify.
10866
+ * @returns A string representation of the provided value.
10867
+ */
10868
+ function stringifyValue(value) {
10869
+ switch (typeof value) {
10870
+ case 'string':
10871
+ {
10872
+ return value;
10873
+ }
10874
+ case 'number':
10875
+ {
10876
+ return numberToString(value);
10877
+ }
10878
+ case 'boolean':
10879
+ {
10880
+ return booleanToString(value);
10881
+ }
10882
+ case 'bigint':
10883
+ {
10884
+ return bigintToString ? bigintToString(value) : '0';
10885
+ }
10886
+ case 'symbol':
10887
+ {
10888
+ return symbolToString ? symbolToString(value) : 'Symbol()';
10889
+ }
10890
+ case 'undefined':
10891
+ {
10892
+ return objectToString(value);
10893
+ }
10894
+ case 'function':
10895
+ case 'object':
10896
+ {
10897
+ if (value === null) {
10898
+ return objectToString(value);
10899
+ }
10900
+ const valueAsRecord = value;
10901
+ const valueToString = lookupGetter(valueAsRecord, 'toString');
10902
+ if (typeof valueToString === 'function') {
10903
+ const stringified = valueToString(valueAsRecord);
10904
+ return typeof stringified === 'string' ? stringified : objectToString(stringified);
10905
+ }
10906
+ return objectToString(value);
10907
+ }
10908
+ default:
10909
+ {
10910
+ return objectToString(value);
10911
+ }
10912
+ }
10913
+ }
10790
10914
  /**
10791
10915
  * This method automatically checks if the prop is function or getter and behaves accordingly.
10792
10916
  *
@@ -10812,6 +10936,14 @@ var purify = {exports: {}};
10812
10936
  }
10813
10937
  return fallbackValue;
10814
10938
  }
10939
+ function isRegex(value) {
10940
+ try {
10941
+ regExpTest(value, '');
10942
+ return true;
10943
+ } catch (_unused) {
10944
+ return false;
10945
+ }
10946
+ }
10815
10947
 
10816
10948
  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']);
10817
10949
  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']);
@@ -10827,15 +10959,14 @@ var purify = {exports: {}};
10827
10959
  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
10828
10960
  const text = freeze(['#text']);
10829
10961
 
10830
- 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']);
10831
- 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']);
10962
+ 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']);
10963
+ 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']);
10832
10964
  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']);
10833
10965
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
10834
10966
 
10835
- // eslint-disable-next-line unicorn/better-regex
10836
- const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
10837
- const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
10838
- const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
10967
+ const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g);
10968
+ const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g);
10969
+ const TMPLIT_EXPR = seal(/\${[\w\W]*/g);
10839
10970
  const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
10840
10971
  const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
10841
10972
  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
@@ -10845,30 +10976,31 @@ var purify = {exports: {}};
10845
10976
  );
10846
10977
  const DOCTYPE_NAME = seal(/^html$/i);
10847
10978
  const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
10979
+ // Markup-significant character probes used by _sanitizeElements.
10980
+ // Shared module-level instances are safe despite the sticky /g flags:
10981
+ // unapply() resets lastIndex for RegExp receivers before every call.
10982
+ const ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g);
10983
+ const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g);
10984
+ const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i);
10985
+ const SELF_CLOSING_TAG = seal(/\/>/i);
10848
10986
 
10849
- var EXPRESSIONS = /*#__PURE__*/Object.freeze({
10850
- __proto__: null,
10851
- ARIA_ATTR: ARIA_ATTR,
10852
- ATTR_WHITESPACE: ATTR_WHITESPACE,
10853
- CUSTOM_ELEMENT: CUSTOM_ELEMENT,
10854
- DATA_ATTR: DATA_ATTR,
10855
- DOCTYPE_NAME: DOCTYPE_NAME,
10856
- ERB_EXPR: ERB_EXPR,
10857
- IS_ALLOWED_URI: IS_ALLOWED_URI,
10858
- IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
10859
- MUSTACHE_EXPR: MUSTACHE_EXPR,
10860
- TMPLIT_EXPR: TMPLIT_EXPR
10861
- });
10862
-
10863
- /* eslint-disable @typescript-eslint/indent */
10864
10987
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
10865
10988
  const NODE_TYPE = {
10866
10989
  element: 1,
10990
+ attribute: 2,
10867
10991
  text: 3,
10992
+ cdataSection: 4,
10993
+ entityReference: 5,
10994
+ // Deprecated
10995
+ entityNode: 6,
10868
10996
  // Deprecated
10869
- progressingInstruction: 7,
10997
+ processingInstruction: 7,
10870
10998
  comment: 8,
10871
- document: 9};
10999
+ document: 9,
11000
+ documentType: 10,
11001
+ documentFragment: 11,
11002
+ notation: 12 // Deprecated
11003
+ };
10872
11004
  const getGlobal = function getGlobal() {
10873
11005
  return typeof window === 'undefined' ? null : window;
10874
11006
  };
@@ -10923,10 +11055,25 @@ var purify = {exports: {}};
10923
11055
  uponSanitizeShadowNode: []
10924
11056
  };
10925
11057
  };
11058
+ /**
11059
+ * Resolve a set-valued configuration option: a fresh set built from
11060
+ * cfg[key] when it is an own array property (seeded with a clone of
11061
+ * options.base when given, case-normalized via options.transform),
11062
+ * the fallback set otherwise.
11063
+ *
11064
+ * @param cfg the cloned, prototype-free configuration object
11065
+ * @param key the configuration property to read
11066
+ * @param fallback the set to use when the option is absent or not an array
11067
+ * @param options transform and optional base set to merge into
11068
+ * @returns the resolved set
11069
+ */
11070
+ const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
11071
+ return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
11072
+ };
10926
11073
  function createDOMPurify() {
10927
11074
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
10928
11075
  const DOMPurify = root => createDOMPurify(root);
10929
- DOMPurify.version = '3.4.0';
11076
+ DOMPurify.version = '3.4.13';
10930
11077
  DOMPurify.removed = [];
10931
11078
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
10932
11079
  // Not running in a browser, provide a factory function
@@ -10934,28 +11081,26 @@ var purify = {exports: {}};
10934
11081
  DOMPurify.isSupported = false;
10935
11082
  return DOMPurify;
10936
11083
  }
10937
- let {
10938
- document
10939
- } = window;
11084
+ let document = window.document;
10940
11085
  const originalDocument = document;
10941
11086
  const currentScript = originalDocument.currentScript;
10942
- const {
10943
- DocumentFragment,
10944
- HTMLTemplateElement,
10945
- Node,
10946
- Element,
10947
- NodeFilter,
10948
- NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
10949
- HTMLFormElement,
10950
- DOMParser,
10951
- trustedTypes
10952
- } = window;
11087
+ const HTMLTemplateElement = window.HTMLTemplateElement,
11088
+ Node = window.Node,
11089
+ Element = window.Element,
11090
+ NodeFilter = window.NodeFilter;
11091
+ const DOMParser = window.DOMParser,
11092
+ trustedTypes = window.trustedTypes;
10953
11093
  const ElementPrototype = Element.prototype;
10954
11094
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
10955
11095
  const remove = lookupGetter(ElementPrototype, 'remove');
10956
11096
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
10957
11097
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
10958
11098
  const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
11099
+ const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot');
11100
+ const getAttributes = lookupGetter(ElementPrototype, 'attributes');
11101
+ const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
11102
+ const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
11103
+ const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, 'ownerDocument') : null;
10959
11104
  // As per issue #47, the web-components registry is inherited by a
10960
11105
  // new document created via createHTMLDocument. As per the spec
10961
11106
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -10970,33 +11115,74 @@ var purify = {exports: {}};
10970
11115
  }
10971
11116
  let trustedTypesPolicy;
10972
11117
  let emptyHTML = '';
10973
- const {
10974
- implementation,
10975
- createNodeIterator,
10976
- createDocumentFragment,
10977
- getElementsByTagName
10978
- } = document;
10979
- const {
10980
- importNode
10981
- } = originalDocument;
11118
+ // The instance's own internal Trusted Types policy. Unlike a caller-supplied
11119
+ // `TRUSTED_TYPES_POLICY`, this is created at most once — Trusted Types throws
11120
+ // on duplicate policy names — and is the only policy allowed to persist
11121
+ // across configurations and survive `clearConfig()`.
11122
+ let defaultTrustedTypesPolicy;
11123
+ let defaultTrustedTypesPolicyResolved = false;
11124
+ // Tracks whether we are already inside a call to the configured Trusted Types
11125
+ // policy (`createHTML` or `createScriptURL`). If a supplied policy callback
11126
+ // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would
11127
+ // re-enter the policy and recurse until the stack overflows. We detect that
11128
+ // re-entry and throw a clear, actionable error instead. The guard is shared
11129
+ // across both callbacks, because either one re-entering `sanitize` triggers
11130
+ // the same unbounded recursion.
11131
+ let IN_TRUSTED_TYPES_POLICY = 0;
11132
+ const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy() {
11133
+ if (IN_TRUSTED_TYPES_POLICY > 0) {
11134
+ 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.');
11135
+ }
11136
+ };
11137
+ const _createTrustedHTML = function _createTrustedHTML(html) {
11138
+ _assertNotInTrustedTypesPolicy();
11139
+ IN_TRUSTED_TYPES_POLICY++;
11140
+ try {
11141
+ return trustedTypesPolicy.createHTML(html);
11142
+ } finally {
11143
+ IN_TRUSTED_TYPES_POLICY--;
11144
+ }
11145
+ };
11146
+ const _createTrustedScriptURL = function _createTrustedScriptURL(scriptUrl) {
11147
+ _assertNotInTrustedTypesPolicy();
11148
+ IN_TRUSTED_TYPES_POLICY++;
11149
+ try {
11150
+ return trustedTypesPolicy.createScriptURL(scriptUrl);
11151
+ } finally {
11152
+ IN_TRUSTED_TYPES_POLICY--;
11153
+ }
11154
+ };
11155
+ // Lazily resolve (and cache) the instance's internal default policy.
11156
+ // Resolution is attempted at most once: a successful `createPolicy` cannot be
11157
+ // repeated (Trusted Types throws on duplicate names), and a failed or
11158
+ // unsupported attempt must not be retried on every parse.
11159
+ const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy() {
11160
+ if (!defaultTrustedTypesPolicyResolved) {
11161
+ defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
11162
+ defaultTrustedTypesPolicyResolved = true;
11163
+ }
11164
+ return defaultTrustedTypesPolicy;
11165
+ };
11166
+ const _document = document,
11167
+ implementation = _document.implementation,
11168
+ createNodeIterator = _document.createNodeIterator,
11169
+ createDocumentFragment = _document.createDocumentFragment,
11170
+ getElementsByTagName = _document.getElementsByTagName;
11171
+ const importNode = originalDocument.importNode;
10982
11172
  let hooks = _createHooksMap();
10983
11173
  /**
10984
11174
  * Expose whether this browser supports running the full DOMPurify.
10985
11175
  */
10986
11176
  DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
10987
- const {
10988
- MUSTACHE_EXPR,
10989
- ERB_EXPR,
10990
- TMPLIT_EXPR,
10991
- DATA_ATTR,
10992
- ARIA_ATTR,
10993
- IS_SCRIPT_OR_DATA,
10994
- ATTR_WHITESPACE,
10995
- CUSTOM_ELEMENT
10996
- } = EXPRESSIONS;
10997
- let {
10998
- IS_ALLOWED_URI: IS_ALLOWED_URI$1
10999
- } = EXPRESSIONS;
11177
+ const MUSTACHE_EXPR$1 = MUSTACHE_EXPR,
11178
+ ERB_EXPR$1 = ERB_EXPR,
11179
+ TMPLIT_EXPR$1 = TMPLIT_EXPR,
11180
+ DATA_ATTR$1 = DATA_ATTR,
11181
+ ARIA_ATTR$1 = ARIA_ATTR,
11182
+ IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA,
11183
+ ATTR_WHITESPACE$1 = ATTR_WHITESPACE,
11184
+ CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT;
11185
+ let IS_ALLOWED_URI$1 = IS_ALLOWED_URI;
11000
11186
  /**
11001
11187
  * We consider the elements and attributes below to be safe. Ideally
11002
11188
  * don't add any new ones but feel free to remove unwanted ones.
@@ -11073,6 +11259,13 @@ var purify = {exports: {}};
11073
11259
  let WHOLE_DOCUMENT = false;
11074
11260
  /* Track whether config is already set on this instance of DOMPurify. */
11075
11261
  let SET_CONFIG = false;
11262
+ /* Pristine allowlist bindings captured at setConfig() time. On the
11263
+ * persistent-config path sanitize() restores the sets from these before
11264
+ * the per-walk hook clone-guard, so a hook's in-call widening cannot
11265
+ * carry across calls. Null until setConfig() is called; reset by
11266
+ * clearConfig(). */
11267
+ let SET_CONFIG_ALLOWED_TAGS = null;
11268
+ let SET_CONFIG_ALLOWED_ATTR = null;
11076
11269
  /* Decide if all elements (e.g. style, script) must be children of
11077
11270
  * document.body. By default, browsers might move them to document.head */
11078
11271
  let FORCE_BODY = false;
@@ -11115,7 +11308,17 @@ var purify = {exports: {}};
11115
11308
  let USE_PROFILES = {};
11116
11309
  /* Tags to ignore content of when KEEP_CONTENT is true */
11117
11310
  let FORBID_CONTENTS = null;
11118
- 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']);
11311
+ const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script',
11312
+ // <selectedcontent> mirrors the selected <option>'s subtree, cloned by
11313
+ // the UA (customizable <select>) — including any on* handlers — and the
11314
+ // engine re-mirrors synchronously whenever a removal changes which
11315
+ // option/selectedcontent is current, even inside DOMPurify's inert
11316
+ // DOMParser document. Hoisting its children on removal re-inserts a fresh
11317
+ // mirror target ahead of the walk, which the engine refills, looping
11318
+ // forever (DoS) and amplifying output. Dropping its content on removal
11319
+ // (rather than hoisting) breaks that cascade; the content is a duplicate
11320
+ // of the option, which is sanitized on its own. See campaign-3 F1/F6.
11321
+ 'selectedcontent', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
11119
11322
  /* Tags that are safe for data: URIs */
11120
11323
  let DATA_URI_TAGS = null;
11121
11324
  const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
@@ -11131,8 +11334,10 @@ var purify = {exports: {}};
11131
11334
  /* Allowed XHTML+XML namespaces */
11132
11335
  let ALLOWED_NAMESPACES = null;
11133
11336
  const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
11134
- let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
11135
- let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
11337
+ const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(['mi', 'mo', 'mn', 'ms', 'mtext']);
11338
+ let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
11339
+ const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
11340
+ let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
11136
11341
  // Certain elements are allowed in both SVG and HTML
11137
11342
  // namespace. We need to specify them explicitly
11138
11343
  // so that they don't get erroneously deleted from
@@ -11174,15 +11379,33 @@ var purify = {exports: {}};
11174
11379
  // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
11175
11380
  transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
11176
11381
  /* Set configuration parameters */
11177
- ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
11178
- ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
11179
- ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
11180
- 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;
11181
- 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;
11182
- FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
11183
- FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
11184
- FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
11185
- USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;
11382
+ ALLOWED_TAGS = _resolveSetOption(cfg, 'ALLOWED_TAGS', DEFAULT_ALLOWED_TAGS, {
11383
+ transform: transformCaseFunc
11384
+ });
11385
+ ALLOWED_ATTR = _resolveSetOption(cfg, 'ALLOWED_ATTR', DEFAULT_ALLOWED_ATTR, {
11386
+ transform: transformCaseFunc
11387
+ });
11388
+ ALLOWED_NAMESPACES = _resolveSetOption(cfg, 'ALLOWED_NAMESPACES', DEFAULT_ALLOWED_NAMESPACES, {
11389
+ transform: stringToString
11390
+ });
11391
+ URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, 'ADD_URI_SAFE_ATTR', DEFAULT_URI_SAFE_ATTRIBUTES, {
11392
+ transform: transformCaseFunc,
11393
+ base: DEFAULT_URI_SAFE_ATTRIBUTES
11394
+ });
11395
+ DATA_URI_TAGS = _resolveSetOption(cfg, 'ADD_DATA_URI_TAGS', DEFAULT_DATA_URI_TAGS, {
11396
+ transform: transformCaseFunc,
11397
+ base: DEFAULT_DATA_URI_TAGS
11398
+ });
11399
+ FORBID_CONTENTS = _resolveSetOption(cfg, 'FORBID_CONTENTS', DEFAULT_FORBID_CONTENTS, {
11400
+ transform: transformCaseFunc
11401
+ });
11402
+ FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
11403
+ transform: transformCaseFunc
11404
+ });
11405
+ FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
11406
+ transform: transformCaseFunc
11407
+ });
11408
+ USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
11186
11409
  ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
11187
11410
  ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
11188
11411
  ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
@@ -11198,20 +11421,22 @@ var purify = {exports: {}};
11198
11421
  SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
11199
11422
  KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
11200
11423
  IN_PLACE = cfg.IN_PLACE || false; // Default false
11201
- IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
11202
- NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
11203
- MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;
11204
- HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;
11205
- CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || create(null);
11206
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
11207
- CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
11208
- }
11209
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
11210
- CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
11211
- }
11212
- if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {
11213
- CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
11214
- }
11424
+ IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
11425
+ NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
11426
+ 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
11427
+ 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
11428
+ const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
11429
+ CUSTOM_ELEMENT_HANDLING = create(null);
11430
+ if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
11431
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; // Default undefined
11432
+ }
11433
+ if (objectHasOwnProperty(customElementHandling, 'attributeNameCheck') && isRegexOrFunction(customElementHandling.attributeNameCheck)) {
11434
+ CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck; // Default undefined
11435
+ }
11436
+ if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') {
11437
+ CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined
11438
+ }
11439
+ seal(CUSTOM_ELEMENT_HANDLING);
11215
11440
  if (SAFE_FOR_TEMPLATES) {
11216
11441
  ALLOW_DATA_ATTR = false;
11217
11442
  }
@@ -11247,36 +11472,36 @@ var purify = {exports: {}};
11247
11472
  EXTRA_ELEMENT_HANDLING.tagCheck = null;
11248
11473
  EXTRA_ELEMENT_HANDLING.attributeCheck = null;
11249
11474
  /* Merge configuration parameters */
11250
- if (cfg.ADD_TAGS) {
11475
+ if (objectHasOwnProperty(cfg, 'ADD_TAGS')) {
11251
11476
  if (typeof cfg.ADD_TAGS === 'function') {
11252
11477
  EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;
11253
- } else {
11478
+ } else if (arrayIsArray(cfg.ADD_TAGS)) {
11254
11479
  if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
11255
11480
  ALLOWED_TAGS = clone(ALLOWED_TAGS);
11256
11481
  }
11257
11482
  addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
11258
11483
  }
11259
11484
  }
11260
- if (cfg.ADD_ATTR) {
11485
+ if (objectHasOwnProperty(cfg, 'ADD_ATTR')) {
11261
11486
  if (typeof cfg.ADD_ATTR === 'function') {
11262
11487
  EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;
11263
- } else {
11488
+ } else if (arrayIsArray(cfg.ADD_ATTR)) {
11264
11489
  if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
11265
11490
  ALLOWED_ATTR = clone(ALLOWED_ATTR);
11266
11491
  }
11267
11492
  addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
11268
11493
  }
11269
11494
  }
11270
- if (cfg.ADD_URI_SAFE_ATTR) {
11495
+ if (objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) {
11271
11496
  addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
11272
11497
  }
11273
- if (cfg.FORBID_CONTENTS) {
11498
+ if (objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS)) {
11274
11499
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
11275
11500
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
11276
11501
  }
11277
11502
  addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
11278
11503
  }
11279
- if (cfg.ADD_FORBID_CONTENTS) {
11504
+ if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
11280
11505
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
11281
11506
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
11282
11507
  }
@@ -11295,6 +11520,13 @@ var purify = {exports: {}};
11295
11520
  addToSet(ALLOWED_TAGS, ['tbody']);
11296
11521
  delete FORBID_TAGS.tbody;
11297
11522
  }
11523
+ // Re-derive the active Trusted Types policy from this configuration on
11524
+ // every parse. The active policy must never be sticky closure state that
11525
+ // outlives the config that set it: a caller-supplied policy left in place
11526
+ // after `clearConfig()` — or after a later call that supplied none, or
11527
+ // `TRUSTED_TYPES_POLICY: null` — could sign a subsequent "default"
11528
+ // `RETURN_TRUSTED_TYPE` result with a foreign, possibly unsafe policy.
11529
+ // See GHSA-vxr8-fq34-vvx9.
11298
11530
  if (cfg.TRUSTED_TYPES_POLICY) {
11299
11531
  if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
11300
11532
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
@@ -11302,18 +11534,45 @@ var purify = {exports: {}};
11302
11534
  if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
11303
11535
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
11304
11536
  }
11305
- // Overwrite existing TrustedTypes policy.
11537
+ // A caller-supplied policy applies to this configuration only.
11538
+ const previousTrustedTypesPolicy = trustedTypesPolicy;
11306
11539
  trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
11307
- // Sign local variables required by `sanitize`.
11308
- emptyHTML = trustedTypesPolicy.createHTML('');
11540
+ // Sign local variables required by `sanitize`. If the supplied policy's
11541
+ // `createHTML` is circular (i.e. it calls `DOMPurify.sanitize`), this
11542
+ // throws via the re-entrancy guard. Restore the previous policy first so
11543
+ // the instance is not left in a poisoned state. See #1422.
11544
+ try {
11545
+ emptyHTML = _createTrustedHTML('');
11546
+ } catch (error) {
11547
+ trustedTypesPolicy = previousTrustedTypesPolicy;
11548
+ throw error;
11549
+ }
11550
+ } else if (cfg.TRUSTED_TYPES_POLICY === null) {
11551
+ // Explicit opt-out for this call: perform no Trusted Types signing and
11552
+ // create nothing (so a strict `trusted-types` CSP that disallows a
11553
+ // `dompurify` policy can still call `sanitize` from inside its own
11554
+ // policy — see #1422). Resetting to `undefined` rather than a sticky
11555
+ // `null` also drops any previously retained caller policy, so it cannot
11556
+ // resurface on a later call, while still allowing the next config-less
11557
+ // call to restore the internal default policy. See GHSA-vxr8-fq34-vvx9.
11558
+ trustedTypesPolicy = undefined;
11559
+ emptyHTML = '';
11309
11560
  } else {
11310
- // Uninitialized policy, attempt to initialize the internal dompurify policy.
11561
+ // No policy supplied: keep the currently active policy if one is set — a
11562
+ // previously supplied policy is intentionally sticky across config-less
11563
+ // calls — otherwise fall back to the instance's own internal policy,
11564
+ // created at most once. (A policy supplied for a *single* call still
11565
+ // lingers by design; what must not linger is a policy whose configuration
11566
+ // has been torn down via `clearConfig()`, which restores the default.)
11311
11567
  if (trustedTypesPolicy === undefined) {
11312
- trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
11568
+ trustedTypesPolicy = _getDefaultTrustedTypesPolicy();
11313
11569
  }
11314
- // If creating the internal policy succeeded sign internal variables.
11315
- if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
11316
- emptyHTML = trustedTypesPolicy.createHTML('');
11570
+ // Sign internal variables only when a policy is active. A falsy policy
11571
+ // (Trusted Types unsupported, creation failed, or an explicit opt-out)
11572
+ // leaves `emptyHTML` as a plain string, so we never call `.createHTML` on
11573
+ // a non-policy and throw. See #1422.
11574
+ if (trustedTypesPolicy && typeof emptyHTML === 'string') {
11575
+ emptyHTML = _createTrustedHTML('');
11317
11576
  }
11318
11577
  }
11319
11578
  // Prevent further manipulation of configuration.
@@ -11328,6 +11587,77 @@ var purify = {exports: {}};
11328
11587
  * correctly. */
11329
11588
  const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
11330
11589
  const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
11590
+ /**
11591
+ * Namespace rules for an element in the SVG namespace.
11592
+ *
11593
+ * @param tagName the element's lowercase tag name
11594
+ * @param parent the (possibly simulated) parent node
11595
+ * @param parentTagName the parent's lowercase tag name
11596
+ * @returns true if a spec-compliant parser could produce this element
11597
+ */
11598
+ const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) {
11599
+ // The only way to switch from HTML namespace to SVG
11600
+ // is via <svg>. If it happens via any other tag, then
11601
+ // it should be killed.
11602
+ if (parent.namespaceURI === HTML_NAMESPACE) {
11603
+ return tagName === 'svg';
11604
+ }
11605
+ // The only way to switch from MathML to SVG is via <svg>
11606
+ // if the parent is either <annotation-xml> or a MathML
11607
+ // text integration point.
11608
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
11609
+ return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
11610
+ }
11611
+ // We only allow elements that are defined in SVG
11612
+ // spec. All others are disallowed in SVG namespace.
11613
+ return Boolean(ALL_SVG_TAGS[tagName]);
11614
+ };
11615
+ /**
11616
+ * Namespace rules for an element in the MathML namespace.
11617
+ *
11618
+ * @param tagName the element's lowercase tag name
11619
+ * @param parent the (possibly simulated) parent node
11620
+ * @param parentTagName the parent's lowercase tag name
11621
+ * @returns true if a spec-compliant parser could produce this element
11622
+ */
11623
+ const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) {
11624
+ // The only way to switch from HTML namespace to MathML
11625
+ // is via <math>. If it happens via any other tag, then
11626
+ // it should be killed.
11627
+ if (parent.namespaceURI === HTML_NAMESPACE) {
11628
+ return tagName === 'math';
11629
+ }
11630
+ // The only way to switch from SVG to MathML is via
11631
+ // <math> and HTML integration points
11632
+ if (parent.namespaceURI === SVG_NAMESPACE) {
11633
+ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
11634
+ }
11635
+ // We only allow elements that are defined in MathML
11636
+ // spec. All others are disallowed in MathML namespace.
11637
+ return Boolean(ALL_MATHML_TAGS[tagName]);
11638
+ };
11639
+ /**
11640
+ * Namespace rules for an element in the HTML namespace.
11641
+ *
11642
+ * @param tagName the element's lowercase tag name
11643
+ * @param parent the (possibly simulated) parent node
11644
+ * @param parentTagName the parent's lowercase tag name
11645
+ * @returns true if a spec-compliant parser could produce this element
11646
+ */
11647
+ const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) {
11648
+ // The only way to switch from SVG to HTML is via
11649
+ // HTML integration points, and from MathML to HTML
11650
+ // is via MathML text integration points
11651
+ if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
11652
+ return false;
11653
+ }
11654
+ if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
11655
+ return false;
11656
+ }
11657
+ // We disallow tags that are specific for MathML
11658
+ // or SVG and should never appear in HTML namespace
11659
+ return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
11660
+ };
11331
11661
  /**
11332
11662
  * @param element a DOM element whose namespace is being checked
11333
11663
  * @returns Return false if the element has a
@@ -11350,51 +11680,13 @@ var purify = {exports: {}};
11350
11680
  return false;
11351
11681
  }
11352
11682
  if (element.namespaceURI === SVG_NAMESPACE) {
11353
- // The only way to switch from HTML namespace to SVG
11354
- // is via <svg>. If it happens via any other tag, then
11355
- // it should be killed.
11356
- if (parent.namespaceURI === HTML_NAMESPACE) {
11357
- return tagName === 'svg';
11358
- }
11359
- // The only way to switch from MathML to SVG is via`
11360
- // svg if parent is either <annotation-xml> or MathML
11361
- // text integration points.
11362
- if (parent.namespaceURI === MATHML_NAMESPACE) {
11363
- return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
11364
- }
11365
- // We only allow elements that are defined in SVG
11366
- // spec. All others are disallowed in SVG namespace.
11367
- return Boolean(ALL_SVG_TAGS[tagName]);
11683
+ return _checkSvgNamespace(tagName, parent, parentTagName);
11368
11684
  }
11369
11685
  if (element.namespaceURI === MATHML_NAMESPACE) {
11370
- // The only way to switch from HTML namespace to MathML
11371
- // is via <math>. If it happens via any other tag, then
11372
- // it should be killed.
11373
- if (parent.namespaceURI === HTML_NAMESPACE) {
11374
- return tagName === 'math';
11375
- }
11376
- // The only way to switch from SVG to MathML is via
11377
- // <math> and HTML integration points
11378
- if (parent.namespaceURI === SVG_NAMESPACE) {
11379
- return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
11380
- }
11381
- // We only allow elements that are defined in MathML
11382
- // spec. All others are disallowed in MathML namespace.
11383
- return Boolean(ALL_MATHML_TAGS[tagName]);
11686
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
11384
11687
  }
11385
11688
  if (element.namespaceURI === HTML_NAMESPACE) {
11386
- // The only way to switch from SVG to HTML is via
11387
- // HTML integration points, and from MathML to HTML
11388
- // is via MathML text integration points
11389
- if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
11390
- return false;
11391
- }
11392
- if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
11393
- return false;
11394
- }
11395
- // We disallow tags that are specific for MathML
11396
- // or SVG and should never appear in HTML namespace
11397
- return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
11689
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
11398
11690
  }
11399
11691
  // For XHTML and XML documents that support custom namespaces
11400
11692
  if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
@@ -11419,7 +11711,81 @@ var purify = {exports: {}};
11419
11711
  // eslint-disable-next-line unicorn/prefer-dom-node-remove
11420
11712
  getParentNode(node).removeChild(node);
11421
11713
  } catch (_) {
11714
+ /* The normal detach failed — this is reached for a parentless node
11715
+ (getParentNode() is null, so .removeChild throws). Element.prototype
11716
+ .remove() is itself a spec no-op on a parentless node, so a recorded
11717
+ "removal" would otherwise hand the caller back an intact,
11718
+ payload-bearing node (e.g. a detached IN_PLACE root the mXSS canary or
11719
+ the style-with-element-child rule decided to kill). Fail closed by
11720
+ throwing — exactly as a clobbered root does at the IN_PLACE entry —
11721
+ rather than trying to "neutralize" the node via its own methods.
11722
+ Neutralizing would mean calling getAttributeNames()/removeAttribute()
11723
+ on the node, both of which a <form> root can clobber via a named child
11724
+ (and _isClobbered does not even probe getAttributeNames), so the
11725
+ neutralize step could itself be silently defeated, leaving the payload
11726
+ intact. A throw touches only the cached, clobber-safe remove() and
11727
+ getParentNode(). Generalizes GHSA-r47g-fvhr-h676 (clobbered-form root)
11728
+ to every root-kill reason. REPORT-3.
11729
+ This lives inside the catch, so it never fires for a normally-removed
11730
+ in-tree node: those have a parent, removeChild() succeeds, and the
11731
+ catch is not entered. Only a kept (parentless) root reaches here. */
11422
11732
  remove(node);
11733
+ if (!getParentNode(node)) {
11734
+ throw typeErrorCreate('a node selected for removal could not be detached from its tree ' + 'and cannot be safely returned; refusing to sanitize in place');
11735
+ }
11736
+ }
11737
+ };
11738
+ /**
11739
+ * _neutralizeRoot
11740
+ *
11741
+ * Fail-closed teardown of an in-place root after the sanitize walk aborts
11742
+ * (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
11743
+ * custom element's reaction detaches a node so `_forceRemove`'s deliberate
11744
+ * parentless guard throws, or any other re-entrant engine mutation — would
11745
+ * otherwise leave the caller's *live* tree half-sanitized, with everything
11746
+ * after the abort point still carrying its handlers. There is no safe way
11747
+ * to resume the walk (the tree mutated under us), so we strip the root bare:
11748
+ * remove every child and every attribute, then let the caller's catch see
11749
+ * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
11750
+ * getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
11751
+ *
11752
+ * @param root the in-place root to empty
11753
+ */
11754
+ const _neutralizeRoot = function _neutralizeRoot(root) {
11755
+ /* Strip every disallowed attribute (on* handlers included) off the whole
11756
+ subtree BEFORE detaching anything. Detaching first would hand back
11757
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
11758
+ whose queued resource event still fires in page scope after we throw.
11759
+ Clobber-safe reads; a doomed clobbered node's own attributes are
11760
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
11761
+ _neutralizeSubtree(root);
11762
+ const childNodes = getChildNodes(root);
11763
+ if (childNodes) {
11764
+ const snapshot = [];
11765
+ arrayForEach(childNodes, child => {
11766
+ arrayPush(snapshot, child);
11767
+ });
11768
+ arrayForEach(snapshot, child => {
11769
+ try {
11770
+ remove(child);
11771
+ } catch (_) {
11772
+ /* Best-effort teardown; a still-attached child is handled below */
11773
+ }
11774
+ });
11775
+ }
11776
+ const attributes = getAttributes(root);
11777
+ if (attributes) {
11778
+ for (let i = attributes.length - 1; i >= 0; --i) {
11779
+ const attribute = attributes[i];
11780
+ const name = attribute && attribute.name;
11781
+ if (typeof name === 'string') {
11782
+ try {
11783
+ root.removeAttribute(name);
11784
+ } catch (_) {
11785
+ /* Clobbered removeAttribute — ignore (fail-closed best effort) */
11786
+ }
11787
+ }
11788
+ }
11423
11789
  }
11424
11790
  };
11425
11791
  /**
@@ -11454,6 +11820,148 @@ var purify = {exports: {}};
11454
11820
  }
11455
11821
  }
11456
11822
  };
11823
+ /**
11824
+ * _stripDisallowedAttributes
11825
+ *
11826
+ * Removes every attribute the active configuration does not allow from a
11827
+ * single element, using the same allowlist as the main attribute pass (so
11828
+ * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
11829
+ * neutralise nodes that are being discarded from an in-place tree.
11830
+ *
11831
+ * @param element the element to strip
11832
+ */
11833
+ const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) {
11834
+ const attributes = getAttributes(element);
11835
+ if (!attributes) {
11836
+ return;
11837
+ }
11838
+ for (let i = attributes.length - 1; i >= 0; --i) {
11839
+ const attribute = attributes[i];
11840
+ const name = attribute && attribute.name;
11841
+ if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) {
11842
+ continue;
11843
+ }
11844
+ try {
11845
+ element.removeAttribute(name);
11846
+ } catch (_) {
11847
+ /* Clobbered removeAttribute on a doomed node — ignore */
11848
+ }
11849
+ }
11850
+ };
11851
+ /**
11852
+ * _neutralizeSubtree
11853
+ *
11854
+ * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT
11855
+ * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary,
11856
+ * namespace, comment, processing-instruction and KEEP_CONTENT:false removals
11857
+ * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
11858
+ * those dropped nodes are detached from the caller's LIVE tree but a
11859
+ * handler-bearing original among them (an `<img onerror>`/`<video>` that was
11860
+ * loading) keeps its queued resource event, which fires in page scope after
11861
+ * sanitize returns. This walks a removed subtree and strips every attribute
11862
+ * the active configuration does not allow — so `on*` handlers are cancelled
11863
+ * through the SAME allowlist that governs kept nodes, not a separate `/^on/`
11864
+ * blocklist. Run synchronously before sanitize returns, i.e. before any
11865
+ * queued event can fire. Hook-free by design: these nodes leave the output,
11866
+ * so firing attribute hooks for them would be surprising. Clobber-safe reads;
11867
+ * a doomed clobbered node may shadow `removeAttribute` (its own attributes are
11868
+ * irrelevant — it is discarded — while its non-clobbered descendants, e.g.
11869
+ * the `<img>`, are reached and scrubbed).
11870
+ *
11871
+ * @param root the root of a removed subtree to neutralise
11872
+ */
11873
+ const _neutralizeSubtree = function _neutralizeSubtree(root) {
11874
+ const stack = [root];
11875
+ while (stack.length > 0) {
11876
+ const node = stack.pop();
11877
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
11878
+ if (nodeType === NODE_TYPE.element) {
11879
+ _stripDisallowedAttributes(node);
11880
+ }
11881
+ const childNodes = getChildNodes(node);
11882
+ if (childNodes) {
11883
+ for (let i = childNodes.length - 1; i >= 0; --i) {
11884
+ stack.push(childNodes[i]);
11885
+ }
11886
+ }
11887
+ }
11888
+ };
11889
+ /**
11890
+ * _neutralizePatchLinkage
11891
+ *
11892
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
11893
+ * hardening, https://github.com/WICG/declarative-partial-updates).
11894
+ *
11895
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
11896
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
11897
+ * reaches each node. On a live in-place root that leaves a window: from the
11898
+ * moment the root is connected until the walk arrives at a given node, that
11899
+ * node's linkage is live. A patch applied on connection/stream can fire as
11900
+ * a microtask during the walk and inject or teleport an unsanitized DOM
11901
+ * range into a region the iterator has already passed and will not revisit,
11902
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
11903
+ * whole tree once up front and sever every linkage before the walk begins,
11904
+ * closing that window.
11905
+ *
11906
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
11907
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
11908
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
11909
+ * to group with the rest of the declarative-partial-updates handling and
11910
+ * stay overridable, consistent with the codebase.
11911
+ *
11912
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
11913
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
11914
+ *
11915
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
11916
+ * .html Q1): this mirrors the existing policy of keeping `for` on
11917
+ * <label>/<output>. If the shipping feature can drive a patch through a
11918
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
11919
+ * attribute check at _isBasicCustomElement's caller must additionally drop
11920
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
11921
+ *
11922
+ * @param root the in-place root to sweep
11923
+ */
11924
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
11925
+ if (!SAFE_FOR_XML) {
11926
+ return;
11927
+ }
11928
+ const stack = [root];
11929
+ while (stack.length > 0) {
11930
+ const node = stack.pop();
11931
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
11932
+ /* Remove range markers (the target side of a patch linkage): every
11933
+ processing instruction, and any markup-bearing comment. */
11934
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
11935
+ try {
11936
+ remove(node);
11937
+ } catch (_) {
11938
+ /* Best-effort */
11939
+ }
11940
+ continue;
11941
+ }
11942
+ /* Strip patch-source attributes (the source side) off elements. */
11943
+ if (nodeType === NODE_TYPE.element) {
11944
+ const element = node;
11945
+ const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
11946
+ try {
11947
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
11948
+ element.removeAttribute('patchsrc');
11949
+ }
11950
+ if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
11951
+ element.removeAttribute('for');
11952
+ }
11953
+ } catch (_) {
11954
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
11955
+ }
11956
+ }
11957
+ const childNodes = getChildNodes(node);
11958
+ if (childNodes) {
11959
+ for (let i = childNodes.length - 1; i >= 0; --i) {
11960
+ stack.push(childNodes[i]);
11961
+ }
11962
+ }
11963
+ }
11964
+ };
11457
11965
  /**
11458
11966
  * _initDocument
11459
11967
  *
@@ -11475,7 +11983,7 @@ var purify = {exports: {}};
11475
11983
  // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
11476
11984
  dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
11477
11985
  }
11478
- const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
11986
+ const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty;
11479
11987
  /*
11480
11988
  * Use the DOMParser API by default, fallback later if needs be
11481
11989
  * DOMParser not work for svg when has multiple root element.
@@ -11511,33 +12019,297 @@ var purify = {exports: {}};
11511
12019
  * @return The created NodeIterator
11512
12020
  */
11513
12021
  const _createNodeIterator = function _createNodeIterator(root) {
11514
- return createNodeIterator.call(root.ownerDocument || root, root,
12022
+ /* Read ownerDocument through the cached Node.prototype getter, never the
12023
+ direct property. HTMLFormElement has [LegacyOverrideBuiltIns], so a
12024
+ clobbering child (<input name="ownerDocument"> or a form-associated
12025
+ external input) shadows the prototype getter and makes a direct read
12026
+ return that <input>. createNodeIterator.call(<input>, ...) then throws
12027
+ "Illegal invocation", and on the IN_PLACE path that throw lands before
12028
+ the walk's fail-closed barrier - leaving the caller's live tree, with
12029
+ any already-armed handler in it, un-neutralized. The cached getter
12030
+ returns the real Document regardless of the clobber. */
12031
+ const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
12032
+ return createNodeIterator.call(doc || root, root,
11515
12033
  // eslint-disable-next-line no-bitwise
11516
12034
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
11517
12035
  };
12036
+ /**
12037
+ * Replace template expression syntax (mustache, ERB, template
12038
+ * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
12039
+ * sites. Order matters: mustache, then ERB, then template literal.
12040
+ *
12041
+ * @param value the string to scrub
12042
+ * @returns the scrubbed string
12043
+ */
12044
+ const _stripTemplateExpressions = function _stripTemplateExpressions(value) {
12045
+ value = stringReplace(value, MUSTACHE_EXPR$1, ' ');
12046
+ value = stringReplace(value, ERB_EXPR$1, ' ');
12047
+ value = stringReplace(value, TMPLIT_EXPR$1, ' ');
12048
+ return value;
12049
+ };
12050
+ /**
12051
+ * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
12052
+ * character data of an element subtree. Used as the final safety net for
12053
+ * SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions
12054
+ * which only form after text-node normalization (e.g. fragments split across
12055
+ * stripped elements) cannot survive into a template-evaluating framework.
12056
+ *
12057
+ * Walks text/comment/CDATA/processing-instruction nodes and mutates `.data`
12058
+ * in place rather than round-tripping through innerHTML. This preserves
12059
+ * descendant node references (important for IN_PLACE callers), avoids a
12060
+ * serialize/reparse cycle, and reads literal character data — which means
12061
+ * `<%...%>` in text content matches the ERB regex against its real bytes
12062
+ * instead of the HTML-entity-escaped form innerHTML would produce.
12063
+ *
12064
+ * Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for
12065
+ * attributes is performed during the per-node `_sanitizeAttributes` pass.
12066
+ *
12067
+ * @param node The root element whose character data should be scrubbed.
12068
+ */
12069
+ const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
12070
+ var _node$querySelectorAl;
12071
+ node.normalize();
12072
+ /* Clobber-safe ownerDocument read, same reasoning as _createNodeIterator:
12073
+ under SAFE_FOR_TEMPLATES this runs on the live IN_PLACE root, which may
12074
+ carry a form-named-getter override of ownerDocument. */
12075
+ const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
12076
+ const walker = createNodeIterator.call(doc || node, node,
12077
+ // eslint-disable-next-line no-bitwise
12078
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
12079
+ let currentNode = walker.nextNode();
12080
+ while (currentNode) {
12081
+ currentNode.data = _stripTemplateExpressions(currentNode.data);
12082
+ currentNode = walker.nextNode();
12083
+ }
12084
+ // NodeIterator does not descend into <template>.content per the DOM spec,
12085
+ // so we must explicitly recurse into each template's content fragment,
12086
+ // mirroring the approach used by _sanitizeShadowDOM.
12087
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, 'template');
12088
+ if (templates) {
12089
+ arrayForEach(templates, tmpl => {
12090
+ if (_isDocumentFragment(tmpl.content)) {
12091
+ _scrubTemplateExpressions2(tmpl.content);
12092
+ }
12093
+ });
12094
+ }
12095
+ };
11518
12096
  /**
11519
12097
  * _isClobbered
11520
12098
  *
12099
+ * Detect DOM-clobbering on HTMLFormElement nodes. Form is the only HTML
12100
+ * interface with [LegacyOverrideBuiltIns]; a descendant element with a
12101
+ * `name` attribute matching a prototype property shadows that property
12102
+ * on direct reads. We use this check at the IN_PLACE entry-point and
12103
+ * during attribute sanitization to refuse clobbered forms.
12104
+ *
11521
12105
  * @param element element to check for clobbering attacks
11522
12106
  * @return true if clobbered, false if safe
11523
12107
  */
11524
12108
  const _isClobbered = function _isClobbered(element) {
11525
- 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');
12109
+ // Realm-independent tag-name probe. If we can't determine the tag
12110
+ // name at all, we can't reason about clobbering — return false
12111
+ // (the caller's other defences still apply).
12112
+ const realTagName = getNodeName ? getNodeName(element) : null;
12113
+ if (typeof realTagName !== 'string') {
12114
+ return false;
12115
+ }
12116
+ if (transformCaseFunc(realTagName) !== 'form') {
12117
+ return false;
12118
+ }
12119
+ return typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' ||
12120
+ // Realm-safe NamedNodeMap detection: equality against the cached
12121
+ // prototype getter. Clobbered .attributes (e.g. <input name="attributes">)
12122
+ // makes the direct read diverge from the cached read; a clean form
12123
+ // (same-realm OR foreign-realm) has both reads pointing at the same
12124
+ // canonical NamedNodeMap.
12125
+ element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' ||
12126
+ // NodeType clobbering probe. Cached Node.prototype.nodeType getter
12127
+ // returns the integer 1 for any Element regardless of realm; direct
12128
+ // read on a clobbered form (e.g. <input name="nodeType">) returns
12129
+ // the named child element. Cheap addition — nodeType is read from
12130
+ // an internal slot, no serialization cost — and removes a residual
12131
+ // clobbering surface used by several mXSS / PI / comment branches
12132
+ // in _sanitizeElements that compare currentNode.nodeType directly.
12133
+ element.nodeType !== getNodeType(element) ||
12134
+ // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named
12135
+ // "childNodes" shadows the prototype getter. Direct reads of
12136
+ // form.childNodes from a clobbered form return the named child
12137
+ // instead of the real NodeList, so any walk that reads it directly
12138
+ // skips the form's real children. Compare the direct read to the
12139
+ // cached Node.prototype getter — when the form's named-property
12140
+ // getter intercepts the read, the two values differ and we flag
12141
+ // the form. This catches every clobbering child type (input,
12142
+ // select, etc.) regardless of whether the named child happens to
12143
+ // carry a numeric .length, which a typeof-based probe would miss
12144
+ // (e.g. HTMLSelectElement.length is a defined unsigned-long).
12145
+ element.childNodes !== getChildNodes(element);
11526
12146
  };
11527
12147
  /**
11528
- * Checks whether the given object is a DOM node.
12148
+ * Checks whether the given value is a DocumentFragment from any realm.
12149
+ *
12150
+ * The realm-independent replacement reads `nodeType` through the cached
12151
+ * Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE
12152
+ * constant (11). nodeType is a numeric value resolved from the node's
12153
+ * internal slot, identical across realms for the same kind of node.
12154
+ *
12155
+ * @param value object to check
12156
+ * @return true if value is a DocumentFragment-shaped node from any realm
12157
+ */
12158
+ const _isDocumentFragment = function _isDocumentFragment(value) {
12159
+ if (!getNodeType || typeof value !== 'object' || value === null) {
12160
+ return false;
12161
+ }
12162
+ try {
12163
+ return getNodeType(value) === NODE_TYPE.documentFragment;
12164
+ } catch (_) {
12165
+ return false;
12166
+ }
12167
+ };
12168
+ /**
12169
+ * Checks whether the given object is a DOM node, including nodes that
12170
+ * originate from a different window/realm (e.g. an iframe's
12171
+ * contentDocument). The previous `value instanceof Node` check was
12172
+ * realm-bound: nodes from a different window failed it, causing
12173
+ * sanitize() to silently stringify them and reset IN_PLACE to false,
12174
+ * returning the original node unsanitized. See GHSA-4w3q-35jp-p934.
11529
12175
  *
11530
12176
  * @param value object to check whether it's a DOM node
11531
- * @return true is object is a DOM node
12177
+ * @return true if value is a DOM node from any realm
11532
12178
  */
11533
12179
  const _isNode = function _isNode(value) {
11534
- return typeof Node === 'function' && value instanceof Node;
12180
+ if (!getNodeType || typeof value !== 'object' || value === null) {
12181
+ return false;
12182
+ }
12183
+ try {
12184
+ return typeof getNodeType(value) === 'number';
12185
+ } catch (_) {
12186
+ return false;
12187
+ }
11535
12188
  };
11536
12189
  function _executeHooks(hooks, currentNode, data) {
12190
+ if (hooks.length === 0) {
12191
+ return;
12192
+ }
11537
12193
  arrayForEach(hooks, hook => {
11538
12194
  hook.call(DOMPurify, currentNode, data, CONFIG);
11539
12195
  });
11540
12196
  }
12197
+ /**
12198
+ * Structural-threat checks that condemn a node regardless of the
12199
+ * allowlists: mXSS via namespace confusion, risky CSS construction,
12200
+ * processing instructions, markup-bearing comments. Pure predicate;
12201
+ * the caller removes. Check order is load-bearing.
12202
+ *
12203
+ * @param currentNode the node to inspect
12204
+ * @param tagName the node's transformCaseFunc'd tag name
12205
+ * @return true if the node must be removed
12206
+ */
12207
+ const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
12208
+ /* Detect mXSS attempts abusing namespace confusion */
12209
+ if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
12210
+ return true;
12211
+ }
12212
+ /* Remove risky CSS construction leading to mXSS */
12213
+ if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
12214
+ return true;
12215
+ }
12216
+ /* Remove any occurrence of processing instructions */
12217
+ if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
12218
+ return true;
12219
+ }
12220
+ /* Remove any kind of possibly harmful comments */
12221
+ if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) {
12222
+ return true;
12223
+ }
12224
+ return false;
12225
+ };
12226
+ /**
12227
+ * Handle a node whose tag is forbidden or not allowlisted: keep
12228
+ * allowed custom elements (false return exits _sanitizeElements
12229
+ * early - the namespace and fallback-tag removal checks are
12230
+ * intentionally skipped for kept custom elements), else hoist
12231
+ * content per KEEP_CONTENT and remove.
12232
+ *
12233
+ * A kept custom element is the ONLY case in which this function
12234
+ * returns false, so the caller uses that return value to run the
12235
+ * afterSanitizeElements hook on the kept element and keep the
12236
+ * element-hook lifecycle consistent with normal allowlisted
12237
+ * elements (GHSA-c2j3-45gr-mqc4).
12238
+ *
12239
+ * @param currentNode the disallowed node
12240
+ * @param tagName the node's transformCaseFunc'd tag name
12241
+ * @return true if the node was removed, false if kept
12242
+ */
12243
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
12244
+ /* Check if we have a custom element to handle */
12245
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
12246
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
12247
+ return false;
12248
+ }
12249
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
12250
+ return false;
12251
+ }
12252
+ }
12253
+ /* Keep content except for bad-listed elements.
12254
+ Use the cached prototype getters exclusively — the previous code
12255
+ had `|| currentNode.parentNode` / `|| currentNode.childNodes`
12256
+ fallbacks, but the cached getters always return the canonical
12257
+ value (or null for a real parent-less node), so the fallback
12258
+ path was dead in safe cases and a clobbering surface in unsafe
12259
+ ones. Falsy cached results stay falsy; the `if (childNodes &&
12260
+ parentNode)` check already gates correctly. */
12261
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
12262
+ const parentNode = getParentNode(currentNode);
12263
+ const childNodes = getChildNodes(currentNode);
12264
+ if (childNodes && parentNode) {
12265
+ const childCount = childNodes.length;
12266
+ /* Hoist by moving each child up one level rather than deep-cloning
12267
+ it. Moving transfers every descendant exactly once, so a chain of
12268
+ nested disallowed elements costs O(n) instead of the O(n^2) that
12269
+ re-cloning the shrinking subtree at each level produced; it also
12270
+ empties the removed original, so `DOMPurify.removed` no longer
12271
+ pins whole subtrees. Moving preserves the in-place guarantee too:
12272
+ an original carrying already-queued resource events (`<img
12273
+ onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
12274
+ relocated and sanitised rather than left detached but still armed.
12275
+ The sole case that must clone is removing the walk root itself.
12276
+ The result is serialised from the root's subtree, so a restrictive
12277
+ ALLOWED_TAGS that strips the root (`body` on the string path) must
12278
+ leave the content inside it, which only cloning does. In IN_PLACE
12279
+ the root is pre-validated as an allowed tag and so is never removed
12280
+ here, so that path always takes the move branch.
12281
+ `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
12282
+ valid whether we move (drops the trailing entry) or clone (leaves
12283
+ the list intact). */
12284
+ for (let i = childCount - 1; i >= 0; --i) {
12285
+ const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
12286
+ parentNode.insertBefore(hoisted, getNextSibling(currentNode));
12287
+ }
12288
+ }
12289
+ }
12290
+ _forceRemove(currentNode);
12291
+ return true;
12292
+ };
12293
+ /**
12294
+ * Fork a hook-mutable allowlist off its shared binding the first time a
12295
+ * (possibly lazily-installed) uponSanitize* hook is about to see it, so the
12296
+ * hook cannot widen the per-instance default or the setConfig binding by
12297
+ * reference and leak past the call. Returns the set unchanged once it is
12298
+ * already call-local, so repeated calls across elements are idempotent.
12299
+ *
12300
+ * @param hookList the uponSanitize* hook array for this event
12301
+ * @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding
12302
+ * @param defaultSet the per-instance DEFAULT_ALLOWED_* constant
12303
+ * @param setConfigSet the captured setConfig() binding, or null
12304
+ * @return a call-local clone if a hook is present and set is still shared,
12305
+ * else set unchanged
12306
+ */
12307
+ const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) {
12308
+ if (hookList.length === 0) {
12309
+ return set;
12310
+ }
12311
+ return set === defaultSet || set === setConfigSet ? clone(set) : set;
12312
+ };
11541
12313
  /**
11542
12314
  * _sanitizeElements
11543
12315
  *
@@ -11547,86 +12319,112 @@ var purify = {exports: {}};
11547
12319
  * @param currentNode to check for permission to exist
11548
12320
  * @return true if node was killed, false if left alive
11549
12321
  */
11550
- const _sanitizeElements = function _sanitizeElements(currentNode) {
11551
- let content = null;
12322
+ // eslint-disable-next-line complexity
12323
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
11552
12324
  /* Execute a hook if present */
11553
12325
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
12326
+ /* A hook may have detached the node - treat it as removed (see the
12327
+ detached-node comment after the uponSanitizeElement hook below). On
12328
+ the IN_PLACE path, neutralize the detached subtree first so a queued
12329
+ resource handler on it cannot fire in page scope after we return. */
12330
+ if (currentNode !== root && getParentNode(currentNode) === null) {
12331
+ if (IN_PLACE) {
12332
+ _neutralizeSubtree(currentNode);
12333
+ }
12334
+ return true;
12335
+ }
11554
12336
  /* Check if element is clobbered or can clobber */
11555
12337
  if (_isClobbered(currentNode)) {
11556
12338
  _forceRemove(currentNode);
11557
12339
  return true;
11558
12340
  }
11559
12341
  /* Now let's check the element's type and name */
11560
- const tagName = transformCaseFunc(currentNode.nodeName);
12342
+ const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
12343
+ /* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement
12344
+ hook may have been installed after that guard sampled the hook arrays
12345
+ (e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still
12346
+ aliasing a shared binding that a widening hook would mutate by
12347
+ reference. Fork it before exposing it to the hook. */
12348
+ ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
11561
12349
  /* Execute a hook if present */
11562
12350
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
11563
12351
  tagName,
11564
12352
  allowedTags: ALLOWED_TAGS
11565
12353
  });
11566
- /* Detect mXSS attempts abusing namespace confusion */
11567
- if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
11568
- _forceRemove(currentNode);
11569
- return true;
11570
- }
11571
- /* Remove risky CSS construction leading to mXSS */
11572
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
11573
- _forceRemove(currentNode);
11574
- return true;
11575
- }
11576
- /* Remove any occurrence of processing instructions */
11577
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
11578
- _forceRemove(currentNode);
12354
+ /* A hook may have detached the node from the tree — a long-standing
12355
+ user pattern (issue #469; draw.io-style foreignObject filtering).
12356
+ Per the cached, unclobberable parentNode getter the node is
12357
+ genuinely out of the tree, so it can reach neither the serialized
12358
+ output nor an IN_PLACE live tree; treat it as removed and stop
12359
+ processing it. Without this guard, the unsafe-node / namespace
12360
+ checks below would call _forceRemove on a parentless node and hit
12361
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
12362
+ wants gone but *cannot* detach (clobbered / parentless roots), the
12363
+ opposite of a node that is already safely gone. The walk root is
12364
+ exempt: a detached IN_PLACE root is legitimate input and must still
12365
+ be fully sanitized, and a kill-decision on it must keep hitting the
12366
+ REPORT-3 throw. Nodes detached by hooks stay the hook's
12367
+ responsibility for placement: they are not recorded in
12368
+ DOMPurify.removed, so the post-walk IN_PLACE pass (which iterates
12369
+ DOMPurify.removed) does not reach them. But a hook-detached subtree
12370
+ can still hold a queued resource-event handler - e.g. an <img onload>
12371
+ that began loading when the caller built the live tree - which fires
12372
+ in page scope after sanitize returns even though the handler never
12373
+ reached the returned tree. That is the audit-5 F1 hazard, and the
12374
+ documented node.remove() hook pattern walks straight into it. So on
12375
+ the IN_PLACE path we neutralize the detached subtree inline here,
12376
+ stripping its non-allow-listed attributes before returning, exactly
12377
+ as the post-walk pass does for _forceRemove'd subtrees. */
12378
+ if (currentNode !== root && getParentNode(currentNode) === null) {
12379
+ if (IN_PLACE) {
12380
+ _neutralizeSubtree(currentNode);
12381
+ }
11579
12382
  return true;
11580
12383
  }
11581
- /* Remove any kind of possibly harmful comments */
11582
- if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
12384
+ /* Remove mXSS vectors, processing instructions and risky comments */
12385
+ if (_isUnsafeNode(currentNode, tagName)) {
11583
12386
  _forceRemove(currentNode);
11584
12387
  return true;
11585
12388
  }
11586
12389
  /* Remove element if anything forbids its presence */
11587
12390
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
11588
- /* Check if we have a custom element to handle */
11589
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
11590
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
11591
- return false;
11592
- }
11593
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
11594
- return false;
11595
- }
11596
- }
11597
- /* Keep content except for bad-listed elements */
11598
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
11599
- const parentNode = getParentNode(currentNode) || currentNode.parentNode;
11600
- const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
11601
- if (childNodes && parentNode) {
11602
- const childCount = childNodes.length;
11603
- for (let i = childCount - 1; i >= 0; --i) {
11604
- const childClone = cloneNode(childNodes[i], true);
11605
- childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
11606
- parentNode.insertBefore(childClone, getNextSibling(currentNode));
11607
- }
11608
- }
11609
- }
11610
- _forceRemove(currentNode);
11611
- return true;
11612
- }
11613
- /* Check whether element has a valid namespace */
11614
- if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
12391
+ const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
12392
+ /* A false return means the node is a custom element kept via
12393
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
12394
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
12395
+ element-hook lifecycle matches normal allowlisted elements: a
12396
+ security policy applied in this hook (e.g. stripping an attribute
12397
+ from every surviving element) must not silently skip kept custom
12398
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
12399
+ tail below - the hook runs, then the walker's subsequent
12400
+ _sanitizeAttributes pass sanitizes the element's attributes. The
12401
+ deliberately skipped namespace and fallback-tag removal checks stay
12402
+ skipped; they are removal decisions, not the hook contract. */
12403
+ if (removed === false) {
12404
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
12405
+ }
12406
+ return removed;
12407
+ }
12408
+ /* Check whether element has a valid namespace.
12409
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
12410
+ nodeType getter rather than `instanceof Element`, which is realm-
12411
+ bound and short-circuits to false for any node minted in a different
12412
+ realm — letting a foreign-realm element with a forbidden namespace
12413
+ slip past the namespace check entirely. */
12414
+ const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
12415
+ if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
11615
12416
  _forceRemove(currentNode);
11616
12417
  return true;
11617
12418
  }
11618
12419
  /* Make sure that older browsers don't get fallback-tag mXSS */
11619
- if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
12420
+ if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) {
11620
12421
  _forceRemove(currentNode);
11621
12422
  return true;
11622
12423
  }
11623
12424
  /* Sanitize element content to be template-safe */
11624
12425
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
11625
12426
  /* Get the element's text content */
11626
- content = currentNode.textContent;
11627
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
11628
- content = stringReplace(content, expr, ' ');
11629
- });
12427
+ const content = _stripTemplateExpressions(currentNode.textContent);
11630
12428
  if (currentNode.textContent !== content) {
11631
12429
  arrayPush(DOMPurify.removed, {
11632
12430
  element: currentNode.cloneNode()
@@ -11652,15 +12450,43 @@ var purify = {exports: {}};
11652
12450
  if (FORBID_ATTR[lcName]) {
11653
12451
  return false;
11654
12452
  }
12453
+ /* Reject declarative-partial-updates patch-linkage attributes
12454
+ (https://github.com/WICG/declarative-partial-updates).
12455
+ Empirical note (Chrome 150, verified — see
12456
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
12457
+ sanitization. For the string path it fires during sanitize()'s own
12458
+ parse, so the walk sees and sanitizes the fully materialized expanded
12459
+ tree — teleports into MathML/SVG integration points included; a
12460
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
12461
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
12462
+ Either way DOMPurify is NOT blind to the patch.
12463
+ This removal is therefore defense-in-depth rather than the sole barrier:
12464
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
12465
+ in the caller's context, and keeps behaviour deterministic if a future
12466
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
12467
+ anywhere else (notably <template for>) it links the element to a patch
12468
+ target and teleports or removes an arbitrary DOM range by id/marker name.
12469
+ `patchsrc` fetches remote markup and is treated as a script-loading
12470
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
12471
+ other structural-threat checks and stays overridable, consistent with
12472
+ the rest of the codebase. PI range markers are already removed by
12473
+ _isUnsafeNode. */
12474
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
12475
+ return false;
12476
+ }
12477
+ if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
12478
+ return false;
12479
+ }
11655
12480
  /* Make sure attribute cannot clobber */
11656
12481
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
11657
12482
  return false;
11658
12483
  }
12484
+ const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
11659
12485
  /* Allow valid data-* attributes: At least one character after "-"
11660
12486
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
11661
12487
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
11662
12488
  We don't need to check the value; it's always URI safe. */
11663
- 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]) {
12489
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted) {
11664
12490
  if (
11665
12491
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
11666
12492
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -11672,11 +12498,15 @@ var purify = {exports: {}};
11672
12498
  return false;
11673
12499
  }
11674
12500
  /* Check value is safe. First, is attr inert? If so, is safe */
11675
- } 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) {
12501
+ } 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) {
11676
12502
  return false;
11677
12503
  } else ;
11678
12504
  return true;
11679
12505
  };
12506
+ /* Names the HTML spec reserves from valid-custom-element-name; these must
12507
+ * never be treated as basic custom elements even when a permissive
12508
+ * CUSTOM_ELEMENT_HANDLING.tagNameCheck is configured. */
12509
+ 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']);
11680
12510
  /**
11681
12511
  * _isBasicCustomElement
11682
12512
  * checks if at least one dash is included in tagName, and it's not the first char
@@ -11686,7 +12516,64 @@ var purify = {exports: {}};
11686
12516
  * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
11687
12517
  */
11688
12518
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
11689
- return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);
12519
+ return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
12520
+ };
12521
+ /**
12522
+ * Wrap an attribute value in the matching Trusted Types object when
12523
+ * the active policy requires it. Namespaced attributes pass through
12524
+ * unchanged (no TT support yet, see
12525
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
12526
+ *
12527
+ * @param lcTag lowercase tag name of the containing element
12528
+ * @param lcName lowercase attribute name
12529
+ * @param namespaceURI the attribute's namespace, if any
12530
+ * @param value the attribute value to wrap
12531
+ * @return the value, wrapped when Trusted Types demand it
12532
+ */
12533
+ const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) {
12534
+ if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function' && !namespaceURI) {
12535
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
12536
+ case 'TrustedHTML':
12537
+ {
12538
+ return _createTrustedHTML(value);
12539
+ }
12540
+ case 'TrustedScriptURL':
12541
+ {
12542
+ return _createTrustedScriptURL(value);
12543
+ }
12544
+ }
12545
+ }
12546
+ return value;
12547
+ };
12548
+ /**
12549
+ * Write a modified attribute value back onto the element. On
12550
+ * success, re-probe for clobbering introduced by the new value and
12551
+ * remove the element when found; otherwise pop the removal entry
12552
+ * recorded by the earlier _removeAttribute (long-standing pairing
12553
+ * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
12554
+ * failure, remove the attribute instead.
12555
+ *
12556
+ * @param currentNode the element carrying the attribute
12557
+ * @param name the attribute name as present on the element
12558
+ * @param namespaceURI the attribute's namespace, if any
12559
+ * @param value the new attribute value
12560
+ */
12561
+ const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
12562
+ try {
12563
+ if (namespaceURI) {
12564
+ currentNode.setAttributeNS(namespaceURI, name, value);
12565
+ } else {
12566
+ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
12567
+ currentNode.setAttribute(name, value);
12568
+ }
12569
+ if (_isClobbered(currentNode)) {
12570
+ _forceRemove(currentNode);
12571
+ } else {
12572
+ arrayPop(DOMPurify.removed);
12573
+ }
12574
+ } catch (_) {
12575
+ _removeAttribute(name, currentNode);
12576
+ }
11690
12577
  };
11691
12578
  /**
11692
12579
  * _sanitizeAttributes
@@ -11701,13 +12588,14 @@ var purify = {exports: {}};
11701
12588
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
11702
12589
  /* Execute a hook if present */
11703
12590
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
11704
- const {
11705
- attributes
11706
- } = currentNode;
12591
+ const attributes = currentNode.attributes;
11707
12592
  /* Check if we have attributes; if not we might have a text node */
11708
12593
  if (!attributes || _isClobbered(currentNode)) {
11709
12594
  return;
11710
12595
  }
12596
+ /* Same lazy-install guard as uponSanitizeElement (see there): fork the
12597
+ attribute allowlist off its shared binding before a hook can see it. */
12598
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
11711
12599
  const hookEvent = {
11712
12600
  attrName: '',
11713
12601
  attrValue: '',
@@ -11716,14 +12604,13 @@ var purify = {exports: {}};
11716
12604
  forceKeepAttr: undefined
11717
12605
  };
11718
12606
  let l = attributes.length;
12607
+ const lcTag = transformCaseFunc(currentNode.nodeName);
11719
12608
  /* Go backwards over all attributes; safely remove bad ones */
11720
12609
  while (l--) {
11721
12610
  const attr = attributes[l];
11722
- const {
11723
- name,
11724
- namespaceURI,
11725
- value: attrValue
11726
- } = attr;
12611
+ const name = attr.name,
12612
+ namespaceURI = attr.namespaceURI,
12613
+ attrValue = attr.value;
11727
12614
  const lcName = transformCaseFunc(name);
11728
12615
  const initValue = attrValue;
11729
12616
  let value = name === 'value' ? initValue : stringTrim(initValue);
@@ -11737,12 +12624,14 @@ var purify = {exports: {}};
11737
12624
  /* Full DOM Clobbering protection via namespace isolation,
11738
12625
  * Prefix id and name attributes with `user-content-`
11739
12626
  */
11740
- if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
12627
+ if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
11741
12628
  // Remove the attribute with this value
11742
12629
  _removeAttribute(name, currentNode);
11743
12630
  // Prefix the value and later re-create the attribute with the sanitized value
11744
12631
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
11745
12632
  }
12633
+ // Else: already prefixed, leave the attribute alone — the prefix is
12634
+ // itself the clobbering protection, and re-applying it is incorrect.
11746
12635
  /* Work around a security issue with comments inside attributes */
11747
12636
  if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
11748
12637
  _removeAttribute(name, currentNode);
@@ -11753,7 +12642,7 @@ var purify = {exports: {}};
11753
12642
  _removeAttribute(name, currentNode);
11754
12643
  continue;
11755
12644
  }
11756
- /* Did the hooks approve of the attribute? */
12645
+ /* Did the hooks force-keep the attribute? */
11757
12646
  if (hookEvent.forceKeepAttr) {
11758
12647
  continue;
11759
12648
  }
@@ -11763,56 +12652,24 @@ var purify = {exports: {}};
11763
12652
  continue;
11764
12653
  }
11765
12654
  /* Work around a security issue in jQuery 3.0 */
11766
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
12655
+ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
11767
12656
  _removeAttribute(name, currentNode);
11768
12657
  continue;
11769
12658
  }
11770
12659
  /* Sanitize attribute content to be template-safe */
11771
12660
  if (SAFE_FOR_TEMPLATES) {
11772
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
11773
- value = stringReplace(value, expr, ' ');
11774
- });
12661
+ value = _stripTemplateExpressions(value);
11775
12662
  }
11776
12663
  /* Is `value` valid for this attribute? */
11777
- const lcTag = transformCaseFunc(currentNode.nodeName);
11778
12664
  if (!_isValidAttribute(lcTag, lcName, value)) {
11779
12665
  _removeAttribute(name, currentNode);
11780
12666
  continue;
11781
12667
  }
11782
12668
  /* Handle attributes that require Trusted Types */
11783
- if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
11784
- if (namespaceURI) ; else {
11785
- switch (trustedTypes.getAttributeType(lcTag, lcName)) {
11786
- case 'TrustedHTML':
11787
- {
11788
- value = trustedTypesPolicy.createHTML(value);
11789
- break;
11790
- }
11791
- case 'TrustedScriptURL':
11792
- {
11793
- value = trustedTypesPolicy.createScriptURL(value);
11794
- break;
11795
- }
11796
- }
11797
- }
11798
- }
12669
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
11799
12670
  /* Handle invalid data-* attribute set by try-catching it */
11800
12671
  if (value !== initValue) {
11801
- try {
11802
- if (namespaceURI) {
11803
- currentNode.setAttributeNS(namespaceURI, name, value);
11804
- } else {
11805
- /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
11806
- currentNode.setAttribute(name, value);
11807
- }
11808
- if (_isClobbered(currentNode)) {
11809
- _forceRemove(currentNode);
11810
- } else {
11811
- arrayPop(DOMPurify.removed);
11812
- }
11813
- } catch (_) {
11814
- _removeAttribute(name, currentNode);
11815
- }
12672
+ _setAttributeValue(currentNode, name, namespaceURI, value);
11816
12673
  }
11817
12674
  }
11818
12675
  /* Execute a hook if present */
@@ -11832,17 +12689,136 @@ var purify = {exports: {}};
11832
12689
  /* Execute a hook if present */
11833
12690
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
11834
12691
  /* Sanitize tags and elements */
11835
- _sanitizeElements(shadowNode);
12692
+ _sanitizeElements(shadowNode, fragment);
11836
12693
  /* Check attributes next */
11837
12694
  _sanitizeAttributes(shadowNode);
11838
- /* Deep shadow DOM detected */
11839
- if (shadowNode.content instanceof DocumentFragment) {
12695
+ /* Deep shadow DOM detected.
12696
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType against the
12697
+ DOCUMENT_FRAGMENT_NODE constant rather than instanceof, so we
12698
+ recurse into <template>.content from foreign realms too. */
12699
+ if (_isDocumentFragment(shadowNode.content)) {
11840
12700
  _sanitizeShadowDOM2(shadowNode.content);
11841
12701
  }
12702
+ /* An element iterated here may itself host an attached
12703
+ shadow root. The default NodeIterator does not enter shadow
12704
+ trees, so a shadow root nested inside template.content was
12705
+ previously reached by no walk at all (the pre-pass at
12706
+ _sanitizeAttachedShadowRoots descends via childNodes, which
12707
+ doesn't enter template.content; the template-content recursion
12708
+ above iterates the content but never inspected shadowRoot).
12709
+ Walk it explicitly. The nodeType guard avoids reading
12710
+ shadowRoot off text / comment / CDATA / PI nodes that the
12711
+ iterator also surfaces. */
12712
+ const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
12713
+ if (shadowNodeType === NODE_TYPE.element) {
12714
+ const innerSr = getShadowRoot(shadowNode);
12715
+ if (_isDocumentFragment(innerSr)) {
12716
+ _sanitizeAttachedShadowRoots(innerSr);
12717
+ _sanitizeShadowDOM2(innerSr);
12718
+ }
12719
+ }
11842
12720
  }
11843
12721
  /* Execute a hook if present */
11844
12722
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
11845
12723
  };
12724
+ /**
12725
+ * _sanitizeAttachedShadowRoots
12726
+ *
12727
+ * Walks `root` and feeds every attached shadow root we encounter into
12728
+ * the existing _sanitizeShadowDOM pipeline. The default node iterator
12729
+ * does not descend into shadow trees, so nodes inside an attached
12730
+ * shadow root would otherwise be skipped entirely.
12731
+ *
12732
+ * Two real input paths put attached shadow roots in front of us:
12733
+ * 1. IN_PLACE on a DOM node that already has shadow roots attached.
12734
+ * 2. DOM-node input where importNode(dirty, true) deep-clones the
12735
+ * shadow root because it was created with `clonable: true`.
12736
+ *
12737
+ * This pass runs once, up front, so the main iteration loop (and the
12738
+ * existing _sanitizeShadowDOM template-content recursion) stay
12739
+ * untouched — string-input paths are not affected.
12740
+ *
12741
+ * @param root the subtree root to walk for attached shadow roots
12742
+ */
12743
+ const _sanitizeAttachedShadowRoots = function _sanitizeAttachedShadowRoots(root) {
12744
+ /* Iterative (explicit stack) rather than per-child recursion. DOM APIs
12745
+ impose no depth cap, so an attacker-shaped tree (JSON/CRDT/editor data
12746
+ built straight into the DOM — the IN_PLACE surface) deeper than the JS
12747
+ call-stack budget would otherwise overflow native recursion here and
12748
+ throw at the IN_PLACE entry pre-pass, before a single node is
12749
+ sanitized, leaving the caller's live tree untouched (fail-open). See
12750
+ campaign-3 F4. A heap stack keeps depth off the call stack.
12751
+ Each work item is either a node to descend into, or a deferred
12752
+ `_sanitizeShadowDOM` for an already-walked shadow root. The deferred
12753
+ form preserves the original post-order discipline: a shadow root's
12754
+ nested shadow roots are discovered before the outer shadow is
12755
+ sanitized (which may remove hosts). Pushes are in reverse of the
12756
+ desired processing order (LIFO): template content, then children, then
12757
+ the shadow-sanitize, then the shadow walk — so the order matches the
12758
+ previous recursion exactly. */
12759
+ const stack = [{
12760
+ node: root,
12761
+ shadow: null
12762
+ }];
12763
+ while (stack.length > 0) {
12764
+ const item = stack.pop();
12765
+ /* Deferred shadow-DOM sanitisation: runs after its subtree was walked. */
12766
+ if (item.shadow) {
12767
+ _sanitizeShadowDOM2(item.shadow);
12768
+ continue;
12769
+ }
12770
+ const node = item.node;
12771
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
12772
+ const isElement = nodeType === NODE_TYPE.element;
12773
+ /* (pushed last → processed first) Children, snapshotted in reverse so
12774
+ the first child is processed first. Snapshotting matters because a
12775
+ hook may detach siblings mid-walk. */
12776
+ const childNodes = getChildNodes(node);
12777
+ if (childNodes) {
12778
+ for (let i = childNodes.length - 1; i >= 0; --i) {
12779
+ stack.push({
12780
+ node: childNodes[i],
12781
+ shadow: null
12782
+ });
12783
+ }
12784
+ }
12785
+ /* (pushed before children → processed after them, matching the old
12786
+ "template content last" order) When the node is a <template>,
12787
+ descend into its content. */
12788
+ if (isElement) {
12789
+ const rootName = getNodeName ? getNodeName(node) : null;
12790
+ if (typeof rootName === 'string' && transformCaseFunc(rootName) === 'template') {
12791
+ const content = node.content;
12792
+ if (_isDocumentFragment(content)) {
12793
+ stack.push({
12794
+ node: content,
12795
+ shadow: null
12796
+ });
12797
+ }
12798
+ }
12799
+ }
12800
+ /* Shadow root (processed first): walk its subtree, then sanitise it.
12801
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
12802
+ rather than `instanceof DocumentFragment`, which is realm-bound and
12803
+ silently skipped foreign-realm shadow roots (e.g.
12804
+ iframe.contentDocument attachShadow). */
12805
+ if (isElement) {
12806
+ const sr = getShadowRoot(node);
12807
+ if (_isDocumentFragment(sr)) {
12808
+ /* Push the deferred sanitise first so it pops after the shadow
12809
+ walk we push next, i.e. nested shadow roots are discovered
12810
+ before this one is sanitised. */
12811
+ stack.push({
12812
+ node: null,
12813
+ shadow: sr
12814
+ }, {
12815
+ node: sr,
12816
+ shadow: null
12817
+ });
12818
+ }
12819
+ }
12820
+ }
12821
+ };
11846
12822
  // eslint-disable-next-line complexity
11847
12823
  DOMPurify.sanitize = function (dirty) {
11848
12824
  let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -11859,13 +12835,9 @@ var purify = {exports: {}};
11859
12835
  }
11860
12836
  /* Stringify, in case dirty is an object */
11861
12837
  if (typeof dirty !== 'string' && !_isNode(dirty)) {
11862
- if (typeof dirty.toString === 'function') {
11863
- dirty = dirty.toString();
11864
- if (typeof dirty !== 'string') {
11865
- throw typeErrorCreate('dirty is not a string, aborting');
11866
- }
11867
- } else {
11868
- throw typeErrorCreate('toString is not a function');
12838
+ dirty = stringifyValue(dirty);
12839
+ if (typeof dirty !== 'string') {
12840
+ throw typeErrorCreate('dirty is not a string, aborting');
11869
12841
  }
11870
12842
  }
11871
12843
  /* Return dirty HTML if DOMPurify cannot run */
@@ -11873,24 +12845,90 @@ var purify = {exports: {}};
11873
12845
  return dirty;
11874
12846
  }
11875
12847
  /* Assign config vars */
11876
- if (!SET_CONFIG) {
12848
+ if (SET_CONFIG) {
12849
+ /* Persistent setConfig() path: _parseConfig is skipped, so the sets are
12850
+ * not re-derived per call. Restore them from the pristine bindings
12851
+ * captured at setConfig() time so a previous call's hook clone (mutated
12852
+ * below) does not carry over. */
12853
+ ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
12854
+ ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
12855
+ } else {
11877
12856
  _parseConfig(cfg);
11878
12857
  }
12858
+ /* Clone the hook-mutable allowlists before the walk whenever an
12859
+ * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
12860
+ * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
12861
+ * a hook that widens them would otherwise mutate the shared set
12862
+ * permanently: across later calls and across every element. Cloning per
12863
+ * walk keeps documented in-call widening working while scoping it to the
12864
+ * call. A single guard for both config paths - the per-call path rebinds
12865
+ * the sets in _parseConfig each call, the persistent path restores them
12866
+ * from the captured bindings just above - so the two cannot diverge. */
12867
+ if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) {
12868
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
12869
+ }
12870
+ if (hooks.uponSanitizeAttribute.length > 0) {
12871
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
12872
+ }
11879
12873
  /* Clean up removed elements */
11880
12874
  DOMPurify.removed = [];
11881
- /* Check if dirty is correctly typed for IN_PLACE */
11882
- if (typeof dirty === 'string') {
11883
- IN_PLACE = false;
11884
- }
11885
- if (IN_PLACE) {
11886
- /* Do some early pre-sanitization to avoid unsafe root nodes */
11887
- if (dirty.nodeName) {
11888
- const tagName = transformCaseFunc(dirty.nodeName);
12875
+ /* Resolve IN_PLACE for this call without mutating persistent config.
12876
+ Writing the IN_PLACE closure variable here leaks under setConfig(),
12877
+ where _parseConfig is skipped on later calls: a single string call would
12878
+ disable in-place mode for every subsequent node call, returning a
12879
+ sanitized copy while leaving the caller's node — which in-place callers
12880
+ keep using and whose return value they ignore — unsanitized. REPORT-2. */
12881
+ const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
12882
+ if (inPlace) {
12883
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
12884
+ linkage across the live tree BEFORE the walk, so no patch can fire
12885
+ mid-walk and inject into an already-processed region. Runs first, so
12886
+ it also covers the forbidden/clobbered roots that throw below. */
12887
+ _neutralizePatchLinkage(dirty);
12888
+ /* Do some early pre-sanitization to avoid unsafe root nodes.
12889
+ Read nodeName through the cached prototype getter — a clobbering
12890
+ child named "nodeName" on the form root would otherwise shadow
12891
+ the property and let this check skip the root-allowlist
12892
+ validation entirely. */
12893
+ const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
12894
+ if (typeof nn === 'string') {
12895
+ const tagName = transformCaseFunc(nn);
11889
12896
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
12897
+ /* Fail closed on a live root: neutralize handlers/children before
12898
+ throwing, exactly as the mid-walk abort path does. */
12899
+ _neutralizeRoot(dirty);
11890
12900
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
11891
12901
  }
11892
12902
  }
11893
- } else if (dirty instanceof Node) {
12903
+ /* Pre-flight the root through _isClobbered. The iterator-driven
12904
+ removal path can not detach a parent-less root: _forceRemove
12905
+ falls through to Element.prototype.remove(), which per spec
12906
+ is a no-op on a node with no parent. A clobbered root would
12907
+ then survive the main loop with its attributes uninspected,
12908
+ because _sanitizeAttributes early-returns on _isClobbered. The
12909
+ result would be an attacker-controlled form, complete with any
12910
+ event-handler attributes the caller passed in, handed back to
12911
+ the application unsanitized. Refuse to sanitize such a root
12912
+ the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
12913
+ if (_isClobbered(dirty)) {
12914
+ /* Fail closed on a live clobbered root before throwing.
12915
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
12916
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
12917
+ _neutralizeRoot(dirty);
12918
+ throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
12919
+ }
12920
+ /* Sanitize attached shadow roots before the main iterator runs.
12921
+ The iterator does not descend into shadow trees. Same fail-closed
12922
+ barrier as the main walk (campaign-3 F2): a custom-element reaction
12923
+ inside a shadow root could abort this pre-pass before the walk runs,
12924
+ which would otherwise leave the entire live tree unsanitized. */
12925
+ try {
12926
+ _sanitizeAttachedShadowRoots(dirty);
12927
+ } catch (error) {
12928
+ _neutralizeRoot(dirty);
12929
+ throw error;
12930
+ }
12931
+ } else if (_isNode(dirty)) {
11894
12932
  /* If dirty is a DOM element, append to an empty document to avoid
11895
12933
  elements being stripped by the parser */
11896
12934
  body = _initDocument('<!---->');
@@ -11904,12 +12942,18 @@ var purify = {exports: {}};
11904
12942
  // eslint-disable-next-line unicorn/prefer-dom-node-append
11905
12943
  body.appendChild(importedNode);
11906
12944
  }
12945
+ /* Clonable shadow roots are deep-cloned by importNode(); sanitize
12946
+ them before the main iterator runs, since the iterator does not
12947
+ descend into shadow trees. The walk routes every read through a
12948
+ cached prototype getter so clobbering descendants on a form root
12949
+ cannot hide a shadow host from this pass. */
12950
+ _sanitizeAttachedShadowRoots(importedNode);
11907
12951
  } else {
11908
12952
  /* Exit directly if we have nothing to do */
11909
12953
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
11910
12954
  // eslint-disable-next-line unicorn/prefer-includes
11911
12955
  dirty.indexOf('<') === -1) {
11912
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
12956
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
11913
12957
  }
11914
12958
  /* Initialize the document to work on */
11915
12959
  body = _initDocument(dirty);
@@ -11923,31 +12967,71 @@ var purify = {exports: {}};
11923
12967
  _forceRemove(body.firstChild);
11924
12968
  }
11925
12969
  /* Get node iterator */
11926
- const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
11927
- /* Now start iterating over the created document */
11928
- while (currentNode = nodeIterator.nextNode()) {
11929
- /* Sanitize tags and elements */
11930
- _sanitizeElements(currentNode);
11931
- /* Check attributes next */
11932
- _sanitizeAttributes(currentNode);
11933
- /* Shadow DOM detected, sanitize it */
11934
- if (currentNode.content instanceof DocumentFragment) {
11935
- _sanitizeShadowDOM2(currentNode.content);
12970
+ const walkRoot = inPlace ? dirty : body;
12971
+ /* Now start iterating over the created document.
12972
+ The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
12973
+ engine/custom-element mutation can detach a node mid-walk so
12974
+ `_forceRemove`'s parentless guard throws, aborting the loop. Without the
12975
+ barrier the caller's in-place tree would be left half-sanitized with the
12976
+ unvisited tail still armed. _createNodeIterator itself is inside the
12977
+ barrier too: constructing the iterator dereferences the root's document,
12978
+ and any failure there (e.g. an exotic/clobbered root) must still fail
12979
+ closed rather than skip the neutralize. On any throw we fail closed -
12980
+ strip the in-place root bare - then rethrow so the existing throw
12981
+ contract is preserved. (String/DOM-copy paths never return the partial
12982
+ body, so the propagating throw is already fail-closed there.) */
12983
+ try {
12984
+ const nodeIterator = _createNodeIterator(walkRoot);
12985
+ while (currentNode = nodeIterator.nextNode()) {
12986
+ /* Sanitize tags and elements */
12987
+ _sanitizeElements(currentNode, walkRoot);
12988
+ /* Check attributes next */
12989
+ _sanitizeAttributes(currentNode);
12990
+ /* Shadow DOM detected, sanitize it.
12991
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
12992
+ instead of instanceof, so foreign-realm <template>.content is
12993
+ walked correctly. */
12994
+ if (_isDocumentFragment(currentNode.content)) {
12995
+ _sanitizeShadowDOM2(currentNode.content);
12996
+ }
12997
+ }
12998
+ } catch (error) {
12999
+ if (inPlace) {
13000
+ _neutralizeRoot(dirty);
13001
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
13002
+ detached from the root, so _neutralizeRoot's subtree pass does not
13003
+ reach them. Defuse them too, mirroring the success-path loop below. */
13004
+ arrayForEach(DOMPurify.removed, entry => {
13005
+ if (entry.element) {
13006
+ _neutralizeSubtree(entry.element);
13007
+ }
13008
+ });
11936
13009
  }
13010
+ throw error;
11937
13011
  }
11938
13012
  /* If we sanitized `dirty` in-place, return it. */
11939
- if (IN_PLACE) {
13013
+ if (inPlace) {
13014
+ /* Fail-closed completion of the audit-5 F1 fix: every node removed from
13015
+ the caller's live tree is detached but may still hold a queued
13016
+ resource-event handler that fires in page scope after we return. The
13017
+ move-hoist covers only disallowed-tag KEEP_CONTENT removals; strip the
13018
+ non-allow-listed attributes off every other removed subtree (clobber,
13019
+ mXSS, namespace, comments, KEEP_CONTENT:false, …) so those handlers are
13020
+ cancelled before any event can fire. Runs synchronously, pre-return. */
13021
+ arrayForEach(DOMPurify.removed, entry => {
13022
+ if (entry.element) {
13023
+ _neutralizeSubtree(entry.element);
13024
+ }
13025
+ });
13026
+ if (SAFE_FOR_TEMPLATES) {
13027
+ _scrubTemplateExpressions2(dirty);
13028
+ }
11940
13029
  return dirty;
11941
13030
  }
11942
13031
  /* Return sanitized string or DOM */
11943
13032
  if (RETURN_DOM) {
11944
13033
  if (SAFE_FOR_TEMPLATES) {
11945
- body.normalize();
11946
- let html = body.innerHTML;
11947
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
11948
- html = stringReplace(html, expr, ' ');
11949
- });
11950
- body.innerHTML = html;
13034
+ _scrubTemplateExpressions2(body);
11951
13035
  }
11952
13036
  if (RETURN_DOM_FRAGMENT) {
11953
13037
  returnNode = createDocumentFragment.call(body.ownerDocument);
@@ -11977,20 +13061,28 @@ var purify = {exports: {}};
11977
13061
  }
11978
13062
  /* Sanitize final string template-safe */
11979
13063
  if (SAFE_FOR_TEMPLATES) {
11980
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
11981
- serializedHTML = stringReplace(serializedHTML, expr, ' ');
11982
- });
13064
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
11983
13065
  }
11984
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
13066
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
11985
13067
  };
11986
13068
  DOMPurify.setConfig = function () {
11987
13069
  let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
11988
13070
  _parseConfig(cfg);
11989
13071
  SET_CONFIG = true;
13072
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
13073
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
11990
13074
  };
11991
13075
  DOMPurify.clearConfig = function () {
11992
13076
  CONFIG = null;
11993
13077
  SET_CONFIG = false;
13078
+ SET_CONFIG_ALLOWED_TAGS = null;
13079
+ SET_CONFIG_ALLOWED_ATTR = null;
13080
+ // Drop any caller-supplied Trusted Types policy so it cannot poison later
13081
+ // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
13082
+ // never recreated — Trusted Types throws on duplicate names) is restored by
13083
+ // the next `_parseConfig`. See GHSA-vxr8-fq34-vvx9.
13084
+ trustedTypesPolicy = defaultTrustedTypesPolicy;
13085
+ emptyHTML = '';
11994
13086
  };
11995
13087
  DOMPurify.isValidAttribute = function (tag, attr, value) {
11996
13088
  /* Initialize shared config vars if necessary. */
@@ -12005,9 +13097,19 @@ var purify = {exports: {}};
12005
13097
  if (typeof hookFunction !== 'function') {
12006
13098
  return;
12007
13099
  }
13100
+ /* Reject unknown entry points. Without this, a non-hook key (e.g.
13101
+ * '__proto__') indexes off the prototype chain rather than a real
13102
+ * hook array, and arrayPush then writes to Object.prototype. Guard
13103
+ * with an own-property check against the known hook names. */
13104
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
13105
+ return;
13106
+ }
12008
13107
  arrayPush(hooks[entryPoint], hookFunction);
12009
13108
  };
12010
13109
  DOMPurify.removeHook = function (entryPoint, hookFunction) {
13110
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
13111
+ return undefined;
13112
+ }
12011
13113
  if (hookFunction !== undefined) {
12012
13114
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
12013
13115
  return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
@@ -12015,6 +13117,9 @@ var purify = {exports: {}};
12015
13117
  return arrayPop(hooks[entryPoint]);
12016
13118
  };
12017
13119
  DOMPurify.removeHooks = function (entryPoint) {
13120
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
13121
+ return;
13122
+ }
12018
13123
  hooks[entryPoint] = [];
12019
13124
  };
12020
13125
  DOMPurify.removeAllHooks = function () {
@@ -13194,7 +14299,7 @@ class MenuGroup {
13194
14299
  }; }
13195
14300
  }
13196
14301
 
13197
- 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}}";
14302
+ 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}}";
13198
14303
  var IcMenuItemStyle0 = icMenuItemCss;
13199
14304
 
13200
14305
  /**
@@ -13219,6 +14324,10 @@ class MenuItem {
13219
14324
  * The variant of the menu item.
13220
14325
  */
13221
14326
  this.variant = "default";
14327
+ /**
14328
+ * If `true`, the menu will close when this menu item is clicked.
14329
+ */
14330
+ this.closeMenuOnSelect = true;
13222
14331
  this.handleClick = (e) => {
13223
14332
  if (isPropDefined(this.submenuTriggerFor)) {
13224
14333
  this.triggerPopoverMenuInstance.emit();
@@ -13319,7 +14428,8 @@ class MenuItem {
13319
14428
  "rel": [1],
13320
14429
  "submenuTriggerFor": [1, "submenu-trigger-for"],
13321
14430
  "target": [1],
13322
- "variant": [1537]
14431
+ "variant": [1537],
14432
+ "closeMenuOnSelect": [4, "close-menu-on-select"]
13323
14433
  },
13324
14434
  "$listeners$": [[2, "click", "handleHostClick"]],
13325
14435
  "$lazyBundleId$": "-",
@@ -13361,11 +14471,14 @@ class NavigationButton {
13361
14471
  */
13362
14472
  this.hostMutationCallback = (mutationList) => {
13363
14473
  mutationList.forEach(({ attributeName }) => {
13364
- if (attributeName) {
14474
+ if (attributeName && MUTABLE_ATTRIBUTES$2.includes(attributeName)) {
13365
14475
  const attribute = this.el.getAttribute(attributeName);
13366
- if (attribute && MUTABLE_ATTRIBUTES$2.includes(attributeName)) {
14476
+ if (attribute) {
13367
14477
  this.inheritedAttributes[attributeName] = attribute;
13368
14478
  }
14479
+ else {
14480
+ delete this.inheritedAttributes[attributeName];
14481
+ }
13369
14482
  }
13370
14483
  });
13371
14484
  };
@@ -13429,11 +14542,11 @@ class NavigationButton {
13429
14542
  target,
13430
14543
  theme,
13431
14544
  };
13432
- return (hAsync(Host, { key: '538ea349b1f0791b0cd68898380fe4cfd41b5496', class: {
14545
+ return (hAsync(Host, { key: 'dd30f288d728c8e8dffd4108d05e0a02b782d9ea', class: {
13433
14546
  "in-side-menu": isMenuMode,
13434
14547
  [`ic-theme-${theme}`]: theme !== "inherit",
13435
14548
  dark: parentThemeDark,
13436
- } }, 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" }))));
14549
+ } }, 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" }))));
13437
14550
  }
13438
14551
  static get delegatesFocus() { return true; }
13439
14552
  get el() { return getElement(this); }
@@ -15205,6 +16318,10 @@ class PopoverMenu {
15205
16318
  * If `true`, the popover menu will be displayed.
15206
16319
  */
15207
16320
  this.open = undefined;
16321
+ /**
16322
+ * 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.
16323
+ */
16324
+ this.closeOnItemSelect = "always";
15208
16325
  this.setButtonFocus = () => {
15209
16326
  var _a;
15210
16327
  (_a = this.popoverMenuEls[this.currentFocus]) === null || _a === void 0 ? void 0 : _a.focus();
@@ -15378,6 +16495,13 @@ class PopoverMenu {
15378
16495
  }
15379
16496
  handleMenuItemClick(ev) {
15380
16497
  if (!ev.detail.submenuTriggerFor && ev.detail.label !== "Back") {
16498
+ if (this.closeOnItemSelect === "never" ||
16499
+ (ev.detail.variant === "toggle" &&
16500
+ this.closeOnItemSelect === "toggle") ||
16501
+ ev.detail.closeMenuOnSelect === false) {
16502
+ this.currentFocus = this.popoverMenuEls.findIndex((menuItem) => menuItem === ev.detail);
16503
+ return;
16504
+ }
15381
16505
  this.closeMenu(false, ev.detail);
15382
16506
  }
15383
16507
  }
@@ -15473,19 +16597,19 @@ class PopoverMenu {
15473
16597
  this.popperProps = props;
15474
16598
  }
15475
16599
  render() {
15476
- return (hAsync(Host, { key: '7f6ecb546f399d0b1b5da98ede2923e196314f54', class: {
16600
+ return (hAsync(Host, { key: 'b664e4a0944016c88fe19232ea7b1cf2fa65cbd9', class: {
15477
16601
  [`ic-theme-${this.theme}`]: this.theme !== "inherit",
15478
- } }, isSlotUsed(this.el, "anchor") && hAsync("slot", { key: '70e80e8ce605af1076d48399bd19ef337fba4e69', name: "anchor" }), hAsync("div", { key: 'd5bae4e0dc2318a501855154eb6f8f904eba823e', class: {
16602
+ } }, isSlotUsed(this.el, "anchor") && hAsync("slot", { key: '978425aac1a58c1f19ad59e8ebe9869492fe8a5d', name: "anchor" }), hAsync("div", { key: 'c36a21a4f9913deebed1c418ce4c85995fa49054', class: {
15479
16603
  ["ic-popover-menu"]: true,
15480
16604
  ["ic-popover-menu-open"]: !!this.open,
15481
- } }, hAsync("div", { key: '552017695fedbac167dfba7972a93ec8fded05ed', id: this.parentPopover === undefined
16605
+ } }, hAsync("div", { key: '5e54d43f8699270ea59ee2d408d099e73b51a5e0', id: this.parentPopover === undefined
15482
16606
  ? `ic-popover-submenu-${this.submenuId}`
15483
16607
  : "", class: {
15484
16608
  menu: true,
15485
- } }, hAsync("span", { key: 'd7acdf61f5003080acfb2fcfb906ade2ea029285', class: {
16609
+ } }, hAsync("span", { key: '50b901cdd6ce1c6e51839cdacb61034f45972a3a', class: {
15486
16610
  "opening-from-parent": this.openingFromParent,
15487
16611
  "opening-from-child": this.openingFromChild,
15488
- } }, 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' })))))));
16612
+ } }, 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' })))))));
15489
16613
  }
15490
16614
  static get delegatesFocus() { return true; }
15491
16615
  get el() { return getElement(this); }
@@ -15505,6 +16629,7 @@ class PopoverMenu {
15505
16629
  "submenuLevel": [2, "submenu-level"],
15506
16630
  "theme": [1],
15507
16631
  "open": [1540],
16632
+ "closeOnItemSelect": [1, "close-on-item-select"],
15508
16633
  "openingFromChild": [32],
15509
16634
  "openingFromParent": [32],
15510
16635
  "popperProps": [32],