@deepseek-ai/dsh-client-ui-sidebar-documentpreview 0.2.0-rc.2 → 0.2.1-alpha.2

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
@@ -1506,6 +1506,23 @@ window.__ModuleLoader__.load({
1506
1506
  */
1507
1507
  const documentTabInfoFactory = (_standard, useTabInfo) => useTabInfo;
1508
1508
  //#endregion
1509
+ //#region lib/types/client/markdown/frontmatter.js
1510
+ /** Leading YAML frontmatter split from a Markdown document before Markdown parsing. */
1511
+ const FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n(?:([\s\S]*?)\r?\n)??(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/u;
1512
+ /**
1513
+ * Split a leading YAML frontmatter block from Markdown text without parsing its YAML.
1514
+ * @param text - Markdown source; an unterminated block is not frontmatter.
1515
+ * @returns frontmatter source and the remaining body, or `undefined` when the text has none.
1516
+ */
1517
+ function splitFrontmatter(text) {
1518
+ const match = FRONTMATTER.exec(text);
1519
+ if (match === null) return void 0;
1520
+ return {
1521
+ source: match[1] ?? "",
1522
+ body: text.slice(match[0].length)
1523
+ };
1524
+ }
1525
+ //#endregion
1509
1526
  //#region lib/types/client/markdown/path-images.js
1510
1527
  /** Local Markdown image destinations served by the authenticated file route. */
1511
1528
  /**
@@ -1533,7 +1550,7 @@ window.__ModuleLoader__.load({
1533
1550
  }
1534
1551
  //#endregion
1535
1552
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-sidebar-documentpreview/src/client/markdown/MarkdownBody.module.css.mjs
1536
- const css$7 = "._0RKuNG_document{min-width:0;font-family:var(--dsw-font,inherit);white-space:normal;padding:10px 12px}";
1553
+ const css$7 = "._0RKuNG_document{min-width:0;font-family:var(--dsw-font,inherit);white-space:normal;padding:10px 12px}._0RKuNG_frontmatter{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);font:var(--dsw-font-markdown-small);color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0 0 16px;padding:10px 12px}._0RKuNG_frontmatter:empty{display:none}._0RKuNG_frontmatterSource{font-family:var(--ds-font-family-code);white-space:inherit;margin:0}";
1537
1554
  const tagId$7 = "@deepseek-ai/dsh-client-ui-sidebar-documentpreview/MarkdownBody.module.css";
1538
1555
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$7) + "]") === null) {
1539
1556
  const tag = document.createElement("style");
@@ -1542,12 +1559,23 @@ window.__ModuleLoader__.load({
1542
1559
  tag.textContent = css$7;
1543
1560
  document.head.appendChild(tag);
1544
1561
  }
1545
- var MarkdownBody_module_css_default = { "document": "_0RKuNG_document" };
1562
+ var MarkdownBody_module_css_default = {
1563
+ "document": "_0RKuNG_document",
1564
+ "frontmatter": "_0RKuNG_frontmatter",
1565
+ "frontmatterSource": "_0RKuNG_frontmatterSource"
1566
+ };
1546
1567
  //#endregion
1547
1568
  //#region lib/types/client/markdown/MarkdownBody.js
1548
1569
  /** One retained Markdown renderer over the document owner's accumulated text. */
1570
+ /** Show the verbatim frontmatter when the field-list chunk cannot load. */
1571
+ function FrontmatterFallback({ fallback }) {
1572
+ return fallback;
1573
+ }
1574
+ const FrontmatterFields = (0, react.lazy)(() => require.async("./client.frontmatter-fields.js").then((module) => ({ default: module.FrontmatterFields }), () => ({ default: FrontmatterFallback })));
1549
1575
  /**
1550
1576
  * Render one accumulated document; EOF completes the primitive's full parse.
1577
+ * A leading YAML frontmatter block renders as a field list instead of Markdown; its
1578
+ * verbatim source stands in while the field-list chunk loads or when it fails.
1551
1579
  * @param props - owner-loaded contents and localized primitive labels.
1552
1580
  * @returns Markdown content, or nothing for a non-text delivery.
1553
1581
  */
@@ -1579,15 +1607,39 @@ window.__ModuleLoader__.load({
1579
1607
  wrapLabel,
1580
1608
  unwrapLabel
1581
1609
  ]);
1610
+ const text = content.kind === "text" ? content.text : "";
1611
+ const split = (0, react.useMemo)(() => splitFrontmatter(text), [text]);
1582
1612
  if (content.kind !== "text") return null;
1583
- return (0, react_jsx_runtime.jsx)("div", {
1613
+ return (0, react_jsx_runtime.jsxs)("div", {
1584
1614
  className: MarkdownBody_module_css_default.document,
1585
1615
  "data-document-markdown": true,
1586
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
1587
- text: content.text,
1616
+ children: [split === void 0 ? null : (0, react_jsx_runtime.jsx)(FrontmatterBox, { source: split.source }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
1617
+ text: split?.body ?? text,
1588
1618
  streaming: !content.eof,
1589
1619
  labels,
1590
1620
  pathImages
1621
+ })]
1622
+ });
1623
+ }
1624
+ /**
1625
+ * Frame one frontmatter block and choose its field list or verbatim source.
1626
+ * @param props - YAML between the frontmatter delimiters.
1627
+ * @returns the metadata box; it collapses when the document has no content.
1628
+ */
1629
+ function FrontmatterBox({ source }) {
1630
+ const verbatim = (0, react_jsx_runtime.jsx)("pre", {
1631
+ className: MarkdownBody_module_css_default.frontmatterSource,
1632
+ children: source
1633
+ });
1634
+ return (0, react_jsx_runtime.jsx)("div", {
1635
+ className: MarkdownBody_module_css_default.frontmatter,
1636
+ "data-document-frontmatter": true,
1637
+ children: (0, react_jsx_runtime.jsx)(react.Suspense, {
1638
+ fallback: verbatim,
1639
+ children: (0, react_jsx_runtime.jsx)(FrontmatterFields, {
1640
+ source,
1641
+ fallback: verbatim
1642
+ })
1591
1643
  })
1592
1644
  });
1593
1645
  }
@@ -1704,8 +1756,11 @@ document.open();document.write(html);document.close();
1704
1756
  })()<\/script>`;
1705
1757
  }
1706
1758
  //#endregion
1707
- //#region ../../../node_modules/.pnpm/dompurify@3.4.11/node_modules/dompurify/dist/purify.es.mjs
1708
- /*! @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
+ }
1709
1764
  function _arrayLikeToArray(r, a) {
1710
1765
  (null == a || a > r.length) && (a = r.length);
1711
1766
  for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
@@ -1717,12 +1772,14 @@ document.open();document.write(html);document.close();
1717
1772
  function _iterableToArrayLimit(r, l) {
1718
1773
  var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
1719
1774
  if (null != t) {
1720
- var e, n, i, u, a = [], f = true, o = false;
1775
+ var e, n, i, u, a = [], f = !0, o = !1;
1721
1776
  try {
1722
- if (i = (t = t.call(r)).next, 0 === l);
1723
- 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);
1724
1781
  } catch (r) {
1725
- o = true, n = r;
1782
+ o = !0, n = r;
1726
1783
  } finally {
1727
1784
  try {
1728
1785
  if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
@@ -1736,6 +1793,7 @@ document.open();document.write(html);document.close();
1736
1793
  function _nonIterableRest() {
1737
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.");
1738
1795
  }
1796
+ /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
1739
1797
  function _slicedToArray(r, e) {
1740
1798
  return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
1741
1799
  }
@@ -1746,9 +1804,64 @@ document.open();document.write(html);document.close();
1746
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;
1747
1805
  }
1748
1806
  }
1749
- const entries = Object.entries, setPrototypeOf = Object.setPrototypeOf, isFrozen = Object.isFrozen, getPrototypeOf = Object.getPrototypeOf, getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
1750
- let freeze = Object.freeze, seal = Object.seal, create = Object.create;
1751
- 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;
1752
1865
  if (!freeze) freeze = function freeze(x) {
1753
1866
  return x;
1754
1867
  };
@@ -1764,9 +1877,11 @@ document.open();document.write(html);document.close();
1764
1877
  return new Func(...args);
1765
1878
  };
1766
1879
  const arrayForEach = unapply(Array.prototype.forEach);
1880
+ Array.prototype.indexOf;
1767
1881
  const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
1768
1882
  const arrayPop = unapply(Array.prototype.pop);
1769
1883
  const arrayPush = unapply(Array.prototype.push);
1884
+ Array.prototype.slice;
1770
1885
  const arraySplice = unapply(Array.prototype.splice);
1771
1886
  const arrayIsArray = Array.isArray;
1772
1887
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
@@ -2344,6 +2459,7 @@ document.open();document.write(html);document.close();
2344
2459
  "direction",
2345
2460
  "display",
2346
2461
  "divisor",
2462
+ "dominant-baseline",
2347
2463
  "dur",
2348
2464
  "edgemode",
2349
2465
  "elevation",
@@ -2425,6 +2541,7 @@ document.open();document.write(html);document.close();
2425
2541
  "patterncontentunits",
2426
2542
  "patterntransform",
2427
2543
  "patternunits",
2544
+ "pointer-events",
2428
2545
  "points",
2429
2546
  "preservealpha",
2430
2547
  "preserveaspectratio",
@@ -2471,6 +2588,7 @@ document.open();document.write(html);document.close();
2471
2588
  "transform-origin",
2472
2589
  "text-anchor",
2473
2590
  "text-decoration",
2591
+ "text-orientation",
2474
2592
  "text-rendering",
2475
2593
  "textlength",
2476
2594
  "type",
@@ -2478,6 +2596,7 @@ document.open();document.write(html);document.close();
2478
2596
  "u2",
2479
2597
  "unicode",
2480
2598
  "values",
2599
+ "vector-effect",
2481
2600
  "viewbox",
2482
2601
  "visibility",
2483
2602
  "version",
@@ -2591,6 +2710,24 @@ document.open();document.write(html);document.close();
2591
2710
  documentFragment: 11,
2592
2711
  notation: 12
2593
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
+ }();
2594
2731
  const getGlobal = function getGlobal() {
2595
2732
  return typeof window === "undefined" ? null : window;
2596
2733
  };
@@ -2650,10 +2787,25 @@ document.open();document.write(html);document.close();
2650
2787
  const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
2651
2788
  return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone$1(options.base) : {}, cfg[key], options.transform) : fallback;
2652
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
+ };
2653
2805
  function createDOMPurify() {
2654
2806
  let window = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
2655
2807
  const DOMPurify = (root) => createDOMPurify(root);
2656
- DOMPurify.version = "3.4.11";
2808
+ DOMPurify.version = "3.4.16";
2657
2809
  DOMPurify.removed = [];
2658
2810
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
2659
2811
  DOMPurify.isSupported = false;
@@ -2670,6 +2822,7 @@ document.open();document.write(html);document.close();
2670
2822
  const ElementPrototype = Element.prototype;
2671
2823
  const cloneNode = lookupGetter(ElementPrototype, "cloneNode");
2672
2824
  const remove = lookupGetter(ElementPrototype, "remove");
2825
+ const removeAttributeNode = lookupGetter(ElementPrototype, "removeAttributeNode");
2673
2826
  const getNextSibling = lookupGetter(ElementPrototype, "nextSibling");
2674
2827
  const getChildNodes = lookupGetter(ElementPrototype, "childNodes");
2675
2828
  const getParentNode = lookupGetter(ElementPrototype, "parentNode");
@@ -2677,6 +2830,13 @@ document.open();document.write(html);document.close();
2677
2830
  const getAttributes = lookupGetter(ElementPrototype, "attributes");
2678
2831
  const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeType") : null;
2679
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
+ };
2680
2840
  if (typeof HTMLTemplateElement === "function") {
2681
2841
  const template = document.createElement("template");
2682
2842
  if (template.content && template.content.ownerDocument) document = template.content.ownerDocument;
@@ -2934,9 +3094,9 @@ document.open();document.write(html);document.close();
2934
3094
  IN_PLACE = cfg.IN_PLACE || false;
2935
3095
  IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI;
2936
3096
  NAMESPACE = typeof cfg.NAMESPACE === "string" ? cfg.NAMESPACE : HTML_NAMESPACE;
2937
- 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);
2938
- 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);
2939
- 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));
2940
3100
  CUSTOM_ELEMENT_HANDLING = create(null);
2941
3101
  if (objectHasOwnProperty(customElementHandling, "tagNameCheck") && isRegexOrFunction(customElementHandling.tagNameCheck)) CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck;
2942
3102
  if (objectHasOwnProperty(customElementHandling, "attributeNameCheck") && isRegexOrFunction(customElementHandling.attributeNameCheck)) CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck;
@@ -2983,11 +3143,6 @@ document.open();document.write(html);document.close();
2983
3143
  addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
2984
3144
  }
2985
3145
  }
2986
- if (objectHasOwnProperty(cfg, "ADD_URI_SAFE_ATTR") && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
2987
- if (objectHasOwnProperty(cfg, "FORBID_CONTENTS") && arrayIsArray(cfg.FORBID_CONTENTS)) {
2988
- if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) FORBID_CONTENTS = clone$1(FORBID_CONTENTS);
2989
- addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
2990
- }
2991
3146
  if (objectHasOwnProperty(cfg, "ADD_FORBID_CONTENTS") && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
2992
3147
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) FORBID_CONTENTS = clone$1(FORBID_CONTENTS);
2993
3148
  addToSet(FORBID_CONTENTS, cfg.ADD_FORBID_CONTENTS, transformCaseFunc);
@@ -3104,6 +3259,32 @@ document.open();document.write(html);document.close();
3104
3259
  }
3105
3260
  };
3106
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
+ /**
3107
3288
  * _neutralizeRoot
3108
3289
  *
3109
3290
  * Fail-closed teardown of an in-place root after the sanitize walk aborts
@@ -3120,6 +3301,7 @@ document.open();document.write(html);document.close();
3120
3301
  * @param root the in-place root to empty
3121
3302
  */
3122
3303
  const _neutralizeRoot = function _neutralizeRoot(root) {
3304
+ _neutralizeSubtree(root);
3123
3305
  const childNodes = getChildNodes(root);
3124
3306
  if (childNodes) {
3125
3307
  const snapshot = [];
@@ -3136,30 +3318,45 @@ document.open();document.write(html);document.close();
3136
3318
  if (attributes) for (let i = attributes.length - 1; i >= 0; --i) {
3137
3319
  const attribute = attributes[i];
3138
3320
  const name = attribute && attribute.name;
3139
- if (typeof name === "string") try {
3140
- root.removeAttribute(name);
3141
- } catch (_) {}
3321
+ if (typeof name === "string") _stripAttributeNode(root, attribute, name);
3142
3322
  }
3143
3323
  };
3144
3324
  /**
3145
3325
  * _removeAttribute
3146
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
+ *
3147
3338
  * @param name an Attribute name
3148
3339
  * @param element a DOM node
3340
+ * @param attr the exact Attr node to remove, when the caller has it
3149
3341
  */
3150
- 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
+ });
3151
3352
  try {
3152
- arrayPush(DOMPurify.removed, {
3153
- attribute: element.getAttributeNode(name),
3154
- from: element
3155
- });
3353
+ if (attr) removeAttributeNode(element, attr);
3354
+ else element.removeAttribute(name);
3156
3355
  } catch (_) {
3157
- arrayPush(DOMPurify.removed, {
3158
- attribute: null,
3159
- from: element
3160
- });
3356
+ try {
3357
+ element.removeAttribute(name);
3358
+ } catch (_) {}
3161
3359
  }
3162
- element.removeAttribute(name);
3163
3360
  if (name === "is") if (RETURN_DOM || RETURN_DOM_FRAGMENT) try {
3164
3361
  _forceRemove(element);
3165
3362
  } catch (_) {}
@@ -3184,9 +3381,7 @@ document.open();document.write(html);document.close();
3184
3381
  const attribute = attributes[i];
3185
3382
  const name = attribute && attribute.name;
3186
3383
  if (typeof name !== "string" || ALLOWED_ATTR[transformCaseFunc(name)]) continue;
3187
- try {
3188
- element.removeAttribute(name);
3189
- } catch (_) {}
3384
+ _stripAttributeNode(element, attribute, name);
3190
3385
  }
3191
3386
  };
3192
3387
  /**
@@ -3215,7 +3410,82 @@ document.open();document.write(html);document.close();
3215
3410
  const stack = [root];
3216
3411
  while (stack.length > 0) {
3217
3412
  const node = stack.pop();
3218
- 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
+ }
3219
3489
  const childNodes = getChildNodes(node);
3220
3490
  if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
3221
3491
  }
@@ -3257,7 +3527,8 @@ document.open();document.write(html);document.close();
3257
3527
  * @return The created NodeIterator
3258
3528
  */
3259
3529
  const _createNodeIterator = function _createNodeIterator(root) {
3260
- 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);
3261
3532
  };
3262
3533
  /**
3263
3534
  * Replace template expression syntax (mustache, ERB, template
@@ -3295,7 +3566,8 @@ document.open();document.write(html);document.close();
3295
3566
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
3296
3567
  var _node$querySelectorAl;
3297
3568
  node.normalize();
3298
- 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);
3299
3571
  let currentNode = walker.nextNode();
3300
3572
  while (currentNode) {
3301
3573
  currentNode.data = _stripTemplateExpressions(currentNode.data);
@@ -3322,7 +3594,7 @@ document.open();document.write(html);document.close();
3322
3594
  const realTagName = getNodeName ? getNodeName(element) : null;
3323
3595
  if (typeof realTagName !== "string") return false;
3324
3596
  if (transformCaseFunc(realTagName) !== "form") return false;
3325
- 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);
3326
3598
  };
3327
3599
  /**
3328
3600
  * Checks whether the given value is a DocumentFragment from any realm.
@@ -3380,34 +3652,57 @@ document.open();document.write(html);document.close();
3380
3652
  */
3381
3653
  const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
3382
3654
  if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) return true;
3383
- 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;
3384
3656
  if (currentNode.nodeType === NODE_TYPE.processingInstruction) return true;
3385
3657
  if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) return true;
3386
3658
  return false;
3387
3659
  };
3388
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
+ /**
3389
3681
  * Handle a node whose tag is forbidden or not allowlisted: keep
3390
3682
  * allowed custom elements (false return exits _sanitizeElements
3391
- * early - namespace/fallback checks and the afterSanitizeElements
3392
- * hook are intentionally skipped for kept custom elements), else
3393
- * 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).
3394
3692
  *
3395
3693
  * @param currentNode the disallowed node
3396
3694
  * @param tagName the node's transformCaseFunc'd tag name
3397
3695
  * @return true if the node was removed, false if kept
3398
3696
  */
3399
- const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
3400
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
3401
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
3402
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) return false;
3403
- }
3697
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
3698
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
3404
3699
  if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
3405
3700
  const parentNode = getParentNode(currentNode);
3406
3701
  const childNodes = getChildNodes(currentNode);
3407
3702
  if (childNodes && parentNode) {
3408
3703
  const childCount = childNodes.length;
3409
3704
  for (let i = childCount - 1; i >= 0; --i) {
3410
- const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
3705
+ const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
3411
3706
  parentNode.insertBefore(hoisted, getNextSibling(currentNode));
3412
3707
  }
3413
3708
  }
@@ -3416,6 +3711,60 @@ document.open();document.write(html);document.close();
3416
3711
  return true;
3417
3712
  };
3418
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
+ /**
3419
3768
  * _sanitizeElements
3420
3769
  *
3421
3770
  * @protect nodeName
@@ -3424,23 +3773,33 @@ document.open();document.write(html);document.close();
3424
3773
  * @param currentNode to check for permission to exist
3425
3774
  * @return true if node was killed, false if left alive
3426
3775
  */
3427
- const _sanitizeElements = function _sanitizeElements(currentNode) {
3776
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
3428
3777
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
3778
+ if (_handleHookDetachedNode(currentNode, root)) return true;
3429
3779
  if (_isClobbered(currentNode)) {
3430
3780
  _forceRemove(currentNode);
3431
3781
  return true;
3432
3782
  }
3433
- 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);
3434
3785
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
3435
3786
  tagName,
3436
3787
  allowedTags: ALLOWED_TAGS
3437
3788
  });
3789
+ if (_handleHookDetachedNode(currentNode, root)) return true;
3438
3790
  if (_isUnsafeNode(currentNode, tagName)) {
3439
3791
  _forceRemove(currentNode);
3440
3792
  return true;
3441
3793
  }
3442
- if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) return _sanitizeDisallowedNode(currentNode, tagName);
3443
- 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)) {
3444
3803
  _forceRemove(currentNode);
3445
3804
  return true;
3446
3805
  }
@@ -3456,7 +3815,7 @@ document.open();document.write(html);document.close();
3456
3815
  }
3457
3816
  }
3458
3817
  _executeHooks(hooks.afterSanitizeElements, currentNode, null);
3459
- return false;
3818
+ return _handleHookDetachedNode(currentNode, root);
3460
3819
  };
3461
3820
  /**
3462
3821
  * _isValidAttribute
@@ -3468,18 +3827,17 @@ document.open();document.write(html);document.close();
3468
3827
  */
3469
3828
  const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
3470
3829
  if (FORBID_ATTR[lcName]) return false;
3830
+ if (_isPatchLinkageAttribute(lcName, lcTag)) return false;
3471
3831
  if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document || value in formElement)) return false;
3472
3832
  const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
3473
- if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName));
3474
- else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName));
3475
- 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)));
3476
- else return false;
3477
- else if (URI_SAFE_ATTRIBUTES[lcName]);
3478
- else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, "")));
3479
- else if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag]);
3480
- else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, "")));
3481
- else if (value) return false;
3482
- 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;
3483
3841
  };
3484
3842
  const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, [
3485
3843
  "annotation-xml",
@@ -3524,24 +3882,38 @@ document.open();document.write(html);document.close();
3524
3882
  /**
3525
3883
  * Write a modified attribute value back onto the element. On
3526
3884
  * success, re-probe for clobbering introduced by the new value and
3527
- * remove the element when found; otherwise pop the removal entry
3528
- * recorded by the earlier _removeAttribute (long-standing pairing
3529
- * 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
3530
3888
  * failure, remove the attribute instead.
3531
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
+ *
3532
3899
  * @param currentNode the element carrying the attribute
3533
3900
  * @param name the attribute name as present on the element
3534
3901
  * @param namespaceURI the attribute's namespace, if any
3535
3902
  * @param value the new attribute value
3903
+ * @return true if the value was written without introducing clobbering
3536
3904
  */
3537
3905
  const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
3538
3906
  try {
3539
3907
  if (namespaceURI) currentNode.setAttributeNS(namespaceURI, name, value);
3540
3908
  else currentNode.setAttribute(name, value);
3541
- if (_isClobbered(currentNode)) _forceRemove(currentNode);
3542
- else arrayPop(DOMPurify.removed);
3909
+ if (_isClobbered(currentNode)) {
3910
+ _forceRemove(currentNode);
3911
+ return false;
3912
+ }
3913
+ return true;
3543
3914
  } catch (_) {
3544
3915
  _removeAttribute(name, currentNode);
3916
+ return false;
3545
3917
  }
3546
3918
  };
3547
3919
  /**
@@ -3553,11 +3925,14 @@ document.open();document.write(html);document.close();
3553
3925
  * @protect setAttribute
3554
3926
  *
3555
3927
  * @param currentNode to sanitize
3928
+ * @param root the current walk root
3556
3929
  */
3557
- const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
3930
+ const _sanitizeAttributes = function _sanitizeAttributes(currentNode, root) {
3558
3931
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
3932
+ if (_handleHookDetachedNode(currentNode, root)) return;
3559
3933
  const attributes = currentNode.attributes;
3560
3934
  if (!attributes || _isClobbered(currentNode)) return;
3935
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
3561
3936
  const hookEvent = {
3562
3937
  attrName: "",
3563
3938
  attrValue: "",
@@ -3573,6 +3948,7 @@ document.open();document.write(html);document.close();
3573
3948
  const lcName = transformCaseFunc(name);
3574
3949
  const initValue = attrValue;
3575
3950
  let value = name === "value" ? initValue : stringTrim(initValue);
3951
+ let recreatedNamedProp = false;
3576
3952
  hookEvent.attrName = lcName;
3577
3953
  hookEvent.attrValue = value;
3578
3954
  hookEvent.keepAttr = true;
@@ -3580,35 +3956,39 @@ document.open();document.write(html);document.close();
3580
3956
  _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent);
3581
3957
  value = hookEvent.attrValue;
3582
3958
  if (SANITIZE_NAMED_PROPS && (lcName === "id" || lcName === "name") && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
3583
- _removeAttribute(name, currentNode);
3959
+ _removeAttribute(name, currentNode, attr);
3584
3960
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
3961
+ recreatedNamedProp = true;
3585
3962
  }
3586
3963
  if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
3587
- _removeAttribute(name, currentNode);
3964
+ _removeAttribute(name, currentNode, attr);
3588
3965
  continue;
3589
3966
  }
3590
3967
  if (lcName === "attributename" && stringMatch(value, "href")) {
3591
- _removeAttribute(name, currentNode);
3968
+ _removeAttribute(name, currentNode, attr);
3592
3969
  continue;
3593
3970
  }
3594
3971
  if (hookEvent.forceKeepAttr) continue;
3595
3972
  if (!hookEvent.keepAttr) {
3596
- _removeAttribute(name, currentNode);
3973
+ _removeAttribute(name, currentNode, attr);
3597
3974
  continue;
3598
3975
  }
3599
3976
  if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
3600
- _removeAttribute(name, currentNode);
3977
+ _removeAttribute(name, currentNode, attr);
3601
3978
  continue;
3602
3979
  }
3603
3980
  if (SAFE_FOR_TEMPLATES) value = _stripTemplateExpressions(value);
3604
3981
  if (!_isValidAttribute(lcTag, lcName, value)) {
3605
- _removeAttribute(name, currentNode);
3982
+ _removeAttribute(name, currentNode, attr);
3606
3983
  continue;
3607
3984
  }
3608
3985
  value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
3609
- if (value !== initValue) _setAttributeValue(currentNode, name, namespaceURI, value);
3986
+ if (value !== initValue) {
3987
+ if (_setAttributeValue(currentNode, name, namespaceURI, value) && recreatedNamedProp) arrayPop(DOMPurify.removed);
3988
+ }
3610
3989
  }
3611
3990
  _executeHooks(hooks.afterSanitizeAttributes, currentNode, null);
3991
+ _handleHookDetachedNode(currentNode, root);
3612
3992
  };
3613
3993
  /**
3614
3994
  * _sanitizeShadowDOM
@@ -3621,10 +4001,10 @@ document.open();document.write(html);document.close();
3621
4001
  _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null);
3622
4002
  while (shadowNode = shadowIterator.nextNode()) {
3623
4003
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
3624
- _sanitizeElements(shadowNode);
3625
- _sanitizeAttributes(shadowNode);
4004
+ _sanitizeElements(shadowNode, fragment);
4005
+ _sanitizeAttributes(shadowNode, fragment);
3626
4006
  if (_isDocumentFragment(shadowNode.content)) _sanitizeShadowDOM2(shadowNode.content);
3627
- if ((getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType) === NODE_TYPE.element) {
4007
+ if (_readNodeType(shadowNode) === NODE_TYPE.element) {
3628
4008
  const innerSr = getShadowRoot(shadowNode);
3629
4009
  if (_isDocumentFragment(innerSr)) {
3630
4010
  _sanitizeAttachedShadowRoots(innerSr);
@@ -3665,7 +4045,7 @@ document.open();document.write(html);document.close();
3665
4045
  continue;
3666
4046
  }
3667
4047
  const node = item.node;
3668
- const isElement = (getNodeType ? getNodeType(node) : node.nodeType) === NODE_TYPE.element;
4048
+ const isElement = _readNodeType(node) === NODE_TYPE.element;
3669
4049
  const childNodes = getChildNodes(node);
3670
4050
  if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push({
3671
4051
  node: childNodes[i],
@@ -3715,12 +4095,19 @@ document.open();document.write(html);document.close();
3715
4095
  DOMPurify.removed = [];
3716
4096
  const inPlace = IN_PLACE && typeof dirty !== "string" && _isNode(dirty);
3717
4097
  if (inPlace) {
3718
- const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
4098
+ _neutralizePatchLinkage(dirty);
4099
+ const nn = _readNodeName(dirty);
3719
4100
  if (typeof nn === "string") {
3720
4101
  const tagName = transformCaseFunc(nn);
3721
- 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");
3722
4110
  }
3723
- if (_isClobbered(dirty)) throw typeErrorCreate("root node is clobbered and cannot be sanitized in-place");
3724
4111
  try {
3725
4112
  _sanitizeAttachedShadowRoots(dirty);
3726
4113
  } catch (error) {
@@ -3733,28 +4120,39 @@ document.open();document.write(html);document.close();
3733
4120
  if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === "BODY") body = importedNode;
3734
4121
  else if (importedNode.nodeName === "HTML") body = importedNode;
3735
4122
  else body.appendChild(importedNode);
3736
- _sanitizeAttachedShadowRoots(importedNode);
4123
+ _sanitizeAttachedShadowRoots(body);
3737
4124
  } else {
3738
4125
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && dirty.indexOf("<") === -1) return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
3739
4126
  body = _initDocument(dirty);
3740
4127
  if (!body) return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : "";
3741
4128
  }
3742
4129
  if (body && FORCE_BODY) _forceRemove(body.firstChild);
3743
- const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
4130
+ const walkRoot = inPlace ? dirty : body;
3744
4131
  try {
4132
+ const nodeIterator = _createNodeIterator(walkRoot);
3745
4133
  while (currentNode = nodeIterator.nextNode()) {
3746
- _sanitizeElements(currentNode);
3747
- _sanitizeAttributes(currentNode);
4134
+ _sanitizeElements(currentNode, walkRoot);
4135
+ _sanitizeAttributes(currentNode, walkRoot);
3748
4136
  if (_isDocumentFragment(currentNode.content)) _sanitizeShadowDOM2(currentNode.content);
3749
4137
  }
3750
4138
  } catch (error) {
3751
- if (inPlace) _neutralizeRoot(dirty);
4139
+ if (inPlace) {
4140
+ _neutralizeRoot(dirty);
4141
+ arrayForEach(DOMPurify.removed, (entry) => {
4142
+ if (entry.element) _neutralizeSubtree(entry.element);
4143
+ });
4144
+ }
3752
4145
  throw error;
3753
4146
  }
3754
4147
  if (inPlace) {
4148
+ let rootWasRemoved = false;
3755
4149
  arrayForEach(DOMPurify.removed, (entry) => {
3756
- if (entry.element) _neutralizeSubtree(entry.element);
4150
+ if (entry.element) {
4151
+ if (entry.element === dirty) rootWasRemoved = true;
4152
+ _neutralizeSubtree(entry.element);
4153
+ }
3757
4154
  });
4155
+ if (rootWasRemoved) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
3758
4156
  if (SAFE_FOR_TEMPLATES) _scrubTemplateExpressions2(dirty);
3759
4157
  return dirty;
3760
4158
  }
@@ -3812,7 +4210,7 @@ document.open();document.write(html);document.close();
3812
4210
  };
3813
4211
  return DOMPurify;
3814
4212
  }
3815
- var purify = createDOMPurify();
4213
+ var purify_default = createDOMPurify();
3816
4214
  //#endregion
3817
4215
  //#region lib/types/client/html/basic-document.js
3818
4216
  /** Static HTML preview with no scripts, network resources, forms or nested frames. */
@@ -3822,7 +4220,7 @@ document.open();document.write(html);document.close();
3822
4220
  * @returns document with the restrictive CSP first in its head.
3823
4221
  */
3824
4222
  function createBasicHtmlDocument(data) {
3825
- const clean = purify.sanitize(decodeText(data), {
4223
+ const clean = purify_default.sanitize(decodeText(data), {
3826
4224
  WHOLE_DOCUMENT: true,
3827
4225
  FORBID_TAGS: [
3828
4226
  "noscript",