@knime/kds-rich-text-editor 11.4.0 → 11.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,8 +9,8 @@ import Link from "@tiptap/extension-link";
9
9
  import { Plugin, PluginKey } from "@tiptap/pm/state";
10
10
  import TextAlign from "@tiptap/extension-text-align";
11
11
  import Paragraph from "@tiptap/extension-paragraph";
12
- import './index.css';//#region ../../node_modules/.pnpm/dompurify@3.4.16/node_modules/dompurify/dist/purify.es.mjs
13
- /*! @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 */
12
+ import './index.css';//#region ../../node_modules/.pnpm/dompurify@3.4.17/node_modules/dompurify/dist/purify.es.mjs
13
+ /*! @license DOMPurify 3.4.17 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.17/LICENSE */
14
14
  function _OverloadYield(e, d) {
15
15
  this.v = e, this.k = d;
16
16
  }
@@ -134,9 +134,12 @@ Array.prototype.indexOf;
134
134
  var arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
135
135
  var arrayPop = unapply(Array.prototype.pop);
136
136
  var arrayPush = unapply(Array.prototype.push);
137
- Array.prototype.slice;
137
+ var arraySlice = unapply(Array.prototype.slice);
138
138
  var arraySplice = unapply(Array.prototype.splice);
139
139
  var arrayIsArray = Array.isArray;
140
+ var setCreate = unconstruct(Set);
141
+ var setAdd = unapply(Set.prototype.add);
142
+ var setHas = unapply(Set.prototype.has);
140
143
  var stringToLowerCase = unapply(String.prototype.toLowerCase);
141
144
  var stringToString = unapply(String.prototype.toString);
142
145
  var stringMatch = unapply(String.prototype.match);
@@ -279,6 +282,25 @@ function lookupGetter(object, prop) {
279
282
  }
280
283
  return fallbackValue;
281
284
  }
285
+ /**
286
+ * Like lookupGetter, but for accessor properties only, and returns null
287
+ * instead of a null-returning fallback when no getter exists on the chain.
288
+ * Lets a caller fall back to a direct property read in environments that
289
+ * lack the getter, where a fallback returning null would be indistinguishable
290
+ * from a legitimate null value (e.g. namespaceURI of a no-namespace element).
291
+ *
292
+ * @param object - The object (usually a prototype) to start the lookup at.
293
+ * @param prop - The accessor property name.
294
+ * @returns The unapplied getter, or null if none was found.
295
+ */
296
+ function lookupAccessor(object, prop) {
297
+ while (object !== null) {
298
+ const desc = getOwnPropertyDescriptor(object, prop);
299
+ if (desc) return desc.get ? unapply(desc.get) : null;
300
+ object = getPrototypeOf(object);
301
+ }
302
+ return null;
303
+ }
282
304
  function isRegex(value) {
283
305
  try {
284
306
  regExpTest(value, "");
@@ -963,7 +985,7 @@ var NODE_TYPE = {
963
985
  documentFragment: 11,
964
986
  notation: 12
965
987
  };
966
- var LITERAL_TEXT_ELEMENT_NAMES = [
988
+ var LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, [
967
989
  "style",
968
990
  "script",
969
991
  "xmp",
@@ -972,15 +994,7 @@ var LITERAL_TEXT_ELEMENT_NAMES = [
972
994
  "noframes",
973
995
  "plaintext",
974
996
  "noscript"
975
- ];
976
- var LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
977
- var LITERAL_TEXT_CLOSE = function() {
978
- const map = {};
979
- arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, (name) => {
980
- map[name] = seal(new RegExp("</" + name + "(?=[\\t\\n\\f\\r />])", "i"));
981
- });
982
- return freeze(map);
983
- }();
997
+ ]));
984
998
  var getGlobal = function getGlobal() {
985
999
  return typeof window === "undefined" ? null : window;
986
1000
  };
