@mulmoclaude/core 4.4.0 → 4.4.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.
@@ -55,7 +55,10 @@ collection's view-mode selector automatically.
55
55
 
56
56
  ## The runtime contract — `window.__MC_VIEW`
57
57
 
58
- The host injects a bootstrap into your page **before any of your scripts run**:
58
+ The host injects a bootstrap at the very start of your `<head>`, so it runs
59
+ **before any script in your `<head>` or `<body>`** — write your view normally
60
+ and `window.__MC_VIEW` is always there. (A `<script>` placed _before_ `<head>`
61
+ would run ahead of it and see no `__MC_VIEW`; there is no reason to write one.)
59
62
 
60
63
  ```js
61
64
  window.__MC_VIEW = {
@@ -63,6 +66,8 @@ window.__MC_VIEW = {
63
66
  token: "<scoped capability token>", // Authorization bearer
64
67
  dataUrl: "http://localhost:3001/api/collections/annual-plan/view-data",
65
68
  onChange: (cb) => unsubscribe, // live refresh — see "Staying live" below
69
+ searchQuery: "", // live text in the app's own search box — see "One search box"
70
+ onSearchQueryChange: (cb) => unsubscribe, // fires when the user types there
66
71
  openItem: (id, mode) => void, // open a record in the host's panel — see "Opening a record"
67
72
  startChat: (prompt, role) => void, // draft a new chat for the user — see "Starting a chat"
68
73
  };
@@ -299,6 +304,69 @@ What you need to know:
299
304
  - It returns an **unsubscribe** function; you rarely need it (the view is torn
300
305
  down with the iframe), but it's there for fine-grained control.
301
306
 
307
+ ### One search box — `searchQuery` / `onSearchQueryChange`
308
+
309
+ The app's **standard search box stays on screen while your view renders**, so
310
+ the user expects it to drive your view too. It does: the host relays what they
311
+ type into the sandboxed frame.
312
+
313
+ **Do not build a second search box.** Read the host's, and you get the
314
+ behaviour the user already assumes.
315
+
316
+ ```js
317
+ const v = window.__MC_VIEW;
318
+
319
+ function render() {
320
+ const q = v.searchQuery.trim().toLowerCase();
321
+ const hits = q ? records.filter((r) => matches(r, q)) : records;
322
+ // …draw hits, highlight where each one matched…
323
+ }
324
+
325
+ v.onSearchQueryChange(render); // the user typed in the app's search box
326
+ v.onChange(reload); // the data changed (a separate wire — see above)
327
+ render();
328
+ ```
329
+
330
+ What you need to know:
331
+
332
+ - **`searchQuery` is always current.** It updates the instant the user types, so
333
+ reading it inside your own render (as above) is never a keystroke behind.
334
+ - **The callback is debounced** — it runs once typing pauses (~150 ms), not
335
+ once per keystroke — and receives the query as its argument
336
+ (`onSearchQueryChange((q) => …)`), so you can use either source.
337
+ - **An empty string means the box is empty** — show everything, don't show
338
+ nothing. It fires on clearing too.
339
+ - **It is scoped to this collection and this session.** Switching collections
340
+ clears the box; nothing is persisted.
341
+ - **Navigating your frame away ends it.** The query arrives on a private
342
+ channel bound to the document the host built, so a page your view navigates
343
+ to gets no further queries — deliberately, since the text is the user's own.
344
+ If that page asks the host for the channel, the host reinstalls your view
345
+ over it rather than handing it over. Open outbound links with
346
+ `target="_blank"` (see the sandbox rules) and your view keeps running.
347
+ - **Reloading your own view is fine.** `location.reload()` makes the host
348
+ reinstall the view and hand it a fresh channel, seeded with whatever is in
349
+ the search box — so the query you see after a reload is the current one, not
350
+ an empty string.
351
+ - **Replacing the document is allowed a few times, not endlessly.** Each
352
+ reinstall — after a reload or after another page claims the channel — costs
353
+ the host a rebuild, so it grants **at most 3** between one user-initiated
354
+ view change and the next (switching view or collection resets the budget).
355
+ Past that your view still renders and still reads its data; only the search
356
+ channel stays disconnected, and `searchQuery` stops updating. Use `onChange`
357
+ to refresh rather than reloading the frame, and you will never reach it.
358
+ - **One direction only.** You read the user's query; you cannot write the app's
359
+ search box from view code. If your view needs a filter the box can't express
360
+ (a date range, a facet), add that control yourself and combine it with
361
+ `searchQuery`.
362
+ - **No extra capability needed**, and it is **not** a data reload: it never
363
+ fires `onChange`, and `onChange` never fires this.
364
+
365
+ This is what makes a "search" view worth writing — the built-in box only
366
+ filters table rows, so a view that shows **which field matched**, with a
367
+ highlighted snippet, is a real upgrade the user drives from the box they were
368
+ already using.
369
+
302
370
  ### Opening a record — `openItem`
303
371
 
304
372
  Your view owns the _layout_ (a grid, a chart, a board); it doesn't have to
@@ -88,7 +88,7 @@ function useClipboardCopy(resetMs = DEFAULT_RESET_MS) {
88
88
  }
89
89
  //#endregion
90
90
  //#region ../../node_modules/dompurify/dist/purify.es.mjs
91
- /*! @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 */
91
+ /*! @license DOMPurify 3.4.14 | (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.14/LICENSE */
92
92
  function _arrayLikeToArray(r, a) {
93
93
  (null == a || a > r.length) && (a = r.length);
94
94
  for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
@@ -817,6 +817,7 @@ var svg = freeze([
817
817
  "patterncontentunits",
818
818
  "patterntransform",
819
819
  "patternunits",
820
+ "pointer-events",
820
821
  "points",
821
822
  "preservealpha",
822
823
  "preserveaspectratio",
@@ -871,6 +872,7 @@ var svg = freeze([
871
872
  "u2",
872
873
  "unicode",
873
874
  "values",
875
+ "vector-effect",
874
876
  "viewbox",
875
877
  "visibility",
876
878
  "version",
@@ -984,6 +986,24 @@ var NODE_TYPE = {
984
986
  documentFragment: 11,
985
987
  notation: 12
986
988
  };
989
+ var LITERAL_TEXT_ELEMENT_NAMES = [
990
+ "style",
991
+ "script",
992
+ "xmp",
993
+ "iframe",
994
+ "noembed",
995
+ "noframes",
996
+ "plaintext",
997
+ "noscript"
998
+ ];
999
+ var LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
1000
+ var LITERAL_TEXT_CLOSE = function() {
1001
+ const map = {};
1002
+ arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, (name) => {
1003
+ map[name] = seal(new RegExp("</" + name + "(?=[\\t\\n\\f\\r />])", "i"));
1004
+ });
1005
+ return freeze(map);
1006
+ }();
987
1007
  var getGlobal = function getGlobal() {
988
1008
  return typeof window === "undefined" ? null : window;
989
1009
  };
@@ -1043,10 +1063,25 @@ var _createHooksMap = function _createHooksMap() {
1043
1063
  var _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
1044
1064
  return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
1045
1065
  };
1066
+ /**
1067
+ * Resolve an object-valued configuration option: a prototype-free clone
1068
+ * of cfg[key] when it is an own, truthy object property, else a fresh
1069
+ * fallback built by makeFallback (fresh on every parse, so a previous
1070
+ * parse can never leak state into the next one).
1071
+ *
1072
+ * @param cfg the cloned, prototype-free configuration object
1073
+ * @param key the configuration property to read
1074
+ * @param makeFallback builds the fallback value when the option is absent
1075
+ * @returns the resolved object
1076
+ */
1077
+ var _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallback) {
1078
+ const value = objectHasOwnProperty(cfg, key) ? cfg[key] : void 0;
1079
+ return value && typeof value === "object" ? clone(value) : makeFallback();
1080
+ };
1046
1081
  function createDOMPurify() {
1047
1082
  let window = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
1048
1083
  const DOMPurify = (root) => createDOMPurify(root);
1049
- DOMPurify.version = "3.4.13";
1084
+ DOMPurify.version = "3.4.14";
1050
1085
  DOMPurify.removed = [];
1051
1086
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
1052
1087
  DOMPurify.isSupported = false;
@@ -1071,6 +1106,12 @@ function createDOMPurify() {
1071
1106
  const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeType") : null;
1072
1107
  const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeName") : null;
1073
1108
  const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, "ownerDocument") : null;
1109
+ const _readNodeType = function _readNodeType(node) {
1110
+ return getNodeType ? getNodeType(node) : node.nodeType;
1111
+ };
1112
+ const _readNodeName = function _readNodeName(node) {
1113
+ return getNodeName ? getNodeName(node) : node.nodeName;
1114
+ };
1074
1115
  if (typeof HTMLTemplateElement === "function") {
1075
1116
  const template = document.createElement("template");
1076
1117
  if (template.content && template.content.ownerDocument) document = template.content.ownerDocument;
@@ -1328,9 +1369,9 @@ function createDOMPurify() {
1328
1369
  IN_PLACE = cfg.IN_PLACE || false;
1329
1370
  IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI;
1330
1371
  NAMESPACE = typeof cfg.NAMESPACE === "string" ? cfg.NAMESPACE : HTML_NAMESPACE;
1331
- MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, "MATHML_TEXT_INTEGRATION_POINTS") && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === "object" ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
1332
- HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, "HTML_INTEGRATION_POINTS") && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === "object" ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
1333
- const customElementHandling = objectHasOwnProperty(cfg, "CUSTOM_ELEMENT_HANDLING") && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === "object" ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
1372
+ MATHML_TEXT_INTEGRATION_POINTS = _resolveObjectOption(cfg, "MATHML_TEXT_INTEGRATION_POINTS", () => addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS));
1373
+ HTML_INTEGRATION_POINTS = _resolveObjectOption(cfg, "HTML_INTEGRATION_POINTS", () => addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS));
1374
+ const customElementHandling = _resolveObjectOption(cfg, "CUSTOM_ELEMENT_HANDLING", () => create(null));
1334
1375
  CUSTOM_ELEMENT_HANDLING = create(null);
1335
1376
  if (objectHasOwnProperty(customElementHandling, "tagNameCheck") && isRegexOrFunction(customElementHandling.tagNameCheck)) CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck;
1336
1377
  if (objectHasOwnProperty(customElementHandling, "attributeNameCheck") && isRegexOrFunction(customElementHandling.attributeNameCheck)) CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck;
@@ -1377,11 +1418,6 @@ function createDOMPurify() {
1377
1418
  addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
1378
1419
  }
1379
1420
  }
1380
- if (objectHasOwnProperty(cfg, "ADD_URI_SAFE_ATTR") && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
1381
- if (objectHasOwnProperty(cfg, "FORBID_CONTENTS") && arrayIsArray(cfg.FORBID_CONTENTS)) {
1382
- if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) FORBID_CONTENTS = clone(FORBID_CONTENTS);
1383
- addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
1384
- }
1385
1421
  if (objectHasOwnProperty(cfg, "ADD_FORBID_CONTENTS") && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
1386
1422
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) FORBID_CONTENTS = clone(FORBID_CONTENTS);
1387
1423
  addToSet(FORBID_CONTENTS, cfg.ADD_FORBID_CONTENTS, transformCaseFunc);
@@ -1498,6 +1534,32 @@ function createDOMPurify() {
1498
1534
  }
1499
1535
  };
1500
1536
  /**
1537
+ * _stripAttributeNode
1538
+ *
1539
+ * Remove a single Attr node case/namespace-exactly on an attribute-teardown
1540
+ * path. Name-based removeAttribute() ASCII-lowercases its lookup key for an
1541
+ * HTML element in an HTML document and so silently misses a case-preserved
1542
+ * handler (e.g. `ONERROR` off an XML/XHTML import) - the same defect
1543
+ * _removeAttribute() was fixed for, which a name-based call would reintroduce
1544
+ * on these IN_PLACE teardown paths. Unlike _removeAttribute this does not
1545
+ * record into DOMPurify.removed: the neutralize passes intentionally do not
1546
+ * book-keep. A clobbered/detached node falls back to best-effort name-based
1547
+ * removal.
1548
+ *
1549
+ * @param element the element to strip the attribute from
1550
+ * @param attribute the Attr node to remove
1551
+ * @param name the attribute's name, for the fallback path
1552
+ */
1553
+ const _stripAttributeNode = function _stripAttributeNode(element, attribute, name) {
1554
+ try {
1555
+ element.removeAttributeNode(attribute);
1556
+ } catch (_) {
1557
+ try {
1558
+ element.removeAttribute(name);
1559
+ } catch (_) {}
1560
+ }
1561
+ };
1562
+ /**
1501
1563
  * _neutralizeRoot
1502
1564
  *
1503
1565
  * Fail-closed teardown of an in-place root after the sanitize walk aborts
@@ -1531,30 +1593,45 @@ function createDOMPurify() {
1531
1593
  if (attributes) for (let i = attributes.length - 1; i >= 0; --i) {
1532
1594
  const attribute = attributes[i];
1533
1595
  const name = attribute && attribute.name;
1534
- if (typeof name === "string") try {
1535
- root.removeAttribute(name);
1536
- } catch (_) {}
1596
+ if (typeof name === "string") _stripAttributeNode(root, attribute, name);
1537
1597
  }
1538
1598
  };
1539
1599
  /**
1540
1600
  * _removeAttribute
1541
1601
  *
1602
+ * Name-based getAttributeNode()/removeAttribute() ASCII-lowercase their
1603
+ * lookup key for HTML elements in an HTML document, so they silently miss an
1604
+ * attribute whose stored qualified name still contains uppercase ASCII
1605
+ * letters. That happens when the node came from a case-preserving source
1606
+ * (an XML/XHTML document imported via importNode(), or createAttributeNS()),
1607
+ * where e.g. `ONERROR` survives the walk: the policy check lowercases to
1608
+ * `onerror` and rejects it, but `removeAttribute('ONERROR')` looks up
1609
+ * `onerror` and finds nothing. Remove the exact Attr node instead, which is
1610
+ * case- and namespace-exact, and fall back to name-based removal only when
1611
+ * the caller could not supply the node.
1612
+ *
1542
1613
  * @param name an Attribute name
1543
1614
  * @param element a DOM node
1615
+ * @param attr the exact Attr node to remove, when the caller has it
1544
1616
  */
1545
- const _removeAttribute = function _removeAttribute(name, element) {
1617
+ const _removeAttribute = function _removeAttribute(name, element, attr) {
1618
+ if (!attr) try {
1619
+ attr = element.getAttributeNode(name);
1620
+ } catch (_) {
1621
+ attr = null;
1622
+ }
1623
+ arrayPush(DOMPurify.removed, {
1624
+ attribute: attr || null,
1625
+ from: element
1626
+ });
1546
1627
  try {
1547
- arrayPush(DOMPurify.removed, {
1548
- attribute: element.getAttributeNode(name),
1549
- from: element
1550
- });
1628
+ if (attr) element.removeAttributeNode(attr);
1629
+ else element.removeAttribute(name);
1551
1630
  } catch (_) {
1552
- arrayPush(DOMPurify.removed, {
1553
- attribute: null,
1554
- from: element
1555
- });
1631
+ try {
1632
+ element.removeAttribute(name);
1633
+ } catch (_) {}
1556
1634
  }
1557
- element.removeAttribute(name);
1558
1635
  if (name === "is") if (RETURN_DOM || RETURN_DOM_FRAGMENT) try {
1559
1636
  _forceRemove(element);
1560
1637
  } catch (_) {}
@@ -1579,9 +1656,7 @@ function createDOMPurify() {
1579
1656
  const attribute = attributes[i];
1580
1657
  const name = attribute && attribute.name;
1581
1658
  if (typeof name !== "string" || ALLOWED_ATTR[transformCaseFunc(name)]) continue;
1582
- try {
1583
- element.removeAttribute(name);
1584
- } catch (_) {}
1659
+ _stripAttributeNode(element, attribute, name);
1585
1660
  }
1586
1661
  };
1587
1662
  /**
@@ -1610,7 +1685,7 @@ function createDOMPurify() {
1610
1685
  const stack = [root];
1611
1686
  while (stack.length > 0) {
1612
1687
  const node = stack.pop();
1613
- if ((getNodeType ? getNodeType(node) : node.nodeType) === NODE_TYPE.element) _stripDisallowedAttributes(node);
1688
+ if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
1614
1689
  const childNodes = getChildNodes(node);
1615
1690
  if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
1616
1691
  }
@@ -1650,12 +1725,28 @@ function createDOMPurify() {
1650
1725
  *
1651
1726
  * @param root the in-place root to sweep
1652
1727
  */
1728
+ /**
1729
+ * Central policy for declarative-partial-updates patch-linkage attributes,
1730
+ * shared by the _neutralizePatchLinkage pre-pass and _isValidAttribute so
1731
+ * the two sites cannot drift: `patchsrc` always links, `for` links
1732
+ * everywhere except on <label>/<output>, and the whole policy is gated on
1733
+ * SAFE_FOR_XML (see the rationale block in _isValidAttribute).
1734
+ *
1735
+ * @param lcName the transformCaseFunc'd attribute name
1736
+ * @param lcTag the transformCaseFunc'd tag name of the carrying element
1737
+ * @return true if the attribute is patch linkage and must be dropped
1738
+ */
1739
+ const _isPatchLinkageAttribute = function _isPatchLinkageAttribute(lcName, lcTag) {
1740
+ if (!SAFE_FOR_XML) return false;
1741
+ if (lcName === "patchsrc") return true;
1742
+ return lcName === "for" && lcTag !== "label" && lcTag !== "output";
1743
+ };
1653
1744
  const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
1654
1745
  if (!SAFE_FOR_XML) return;
1655
1746
  const stack = [root];
1656
1747
  while (stack.length > 0) {
1657
1748
  const node = stack.pop();
1658
- const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
1749
+ const nodeType = _readNodeType(node);
1659
1750
  if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
1660
1751
  try {
1661
1752
  remove(node);
@@ -1664,10 +1755,10 @@ function createDOMPurify() {
1664
1755
  }
1665
1756
  if (nodeType === NODE_TYPE.element) {
1666
1757
  const element = node;
1667
- const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
1758
+ const lcTag = transformCaseFunc(_readNodeName(node));
1668
1759
  try {
1669
1760
  if (element.hasAttribute && element.hasAttribute("patchsrc")) element.removeAttribute("patchsrc");
1670
- if (element.hasAttribute && element.hasAttribute("for") && lcTag !== "label" && lcTag !== "output") element.removeAttribute("for");
1761
+ if (element.hasAttribute && element.hasAttribute("for") && _isPatchLinkageAttribute("for", lcTag)) element.removeAttribute("for");
1671
1762
  } catch (_) {}
1672
1763
  }
1673
1764
  const childNodes = getChildNodes(node);
@@ -1836,12 +1927,32 @@ function createDOMPurify() {
1836
1927
  */
1837
1928
  const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
1838
1929
  if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) return true;
1839
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === "style" && _isNode(currentNode.firstElementChild)) return true;
1930
+ 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;
1840
1931
  if (currentNode.nodeType === NODE_TYPE.processingInstruction) return true;
1841
1932
  if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) return true;
1842
1933
  return false;
1843
1934
  };
1844
1935
  /**
1936
+ * Evaluate a CUSTOM_ELEMENT_HANDLING check (a RegExp or a predicate
1937
+ * function, per the validation in _parseConfig) against a name.
1938
+ * Additional arguments are forwarded to predicate functions - the
1939
+ * attributeNameCheck predicate receives the tag name as its second
1940
+ * argument. A null/absent check never matches.
1941
+ *
1942
+ * @param check the configured tagNameCheck / attributeNameCheck value
1943
+ * @param name the name to test
1944
+ * @param args extra arguments forwarded to a predicate function
1945
+ * @return true if the check matches the name
1946
+ */
1947
+ const _matchesNameCheck = function _matchesNameCheck(check, name) {
1948
+ if (check instanceof RegExp) return regExpTest(check, name);
1949
+ if (check instanceof Function) {
1950
+ for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) args[_key - 2] = arguments[_key];
1951
+ return Boolean(check(name, ...args));
1952
+ }
1953
+ return false;
1954
+ };
1955
+ /**
1845
1956
  * Handle a node whose tag is forbidden or not allowlisted: keep
1846
1957
  * allowed custom elements (false return exits _sanitizeElements
1847
1958
  * early - the namespace and fallback-tag removal checks are
@@ -1859,10 +1970,7 @@ function createDOMPurify() {
1859
1970
  * @return true if the node was removed, false if kept
1860
1971
  */
1861
1972
  const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
1862
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1863
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
1864
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) return false;
1865
- }
1973
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
1866
1974
  if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1867
1975
  const parentNode = getParentNode(currentNode);
1868
1976
  const childNodes = getChildNodes(currentNode);
@@ -1896,6 +2004,42 @@ function createDOMPurify() {
1896
2004
  return set === defaultSet || set === setConfigSet ? clone(set) : set;
1897
2005
  };
1898
2006
  /**
2007
+ * Shared guard for a node that a hook has detached from the walk tree,
2008
+ * used after each element-hook site in _sanitizeElements. Detaching is a
2009
+ * long-standing user pattern (issue #469; draw.io-style foreignObject
2010
+ * filtering). Per the cached, unclobberable parentNode getter the node is
2011
+ * genuinely out of the tree, so it can reach neither the serialized
2012
+ * output nor an IN_PLACE live tree; treat it as removed and stop
2013
+ * processing it. Without this guard, the unsafe-node / namespace checks
2014
+ * would call _forceRemove on a parentless node and hit the REPORT-3
2015
+ * fail-closed throw — which exists for nodes DOMPurify wants gone but
2016
+ * *cannot* detach (clobbered / parentless roots), the opposite of a node
2017
+ * that is already safely gone. The walk root is exempt: a detached
2018
+ * IN_PLACE root is legitimate input and must still be fully sanitized,
2019
+ * and a kill-decision on it must keep hitting the REPORT-3 throw.
2020
+ *
2021
+ * Nodes detached by hooks stay the hook's responsibility for placement:
2022
+ * they are not recorded in DOMPurify.removed, so the post-walk IN_PLACE
2023
+ * pass (which iterates DOMPurify.removed) does not reach them. But a
2024
+ * hook-detached subtree can still hold a queued resource-event handler -
2025
+ * e.g. an <img onload> that began loading when the caller built the live
2026
+ * tree - which fires in page scope after sanitize returns even though the
2027
+ * handler never reached the returned tree. That is the audit-5 F1 hazard,
2028
+ * and the documented node.remove() hook pattern walks straight into it.
2029
+ * So on the IN_PLACE path we neutralize the detached subtree inline,
2030
+ * stripping its non-allow-listed attributes before returning, exactly as
2031
+ * the post-walk pass does for _forceRemove'd subtrees.
2032
+ *
2033
+ * @param currentNode the node a hook may have detached
2034
+ * @param root the current walk root
2035
+ * @return true if the node is detached and now handled, false otherwise
2036
+ */
2037
+ const _handleHookDetachedNode = function _handleHookDetachedNode(currentNode, root) {
2038
+ if (currentNode === root || getParentNode(currentNode) !== null) return false;
2039
+ if (IN_PLACE) _neutralizeSubtree(currentNode);
2040
+ return true;
2041
+ };
2042
+ /**
1899
2043
  * _sanitizeElements
1900
2044
  *
1901
2045
  * @protect nodeName
@@ -1906,24 +2050,18 @@ function createDOMPurify() {
1906
2050
  */
1907
2051
  const _sanitizeElements = function _sanitizeElements(currentNode, root) {
1908
2052
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1909
- if (currentNode !== root && getParentNode(currentNode) === null) {
1910
- if (IN_PLACE) _neutralizeSubtree(currentNode);
1911
- return true;
1912
- }
2053
+ if (_handleHookDetachedNode(currentNode, root)) return true;
1913
2054
  if (_isClobbered(currentNode)) {
1914
2055
  _forceRemove(currentNode);
1915
2056
  return true;
1916
2057
  }
1917
- const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
2058
+ const tagName = transformCaseFunc(_readNodeName(currentNode));
1918
2059
  ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
1919
2060
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
1920
2061
  tagName,
1921
2062
  allowedTags: ALLOWED_TAGS
1922
2063
  });
1923
- if (currentNode !== root && getParentNode(currentNode) === null) {
1924
- if (IN_PLACE) _neutralizeSubtree(currentNode);
1925
- return true;
1926
- }
2064
+ if (_handleHookDetachedNode(currentNode, root)) return true;
1927
2065
  if (_isUnsafeNode(currentNode, tagName)) {
1928
2066
  _forceRemove(currentNode);
1929
2067
  return true;
@@ -1933,7 +2071,7 @@ function createDOMPurify() {
1933
2071
  if (removed === false) _executeHooks(hooks.afterSanitizeElements, currentNode, null);
1934
2072
  return removed;
1935
2073
  }
1936
- if ((getNodeType ? getNodeType(currentNode) : currentNode.nodeType) === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
2074
+ if (_readNodeType(currentNode) === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
1937
2075
  _forceRemove(currentNode);
1938
2076
  return true;
1939
2077
  }
@@ -1961,20 +2099,17 @@ function createDOMPurify() {
1961
2099
  */
1962
2100
  const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
1963
2101
  if (FORBID_ATTR[lcName]) return false;
1964
- if (SAFE_FOR_XML && lcName === "patchsrc") return false;
1965
- if (SAFE_FOR_XML && lcName === "for" && lcTag !== "label" && lcTag !== "output") return false;
2102
+ if (_isPatchLinkageAttribute(lcName, lcTag)) return false;
1966
2103
  if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document || value in formElement)) return false;
1967
2104
  const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
1968
- if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName));
1969
- else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName));
1970
- 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)));
1971
- else return false;
1972
- else if (URI_SAFE_ATTRIBUTES[lcName]);
1973
- else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, "")));
1974
- else if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag]);
1975
- else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, "")));
1976
- else if (value) return false;
1977
- return true;
2105
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) return true;
2106
+ if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) return true;
2107
+ 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);
2108
+ if (URI_SAFE_ATTRIBUTES[lcName]) return true;
2109
+ if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) return true;
2110
+ if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag]) return true;
2111
+ if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) return true;
2112
+ return !value;
1978
2113
  };
1979
2114
  const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, [
1980
2115
  "annotation-xml",
@@ -2076,29 +2211,29 @@ function createDOMPurify() {
2076
2211
  _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent);
2077
2212
  value = hookEvent.attrValue;
2078
2213
  if (SANITIZE_NAMED_PROPS && (lcName === "id" || lcName === "name") && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
2079
- _removeAttribute(name, currentNode);
2214
+ _removeAttribute(name, currentNode, attr);
2080
2215
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
2081
2216
  }
2082
2217
  if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
2083
- _removeAttribute(name, currentNode);
2218
+ _removeAttribute(name, currentNode, attr);
2084
2219
  continue;
2085
2220
  }
2086
2221
  if (lcName === "attributename" && stringMatch(value, "href")) {
2087
- _removeAttribute(name, currentNode);
2222
+ _removeAttribute(name, currentNode, attr);
2088
2223
  continue;
2089
2224
  }
2090
2225
  if (hookEvent.forceKeepAttr) continue;
2091
2226
  if (!hookEvent.keepAttr) {
2092
- _removeAttribute(name, currentNode);
2227
+ _removeAttribute(name, currentNode, attr);
2093
2228
  continue;
2094
2229
  }
2095
2230
  if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
2096
- _removeAttribute(name, currentNode);
2231
+ _removeAttribute(name, currentNode, attr);
2097
2232
  continue;
2098
2233
  }
2099
2234
  if (SAFE_FOR_TEMPLATES) value = _stripTemplateExpressions(value);
2100
2235
  if (!_isValidAttribute(lcTag, lcName, value)) {
2101
- _removeAttribute(name, currentNode);
2236
+ _removeAttribute(name, currentNode, attr);
2102
2237
  continue;
2103
2238
  }
2104
2239
  value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
@@ -2120,7 +2255,7 @@ function createDOMPurify() {
2120
2255
  _sanitizeElements(shadowNode, fragment);
2121
2256
  _sanitizeAttributes(shadowNode);
2122
2257
  if (_isDocumentFragment(shadowNode.content)) _sanitizeShadowDOM2(shadowNode.content);
2123
- if ((getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType) === NODE_TYPE.element) {
2258
+ if (_readNodeType(shadowNode) === NODE_TYPE.element) {
2124
2259
  const innerSr = getShadowRoot(shadowNode);
2125
2260
  if (_isDocumentFragment(innerSr)) {
2126
2261
  _sanitizeAttachedShadowRoots(innerSr);
@@ -2161,7 +2296,7 @@ function createDOMPurify() {
2161
2296
  continue;
2162
2297
  }
2163
2298
  const node = item.node;
2164
- const isElement = (getNodeType ? getNodeType(node) : node.nodeType) === NODE_TYPE.element;
2299
+ const isElement = _readNodeType(node) === NODE_TYPE.element;
2165
2300
  const childNodes = getChildNodes(node);
2166
2301
  if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push({
2167
2302
  node: childNodes[i],
@@ -2212,7 +2347,7 @@ function createDOMPurify() {
2212
2347
  const inPlace = IN_PLACE && typeof dirty !== "string" && _isNode(dirty);
2213
2348
  if (inPlace) {
2214
2349
  _neutralizePatchLinkage(dirty);
2215
- const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
2350
+ const nn = _readNodeName(dirty);
2216
2351
  if (typeof nn === "string") {
2217
2352
  const tagName = transformCaseFunc(nn);
2218
2353
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {