@ukic/web-components 3.29.0 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/components/ic-accordion.js +53 -38
  2. package/components/ic-accordion.js.map +1 -1
  3. package/components/ic-menu2.js +274 -35
  4. package/components/ic-menu2.js.map +1 -1
  5. package/components/ic-navigation-button.js +8 -5
  6. package/components/ic-navigation-button.js.map +1 -1
  7. package/components/ic-pagination-bar.js.map +1 -1
  8. package/components/ic-skip-link.js.map +1 -1
  9. package/components/ic-status-tag.js.map +1 -1
  10. package/components/ic-switch.js.map +1 -1
  11. package/dist/cjs/ic-accordion.cjs.entry.js +53 -38
  12. package/dist/cjs/ic-accordion.cjs.entry.js.map +1 -1
  13. package/dist/cjs/ic-button_3.cjs.entry.js.map +1 -1
  14. package/dist/cjs/ic-horizontal-scroll.cjs.entry.js.map +1 -1
  15. package/dist/cjs/ic-input-component-container_3.cjs.entry.js +274 -35
  16. package/dist/cjs/ic-input-component-container_3.cjs.entry.js.map +1 -1
  17. package/dist/cjs/ic-navigation-button.cjs.entry.js +8 -5
  18. package/dist/cjs/ic-navigation-button.cjs.entry.js.map +1 -1
  19. package/dist/cjs/ic-section-container.cjs.entry.js.map +1 -1
  20. package/dist/collection/components/ic-accordion/ic-accordion.js +53 -38
  21. package/dist/collection/components/ic-accordion/ic-accordion.js.map +1 -1
  22. package/dist/collection/components/ic-navigation-button/ic-navigation-button.js +8 -5
  23. package/dist/collection/components/ic-navigation-button/ic-navigation-button.js.map +1 -1
  24. package/dist/components/ic-accordion.js +53 -38
  25. package/dist/components/ic-accordion.js.map +1 -1
  26. package/dist/components/ic-back-to-top.js.map +1 -1
  27. package/dist/components/ic-hero.js.map +1 -1
  28. package/dist/components/ic-menu2.js +274 -35
  29. package/dist/components/ic-menu2.js.map +1 -1
  30. package/dist/components/ic-navigation-button.js +8 -5
  31. package/dist/components/ic-navigation-button.js.map +1 -1
  32. package/dist/components/ic-pagination-bar.js.map +1 -1
  33. package/dist/components/ic-popover-menu.js.map +1 -1
  34. package/dist/components/ic-radio-group.js.map +1 -1
  35. package/dist/components/ic-radio-option.js.map +1 -1
  36. package/dist/components/ic-search-bar.js.map +1 -1
  37. package/dist/components/ic-skip-link.js.map +1 -1
  38. package/dist/components/ic-status-tag.js.map +1 -1
  39. package/dist/components/ic-switch.js.map +1 -1
  40. package/dist/core/core.esm.js +1 -1
  41. package/dist/core/{p-9ced00a7.entry.js → p-41bbdc4c.entry.js} +2 -2
  42. package/dist/core/{p-9ced00a7.entry.js.map → p-41bbdc4c.entry.js.map} +1 -1
  43. package/dist/core/p-8fc5e9f9.entry.js.map +1 -1
  44. package/dist/core/p-98dee727.entry.js.map +1 -1
  45. package/dist/core/{p-fc4b7d2b.entry.js → p-9cf35c23.entry.js} +2 -2
  46. package/dist/core/p-9cf35c23.entry.js.map +1 -0
  47. package/dist/core/p-a8d98164.entry.js.map +1 -1
  48. package/dist/core/p-ccba84b7.entry.js +3 -0
  49. package/dist/core/p-ccba84b7.entry.js.map +1 -0
  50. package/dist/esm/ic-accordion.entry.js +53 -38
  51. package/dist/esm/ic-accordion.entry.js.map +1 -1
  52. package/dist/esm/ic-button_3.entry.js.map +1 -1
  53. package/dist/esm/ic-horizontal-scroll.entry.js.map +1 -1
  54. package/dist/esm/ic-input-component-container_3.entry.js +274 -35
  55. package/dist/esm/ic-input-component-container_3.entry.js.map +1 -1
  56. package/dist/esm/ic-navigation-button.entry.js +8 -5
  57. package/dist/esm/ic-navigation-button.entry.js.map +1 -1
  58. package/dist/esm/ic-section-container.entry.js.map +1 -1
  59. package/dist/types/components/ic-accordion/ic-accordion.d.ts +2 -0
  60. package/hydrate/index.js +334 -77
  61. package/hydrate/index.mjs +334 -77
  62. package/package.json +1 -1
  63. package/dist/core/p-305d9639.entry.js +0 -3
  64. package/dist/core/p-305d9639.entry.js.map +0 -1
  65. package/dist/core/p-fc4b7d2b.entry.js.map +0 -1