@@ -1058,7 +1072,7 @@ var _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallback)
1058
1072
  function createDOMPurify() {
1059
1073
  let window = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
1060
1074
  const DOMPurify = (root) => createDOMPurify(root);
1061
- DOMPurify.version = "3.4.16";
1075
+ DOMPurify.version = "3.4.17";
1062
1076
  DOMPurify.removed = [];
1063
1077
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
1064
1078
  DOMPurify.isSupported = false;
@@ -1090,6 +1104,10 @@ function createDOMPurify() {
1090
1104
  const _readNodeName = function _readNodeName(node) {
1091
1105
  return getNodeName ? getNodeName(node) : node.nodeName;
1092
1106
  };
1107
+ const getNamespaceURI = lookupAccessor(ElementPrototype, "namespaceURI");
1108
+ const _readNamespaceURI = function _readNamespaceURI(element) {
1109
+ return getNamespaceURI ? getNamespaceURI(element) : element.namespaceURI;
1110
+ };
1093
1111
  if (typeof HTMLTemplateElement === "function") {
1094
1112
  const template = document.createElement("template");
1095
1113
  if (template.content && template.content.ownerDocument) document = template.content.ownerDocument;
@@ -1210,6 +1228,7 @@ function createDOMPurify() {
1210
1228
  const SANITIZE_NAMED_PROPS_PREFIX = "user-content-";
1211
1229
  let KEEP_CONTENT = true;
1212
1230
  let IN_PLACE = false;
1231
+ let IN_PLACE_VISITED = null;
1213
1232
  let USE_PROFILES = {};
1214
1233
  let FORBID_CONTENTS = null;
1215
1234
  const DEFAULT_FORBID_CONTENTS = addToSet({}, [
@@ -1483,18 +1502,23 @@ function createDOMPurify() {
1483
1502
  * return. Return true otherwise.
1484
1503
  */
1485
1504
  const _checkValidNamespace = function _checkValidNamespace(element) {
1486
- let parent = getParentNode(element);
1487
- if (!parent || !parent.tagName) parent = {
1488
- namespaceURI: NAMESPACE,
1489
- tagName: "template"
1490
- };
1491
- const tagName = stringToLowerCase(element.tagName);
1492
- const parentTagName = stringToLowerCase(parent.tagName);
1493
- if (!ALLOWED_NAMESPACES[element.namespaceURI]) return false;
1494
- if (element.namespaceURI === SVG_NAMESPACE) return _checkSvgNamespace(tagName, parent, parentTagName);
1495
- if (element.namespaceURI === MATHML_NAMESPACE) return _checkMathMlNamespace(tagName, parent, parentTagName);
1496
- if (element.namespaceURI === HTML_NAMESPACE) return _checkHtmlNamespace(tagName, parent, parentTagName);
1497
- if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[element.namespaceURI]) return true;
1505
+ const elementNamespace = _readNamespaceURI(element);
1506
+ const tagName = stringToLowerCase(_readNodeName(element));
1507
+ const parentNode = getParentNode(element);
1508
+ let parent;
1509
+ let parentTagName;
1510
+ if (parentNode && _readNodeType(parentNode) === NODE_TYPE.element) {
1511
+ parent = { namespaceURI: _readNamespaceURI(parentNode) };
1512
+ parentTagName = stringToLowerCase(_readNodeName(parentNode));
1513
+ } else {
1514
+ parent = { namespaceURI: NAMESPACE };
1515
+ parentTagName = "template";
1516
+ }
1517
+ if (!ALLOWED_NAMESPACES[elementNamespace]) return false;
1518
+ if (elementNamespace === SVG_NAMESPACE) return _checkSvgNamespace(tagName, parent, parentTagName);
1519
+ if (elementNamespace === MATHML_NAMESPACE) return _checkMathMlNamespace(tagName, parent, parentTagName);
1520
+ if (elementNamespace === HTML_NAMESPACE) return _checkHtmlNamespace(tagName, parent, parentTagName);
1521
+ if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[elementNamespace]) return true;
1498
1522
  return false;
1499
1523
  };
1500
1524
  /**
@@ -1538,36 +1562,21 @@ function createDOMPurify() {
1538
1562
  }
1539
1563
  };
1540
1564
  /**
1541
- * _neutralizeRoot
1542
- *
1543
- * Fail-closed teardown of an in-place root after the sanitize walk aborts
1544
- * (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
1545
- * custom element's reaction detaches a node so `_forceRemove`'s deliberate
1546
- * parentless guard throws, or any other re-entrant engine mutation — would
1547
- * otherwise leave the caller's *live* tree half-sanitized, with everything
1548
- * after the abort point still carrying its handlers. There is no safe way
1549
- * to resume the walk (the tree mutated under us), so we strip the root bare:
1550
- * remove every child and every attribute, then let the caller's catch see
1551
- * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
1552
- * getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
1565
+ * Fail-closed teardown of an in-place root: strip every disallowed attribute
1566
+ * in the subtree first, so nothing armed is left behind, then remove all
1567
+ * children and the root's own attributes.
1553
1568
  *
1554
1569
  * @param root the in-place root to empty
1555
1570
  */
1556
1571
  const _neutralizeRoot = function _neutralizeRoot(root) {
1557
1572
  _neutralizeSubtree(root);
1558
1573
  const childNodes = getChildNodes(root);
1559
- if (childNodes) {
1560
- const snapshot = [];
1561
- arrayForEach(childNodes, (child) => {
1562
- arrayPush(snapshot, child);
1563
- });
1564
- arrayForEach(snapshot, (child) => {
1565
- try {
1566
- remove(child);
1567
- } catch (_) {}
1568
- });
1569
- }
1570
- const attributes = getAttributes(root);
1574
+ if (childNodes) arrayForEach(arraySlice(childNodes), (child) => {
1575
+ try {
1576
+ remove(child);
1577
+ } catch (_) {}
1578
+ });
1579
+ const attributes = _readNodeType(root) === NODE_TYPE.element ? getAttributes(root) : null;
1571
1580
  if (attributes) for (let i = attributes.length - 1; i >= 0; --i) {
1572
1581
  const attribute = attributes[i];
1573
1582
  const name = attribute && attribute.name;
@@ -1575,18 +1584,9 @@ function createDOMPurify() {
1575
1584
  }
1576
1585
  };
1577
1586
  /**
1578
- * _removeAttribute
1579
- *
1580
- * Name-based getAttributeNode()/removeAttribute() ASCII-lowercase their
1581
- * lookup key for HTML elements in an HTML document, so they silently miss an
1582
- * attribute whose stored qualified name still contains uppercase ASCII
1583
- * letters. That happens when the node came from a case-preserving source
1584
- * (an XML/XHTML document imported via importNode(), or createAttributeNS()),
1585
- * where e.g. `ONERROR` survives the walk: the policy check lowercases to
1586
- * `onerror` and rejects it, but `removeAttribute('ONERROR')` looks up
1587
- * `onerror` and finds nothing. Remove the exact Attr node instead, which is
1588
- * case- and namespace-exact, and fall back to name-based removal only when
1589
- * the caller could not supply the node.
1587
+ * Removes an attribute, preferably as its exact Attr node: name-based
1588
+ * removal lowercases the name in HTML documents and misses case-preserved
1589
+ * names, e.g. from imported XHTML.
1590
1590
  *
1591
1591
  * @param name an Attribute name
1592
1592
  * @param element a DOM node
@@ -1618,12 +1618,8 @@ function createDOMPurify() {
1618
1618
  } catch (_) {}
1619
1619
  };
1620
1620
  /**
1621
- * _stripDisallowedAttributes
1622
- *
1623
- * Removes every attribute the active configuration does not allow from a
1624
- * single element, using the same allowlist as the main attribute pass (so
1625
- * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
1626
- * neutralise nodes that are being discarded from an in-place tree.
1621
+ * Removes every attribute the configuration does not allow. Used to
1622
+ * neutralize nodes discarded from an in-place tree.
1627
1623
  *
1628
1624
  * @param element the element to strip
1629
1625
  */
@@ -1638,77 +1634,158 @@ function createDOMPurify() {
1638
1634
  }
1639
1635
  };
1640
1636
  /**
1641
- * _neutralizeSubtree
1642
- *
1643
- * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT
1644
- * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary,
1645
- * namespace, comment, processing-instruction and KEEP_CONTENT:false removals
1646
- * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
1647
- * those dropped nodes are detached from the caller's LIVE tree but a
1648
- * handler-bearing original among them (an `<img onerror>`/`<video>` that was
1649
- * loading) keeps its queued resource event, which fires in page scope after
1650
- * sanitize returns. This walks a removed subtree and strips every attribute
1651
- * the active configuration does not allow — so `on*` handlers are cancelled
1652
- * through the SAME allowlist that governs kept nodes, not a separate `/^on/`
1653
- * blocklist. Run synchronously before sanitize returns, i.e. before any
1654
- * queued event can fire. Hook-free by design: these nodes leave the output,
1655
- * so firing attribute hooks for them would be surprising. Clobber-safe reads;
1656
- * a doomed clobbered node may shadow `removeAttribute` (its own attributes are
1657
- * irrelevant — it is discarded — while its non-clobbered descendants, e.g.
1658
- * the `<img>`, are reached and scrubbed).
1637
+ * Iterative depth-first walk, so deep trees cannot overflow the call stack,
1638
+ * reading through the cached getters, so clobbering cannot hide children.
1639
+ * When `visit` returns false, the node's children are skipped. With
1640
+ * `intoShadow`, open shadow roots are walked after their host's children.
1659
1641
  *
1660
- * @param root the root of a removed subtree to neutralise
1642
+ * @param root where to start
1643
+ * @param visit called for every node
1644
+ * @param intoShadow whether to enter open shadow roots
1661
1645
  */
1662
- const _neutralizeSubtree = function _neutralizeSubtree(root) {
1646
+ const _walkTree = function _walkTree(root, visit) {
1647
+ let intoShadow = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false;
1663
1648
  const stack = [root];
1664
1649
  while (stack.length > 0) {
1665
1650
  const node = stack.pop();
1666
- if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
1651
+ if (visit(node) === false) continue;
1652
+ if (intoShadow && _readNodeType(node) === NODE_TYPE.element) {
1653
+ const shadowRoot = getShadowRoot(node);
1654
+ if (_isDocumentFragment(shadowRoot)) stack.push(shadowRoot);
1655
+ }
1667
1656
  const childNodes = getChildNodes(node);
1668
1657
  if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
1669
1658
  }
1670
1659
  };
1671
1660
  /**
1672
- * _neutralizePatchLinkage
1661
+ * Strips disallowed attributes from every element of a discarded subtree,
1662
+ * so a handler that is already queued (a loading image) cannot fire.
1673
1663
  *
1674
- * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
1675
- * hardening, https://github.com/WICG/declarative-partial-updates).
1664
+ * @param root the root of a removed subtree
1665
+ */
1666
+ const _neutralizeSubtree = function _neutralizeSubtree(root) {
1667
+ _walkTree(root, (node) => {
1668
+ if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
1669
+ });
1670
+ };
1671
+ /**
1672
+ * All nodes under `root`, root first, including open shadow roots. Template
1673
+ * content is not entered, it is inert.
1674
+ *
1675
+ * @param root the in-place root
1676
+ * @return the nodes
1677
+ */
1678
+ const _collectLiveNodes = function _collectLiveNodes(root) {
1679
+ const nodes = [];
1680
+ _walkTree(root, (node) => {
1681
+ nodes.push(node);
1682
+ }, true);
1683
+ return nodes;
1684
+ };
1685
+ /**
1686
+ * Strips disallowed attributes from every element that was under the
1687
+ * in-place root when the call started and no longer is, whatever removed
1688
+ * or moved it.
1676
1689
  *
1677
- * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
1678
- * markers (PIs / markup comments) node-by-node, in document order, AS it
1679
- * reaches each node. On a live in-place root that leaves a window: from the
1680
- * moment the root is connected until the walk arrives at a given node, that
1681
- * node's linkage is live. A patch applied on connection/stream can fire as
1682
- * a microtask during the walk and inject or teleport an unsanitized DOM
1683
- * range into a region the iterator has already passed and will not revisit,
1684
- * so the post-return "tree is sanitized" contract is violated. Sweep the
1685
- * whole tree once up front and sever every linkage before the walk begins,
1686
- * closing that window.
1690
+ * @param root the in-place root
1691
+ * @param before the root's nodes, collected before the walk
1692
+ */
1693
+ const _neutralizeEscapedElements = function _neutralizeEscapedElements(root, before) {
1694
+ const reachable = setCreate();
1695
+ arrayForEach(_collectLiveNodes(root), (node) => {
1696
+ if (_readNodeType(node) === NODE_TYPE.element) setAdd(reachable, node);
1697
+ });
1698
+ arrayForEach(before, (node) => {
1699
+ if (_readNodeType(node) === NODE_TYPE.element && !setHas(reachable, node)) _stripDisallowedAttributes(node);
1700
+ });
1701
+ };
1702
+ /**
1703
+ * Neutralizes everything that left the in-place tree during the call: what
1704
+ * DOMPurify removed, and what hooks or custom elements removed or moved out.
1687
1705
  *
1688
- * This CANNOT undo a patch that already fired before sanitize ran — that is
1689
- * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
1690
- * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
1691
- * to group with the rest of the declarative-partial-updates handling and
1692
- * stay overridable, consistent with the codebase.
1706
+ * @param root the in-place root
1707
+ * @param before the root's nodes, collected before the walk
1708
+ * @return true if the root itself was removed
1709
+ */
1710
+ const _neutralizeDiscarded = function _neutralizeDiscarded(root, before) {
1711
+ let rootRemoved = false;
1712
+ arrayForEach(DOMPurify.removed, (entry) => {
1713
+ if (entry.element) {
1714
+ rootRemoved = rootRemoved || entry.element === root;
1715
+ _neutralizeSubtree(entry.element);
1716
+ }
1717
+ });
1718
+ _neutralizeEscapedElements(root, before);
1719
+ return rootRemoved;
1720
+ };
1721
+ const MAX_UNVISITED_ROUNDS = 8;
1722
+ /**
1723
+ * The walk only moves forward, so a node moved to a position it already
1724
+ * passed is never visited. Give every original node that is still in the
1725
+ * tree but unvisited the normal treatment, hooks included. Nodes created
1726
+ * during the call are left alone. Fails closed if the tree keeps changing.
1693
1727
  *
1694
- * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
1695
- * clobbered root cannot defeat the sweep of its non-clobbered descendants.
1728
+ * @param root the in-place root
1729
+ * @param before the root's nodes, collected before the walk
1730
+ */
1731
+ const _sanitizeUnvisitedNodes = function _sanitizeUnvisitedNodes(root, before) {
1732
+ const visited = IN_PLACE_VISITED;
1733
+ let original = null;
1734
+ const getOriginal = () => {
1735
+ if (!original) {
1736
+ original = setCreate();
1737
+ arrayForEach(before, (beforeNode) => {
1738
+ setAdd(original, beforeNode);
1739
+ });
1740
+ }
1741
+ return original;
1742
+ };
1743
+ for (let round = 0;; round++) {
1744
+ const live = _collectLiveNodes(root);
1745
+ const containers = [root];
1746
+ let pending = false;
1747
+ arrayForEach(live, (node) => {
1748
+ if (_readNodeType(node) === NODE_TYPE.documentFragment) arrayPush(containers, node);
1749
+ else if (!pending && !setHas(visited, node)) pending = setHas(getOriginal(), node);
1750
+ });
1751
+ if (!pending) return;
1752
+ if (round >= MAX_UNVISITED_ROUNDS) throw typeErrorCreate("the tree kept changing during in-place sanitization; refusing to return it");
1753
+ arrayForEach(containers, (container, index) => {
1754
+ _sanitizeUnvisitedIn(container, index > 0, root, getOriginal());
1755
+ });
1756
+ }
1757
+ };
1758
+ /**
1759
+ * One _sanitizeUnvisitedNodes pass over the root or an open shadow root.
1696
1760
  *
1697
- * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
1698
- * .html Q1): this mirrors the existing policy of keeping `for` on
1699
- * <label>/<output>. If the shipping feature can drive a patch through a
1700
- * surviving `for`-on-label/output + `id` pair, this pre-pass and the
1701
- * attribute check at _isBasicCustomElement's caller must additionally drop
1702
- * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
1761
+ * @param container the root or an open shadow root
1762
+ * @param inShadow whether container is a shadow root
1763
+ * @param root the in-place root
1764
+ * @param original the root's nodes collected before the walk, as a set
1765
+ */
1766
+ const _sanitizeUnvisitedIn = function _sanitizeUnvisitedIn(container, inShadow, root, original) {
1767
+ const iterator = _createNodeIterator(container);
1768
+ let node;
1769
+ while (node = iterator.nextNode()) {
1770
+ if (!setHas(original, node) || setHas(IN_PLACE_VISITED, node)) continue;
1771
+ if (inShadow) _executeHooks(hooks.uponSanitizeShadowNode, node, null);
1772
+ _sanitizeElements(node, root);
1773
+ _sanitizeAttributes(node, root);
1774
+ if (_isDocumentFragment(node.content)) _sanitizeShadowDOM2(node.content);
1775
+ }
1776
+ };
1777
+ /**
1778
+ * Records that _sanitizeElements processed `node`. No-op outside IN_PLACE.
1703
1779
  *
1704
- * @param root the in-place root to sweep
1780
+ * @param node the node being processed
1705
1781
  */
1782
+ const _markVisited = function _markVisited(node) {
1783
+ if (IN_PLACE_VISITED) setAdd(IN_PLACE_VISITED, node);
1784
+ };
1706
1785
  /**
1707
- * Central policy for declarative-partial-updates patch-linkage attributes,
1708
- * shared by the _neutralizePatchLinkage pre-pass and _isValidAttribute so
1709
- * the two sites cannot drift: `patchsrc` always links, `for` links
1710
- * everywhere except on <label>/<output>, and the whole policy is gated on
1711
- * SAFE_FOR_XML (see the rationale block in _isValidAttribute).
1786
+ * Patch-linkage attributes (declarative partial updates): `patchsrc`
1787
+ * always, `for` everywhere except on <label> and <output>. Gated on
1788
+ * SAFE_FOR_XML. Shared by the IN_PLACE pre-pass and _isValidAttribute.
1712
1789
  *
1713
1790
  * @param lcName the transformCaseFunc'd attribute name
1714
1791
  * @param lcTag the transformCaseFunc'd tag name of the carrying element
@@ -1719,17 +1796,26 @@ function createDOMPurify() {
1719
1796
  if (lcName === "patchsrc") return true;
1720
1797
  return lcName === "for" && lcTag !== "label" && lcTag !== "output";
1721
1798
  };
1799
+ /**
1800
+ * IN_PLACE pre-pass: sever all patch linkage before the walk, so no patch
1801
+ * can fire mid-walk into a part of the tree the walker already passed.
1802
+ * Removes range markers (processing instructions, comments with markup)
1803
+ * and strips linkage attributes. It cannot undo a patch that already fired.
1804
+ *
1805
+ * TODO: if the shipped feature can patch through `for` on <label>/<output>,
1806
+ * drop that pair here too (see test/declarative-patch-probe.html).
1807
+ *
1808
+ * @param root the in-place root to sweep
1809
+ */
1722
1810
  const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
1723
1811
  if (!SAFE_FOR_XML) return;
1724
- const stack = [root];
1725
- while (stack.length > 0) {
1726
- const node = stack.pop();
1812
+ _walkTree(root, (node) => {
1727
1813
  const nodeType = _readNodeType(node);
1728
1814
  if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
1729
1815
  try {
1730
1816
  remove(node);
1731
1817
  } catch (_) {}
1732
- continue;
1818
+ return false;
1733
1819
  }
1734
1820
  if (nodeType === NODE_TYPE.element) {
1735
1821
  const element = node;
@@ -1739,9 +1825,8 @@ function createDOMPurify() {
1739
1825
  if (element.hasAttribute && element.hasAttribute("for") && _isPatchLinkageAttribute("for", lcTag)) element.removeAttribute("for");
1740
1826
  } catch (_) {}
1741
1827
  }
1742
- const childNodes = getChildNodes(node);
1743
- if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
1744
- }
1828
+ return true;
1829
+ });
1745
1830
  };
1746
1831
  /**
1747
1832
  * _initDocument
@@ -1818,6 +1903,27 @@ function createDOMPurify() {
1818
1903
  */
1819
1904
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1820
1905
  var _node$querySelectorAl;
1906
+ _scrubOwnCharacterData(node);
1907
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, "template");
1908
+ if (templates) arrayForEach(templates, (tmpl) => {
1909
+ if (_isDocumentFragment(tmpl.content)) _scrubTemplateExpressions2(tmpl.content);
1910
+ });
1911
+ };
1912
+ /**
1913
+ * _scrubOwnCharacterData
1914
+ *
1915
+ * Non-recursive core of _scrubTemplateExpressions: normalize `node` and
1916
+ * strip template expressions from its own character data only. Does not
1917
+ * enter <template>.content or attached shadow roots (NodeIterator never
1918
+ * does). Used by _sanitizeShadowDOM, where every nested template content
1919
+ * and shadow root is already walked - and therefore scrubbed - by its own
1920
+ * recursive _sanitizeShadowDOM call; recursing here as well would rescrub
1921
+ * each nested fragment once per ancestor, which is quadratic in nesting
1922
+ * depth.
1923
+ *
1924
+ * @param node The root whose own character data should be scrubbed.
1925
+ */
1926
+ const _scrubOwnCharacterData = function _scrubOwnCharacterData(node) {
1821
1927
  node.normalize();
1822
1928
  const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
1823
1929
  const walker = createNodeIterator.call(doc || node, node, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
@@ -1826,10 +1932,6 @@ function createDOMPurify() {
1826
1932
  currentNode.data = _stripTemplateExpressions(currentNode.data);
1827
1933
  currentNode = walker.nextNode();
1828
1934
  }
1829
- const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, "template");
1830
- if (templates) arrayForEach(templates, (tmpl) => {
1831
- if (_isDocumentFragment(tmpl.content)) _scrubTemplateExpressions2(tmpl.content);
1832
- });
1833
1935
  };
1834
1936
  /**
1835
1937
  * _isClobbered
@@ -1905,7 +2007,7 @@ function createDOMPurify() {
1905
2007
  */
1906
2008
  const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
1907
2009
  if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) return true;
1908
- 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;
2010
+ if (SAFE_FOR_XML && LITERAL_TEXT_ELEMENTS[tagName] && _readNodeType(currentNode) === NODE_TYPE.element && _readNamespaceURI(currentNode) === HTML_NAMESPACE && (_isNode(currentNode.firstElementChild) || typeof currentNode.textContent === "string" && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent))) return true;
1909
2011
  if (currentNode.nodeType === NODE_TYPE.processingInstruction) return true;
