@deepseek-ai/dsh-client-ui-sidebar-documentpreview 0.2.1-alpha.1 → 0.2.1-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1080,7 +1080,7 @@ window.__ModuleLoader__.load({
1080
1080
  return {
1081
1081
  id: TEXTPREVIEW_ID,
1082
1082
  kind: TEXTPREVIEW_KIND,
1083
- patterns: ["dsh-resource://file/**"],
1083
+ patterns: ["dsh-resource://file/[\\s\\S]+"],
1084
1084
  priority: "fallback",
1085
1085
  canOpen: (address) => parseFileAddress(address)?.scope === "session",
1086
1086
  title: basenameOf
@@ -1756,8 +1756,11 @@ document.open();document.write(html);document.close();
1756
1756
  })()<\/script>`;
1757
1757
  }
1758
1758
  //#endregion
1759
- //#region ../../../node_modules/.pnpm/dompurify@3.4.11/node_modules/dompurify/dist/purify.es.mjs
1760
- /*! @license DOMPurify 3.4.11 | (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.11/LICENSE */
1759
+ //#region ../../../node_modules/.pnpm/dompurify@3.4.16/node_modules/dompurify/dist/purify.es.mjs
1760
+ /*! @license DOMPurify 3.4.16 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.16/LICENSE */
1761
+ function _OverloadYield(e, d) {
1762
+ this.v = e, this.k = d;
1763
+ }
1761
1764
  function _arrayLikeToArray(r, a) {
1762
1765
  (null == a || a > r.length) && (a = r.length);
1763
1766
  for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
@@ -1769,12 +1772,14 @@ document.open();document.write(html);document.close();
1769
1772
  function _iterableToArrayLimit(r, l) {
1770
1773
  var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
1771
1774
  if (null != t) {
1772
- var e, n, i, u, a = [], f = true, o = false;
1775
+ var e, n, i, u, a = [], f = !0, o = !1;
1773
1776
  try {
1774
- if (i = (t = t.call(r)).next, 0 === l);
1775
- else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
1777
+ if (i = (t = t.call(r)).next, 0 === l) {
1778
+ if (Object(t) !== t) return;
1779
+ f = !1;
1780
+ } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
1776
1781
  } catch (r) {
1777
- o = true, n = r;
1782
+ o = !0, n = r;
1778
1783
  } finally {
1779
1784
  try {
1780
1785
  if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
@@ -1788,6 +1793,7 @@ document.open();document.write(html);document.close();
1788
1793
  function _nonIterableRest() {
1789
1794
  throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
1790
1795
  }
1796
+ /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
1791
1797
  function _slicedToArray(r, e) {
1792
1798
  return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
1793
1799
  }
@@ -1798,9 +1804,64 @@ document.open();document.write(html);document.close();
1798
1804
  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;
1799
1805
  }
1800
1806
  }
1801
- const entries = Object.entries, setPrototypeOf = Object.setPrototypeOf, isFrozen = Object.isFrozen, getPrototypeOf = Object.getPrototypeOf, getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
1802
- let freeze = Object.freeze, seal = Object.seal, create = Object.create;
1803
- let _ref = typeof Reflect !== "undefined" && Reflect, apply$7 = _ref.apply, construct = _ref.construct;
1807
+ function AsyncGenerator(e) {
1808
+ var t, n;
1809
+ function resume(t, n) {
1810
+ try {
1811
+ var r = e[t](n), o = r.value, u = o instanceof _OverloadYield;
1812
+ Promise.resolve(u ? o.v : o).then(function(n) {
1813
+ if (u) {
1814
+ var i = "return" === t && o.k ? t : "next";
1815
+ if (!o.k || n.done) return resume(i, n);
1816
+ n = e[i](n).value;
1817
+ }
1818
+ settle(!!r.done, n);
1819
+ }, function(e) {
1820
+ resume("throw", e);
1821
+ });
1822
+ } catch (e) {
1823
+ settle(2, e);
1824
+ }
1825
+ }
1826
+ function settle(e, r) {
1827
+ 2 === e ? t.reject(r) : t.resolve({
1828
+ value: r,
1829
+ done: e
1830
+ }), (t = t.next) ? resume(t.key, t.arg) : n = null;
1831
+ }
1832
+ this._invoke = function(e, r) {
1833
+ return new Promise(function(o, u) {
1834
+ var i = {
1835
+ key: e,
1836
+ arg: r,
1837
+ resolve: o,
1838
+ reject: u,
1839
+ next: null
1840
+ };
1841
+ n ? n = n.next = i : (t = n = i, resume(e, r));
1842
+ });
1843
+ }, "function" != typeof e.return && (this.return = void 0);
1844
+ }
1845
+ AsyncGenerator.prototype["function" == typeof Symbol && Symbol.asyncIterator || "@@asyncIterator"] = function() {
1846
+ return this;
1847
+ }, AsyncGenerator.prototype.next = function(e) {
1848
+ return this._invoke("next", e);
1849
+ }, AsyncGenerator.prototype.throw = function(e) {
1850
+ return this._invoke("throw", e);
1851
+ }, AsyncGenerator.prototype.return = function(e) {
1852
+ return this._invoke("return", e);
1853
+ };
1854
+ const entries = Object.entries;
1855
+ const setPrototypeOf = Object.setPrototypeOf;
1856
+ const isFrozen = Object.isFrozen;
1857
+ const getPrototypeOf = Object.getPrototypeOf;
1858
+ const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
1859
+ let freeze = Object.freeze;
1860
+ let seal = Object.seal;
1861
+ let create = Object.create;
1862
+ let _ref = typeof Reflect !== "undefined" && Reflect;
1863
+ let apply$7 = _ref.apply;
1864
+ let construct = _ref.construct;
1804
1865
  if (!freeze) freeze = function freeze(x) {
1805
1866
  return x;
1806
1867
  };
@@ -1816,9 +1877,11 @@ document.open();document.write(html);document.close();
1816
1877
  return new Func(...args);
1817
1878
  };
1818
1879
  const arrayForEach = unapply(Array.prototype.forEach);
1880
+ Array.prototype.indexOf;
1819
1881
  const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
1820
1882
  const arrayPop = unapply(Array.prototype.pop);
1821
1883
  const arrayPush = unapply(Array.prototype.push);
1884
+ Array.prototype.slice;
1822
1885
  const arraySplice = unapply(Array.prototype.splice);
1823
1886
  const arrayIsArray = Array.isArray;
1824
1887
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
@@ -2396,6 +2459,7 @@ document.open();document.write(html);document.close();
2396
2459
  "direction",
2397
2460
  "display",
2398
2461
  "divisor",
2462
+ "dominant-baseline",
2399
2463
  "dur",
2400
2464
  "edgemode",
2401
2465
  "elevation",
@@ -2477,6 +2541,7 @@ document.open();document.write(html);document.close();
2477
2541
  "patterncontentunits",
2478
2542
  "patterntransform",
2479
2543
  "patternunits",
2544
+ "pointer-events",
2480
2545
  "points",
2481
2546
  "preservealpha",
2482
2547
  "preserveaspectratio",
@@ -2523,6 +2588,7 @@ document.open();document.write(html);document.close();
2523
2588
  "transform-origin",
2524
2589
  "text-anchor",
2525
2590
  "text-decoration",
2591
+ "text-orientation",
2526
2592
  "text-rendering",
2527
2593
  "textlength",
2528
2594
  "type",
@@ -2530,6 +2596,7 @@ document.open();document.write(html);document.close();
2530
2596
  "u2",
2531
2597
  "unicode",
2532
2598
  "values",
2599
+ "vector-effect",
2533
2600
  "viewbox",
2534
2601
  "visibility",
2535
2602
  "version",
@@ -2643,6 +2710,24 @@ document.open();document.write(html);document.close();
2643
2710
  documentFragment: 11,
2644
2711
  notation: 12
2645
2712
  };
2713
+ const LITERAL_TEXT_ELEMENT_NAMES = [
2714
+ "style",
2715
+ "script",
2716
+ "xmp",
2717
+ "iframe",
2718
+ "noembed",
2719
+ "noframes",
2720
+ "plaintext",
2721
+ "noscript"
2722
+ ];
2723
+ const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
2724
+ const LITERAL_TEXT_CLOSE = function() {
2725
+ const map = {};
2726
+ arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, (name) => {
2727
+ map[name] = seal(new RegExp("</" + name + "(?=[\\t\\n\\f\\r />])", "i"));
2728
+ });
2729
+ return freeze(map);
2730
+ }();
2646
2731
  const getGlobal = function getGlobal() {
2647
2732
  return typeof window === "undefined" ? null : window;
2648
2733
  };
@@ -2702,10 +2787,25 @@ document.open();document.write(html);document.close();
2702
2787
  const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
2703
2788
  return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone$1(options.base) : {}, cfg[key], options.transform) : fallback;
2704
2789
  };
2790
+ /**
2791
+ * Resolve an object-valued configuration option: a prototype-free clone
2792
+ * of cfg[key] when it is an own, truthy object property, else a fresh
2793
+ * fallback built by makeFallback (fresh on every parse, so a previous
2794
+ * parse can never leak state into the next one).
2795
+ *
2796
+ * @param cfg the cloned, prototype-free configuration object
2797
+ * @param key the configuration property to read
2798
+ * @param makeFallback builds the fallback value when the option is absent
2799
+ * @returns the resolved object
2800
+ */
2801
+ const _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallback) {
2802
+ const value = objectHasOwnProperty(cfg, key) ? cfg[key] : void 0;
2803
+ return value && typeof value === "object" ? clone$1(value) : makeFallback();
2804
+ };
2705
2805
  function createDOMPurify() {
2706
2806
  let window = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
2707
2807
  const DOMPurify = (root) => createDOMPurify(root);
2708
- DOMPurify.version = "3.4.11";
2808
+ DOMPurify.version = "3.4.16";
2709
2809
  DOMPurify.removed = [];
2710
2810
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
2711
2811
  DOMPurify.isSupported = false;
@@ -2722,6 +2822,7 @@ document.open();document.write(html);document.close();
2722
2822
  const ElementPrototype = Element.prototype;
2723
2823
  const cloneNode = lookupGetter(ElementPrototype, "cloneNode");
2724
2824
  const remove = lookupGetter(ElementPrototype, "remove");
2825
+ const removeAttributeNode = lookupGetter(ElementPrototype, "removeAttributeNode");
2725
2826
  const getNextSibling = lookupGetter(ElementPrototype, "nextSibling");
2726
2827
  const getChildNodes = lookupGetter(ElementPrototype, "childNodes");
2727
2828
  const getParentNode = lookupGetter(ElementPrototype, "parentNode");
@@ -2729,6 +2830,13 @@ document.open();document.write(html);document.close();
2729
2830
  const getAttributes = lookupGetter(ElementPrototype, "attributes");
2730
2831
  const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeType") : null;
2731
2832
  const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeName") : null;
2833
+ const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, "ownerDocument") : null;
2834
+ const _readNodeType = function _readNodeType(node) {
2835
+ return getNodeType ? getNodeType(node) : node.nodeType;
2836
+ };
2837
+ const _readNodeName = function _readNodeName(node) {
2838
+ return getNodeName ? getNodeName(node) : node.nodeName;
2839
+ };
2732
2840
  if (typeof HTMLTemplateElement === "function") {
2733
2841
  const template = document.createElement("template");
2734
2842
  if (template.content && template.content.ownerDocument) document = template.content.ownerDocument;
@@ -2986,9 +3094,9 @@ document.open();document.write(html);document.close();
2986
3094
  IN_PLACE = cfg.IN_PLACE || false;
2987
3095
  IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI;
2988
3096
  NAMESPACE = typeof cfg.NAMESPACE === "string" ? cfg.NAMESPACE : HTML_NAMESPACE;
2989
- MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, "MATHML_TEXT_INTEGRATION_POINTS") && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === "object" ? clone$1(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
2990
- HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, "HTML_INTEGRATION_POINTS") && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === "object" ? clone$1(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
2991
- const customElementHandling = objectHasOwnProperty(cfg, "CUSTOM_ELEMENT_HANDLING") && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === "object" ? clone$1(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
3097
+ MATHML_TEXT_INTEGRATION_POINTS = _resolveObjectOption(cfg, "MATHML_TEXT_INTEGRATION_POINTS", () => addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS));
3098
+ HTML_INTEGRATION_POINTS = _resolveObjectOption(cfg, "HTML_INTEGRATION_POINTS", () => addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS));
3099
+ const customElementHandling = _resolveObjectOption(cfg, "CUSTOM_ELEMENT_HANDLING", () => create(null));
2992
3100
  CUSTOM_ELEMENT_HANDLING = create(null);
2993
3101
  if (objectHasOwnProperty(customElementHandling, "tagNameCheck") && isRegexOrFunction(customElementHandling.tagNameCheck)) CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck;
2994
3102
  if (objectHasOwnProperty(customElementHandling, "attributeNameCheck") && isRegexOrFunction(customElementHandling.attributeNameCheck)) CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck;
@@ -3035,11 +3143,6 @@ document.open();document.write(html);document.close();
3035
3143
  addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
3036
3144
  }
3037
3145
  }
3038
- if (objectHasOwnProperty(cfg, "ADD_URI_SAFE_ATTR") && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
3039
- if (objectHasOwnProperty(cfg, "FORBID_CONTENTS") && arrayIsArray(cfg.FORBID_CONTENTS)) {
3040
- if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) FORBID_CONTENTS = clone$1(FORBID_CONTENTS);
3041
- addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
3042
- }
3043
3146
  if (objectHasOwnProperty(cfg, "ADD_FORBID_CONTENTS") && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
3044
3147
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) FORBID_CONTENTS = clone$1(FORBID_CONTENTS);
3045
3148
  addToSet(FORBID_CONTENTS, cfg.ADD_FORBID_CONTENTS, transformCaseFunc);
@@ -3156,6 +3259,32 @@ document.open();document.write(html);document.close();
3156
3259
  }
3157
3260
  };
3158
3261
  /**
3262
+ * _stripAttributeNode
3263
+ *
3264
+ * Remove a single Attr node case/namespace-exactly on an attribute-teardown
3265
+ * path. Name-based removeAttribute() ASCII-lowercases its lookup key for an
3266
+ * HTML element in an HTML document and so silently misses a case-preserved
3267
+ * handler (e.g. `ONERROR` off an XML/XHTML import) - the same defect
3268
+ * _removeAttribute() was fixed for, which a name-based call would reintroduce
3269
+ * on these IN_PLACE teardown paths. Unlike _removeAttribute this does not
3270
+ * record into DOMPurify.removed: the neutralize passes intentionally do not
3271
+ * book-keep. A clobbered/detached node falls back to best-effort name-based
3272
+ * removal.
3273
+ *
3274
+ * @param element the element to strip the attribute from
3275
+ * @param attribute the Attr node to remove
3276
+ * @param name the attribute's name, for the fallback path
3277
+ */
3278
+ const _stripAttributeNode = function _stripAttributeNode(element, attribute, name) {
3279
+ try {
3280
+ removeAttributeNode(element, attribute);
3281
+ } catch (_) {
3282
+ try {
3283
+ element.removeAttribute(name);
3284
+ } catch (_) {}
3285
+ }
3286
+ };
3287
+ /**
3159
3288
  * _neutralizeRoot
3160
3289
  *
3161
3290
  * Fail-closed teardown of an in-place root after the sanitize walk aborts
@@ -3172,6 +3301,7 @@ document.open();document.write(html);document.close();
3172
3301
  * @param root the in-place root to empty
3173
3302
  */
3174
3303
  const _neutralizeRoot = function _neutralizeRoot(root) {
3304
+ _neutralizeSubtree(root);
3175
3305
  const childNodes = getChildNodes(root);
3176
3306
  if (childNodes) {
3177
3307
  const snapshot = [];
@@ -3188,30 +3318,45 @@ document.open();document.write(html);document.close();
3188
3318
  if (attributes) for (let i = attributes.length - 1; i >= 0; --i) {
3189
3319
  const attribute = attributes[i];
3190
3320
  const name = attribute && attribute.name;
3191
- if (typeof name === "string") try {
3192
- root.removeAttribute(name);
3193
- } catch (_) {}
3321
+ if (typeof name === "string") _stripAttributeNode(root, attribute, name);
3194
3322
  }
3195
3323
  };
3196
3324
  /**
3197
3325
  * _removeAttribute
3198
3326
  *
3327
+ * Name-based getAttributeNode()/removeAttribute() ASCII-lowercase their
3328
+ * lookup key for HTML elements in an HTML document, so they silently miss an
3329
+ * attribute whose stored qualified name still contains uppercase ASCII
3330
+ * letters. That happens when the node came from a case-preserving source
3331
+ * (an XML/XHTML document imported via importNode(), or createAttributeNS()),
3332
+ * where e.g. `ONERROR` survives the walk: the policy check lowercases to
3333
+ * `onerror` and rejects it, but `removeAttribute('ONERROR')` looks up
3334
+ * `onerror` and finds nothing. Remove the exact Attr node instead, which is
3335
+ * case- and namespace-exact, and fall back to name-based removal only when
3336
+ * the caller could not supply the node.
3337
+ *
3199
3338
  * @param name an Attribute name
3200
3339
  * @param element a DOM node
3340
+ * @param attr the exact Attr node to remove, when the caller has it
3201
3341
  */
3202
- const _removeAttribute = function _removeAttribute(name, element) {
3342
+ const _removeAttribute = function _removeAttribute(name, element, attr) {
3343
+ if (!attr) try {
3344
+ attr = element.getAttributeNode(name);
3345
+ } catch (_) {
3346
+ attr = null;
3347
+ }
3348
+ arrayPush(DOMPurify.removed, {
3349
+ attribute: attr || null,
3350
+ from: element
3351
+ });
3203
3352
  try {
3204
- arrayPush(DOMPurify.removed, {
3205
- attribute: element.getAttributeNode(name),
3206
- from: element
3207
- });
3353
+ if (attr) removeAttributeNode(element, attr);
3354
+ else element.removeAttribute(name);
3208
3355
  } catch (_) {
3209
- arrayPush(DOMPurify.removed, {
3210
- attribute: null,
3211
- from: element
3212
- });
3356
+ try {
3357
+ element.removeAttribute(name);
3358
+ } catch (_) {}
3213
3359
  }
3214
- element.removeAttribute(name);
3215
3360
  if (name === "is") if (RETURN_DOM || RETURN_DOM_FRAGMENT) try {
3216
3361
  _forceRemove(element);
3217
3362
  } catch (_) {}
@@ -3236,9 +3381,7 @@ document.open();document.write(html);document.close();
3236
3381
  const attribute = attributes[i];
3237
3382
  const name = attribute && attribute.name;
3238
3383
  if (typeof name !== "string" || ALLOWED_ATTR[transformCaseFunc(name)]) continue;
3239
- try {
3240
- element.removeAttribute(name);
3241
- } catch (_) {}
3384
+ _stripAttributeNode(element, attribute, name);
3242
3385
  }
3243
3386
  };
3244
3387
  /**
@@ -3267,7 +3410,82 @@ document.open();document.write(html);document.close();
3267
3410
  const stack = [root];
3268
3411
  while (stack.length > 0) {
3269
3412
  const node = stack.pop();
3270
- if ((getNodeType ? getNodeType(node) : node.nodeType) === NODE_TYPE.element) _stripDisallowedAttributes(node);
3413
+ if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
3414
+ const childNodes = getChildNodes(node);
3415
+ if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
3416
+ }
3417
+ };
3418
+ /**
3419
+ * _neutralizePatchLinkage
3420
+ *
3421
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
3422
+ * hardening, https://github.com/WICG/declarative-partial-updates).
3423
+ *
3424
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
3425
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
3426
+ * reaches each node. On a live in-place root that leaves a window: from the
3427
+ * moment the root is connected until the walk arrives at a given node, that
3428
+ * node's linkage is live. A patch applied on connection/stream can fire as
3429
+ * a microtask during the walk and inject or teleport an unsanitized DOM
3430
+ * range into a region the iterator has already passed and will not revisit,
3431
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
3432
+ * whole tree once up front and sever every linkage before the walk begins,
3433
+ * closing that window.
3434
+ *
3435
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
3436
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
3437
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
3438
+ * to group with the rest of the declarative-partial-updates handling and
3439
+ * stay overridable, consistent with the codebase.
3440
+ *
3441
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
3442
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
3443
+ *
3444
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
3445
+ * .html Q1): this mirrors the existing policy of keeping `for` on
3446
+ * <label>/<output>. If the shipping feature can drive a patch through a
3447
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
3448
+ * attribute check at _isBasicCustomElement's caller must additionally drop
3449
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
3450
+ *
3451
+ * @param root the in-place root to sweep
3452
+ */
3453
+ /**
3454
+ * Central policy for declarative-partial-updates patch-linkage attributes,
3455
+ * shared by the _neutralizePatchLinkage pre-pass and _isValidAttribute so
3456
+ * the two sites cannot drift: `patchsrc` always links, `for` links
3457
+ * everywhere except on <label>/<output>, and the whole policy is gated on
3458
+ * SAFE_FOR_XML (see the rationale block in _isValidAttribute).
3459
+ *
3460
+ * @param lcName the transformCaseFunc'd attribute name
3461
+ * @param lcTag the transformCaseFunc'd tag name of the carrying element
3462
+ * @return true if the attribute is patch linkage and must be dropped
3463
+ */
3464
+ const _isPatchLinkageAttribute = function _isPatchLinkageAttribute(lcName, lcTag) {
3465
+ if (!SAFE_FOR_XML) return false;
3466
+ if (lcName === "patchsrc") return true;
3467
+ return lcName === "for" && lcTag !== "label" && lcTag !== "output";
3468
+ };
3469
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
3470
+ if (!SAFE_FOR_XML) return;
3471
+ const stack = [root];
3472
+ while (stack.length > 0) {
3473
+ const node = stack.pop();
3474
+ const nodeType = _readNodeType(node);
3475
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
3476
+ try {
3477
+ remove(node);
3478
+ } catch (_) {}
3479
+ continue;
3480
+ }
3481
+ if (nodeType === NODE_TYPE.element) {
3482
+ const element = node;
3483
+ const lcTag = transformCaseFunc(_readNodeName(node));
3484
+ try {
3485
+ if (element.hasAttribute && element.hasAttribute("patchsrc")) element.removeAttribute("patchsrc");
3486
+ if (element.hasAttribute && element.hasAttribute("for") && _isPatchLinkageAttribute("for", lcTag)) element.removeAttribute("for");
3487
+ } catch (_) {}
3488
+ }
3271
3489
  const childNodes = getChildNodes(node);
3272
3490
  if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
3273
3491
  }
@@ -3309,7 +3527,8 @@ document.open();document.write(html);document.close();
3309
3527
  * @return The created NodeIterator
3310
3528
  */
3311
3529
  const _createNodeIterator = function _createNodeIterator(root) {
3312
- return createNodeIterator.call(root.ownerDocument || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
3530
+ const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
3531
+ return createNodeIterator.call(doc || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
3313
3532
  };
3314
3533
  /**
3315
3534
  * Replace template expression syntax (mustache, ERB, template
@@ -3347,7 +3566,8 @@ document.open();document.write(html);document.close();
3347
3566
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
3348
3567
  var _node$querySelectorAl;
3349
3568
  node.normalize();
3350
- const walker = createNodeIterator.call(node.ownerDocument || node, node, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
3569
+ const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
3570
+ const walker = createNodeIterator.call(doc || node, node, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
3351
3571
  let currentNode = walker.nextNode();
3352
3572
  while (currentNode) {
3353
3573
  currentNode.data = _stripTemplateExpressions(currentNode.data);
@@ -3374,7 +3594,7 @@ document.open();document.write(html);document.close();
3374
3594
  const realTagName = getNodeName ? getNodeName(element) : null;
3375
3595
  if (typeof realTagName !== "string") return false;
3376
3596
  if (transformCaseFunc(realTagName) !== "form") return false;
3377
- return typeof element.nodeName !== "string" || typeof element.textContent !== "string" || typeof element.removeChild !== "function" || element.attributes !== getAttributes(element) || typeof element.removeAttribute !== "function" || typeof element.setAttribute !== "function" || typeof element.namespaceURI !== "string" || typeof element.insertBefore !== "function" || typeof element.hasChildNodes !== "function" || element.nodeType !== getNodeType(element) || element.childNodes !== getChildNodes(element);
3597
+ return typeof element.nodeName !== "string" || typeof element.textContent !== "string" || typeof element.removeChild !== "function" || element.attributes !== getAttributes(element) || typeof element.removeAttribute !== "function" || typeof element.removeAttributeNode !== "function" || typeof element.getAttributeNode !== "function" || typeof element.setAttribute !== "function" || typeof element.namespaceURI !== "string" || typeof element.insertBefore !== "function" || typeof element.hasChildNodes !== "function" || element.nodeType !== getNodeType(element) || element.childNodes !== getChildNodes(element);
3378
3598
  };
3379
3599
  /**
3380
3600
  * Checks whether the given value is a DocumentFragment from any realm.
@@ -3432,34 +3652,57 @@ document.open();document.write(html);document.close();
3432
3652
  */
3433
3653
  const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
3434
3654
  if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) return true;
3435
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === "style" && _isNode(currentNode.firstElementChild)) return true;
3655
+ if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && LITERAL_TEXT_ELEMENTS[tagName] && (_isNode(currentNode.firstElementChild) || typeof currentNode.textContent === "string" && regExpTest(LITERAL_TEXT_CLOSE[tagName], currentNode.textContent))) return true;
3436
3656
  if (currentNode.nodeType === NODE_TYPE.processingInstruction) return true;
3437
3657
  if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) return true;
3438
3658
  return false;
3439
3659
  };
3440
3660
  /**
3661
+ * Evaluate a CUSTOM_ELEMENT_HANDLING check (a RegExp or a predicate
3662
+ * function, per the validation in _parseConfig) against a name.
3663
+ * Additional arguments are forwarded to predicate functions - the
3664
+ * attributeNameCheck predicate receives the tag name as its second
3665
+ * argument. A null/absent check never matches.
3666
+ *
3667
+ * @param check the configured tagNameCheck / attributeNameCheck value
3668
+ * @param name the name to test
3669
+ * @param args extra arguments forwarded to a predicate function
3670
+ * @return true if the check matches the name
3671
+ */
3672
+ const _matchesNameCheck = function _matchesNameCheck(check, name) {
3673
+ if (check instanceof RegExp) return regExpTest(check, name);
3674
+ if (check instanceof Function) {
3675
+ for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) args[_key - 2] = arguments[_key];
3676
+ return Boolean(check(name, ...args));
3677
+ }
3678
+ return false;
3679
+ };
3680
+ /**
3441
3681
  * Handle a node whose tag is forbidden or not allowlisted: keep
3442
3682
  * allowed custom elements (false return exits _sanitizeElements
3443
- * early - namespace/fallback checks and the afterSanitizeElements
3444
- * hook are intentionally skipped for kept custom elements), else
3445
- * hoist content per KEEP_CONTENT and remove.
3683
+ * early - the namespace and fallback-tag removal checks are
3684
+ * intentionally skipped for kept custom elements), else hoist
3685
+ * content per KEEP_CONTENT and remove.
3686
+ *
3687
+ * A kept custom element is the ONLY case in which this function
3688
+ * returns false, so the caller uses that return value to run the
3689
+ * afterSanitizeElements hook on the kept element and keep the
3690
+ * element-hook lifecycle consistent with normal allowlisted
3691
+ * elements (GHSA-c2j3-45gr-mqc4).
3446
3692
  *
3447
3693
  * @param currentNode the disallowed node
3448
3694
  * @param tagName the node's transformCaseFunc'd tag name
3449
3695
  * @return true if the node was removed, false if kept
3450
3696
  */
3451
- const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
3452
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
3453
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
3454
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) return false;
3455
- }
3697
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
3698
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
3456
3699
  if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
3457
3700
  const parentNode = getParentNode(currentNode);
3458
3701
  const childNodes = getChildNodes(currentNode);
3459
3702
  if (childNodes && parentNode) {
3460
3703
  const childCount = childNodes.length;
3461
3704
  for (let i = childCount - 1; i >= 0; --i) {
3462
- const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
3705
+ const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
3463
3706
  parentNode.insertBefore(hoisted, getNextSibling(currentNode));
3464
3707
  }
3465
3708
  }
@@ -3468,6 +3711,60 @@ document.open();document.write(html);document.close();
3468
3711
  return true;
3469
3712
  };
3470
3713
  /**
3714
+ * Fork a hook-mutable allowlist off its shared binding the first time a
3715
+ * (possibly lazily-installed) uponSanitize* hook is about to see it, so the
3716
+ * hook cannot widen the per-instance default or the setConfig binding by
3717
+ * reference and leak past the call. Returns the set unchanged once it is
3718
+ * already call-local, so repeated calls across elements are idempotent.
3719
+ *
3720
+ * @param hookList the uponSanitize* hook array for this event
3721
+ * @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding
3722
+ * @param defaultSet the per-instance DEFAULT_ALLOWED_* constant
3723
+ * @param setConfigSet the captured setConfig() binding, or null
3724
+ * @return a call-local clone if a hook is present and set is still shared,
3725
+ * else set unchanged
3726
+ */
3727
+ const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) {
3728
+ if (hookList.length === 0) return set;
3729
+ return set === defaultSet || set === setConfigSet ? clone$1(set) : set;
3730
+ };
3731
+ /**
3732
+ * Shared guard for a node that a hook has detached from the walk tree,
3733
+ * used after each element-hook site in _sanitizeElements. Detaching is a
3734
+ * long-standing user pattern (issue #469; draw.io-style foreignObject
3735
+ * filtering). Per the cached, unclobberable parentNode getter the node is
3736
+ * genuinely out of the tree, so it can reach neither the serialized
3737
+ * output nor an IN_PLACE live tree; treat it as removed and stop
3738
+ * processing it. Without this guard, the unsafe-node / namespace checks
3739
+ * would call _forceRemove on a parentless node and hit the REPORT-3
3740
+ * fail-closed throw — which exists for nodes DOMPurify wants gone but
3741
+ * *cannot* detach (clobbered / parentless roots), the opposite of a node
3742
+ * that is already safely gone. The walk root is exempt: a detached
3743
+ * IN_PLACE root is legitimate input and must still be fully sanitized,
3744
+ * and a kill-decision on it must keep hitting the REPORT-3 throw.
3745
+ *
3746
+ * Nodes detached by hooks stay the hook's responsibility for placement:
3747
+ * they are not recorded in DOMPurify.removed, so the post-walk IN_PLACE
3748
+ * pass (which iterates DOMPurify.removed) does not reach them. But a
3749
+ * hook-detached subtree can still hold a queued resource-event handler -
3750
+ * e.g. an <img onload> that began loading when the caller built the live
3751
+ * tree - which fires in page scope after sanitize returns even though the
3752
+ * handler never reached the returned tree. That is the audit-5 F1 hazard,
3753
+ * and the documented node.remove() hook pattern walks straight into it.
3754
+ * So on the IN_PLACE path we neutralize the detached subtree inline,
3755
+ * stripping its non-allow-listed attributes before returning, exactly as
3756
+ * the post-walk pass does for _forceRemove'd subtrees.
3757
+ *
3758
+ * @param currentNode the node a hook may have detached
3759
+ * @param root the current walk root
3760
+ * @return true if the node is detached and now handled, false otherwise
3761
+ */
3762
+ const _handleHookDetachedNode = function _handleHookDetachedNode(currentNode, root) {
3763
+ if (currentNode === root || getParentNode(currentNode) !== null) return false;
3764
+ if (IN_PLACE) _neutralizeSubtree(currentNode);
3765
+ return true;
3766
+ };
3767
+ /**
3471
3768
  * _sanitizeElements
3472
3769
  *
3473
3770
  * @protect nodeName
@@ -3476,23 +3773,33 @@ document.open();document.write(html);document.close();
3476
3773
  * @param currentNode to check for permission to exist
3477
3774
  * @return true if node was killed, false if left alive
3478
3775
  */
3479
- const _sanitizeElements = function _sanitizeElements(currentNode) {
3776
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
3480
3777
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
3778
+ if (_handleHookDetachedNode(currentNode, root)) return true;
3481
3779
  if (_isClobbered(currentNode)) {
3482
3780
  _forceRemove(currentNode);
3483
3781
  return true;
3484
3782
  }
3485
- const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
3783
+ const tagName = transformCaseFunc(_readNodeName(currentNode));
3784
+ ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
3486
3785
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
3487
3786
  tagName,
3488
3787
  allowedTags: ALLOWED_TAGS
3489
3788
  });
3789
+ if (_handleHookDetachedNode(currentNode, root)) return true;
3490
3790
  if (_isUnsafeNode(currentNode, tagName)) {
3491
3791
  _forceRemove(currentNode);
3492
3792
  return true;
3493
3793
  }
3494
- if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) return _sanitizeDisallowedNode(currentNode, tagName);
3495
- if ((getNodeType ? getNodeType(currentNode) : currentNode.nodeType) === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
3794
+ if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
3795
+ const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
3796
+ if (removed === false) {
3797
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
3798
+ if (_handleHookDetachedNode(currentNode, root)) return true;
3799
+ }
3800
+ return removed;
3801
+ }
3802
+ if (_readNodeType(currentNode) === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
3496
3803
  _forceRemove(currentNode);
3497
3804
  return true;
3498
3805
  }
@@ -3508,7 +3815,7 @@ document.open();document.write(html);document.close();
3508
3815
  }
3509
3816
  }
3510
3817
  _executeHooks(hooks.afterSanitizeElements, currentNode, null);
3511
- return false;
3818
+ return _handleHookDetachedNode(currentNode, root);
3512
3819
  };
3513
3820
  /**
3514
3821
  * _isValidAttribute
@@ -3520,18 +3827,17 @@ document.open();document.write(html);document.close();
3520
3827
  */
3521
3828
  const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
3522
3829
  if (FORBID_ATTR[lcName]) return false;
3830
+ if (_isPatchLinkageAttribute(lcName, lcTag)) return false;
3523
3831
  if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document || value in formElement)) return false;
3524
3832
  const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
3525
- if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName));
3526
- else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName));
3527
- else if (!nameIsPermitted) if (_isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName, lcTag)) || lcName === "is" && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value)));
3528
- else return false;
3529
- else if (URI_SAFE_ATTRIBUTES[lcName]);
3530
- else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, "")));
3531
- else if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag]);
3532
- else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, "")));
3533
- else if (value) return false;
3534
- return true;
3833
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) return true;
3834
+ if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) return true;
3835
+ if (!nameIsPermitted) return _isBasicCustomElement(lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName, lcTag) || lcName === "is" && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value);
3836
+ if (URI_SAFE_ATTRIBUTES[lcName]) return true;
3837
+ if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) return true;
3838
+ if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag]) return true;
3839
+ if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) return true;
3840
+ return !value;
3535
3841
  };
3536
3842
  const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, [
3537
3843
  "annotation-xml",
@@ -3576,24 +3882,38 @@ document.open();document.write(html);document.close();
3576
3882
  /**
3577
3883
  * Write a modified attribute value back onto the element. On
3578
3884
  * success, re-probe for clobbering introduced by the new value and
3579
- * remove the element when found; otherwise pop the removal entry
3580
- * recorded by the earlier _removeAttribute (long-standing pairing
3581
- * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
3885
+ * remove the element when found; otherwise, when this writeback is the
3886
+ * recreate half of the SANITIZE_NAMED_PROPS remove-and-recreate, pop the
3887
+ * removal entry that path recorded so it does not show as removed. On
3582
3888
  * failure, remove the attribute instead.
3583
3889
  *
3890
+ * Returns true only on a clean write (the value was set and the new value
3891
+ * introduced no clobbering). The caller uses that, together with its own
3892
+ * knowledge of whether this attribute pushed a DOMPurify.removed record, to
3893
+ * decide whether to pop that record. The pop must happen ONLY for the
3894
+ * named-prop remove-and-recreate; popping on any other value change (trim,
3895
+ * template scrubbing, Trusted Types) would consume an unrelated _forceRemove
3896
+ * subtree-cleanup record and let that detached subtree keep a live event
3897
+ * handler through the IN_PLACE neutralization pass (SO-001).
3898
+ *
3584
3899
  * @param currentNode the element carrying the attribute
3585
3900
  * @param name the attribute name as present on the element
3586
3901
  * @param namespaceURI the attribute's namespace, if any
3587
3902
  * @param value the new attribute value
3903
+ * @return true if the value was written without introducing clobbering
3588
3904
  */
3589
3905
  const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
3590
3906
  try {
3591
3907
  if (namespaceURI) currentNode.setAttributeNS(namespaceURI, name, value);
3592
3908
  else currentNode.setAttribute(name, value);
3593
- if (_isClobbered(currentNode)) _forceRemove(currentNode);
3594
- else arrayPop(DOMPurify.removed);
3909
+ if (_isClobbered(currentNode)) {
3910
+ _forceRemove(currentNode);
3911
+ return false;
3912
+ }
3913
+ return true;
3595
3914
  } catch (_) {
3596
3915
  _removeAttribute(name, currentNode);
3916
+ return false;
3597
3917
  }
3598
3918
  };
3599
3919
  /**
@@ -3605,11 +3925,14 @@ document.open();document.write(html);document.close();
3605
3925
  * @protect setAttribute
3606
3926
  *
3607
3927
  * @param currentNode to sanitize
3928
+ * @param root the current walk root
3608
3929
  */
3609
- const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
3930
+ const _sanitizeAttributes = function _sanitizeAttributes(currentNode, root) {
3610
3931
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
3932
+ if (_handleHookDetachedNode(currentNode, root)) return;
3611
3933
  const attributes = currentNode.attributes;
3612
3934
  if (!attributes || _isClobbered(currentNode)) return;
3935
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
3613
3936
  const hookEvent = {
3614
3937
  attrName: "",
3615
3938
  attrValue: "",
@@ -3625,6 +3948,7 @@ document.open();document.write(html);document.close();
3625
3948
  const lcName = transformCaseFunc(name);
3626
3949
  const initValue = attrValue;
3627
3950
  let value = name === "value" ? initValue : stringTrim(initValue);
3951
+ let recreatedNamedProp = false;
3628
3952
  hookEvent.attrName = lcName;
3629
3953
  hookEvent.attrValue = value;
3630
3954
  hookEvent.keepAttr = true;
@@ -3632,35 +3956,39 @@ document.open();document.write(html);document.close();
3632
3956
  _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent);
3633
3957
  value = hookEvent.attrValue;
3634
3958
  if (SANITIZE_NAMED_PROPS && (lcName === "id" || lcName === "name") && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
3635
- _removeAttribute(name, currentNode);
3959
+ _removeAttribute(name, currentNode, attr);
3636
3960
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
3961
+ recreatedNamedProp = true;
3637
3962
  }
3638
3963
  if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
3639
- _removeAttribute(name, currentNode);
3964
+ _removeAttribute(name, currentNode, attr);
3640
3965
  continue;
3641
3966
  }
3642
3967
  if (lcName === "attributename" && stringMatch(value, "href")) {
3643
- _removeAttribute(name, currentNode);
3968
+ _removeAttribute(name, currentNode, attr);
3644
3969
  continue;
3645
3970
  }
3646
3971
  if (hookEvent.forceKeepAttr) continue;
3647
3972
  if (!hookEvent.keepAttr) {
3648
- _removeAttribute(name, currentNode);
3973
+ _removeAttribute(name, currentNode, attr);
3649
3974
  continue;
3650
3975
  }
3651
3976
  if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
3652
- _removeAttribute(name, currentNode);
3977
+ _removeAttribute(name, currentNode, attr);
3653
3978
  continue;
3654
3979
  }
3655
3980
  if (SAFE_FOR_TEMPLATES) value = _stripTemplateExpressions(value);
3656
3981
  if (!_isValidAttribute(lcTag, lcName, value)) {
3657
- _removeAttribute(name, currentNode);
3982
+ _removeAttribute(name, currentNode, attr);
3658
3983
  continue;
3659
3984
  }
3660
3985
  value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
3661
- if (value !== initValue) _setAttributeValue(currentNode, name, namespaceURI, value);
3986
+ if (value !== initValue) {
3987
+ if (_setAttributeValue(currentNode, name, namespaceURI, value) && recreatedNamedProp) arrayPop(DOMPurify.removed);
3988
+ }
3662
3989
  }
3663
3990
  _executeHooks(hooks.afterSanitizeAttributes, currentNode, null);
3991
+ _handleHookDetachedNode(currentNode, root);
3664
3992
  };
3665
3993
  /**
3666
3994
  * _sanitizeShadowDOM
@@ -3673,10 +4001,10 @@ document.open();document.write(html);document.close();
3673
4001
  _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null);
3674
4002
  while (shadowNode = shadowIterator.nextNode()) {
3675
4003
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
3676
- _sanitizeElements(shadowNode);
3677
- _sanitizeAttributes(shadowNode);
4004
+ _sanitizeElements(shadowNode, fragment);
4005
+ _sanitizeAttributes(shadowNode, fragment);
3678
4006
  if (_isDocumentFragment(shadowNode.content)) _sanitizeShadowDOM2(shadowNode.content);
3679
- if ((getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType) === NODE_TYPE.element) {
4007
+ if (_readNodeType(shadowNode) === NODE_TYPE.element) {
3680
4008
  const innerSr = getShadowRoot(shadowNode);
3681
4009
  if (_isDocumentFragment(innerSr)) {
3682
4010
  _sanitizeAttachedShadowRoots(innerSr);
@@ -3717,7 +4045,7 @@ document.open();document.write(html);document.close();
3717
4045
  continue;
3718
4046
  }
3719
4047
  const node = item.node;
3720
- const isElement = (getNodeType ? getNodeType(node) : node.nodeType) === NODE_TYPE.element;
4048
+ const isElement = _readNodeType(node) === NODE_TYPE.element;
3721
4049
  const childNodes = getChildNodes(node);
3722
4050
  if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push({
3723
4051
  node: childNodes[i],
@@ -3767,12 +4095,19 @@ document.open();document.write(html);document.close();
3767
4095
  DOMPurify.removed = [];
3768
4096
  const inPlace = IN_PLACE && typeof dirty !== "string" && _isNode(dirty);
3769
4097
  if (inPlace) {
3770
- const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
4098
+ _neutralizePatchLinkage(dirty);
4099
+ const nn = _readNodeName(dirty);
3771
4100
  if (typeof nn === "string") {
3772
4101
  const tagName = transformCaseFunc(nn);
3773
- if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) throw typeErrorCreate("root node is forbidden and cannot be sanitized in-place");
4102
+ if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
4103
+ _neutralizeRoot(dirty);
4104
+ throw typeErrorCreate("root node is forbidden and cannot be sanitized in-place");
4105
+ }
4106
+ }
4107
+ if (_isClobbered(dirty)) {
4108
+ _neutralizeRoot(dirty);
4109
+ throw typeErrorCreate("root node is clobbered and cannot be sanitized in-place");
3774
4110
  }
3775
- if (_isClobbered(dirty)) throw typeErrorCreate("root node is clobbered and cannot be sanitized in-place");
3776
4111
  try {
3777
4112
  _sanitizeAttachedShadowRoots(dirty);
3778
4113
  } catch (error) {
@@ -3785,28 +4120,39 @@ document.open();document.write(html);document.close();
3785
4120
  if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === "BODY") body = importedNode;
3786
4121
  else if (importedNode.nodeName === "HTML") body = importedNode;
3787
4122
  else body.appendChild(importedNode);
3788
- _sanitizeAttachedShadowRoots(importedNode);
4123
+ _sanitizeAttachedShadowRoots(body);
3789
4124
  } else {
3790
4125
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && dirty.indexOf("<") === -1) return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
3791
4126
  body = _initDocument(dirty);
3792
4127
  if (!body) return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : "";
3793
4128
  }
3794
4129
  if (body && FORCE_BODY) _forceRemove(body.firstChild);
3795
- const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
4130
+ const walkRoot = inPlace ? dirty : body;
3796
4131
  try {
4132
+ const nodeIterator = _createNodeIterator(walkRoot);
3797
4133
  while (currentNode = nodeIterator.nextNode()) {
3798
- _sanitizeElements(currentNode);
3799
- _sanitizeAttributes(currentNode);
4134
+ _sanitizeElements(currentNode, walkRoot);
4135
+ _sanitizeAttributes(currentNode, walkRoot);
3800
4136
  if (_isDocumentFragment(currentNode.content)) _sanitizeShadowDOM2(currentNode.content);
3801
4137
  }
3802
4138
  } catch (error) {
3803
- if (inPlace) _neutralizeRoot(dirty);
4139
+ if (inPlace) {
4140
+ _neutralizeRoot(dirty);
4141
+ arrayForEach(DOMPurify.removed, (entry) => {
4142
+ if (entry.element) _neutralizeSubtree(entry.element);
4143
+ });
4144
+ }
3804
4145
  throw error;
3805
4146
  }
3806
4147
  if (inPlace) {
4148
+ let rootWasRemoved = false;
3807
4149
  arrayForEach(DOMPurify.removed, (entry) => {
3808
- if (entry.element) _neutralizeSubtree(entry.element);
4150
+ if (entry.element) {
4151
+ if (entry.element === dirty) rootWasRemoved = true;
4152
+ _neutralizeSubtree(entry.element);
4153
+ }
3809
4154
  });
4155
+ if (rootWasRemoved) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
3810
4156
  if (SAFE_FOR_TEMPLATES) _scrubTemplateExpressions2(dirty);
3811
4157
  return dirty;
3812
4158
  }
@@ -3864,7 +4210,7 @@ document.open();document.write(html);document.close();
3864
4210
  };
3865
4211
  return DOMPurify;
3866
4212
  }
3867
- var purify = createDOMPurify();
4213
+ var purify_default = createDOMPurify();
3868
4214
  //#endregion
3869
4215
  //#region lib/types/client/html/basic-document.js
3870
4216
  /** Static HTML preview with no scripts, network resources, forms or nested frames. */
@@ -3874,7 +4220,7 @@ document.open();document.write(html);document.close();
3874
4220
  * @returns document with the restrictive CSP first in its head.
3875
4221
  */
3876
4222
  function createBasicHtmlDocument(data) {
3877
- const clean = purify.sanitize(decodeText(data), {
4223
+ const clean = purify_default.sanitize(decodeText(data), {
3878
4224
  WHOLE_DOCUMENT: true,
3879
4225
  FORBID_TAGS: [
3880
4226
  "noscript",