@@ -11,7 +11,7 @@ var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof win
11
11
 
12
12
  var purify = {exports: {}};
13
13
 
14
- /*! @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 */
14
+ /*! @license DOMPurify 3.4.13 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.13/LICENSE */
15
15
 
16
16
  (function (module, exports) {
17
17
  (function (global, factory) {
@@ -328,7 +328,7 @@ var purify = {exports: {}};
328
328
  const text = freeze(['#text']);
329
329
 
330
330
  const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'command', 'commandfor', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns']);
331
- const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
331
+ const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dominant-baseline', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-orientation', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
332
332
  const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lquote', 'lspace', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
333
333
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
334
334
 
@@ -441,7 +441,7 @@ var purify = {exports: {}};
441
441
  function createDOMPurify() {
442
442
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
443
443
  const DOMPurify = root => createDOMPurify(root);
444
- DOMPurify.version = '3.4.11';
444
+ DOMPurify.version = '3.4.13';
445
445
  DOMPurify.removed = [];
446
446
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
447
447
  // Not running in a browser, provide a factory function
@@ -468,6 +468,7 @@ var purify = {exports: {}};
468
468
  const getAttributes = lookupGetter(ElementPrototype, 'attributes');
469
469
  const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
470
470
  const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
471
+ const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, 'ownerDocument') : null;
471
472
  // As per issue #47, the web-components registry is inherited by a
472
473
  // new document created via createHTMLDocument. As per the spec
473
474
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -1119,6 +1120,13 @@ var purify = {exports: {}};
1119
1120
  * @param root the in-place root to empty
1120
1121
  */
1121
1122
  const _neutralizeRoot = function _neutralizeRoot(root) {
1123
+ /* Strip every disallowed attribute (on* handlers included) off the whole
1124
+ subtree BEFORE detaching anything. Detaching first would hand back
1125
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
1126
+ whose queued resource event still fires in page scope after we throw.
1127
+ Clobber-safe reads; a doomed clobbered node's own attributes are
1128
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
1129
+ _neutralizeSubtree(root);
1122
1130
  const childNodes = getChildNodes(root);
1123
1131
  if (childNodes) {
1124
1132
  const snapshot = [];
@@ -1246,6 +1254,82 @@ var purify = {exports: {}};
1246
1254
  }
1247
1255
  }
1248
1256
  };
1257
+ /**
1258
+ * _neutralizePatchLinkage
1259
+ *
1260
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
1261
+ * hardening, https://github.com/WICG/declarative-partial-updates).
1262
+ *
1263
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
1264
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
1265
+ * reaches each node. On a live in-place root that leaves a window: from the
1266
+ * moment the root is connected until the walk arrives at a given node, that
1267
+ * node's linkage is live. A patch applied on connection/stream can fire as
1268
+ * a microtask during the walk and inject or teleport an unsanitized DOM
1269
+ * range into a region the iterator has already passed and will not revisit,
1270
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
1271
+ * whole tree once up front and sever every linkage before the walk begins,
1272
+ * closing that window.
1273
+ *
1274
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
1275
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
1276
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
1277
+ * to group with the rest of the declarative-partial-updates handling and
1278
+ * stay overridable, consistent with the codebase.
1279
+ *
1280
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
1281
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
1282
+ *
1283
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
1284
+ * .html Q1): this mirrors the existing policy of keeping `for` on
1285
+ * <label>/<output>. If the shipping feature can drive a patch through a
1286
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
1287
+ * attribute check at _isBasicCustomElement's caller must additionally drop
1288
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
1289
+ *
1290
+ * @param root the in-place root to sweep
1291
+ */
1292
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
1293
+ if (!SAFE_FOR_XML) {
1294
+ return;
1295
+ }
1296
+ const stack = [root];
1297
+ while (stack.length > 0) {
1298
+ const node = stack.pop();
1299
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
1300
+ /* Remove range markers (the target side of a patch linkage): every
1301
+ processing instruction, and any markup-bearing comment. */
1302
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
1303
+ try {
1304
+ remove(node);
1305
+ } catch (_) {
1306
+ /* Best-effort */
1307
+ }
1308
+ continue;
1309
+ }
1310
+ /* Strip patch-source attributes (the source side) off elements. */
1311
+ if (nodeType === NODE_TYPE.element) {
1312
+ const element = node;
1313
+ const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
1314
+ try {
1315
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
1316
+ element.removeAttribute('patchsrc');
1317
+ }
1318
+ if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
1319
+ element.removeAttribute('for');
1320
+ }
1321
+ } catch (_) {
1322
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
1323
+ }
1324
+ }
1325
+ const childNodes = getChildNodes(node);
1326
+ if (childNodes) {
1327
+ for (let i = childNodes.length - 1; i >= 0; --i) {
1328
+ stack.push(childNodes[i]);
1329
+ }
1330
+ }
1331
+ }
1332
+ };
1249
1333
  /**
1250
1334
  * _initDocument
1251
1335
  *
@@ -1303,7 +1387,17 @@ var purify = {exports: {}};
1303
1387
  * @return The created NodeIterator
1304
1388
  */
1305
1389
  const _createNodeIterator = function _createNodeIterator(root) {
1306
- return createNodeIterator.call(root.ownerDocument || root, root,
1390
+ /* Read ownerDocument through the cached Node.prototype getter, never the
1391
+ direct property. HTMLFormElement has [LegacyOverrideBuiltIns], so a
1392
+ clobbering child (<input name="ownerDocument"> or a form-associated
1393
+ external input) shadows the prototype getter and makes a direct read
1394
+ return that <input>. createNodeIterator.call(<input>, ...) then throws
1395
+ "Illegal invocation", and on the IN_PLACE path that throw lands before
1396
+ the walk's fail-closed barrier - leaving the caller's live tree, with
1397
+ any already-armed handler in it, un-neutralized. The cached getter
1398
+ returns the real Document regardless of the clobber. */
1399
+ const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
1400
+ return createNodeIterator.call(doc || root, root,
1307
1401
  // eslint-disable-next-line no-bitwise
1308
1402
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
1309
1403
  };
@@ -1343,7 +1437,11 @@ var purify = {exports: {}};
1343
1437
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1344
1438
  var _node$querySelectorAl;
1345
1439
  node.normalize();
1346
- const walker = createNodeIterator.call(node.ownerDocument || node, node,
1440
+ /* Clobber-safe ownerDocument read, same reasoning as _createNodeIterator:
1441
+ under SAFE_FOR_TEMPLATES this runs on the live IN_PLACE root, which may
1442
+ carry a form-named-getter override of ownerDocument. */
1443
+ const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
1444
+ const walker = createNodeIterator.call(doc || node, node,
1347
1445
  // eslint-disable-next-line no-bitwise
1348
1446
  NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
1349
1447
  let currentNode = walker.nextNode();
@@ -1496,15 +1594,21 @@ var purify = {exports: {}};
1496
1594
  /**
1497
1595
  * Handle a node whose tag is forbidden or not allowlisted: keep
1498
1596
  * allowed custom elements (false return exits _sanitizeElements
1499
- * early - namespace/fallback checks and the afterSanitizeElements
1500
- * hook are intentionally skipped for kept custom elements), else
1501
- * hoist content per KEEP_CONTENT and remove.
1597
+ * early - the namespace and fallback-tag removal checks are
1598
+ * intentionally skipped for kept custom elements), else hoist
1599
+ * content per KEEP_CONTENT and remove.
1600
+ *
1601
+ * A kept custom element is the ONLY case in which this function
1602
+ * returns false, so the caller uses that return value to run the
1603
+ * afterSanitizeElements hook on the kept element and keep the
1604
+ * element-hook lifecycle consistent with normal allowlisted
1605
+ * elements (GHSA-c2j3-45gr-mqc4).
1502
1606
  *
1503
1607
  * @param currentNode the disallowed node
1504
1608
  * @param tagName the node's transformCaseFunc'd tag name
1505
1609
  * @return true if the node was removed, false if kept
1506
1610
  */
1507
- const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
1611
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
1508
1612
  /* Check if we have a custom element to handle */
1509
1613
  if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1510
1614
  if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
@@ -1527,27 +1631,26 @@ var purify = {exports: {}};
1527
1631
  const childNodes = getChildNodes(currentNode);
1528
1632
  if (childNodes && parentNode) {
1529
1633
  const childCount = childNodes.length;
1530
- /* In-place: hoist the *original* children so the iterator visits
1531
- and sanitises them through the same allowlist pass as every other
1532
- node. The caller built the tree in the live document, so the
1533
- originals carry already-queued resource events (`<img onerror>`,
1534
- `<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
1535
- those originals detached but still armed, firing in page scope
1536
- while the returned tree looked clean. Moving is safe in-place: the
1537
- root is pre-validated as an allowed tag and so is never the node
1538
- being removed, which keeps `parentNode` inside the iterator root
1539
- and the relocated child inside the serialised tree.
1540
- Otherwise (string / DOM-copy paths): clone. The iterator is rooted
1541
- at — and the result serialised from — `body`, so a restrictive
1542
- ALLOWED_TAGS that removes `body` itself must leave its content in
1543
- place, which only cloning does; and those paths parse into an
1544
- inert document, so their discarded originals never had a queued
1545
- event to neutralise.
1634
+ /* Hoist by moving each child up one level rather than deep-cloning
1635
+ it. Moving transfers every descendant exactly once, so a chain of
1636
+ nested disallowed elements costs O(n) instead of the O(n^2) that
1637
+ re-cloning the shrinking subtree at each level produced; it also
1638
+ empties the removed original, so `DOMPurify.removed` no longer
1639
+ pins whole subtrees. Moving preserves the in-place guarantee too:
1640
+ an original carrying already-queued resource events (`<img
1641
+ onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
1642
+ relocated and sanitised rather than left detached but still armed.
1643
+ The sole case that must clone is removing the walk root itself.
1644
+ The result is serialised from the root's subtree, so a restrictive
1645
+ ALLOWED_TAGS that strips the root (`body` on the string path) must
1646
+ leave the content inside it, which only cloning does. In IN_PLACE
1647
+ the root is pre-validated as an allowed tag and so is never removed
1648
+ here, so that path always takes the move branch.
1546
1649
  `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
1547
1650
  valid whether we move (drops the trailing entry) or clone (leaves
1548
1651
  the list intact). */
1549
1652
  for (let i = childCount - 1; i >= 0; --i) {
1550
- const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
1653
+ const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
1551
1654
  parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1552
1655
  }
1553
1656
  }
@@ -1555,6 +1658,26 @@ var purify = {exports: {}};
1555
1658
  _forceRemove(currentNode);
1556
1659
  return true;
1557
1660
  };
1661
+ /**
1662
+ * Fork a hook-mutable allowlist off its shared binding the first time a
1663
+ * (possibly lazily-installed) uponSanitize* hook is about to see it, so the
1664
+ * hook cannot widen the per-instance default or the setConfig binding by
1665
+ * reference and leak past the call. Returns the set unchanged once it is
1666
+ * already call-local, so repeated calls across elements are idempotent.
1667
+ *
1668
+ * @param hookList the uponSanitize* hook array for this event
1669
+ * @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding
1670
+ * @param defaultSet the per-instance DEFAULT_ALLOWED_* constant
1671
+ * @param setConfigSet the captured setConfig() binding, or null
1672
+ * @return a call-local clone if a hook is present and set is still shared,
1673
+ * else set unchanged
1674
+ */
1675
+ const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) {
1676
+ if (hookList.length === 0) {
1677
+ return set;
1678
+ }
1679
+ return set === defaultSet || set === setConfigSet ? clone(set) : set;
1680
+ };
1558
1681
  /**
1559
1682
  * _sanitizeElements
1560
1683
  *
@@ -1564,9 +1687,20 @@ var purify = {exports: {}};
1564
1687
  * @param currentNode to check for permission to exist
1565
1688
  * @return true if node was killed, false if left alive
1566
1689
  */
1567
- const _sanitizeElements = function _sanitizeElements(currentNode) {
1690
+ // eslint-disable-next-line complexity
1691
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
1568
1692
  /* Execute a hook if present */
1569
1693
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1694
+ /* A hook may have detached the node - treat it as removed (see the
1695
+ detached-node comment after the uponSanitizeElement hook below). On
1696
+ the IN_PLACE path, neutralize the detached subtree first so a queued
1697
+ resource handler on it cannot fire in page scope after we return. */
1698
+ if (currentNode !== root && getParentNode(currentNode) === null) {
1699
+ if (IN_PLACE) {
1700
+ _neutralizeSubtree(currentNode);
1701
+ }
1702
+ return true;
1703
+ }
1570
1704
  /* Check if element is clobbered or can clobber */
1571
1705
  if (_isClobbered(currentNode)) {
1572
1706
  _forceRemove(currentNode);
@@ -1574,11 +1708,47 @@ var purify = {exports: {}};
1574
1708
  }
1575
1709
  /* Now let's check the element's type and name */
1576
1710
  const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
1711
+ /* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement
1712
+ hook may have been installed after that guard sampled the hook arrays
1713
+ (e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still
1714
+ aliasing a shared binding that a widening hook would mutate by
1715
+ reference. Fork it before exposing it to the hook. */
1716
+ ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
1577
1717
  /* Execute a hook if present */
1578
1718
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
1579
1719
  tagName,
1580
1720
  allowedTags: ALLOWED_TAGS
1581
1721
  });
1722
+ /* A hook may have detached the node from the tree — a long-standing
1723
+ user pattern (issue #469; draw.io-style foreignObject filtering).
1724
+ Per the cached, unclobberable parentNode getter the node is
1725
+ genuinely out of the tree, so it can reach neither the serialized
1726
+ output nor an IN_PLACE live tree; treat it as removed and stop
1727
+ processing it. Without this guard, the unsafe-node / namespace
1728
+ checks below would call _forceRemove on a parentless node and hit
1729
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
1730
+ wants gone but *cannot* detach (clobbered / parentless roots), the
1731
+ opposite of a node that is already safely gone. The walk root is
1732
+ exempt: a detached IN_PLACE root is legitimate input and must still
1733
+ be fully sanitized, and a kill-decision on it must keep hitting the
1734
+ REPORT-3 throw. Nodes detached by hooks stay the hook's
1735
+ responsibility for placement: they are not recorded in
1736
+ DOMPurify.removed, so the post-walk IN_PLACE pass (which iterates
1737
+ DOMPurify.removed) does not reach them. But a hook-detached subtree
1738
+ can still hold a queued resource-event handler - e.g. an <img onload>
1739
+ that began loading when the caller built the live tree - which fires
1740
+ in page scope after sanitize returns even though the handler never
1741
+ reached the returned tree. That is the audit-5 F1 hazard, and the
1742
+ documented node.remove() hook pattern walks straight into it. So on
1743
+ the IN_PLACE path we neutralize the detached subtree inline here,
1744
+ stripping its non-allow-listed attributes before returning, exactly
1745
+ as the post-walk pass does for _forceRemove'd subtrees. */
1746
+ if (currentNode !== root && getParentNode(currentNode) === null) {
1747
+ if (IN_PLACE) {
1748
+ _neutralizeSubtree(currentNode);
1749
+ }
1750
+ return true;
1751
+ }
1582
1752
  /* Remove mXSS vectors, processing instructions and risky comments */
1583
1753
  if (_isUnsafeNode(currentNode, tagName)) {
1584
1754
  _forceRemove(currentNode);
@@ -1586,7 +1756,22 @@ var purify = {exports: {}};
1586
1756
  }
1587
1757
  /* Remove element if anything forbids its presence */
1588
1758
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
1589
- return _sanitizeDisallowedNode(currentNode, tagName);
1759
+ const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
1760
+ /* A false return means the node is a custom element kept via
1761
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
1762
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
1763
+ element-hook lifecycle matches normal allowlisted elements: a
1764
+ security policy applied in this hook (e.g. stripping an attribute
1765
+ from every surviving element) must not silently skip kept custom
1766
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
1767
+ tail below - the hook runs, then the walker's subsequent
1768
+ _sanitizeAttributes pass sanitizes the element's attributes. The
1769
+ deliberately skipped namespace and fallback-tag removal checks stay
1770
+ skipped; they are removal decisions, not the hook contract. */
1771
+ if (removed === false) {
1772
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
1773
+ }
1774
+ return removed;
1590
1775
  }
1591
1776
  /* Check whether element has a valid namespace.
1592
1777
  Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
@@ -1633,6 +1818,33 @@ var purify = {exports: {}};
1633
1818
  if (FORBID_ATTR[lcName]) {
1634
1819
  return false;
1635
1820
  }
1821
+ /* Reject declarative-partial-updates patch-linkage attributes
1822
+ (https://github.com/WICG/declarative-partial-updates).
1823
+ Empirical note (Chrome 150, verified — see
1824
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
1825
+ sanitization. For the string path it fires during sanitize()'s own
1826
+ parse, so the walk sees and sanitizes the fully materialized expanded
1827
+ tree — teleports into MathML/SVG integration points included; a
1828
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
1829
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
1830
+ Either way DOMPurify is NOT blind to the patch.
1831
+ This removal is therefore defense-in-depth rather than the sole barrier:
1832
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
1833
+ in the caller's context, and keeps behaviour deterministic if a future
1834
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
1835
+ anywhere else (notably <template for>) it links the element to a patch
1836
+ target and teleports or removes an arbitrary DOM range by id/marker name.
1837
+ `patchsrc` fetches remote markup and is treated as a script-loading
1838
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
1839
+ other structural-threat checks and stays overridable, consistent with
1840
+ the rest of the codebase. PI range markers are already removed by
1841
+ _isUnsafeNode. */
1842
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
1843
+ return false;
1844
+ }
1845
+ if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
1846
+ return false;
1847
+ }
1636
1848
  /* Make sure attribute cannot clobber */
1637
1849
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
1638
1850
  return false;
@@ -1749,6 +1961,9 @@ var purify = {exports: {}};
1749
1961
  if (!attributes || _isClobbered(currentNode)) {
1750
1962
  return;
1751
1963
  }
1964
+ /* Same lazy-install guard as uponSanitizeElement (see there): fork the
1965
+ attribute allowlist off its shared binding before a hook can see it. */
1966
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
1752
1967
  const hookEvent = {
1753
1968
  attrName: '',
1754
1969
  attrValue: '',
@@ -1842,7 +2057,7 @@ var purify = {exports: {}};
1842
2057
  /* Execute a hook if present */
1843
2058
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
1844
2059
  /* Sanitize tags and elements */
1845
- _sanitizeElements(shadowNode);
2060
+ _sanitizeElements(shadowNode, fragment);
1846
2061
  /* Check attributes next */
1847
2062
  _sanitizeAttributes(shadowNode);
1848
2063
  /* Deep shadow DOM detected.
@@ -2033,6 +2248,11 @@ var purify = {exports: {}};
2033
2248
  keep using and whose return value they ignore — unsanitized. REPORT-2. */
2034
2249
  const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
2035
2250
  if (inPlace) {
2251
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
2252
+ linkage across the live tree BEFORE the walk, so no patch can fire
2253
+ mid-walk and inject into an already-processed region. Runs first, so
2254
+ it also covers the forbidden/clobbered roots that throw below. */
2255
+ _neutralizePatchLinkage(dirty);
2036
2256
  /* Do some early pre-sanitization to avoid unsafe root nodes.
2037
2257
  Read nodeName through the cached prototype getter — a clobbering
2038
2258
  child named "nodeName" on the form root would otherwise shadow
@@ -2042,6 +2262,9 @@ var purify = {exports: {}};
2042
2262
  if (typeof nn === 'string') {
2043
2263
  const tagName = transformCaseFunc(nn);
2044
2264
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
2265
+ /* Fail closed on a live root: neutralize handlers/children before
2266
+ throwing, exactly as the mid-walk abort path does. */
2267
+ _neutralizeRoot(dirty);
2045
2268
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
2046
2269
  }
2047
2270
  }
@@ -2056,6 +2279,10 @@ var purify = {exports: {}};
2056
2279
  the application unsanitized. Refuse to sanitize such a root
2057
2280
  the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
2058
2281
  if (_isClobbered(dirty)) {
2282
+ /* Fail closed on a live clobbered root before throwing.
2283
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
2284
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
2285
+ _neutralizeRoot(dirty);
2059
2286
  throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
2060
2287
  }
2061
2288
  /* Sanitize attached shadow roots before the main iterator runs.
@@ -2108,20 +2335,24 @@ var purify = {exports: {}};
2108
2335
  _forceRemove(body.firstChild);
2109
2336
  }
2110
2337
  /* Get node iterator */
2111
- const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
2338
+ const walkRoot = inPlace ? dirty : body;
2112
2339
  /* Now start iterating over the created document.
2113
2340
  The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
2114
2341
  engine/custom-element mutation can detach a node mid-walk so
2115
2342
  `_forceRemove`'s parentless guard throws, aborting the loop. Without the
2116
2343
  barrier the caller's in-place tree would be left half-sanitized with the
2117
- unvisited tail still armed. On any throw we fail closed — strip the
2118
- in-place root bare — then rethrow so the existing throw contract is
2119
- preserved. (String/DOM-copy paths never return the partial body, so the
2120
- propagating throw is already fail-closed there.) */
2344
+ unvisited tail still armed. _createNodeIterator itself is inside the
2345
+ barrier too: constructing the iterator dereferences the root's document,
2346
+ and any failure there (e.g. an exotic/clobbered root) must still fail
2347
+ closed rather than skip the neutralize. On any throw we fail closed -
2348
+ strip the in-place root bare - then rethrow so the existing throw
2349
+ contract is preserved. (String/DOM-copy paths never return the partial
2350
+ body, so the propagating throw is already fail-closed there.) */
2121
2351
  try {
2352
+ const nodeIterator = _createNodeIterator(walkRoot);
2122
2353
  while (currentNode = nodeIterator.nextNode()) {
2123
2354
  /* Sanitize tags and elements */
2124
- _sanitizeElements(currentNode);
2355
+ _sanitizeElements(currentNode, walkRoot);
2125
2356
  /* Check attributes next */
2126
2357
  _sanitizeAttributes(currentNode);
2127
2358
  /* Shadow DOM detected, sanitize it.
@@ -2135,6 +2366,14 @@ var purify = {exports: {}};
2135
2366
  } catch (error) {
2136
2367
  if (inPlace) {
2137
2368
  _neutralizeRoot(dirty);
2369
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
2370
+ detached from the root, so _neutralizeRoot's subtree pass does not
2371
+ reach them. Defuse them too, mirroring the success-path loop below. */
2372
+ arrayForEach(DOMPurify.removed, entry => {
2373
+ if (entry.element) {
2374
+ _neutralizeSubtree(entry.element);
2375
+ }
2376
+ });
2138
2377
  }
2139
2378
  throw error;
2140
2379
  }