1910
2012
  if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) return true;
1911
2013
  return false;
@@ -1949,7 +2051,7 @@ function createDOMPurify() {
1949
2051
  */
1950
2052
  const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
1951
2053
  if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
1952
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
2054
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName] && !FORBID_CONTENTS[stringToLowerCase(tagName)]) {
1953
2055
  const parentNode = getParentNode(currentNode);
1954
2056
  const childNodes = getChildNodes(currentNode);
1955
2057
  if (childNodes && parentNode) {
@@ -1982,35 +2084,13 @@ function createDOMPurify() {
1982
2084
  return set === defaultSet || set === setConfigSet ? clone(set) : set;
1983
2085
  };
1984
2086
  /**
1985
- * Shared guard for a node that a hook has detached from the walk tree,
1986
- * used after each element-hook site in _sanitizeElements. Detaching is a
1987
- * long-standing user pattern (issue #469; draw.io-style foreignObject
1988
- * filtering). Per the cached, unclobberable parentNode getter the node is
1989
- * genuinely out of the tree, so it can reach neither the serialized
1990
- * output nor an IN_PLACE live tree; treat it as removed and stop
1991
- * processing it. Without this guard, the unsafe-node / namespace checks
1992
- * would call _forceRemove on a parentless node and hit the REPORT-3
1993
- * fail-closed throw — which exists for nodes DOMPurify wants gone but
1994
- * *cannot* detach (clobbered / parentless roots), the opposite of a node
1995
- * that is already safely gone. The walk root is exempt: a detached
1996
- * IN_PLACE root is legitimate input and must still be fully sanitized,
1997
- * and a kill-decision on it must keep hitting the REPORT-3 throw.
1998
- *
1999
- * Nodes detached by hooks stay the hook's responsibility for placement:
2000
- * they are not recorded in DOMPurify.removed, so the post-walk IN_PLACE
2001
- * pass (which iterates DOMPurify.removed) does not reach them. But a
2002
- * hook-detached subtree can still hold a queued resource-event handler -
2003
- * e.g. an <img onload> that began loading when the caller built the live
2004
- * tree - which fires in page scope after sanitize returns even though the
2005
- * handler never reached the returned tree. That is the audit-5 F1 hazard,
2006
- * and the documented node.remove() hook pattern walks straight into it.
2007
- * So on the IN_PLACE path we neutralize the detached subtree inline,
2008
- * stripping its non-allow-listed attributes before returning, exactly as
2009
- * the post-walk pass does for _forceRemove'd subtrees.
2087
+ * A hook may have detached the node it was called for. If so, treat it as
2088
+ * removed and stop processing it; the walk root is exempt. On the IN_PLACE
2089
+ * path the detached subtree is neutralized right away.
2010
2090
  *
2011
2091
  * @param currentNode the node a hook may have detached
2012
2092
  * @param root the current walk root
2013
- * @return true if the node is detached and now handled, false otherwise
2093
+ * @return true if the node was detached
2014
2094
  */
2015
2095
  const _handleHookDetachedNode = function _handleHookDetachedNode(currentNode, root) {
2016
2096
  if (currentNode === root || getParentNode(currentNode) !== null) return false;
@@ -2027,6 +2107,7 @@ function createDOMPurify() {
2027
2107
  * @return true if node was killed, false if left alive
2028
2108
  */
2029
2109
  const _sanitizeElements = function _sanitizeElements(currentNode, root) {
2110
+ _markVisited(currentNode);
2030
2111
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
2031
2112
  if (_handleHookDetachedNode(currentNode, root)) return true;
2032
2113
  if (_isClobbered(currentNode)) {
@@ -2170,6 +2251,22 @@ function createDOMPurify() {
2170
2251
  }
2171
2252
  };
2172
2253
  /**
2254
+ * _isUnsafeAttributeValue
2255
+ *
2256
+ * Value-based attribute checks that the attribute walk applies before any
2257
+ * hook can force-keep an attribute, shared with the public
2258
+ * isValidAttribute() so that it never approves a value sanitize() would
2259
+ * strip.
2260
+ *
2261
+ * @param name the attribute name as found on the node (case preserved)
2262
+ * @param value the attribute value
2263
+ * @returns true if the attribute must be removed because of its value
2264
+ */
2265
+ const _isUnsafeAttributeValue = function _isUnsafeAttributeValue(name, value) {
2266
+ if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) return true;
2267
+ return stringToLowerCase(name) === "attributename" && stringMatch(value, "href") !== null;
2268
+ };
2269
+ /**
2173
2270
  * _sanitizeAttributes
2174
2271
  *
2175
2272
  * @protect attributes
@@ -2213,11 +2310,7 @@ function createDOMPurify() {
2213
2310
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
2214
2311
  recreatedNamedProp = true;
2215
2312
  }
2216
- if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
2217
- _removeAttribute(name, currentNode, attr);
2218
- continue;
2219
- }
2220
- if (lcName === "attributename" && stringMatch(value, "href")) {
2313
+ if (_isUnsafeAttributeValue(name, value)) {
2221
2314
  _removeAttribute(name, currentNode, attr);
2222
2315
  continue;
2223
2316
  }
@@ -2265,24 +2358,13 @@ function createDOMPurify() {
2265
2358
  }
2266
2359
  }
2267
2360
  }
2361
+ if (SAFE_FOR_TEMPLATES) _scrubOwnCharacterData(fragment);
2268
2362
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
2269
2363
  };
2270
2364
  /**
2271
- * _sanitizeAttachedShadowRoots
2272
- *
2273
- * Walks `root` and feeds every attached shadow root we encounter into
2274
- * the existing _sanitizeShadowDOM pipeline. The default node iterator
2275
- * does not descend into shadow trees, so nodes inside an attached
2276
- * shadow root would otherwise be skipped entirely.
2277
- *
2278
- * Two real input paths put attached shadow roots in front of us:
2279
- * 1. IN_PLACE on a DOM node that already has shadow roots attached.
2280
- * 2. DOM-node input where importNode(dirty, true) deep-clones the
2281
- * shadow root because it was created with `clonable: true`.
2282
- *
2283
- * This pass runs once, up front, so the main iteration loop (and the
2284
- * existing _sanitizeShadowDOM template-content recursion) stay
2285
- * untouched — string-input paths are not affected.
2365
+ * Sanitizes every attached shadow root under `root`, since the node
2366
+ * iterator does not enter shadow trees. Needed for IN_PLACE input and for
2367
+ * DOM input whose clonable shadow roots were copied by importNode().
2286
2368
  *
2287
2369
  * @param root the subtree root to walk for attached shadow roots
2288
2370
  */
@@ -2347,6 +2429,8 @@ function createDOMPurify() {
2347
2429
  if (hooks.uponSanitizeAttribute.length > 0) ALLOWED_ATTR = clone(ALLOWED_ATTR);
2348
2430
  DOMPurify.removed = [];
2349
2431
  const inPlace = IN_PLACE && typeof dirty !== "string" && _isNode(dirty);
2432
+ let inPlaceNodes = [];
2433
+ const outerVisited = IN_PLACE_VISITED;
2350
2434
  if (inPlace) {
2351
2435
  _neutralizePatchLinkage(dirty);
2352
2436
  const nn = _readNodeName(dirty);
@@ -2361,12 +2445,8 @@ function createDOMPurify() {
2361
2445
  _neutralizeRoot(dirty);
2362
2446
  throw typeErrorCreate("root node is clobbered and cannot be sanitized in-place");
2363
2447
  }
2364
- try {
2365
- _sanitizeAttachedShadowRoots(dirty);
2366
- } catch (error) {
2367
- _neutralizeRoot(dirty);
2368
- throw error;
2369
- }
2448
+ inPlaceNodes = _collectLiveNodes(dirty);
2449
+ IN_PLACE_VISITED = setCreate();
2370
2450
  } else if (_isNode(dirty)) {
2371
2451
  body = _initDocument("<!---->");
2372
2452
  importedNode = body.ownerDocument.importNode(dirty, true);
@@ -2382,30 +2462,25 @@ function createDOMPurify() {
2382
2462
  if (body && FORCE_BODY) _forceRemove(body.firstChild);
2383
2463
  const walkRoot = inPlace ? dirty : body;
2384
2464
  try {
2465
+ if (inPlace) _sanitizeAttachedShadowRoots(dirty);
2385
2466
  const nodeIterator = _createNodeIterator(walkRoot);
2386
2467
  while (currentNode = nodeIterator.nextNode()) {
2387
2468
  _sanitizeElements(currentNode, walkRoot);
2388
2469
  _sanitizeAttributes(currentNode, walkRoot);
2389
2470
  if (_isDocumentFragment(currentNode.content)) _sanitizeShadowDOM2(currentNode.content);
2390
2471
  }
2472
+ if (inPlace) _sanitizeUnvisitedNodes(dirty, inPlaceNodes);
2391
2473
  } catch (error) {
2392
2474
  if (inPlace) {
2393
2475
  _neutralizeRoot(dirty);
2394
- arrayForEach(DOMPurify.removed, (entry) => {
2395
- if (entry.element) _neutralizeSubtree(entry.element);
2396
- });
2476
+ _neutralizeDiscarded(dirty, inPlaceNodes);
2397
2477
  }
2398
2478
  throw error;
2479
+ } finally {
2480
+ IN_PLACE_VISITED = outerVisited;
2399
2481
  }
2400
2482
  if (inPlace) {
2401
- let rootWasRemoved = false;
2402
- arrayForEach(DOMPurify.removed, (entry) => {
2403
- if (entry.element) {
2404
- if (entry.element === dirty) rootWasRemoved = true;
2405
- _neutralizeSubtree(entry.element);
2406
- }
2407
- });
2408
- if (rootWasRemoved) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
2483
+ if (_neutralizeDiscarded(dirty, inPlaceNodes)) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
2409
2484
  if (SAFE_FOR_TEMPLATES) _scrubTemplateExpressions2(dirty);
2410
2485
  return dirty;
2411
2486
  }
@@ -2442,6 +2517,7 @@ function createDOMPurify() {
2442
2517
  if (!CONFIG) _parseConfig({});
2443
2518
  const lcTag = transformCaseFunc(tag);
2444
2519
  const lcName = transformCaseFunc(attr);
2520
+ if (typeof value === "string" && (_isUnsafeAttributeValue(attr, value) || !ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value))) return false;
2445
2521
  return _isValidAttribute(lcTag, lcName, value);
2446
2522
  };
2447
2523
  DOMPurify.addHook = function(entryPoint, hookFunction) {