tinymce-rails 8.8.0 → 8.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0ad475c4d29c09fec3f23063f259e25433a693b8e3089b3c71672fa1960145d
4
- data.tar.gz: 0057cfc82316d4044b8eeb4329c3b579f01fbed1ab0a4c30cd611c3f9bbdc20d
3
+ metadata.gz: 0c76539b0f4c153f018da0a4c42acce9a1dcea31efb03dabc9f44d1e1d61b3ec
4
+ data.tar.gz: b0ec30ffc6de0ffa5fcd0720388ef73f7ca5af1c966baac68e911c9699a59a29
5
5
  SHA512:
6
- metadata.gz: 50da8d1712d8feaa841c963c99d582691127ac5ff0884ce358a7e80e30bd648d28dae20960e6a498d75197df23b67efc5a84e990cba4ebe9b7557765b4ba2522
7
- data.tar.gz: 5a2ae7c407fcbb94219ba359a86a0bb556322c956d816489b5990999f0fec2c4cc2863a870bc5cc199f9d39228e860f4eafb75b7de938977e35290cd77c7c600
6
+ metadata.gz: 3229b680d9cb17075ef7686f230ebc0e5bc3ae090e4623c28c025ecd98c96cc9fa537d1a35010baceb03d365ae6590babaad2dce34f3f6ee53e0a25d47b6d94f
7
+ data.tar.gz: 03ac9f068a069403e8c8d3df13172a213794fd1cba3d63a1d703372919bd9873a80f2bf1180587dac4f1943e7c290fc182e71f69fa88e5b9c4f718284b506323
@@ -1,5 +1,5 @@
1
1
  /**
2
- * TinyMCE version 8.8.0 (2026-07-15)
2
+ * TinyMCE version 8.8.2 (2026-07-27)
3
3
  */
4
4
 
5
5
  (function () {
@@ -12153,6 +12153,13 @@
12153
12153
  const selector = isEmpty$5(formatNoneditableSelector) ? baseDataSelector : `${baseDataSelector},${formatNoneditableSelector}`;
12154
12154
  return is$2(SugarElement.fromDom(node), selector);
12155
12155
  };
12156
+ const hasEditableDescendants = (dom, node) => dom.select('[contenteditable="true"]', node).length > 0;
12157
+ const getEditableDescendants = (dom, parent) => foldl(from(parent.childNodes), (editableDescendants, child) => [
12158
+ ...editableDescendants,
12159
+ ...(isElement$7(child) && dom.getContentEditable(child) === 'true'
12160
+ ? [child]
12161
+ : getEditableDescendants(dom, child))
12162
+ ], []);
12156
12163
  // A noneditable element is wrappable if it:
12157
12164
  // - is valid target (has data-mce-cef-wrappable attribute or matches selector from option)
12158
12165
  // - has no editable descendants - removing formats in the editable region can result in the wrapped noneditable being split which is undesirable
@@ -12161,7 +12168,7 @@
12161
12168
  return (isElementNode$1(node) &&
12162
12169
  dom.getContentEditable(node) === 'false' &&
12163
12170
  isWrapNoneditableTarget(editor, node) &&
12164
- dom.select('[contenteditable="true"]', node).length === 0);
12171
+ !hasEditableDescendants(dom, node));
12165
12172
  };
12166
12173
  /**
12167
12174
  * Replaces variables in the value. The variable format is %var.
@@ -16769,14 +16776,23 @@
16769
16776
  }
16770
16777
  return undefined;
16771
16778
  };
16779
+ const containsFormatDeep = (editor, root, name, vars, similar) => isNonNullable(matchNode$1(editor, root, name, vars, similar)) ||
16780
+ exists(from(root.childNodes), (child) => containsFormatDeep(editor, child, name, vars, similar));
16781
+ const matchEditableDescendants = (editor, node, name, vars, similar) => {
16782
+ const dom = editor.dom;
16783
+ if (!isElement$7(node) || dom.getContentEditable(node) !== 'false' || isWrappableNoneditable(editor, node)) {
16784
+ return false;
16785
+ }
16786
+ return exists(getEditableDescendants(dom, node), (descendant) => containsFormatDeep(editor, descendant, name, vars, similar));
16787
+ };
16772
16788
  const match$2 = (editor, name, vars, node, similar) => {
16773
16789
  // Check specified node
16774
16790
  if (node) {
16775
- return matchParents(editor, node, name, vars, similar);
16791
+ return matchParents(editor, node, name, vars, similar) || matchEditableDescendants(editor, node, name, vars, similar);
16776
16792
  }
16777
16793
  // Check selected node
16778
16794
  node = editor.selection.getNode();
16779
- if (matchParents(editor, node, name, vars, similar)) {
16795
+ if (matchParents(editor, node, name, vars, similar) || matchEditableDescendants(editor, node, name, vars, similar)) {
16780
16796
  return true;
16781
16797
  }
16782
16798
  // Check start node if it's different
@@ -17181,6 +17197,18 @@
17181
17197
  return block;
17182
17198
  }
17183
17199
  });
17200
+ // Resolve list items from the
17201
+ // selected cells directly instead, matching how SelectionUtils.runOnRanges resolves fake selections.
17202
+ const getCellSelectionListItems = (editor) => {
17203
+ const fakeSelectionNodes = getCellsFromEditor(editor);
17204
+ if (fakeSelectionNodes.length > 0) {
17205
+ const listItems = bind$3(fakeSelectionNodes, (cell) => descendants(cell, 'li'));
17206
+ return Optional.some(map$3(listItems, (item) => item.dom));
17207
+ }
17208
+ else {
17209
+ return Optional.none();
17210
+ }
17211
+ };
17184
17212
  const getFullySelectedBlocks = (selection) => {
17185
17213
  if (selection.isCollapsed()) {
17186
17214
  return [];
@@ -17197,8 +17225,15 @@
17197
17225
  return first.concat(middle).concat(last);
17198
17226
  }
17199
17227
  };
17200
- const getFullySelectedListItems = (selection) => filter$5(getFullySelectedBlocks(selection), isEditableListItem(selection.dom));
17201
- const getPartiallySelectedListItems = (selection) => filter$5(getAndOnlyNormalizeFirstBlockIf(selection, (el) => !isListItem$3(el)), isEditableListItem(selection.dom));
17228
+ const getFullySelectedListItems = (editor) => {
17229
+ const items = getCellSelectionListItems(editor).getOrThunk(() => getFullySelectedBlocks(editor.selection));
17230
+ return filter$5(items, isEditableListItem(editor.selection.dom));
17231
+ };
17232
+ const getPartiallySelectedListItems = (editor) => {
17233
+ const items = getCellSelectionListItems(editor)
17234
+ .getOrThunk(() => getAndOnlyNormalizeFirstBlockIf(editor.selection, (el) => !isListItem$3(el)));
17235
+ return filter$5(items, isEditableListItem(editor.selection.dom));
17236
+ };
17202
17237
 
17203
17238
  const each$8 = Tools.each;
17204
17239
  const isElementNode = (node) => isElement$7(node) && !isBookmarkNode$1(node) && !isCaretNode(node) && !isBogus$1(node);
@@ -17440,14 +17475,14 @@
17440
17475
  };
17441
17476
  const removeListStyleFormats = (editor, name, vars) => {
17442
17477
  if (name === 'removeformat') {
17443
- each$e(getPartiallySelectedListItems(editor.selection), (li) => {
17478
+ each$e(getPartiallySelectedListItems(editor), (li) => {
17444
17479
  each$e(listItemStyles, (name) => editor.dom.setStyle(li, name, ''));
17445
17480
  removeEmptyStyleAttributeIfNeeded(editor.dom, li);
17446
17481
  });
17447
17482
  }
17448
17483
  else {
17449
17484
  getExpandedListItemFormat(editor.formatter, name).each((liFmt) => {
17450
- each$e(getPartiallySelectedListItems(editor.selection), (li) => removeStyles$1(editor.dom, li, liFmt, vars, null));
17485
+ each$e(getPartiallySelectedListItems(editor), (li) => removeStyles$1(editor.dom, li, liFmt, vars, null));
17451
17486
  });
17452
17487
  }
17453
17488
  };
@@ -18628,7 +18663,7 @@
18628
18663
  }
18629
18664
  };
18630
18665
 
18631
- /*! @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 */
18666
+ /*! @license DOMPurify 3.4.12 | (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.12/LICENSE */
18632
18667
 
18633
18668
  function _arrayLikeToArray(r, a) {
18634
18669
  (null == a || a > r.length) && (a = r.length);
@@ -18941,7 +18976,7 @@
18941
18976
  const text = freeze(['#text']);
18942
18977
 
18943
18978
  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']);
18944
- 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']);
18979
+ 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']);
18945
18980
  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']);
18946
18981
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
18947
18982
 
@@ -19054,7 +19089,7 @@
19054
19089
  function createDOMPurify() {
19055
19090
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
19056
19091
  const DOMPurify = root => createDOMPurify(root);
19057
- DOMPurify.version = '3.4.11';
19092
+ DOMPurify.version = '3.4.12';
19058
19093
  DOMPurify.removed = [];
19059
19094
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
19060
19095
  // Not running in a browser, provide a factory function
@@ -19736,6 +19771,13 @@
19736
19771
  * @param root the in-place root to empty
19737
19772
  */
19738
19773
  const _neutralizeRoot = function _neutralizeRoot(root) {
19774
+ /* Strip every disallowed attribute (on* handlers included) off the whole
19775
+ subtree BEFORE detaching anything. Detaching first would hand back
19776
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
19777
+ whose queued resource event still fires in page scope after we throw.
19778
+ Clobber-safe reads; a doomed clobbered node's own attributes are
19779
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
19780
+ _neutralizeSubtree(root);
19739
19781
  const childNodes = getChildNodes(root);
19740
19782
  if (childNodes) {
19741
19783
  const snapshot = [];
@@ -19863,6 +19905,82 @@
19863
19905
  }
19864
19906
  }
19865
19907
  };
19908
+ /**
19909
+ * _neutralizePatchLinkage
19910
+ *
19911
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
19912
+ * hardening, https://github.com/WICG/declarative-partial-updates).
19913
+ *
19914
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
19915
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
19916
+ * reaches each node. On a live in-place root that leaves a window: from the
19917
+ * moment the root is connected until the walk arrives at a given node, that
19918
+ * node's linkage is live. A patch applied on connection/stream can fire as
19919
+ * a microtask during the walk and inject or teleport an unsanitized DOM
19920
+ * range into a region the iterator has already passed and will not revisit,
19921
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
19922
+ * whole tree once up front and sever every linkage before the walk begins,
19923
+ * closing that window.
19924
+ *
19925
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
19926
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
19927
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
19928
+ * to group with the rest of the declarative-partial-updates handling and
19929
+ * stay overridable, consistent with the codebase.
19930
+ *
19931
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
19932
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
19933
+ *
19934
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
19935
+ * .html Q1): this mirrors the existing policy of keeping `for` on
19936
+ * <label>/<output>. If the shipping feature can drive a patch through a
19937
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
19938
+ * attribute check at _isBasicCustomElement's caller must additionally drop
19939
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
19940
+ *
19941
+ * @param root the in-place root to sweep
19942
+ */
19943
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
19944
+ if (!SAFE_FOR_XML) {
19945
+ return;
19946
+ }
19947
+ const stack = [root];
19948
+ while (stack.length > 0) {
19949
+ const node = stack.pop();
19950
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
19951
+ /* Remove range markers (the target side of a patch linkage): every
19952
+ processing instruction, and any markup-bearing comment. */
19953
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
19954
+ try {
19955
+ remove(node);
19956
+ } catch (_) {
19957
+ /* Best-effort */
19958
+ }
19959
+ continue;
19960
+ }
19961
+ /* Strip patch-source attributes (the source side) off elements. */
19962
+ if (nodeType === NODE_TYPE.element) {
19963
+ const element = node;
19964
+ const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
19965
+ try {
19966
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
19967
+ element.removeAttribute('patchsrc');
19968
+ }
19969
+ if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
19970
+ element.removeAttribute('for');
19971
+ }
19972
+ } catch (_) {
19973
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
19974
+ }
19975
+ }
19976
+ const childNodes = getChildNodes(node);
19977
+ if (childNodes) {
19978
+ for (let i = childNodes.length - 1; i >= 0; --i) {
19979
+ stack.push(childNodes[i]);
19980
+ }
19981
+ }
19982
+ }
19983
+ };
19866
19984
  /**
19867
19985
  * _initDocument
19868
19986
  *
@@ -20113,9 +20231,15 @@
20113
20231
  /**
20114
20232
  * Handle a node whose tag is forbidden or not allowlisted: keep
20115
20233
  * allowed custom elements (false return exits _sanitizeElements
20116
- * early - namespace/fallback checks and the afterSanitizeElements
20117
- * hook are intentionally skipped for kept custom elements), else
20118
- * hoist content per KEEP_CONTENT and remove.
20234
+ * early - the namespace and fallback-tag removal checks are
20235
+ * intentionally skipped for kept custom elements), else hoist
20236
+ * content per KEEP_CONTENT and remove.
20237
+ *
20238
+ * A kept custom element is the ONLY case in which this function
20239
+ * returns false, so the caller uses that return value to run the
20240
+ * afterSanitizeElements hook on the kept element and keep the
20241
+ * element-hook lifecycle consistent with normal allowlisted
20242
+ * elements (GHSA-c2j3-45gr-mqc4).
20119
20243
  *
20120
20244
  * @param currentNode the disallowed node
20121
20245
  * @param tagName the node's transformCaseFunc'd tag name
@@ -20181,9 +20305,15 @@
20181
20305
  * @param currentNode to check for permission to exist
20182
20306
  * @return true if node was killed, false if left alive
20183
20307
  */
20184
- const _sanitizeElements = function _sanitizeElements(currentNode) {
20308
+ // eslint-disable-next-line complexity
20309
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
20185
20310
  /* Execute a hook if present */
20186
20311
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
20312
+ /* A hook may have detached the node — treat it as removed (see the
20313
+ detached-node comment after the uponSanitizeElement hook below). */
20314
+ if (currentNode !== root && getParentNode(currentNode) === null) {
20315
+ return true;
20316
+ }
20187
20317
  /* Check if element is clobbered or can clobber */
20188
20318
  if (_isClobbered(currentNode)) {
20189
20319
  _forceRemove(currentNode);
@@ -20196,6 +20326,24 @@
20196
20326
  tagName,
20197
20327
  allowedTags: ALLOWED_TAGS
20198
20328
  });
20329
+ /* A hook may have detached the node from the tree — a long-standing
20330
+ user pattern (issue #469; draw.io-style foreignObject filtering).
20331
+ Per the cached, unclobberable parentNode getter the node is
20332
+ genuinely out of the tree, so it can reach neither the serialized
20333
+ output nor an IN_PLACE live tree; treat it as removed and stop
20334
+ processing it. Without this guard, the unsafe-node / namespace
20335
+ checks below would call _forceRemove on a parentless node and hit
20336
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
20337
+ wants gone but *cannot* detach (clobbered / parentless roots), the
20338
+ opposite of a node that is already safely gone. The walk root is
20339
+ exempt: a detached IN_PLACE root is legitimate input and must still
20340
+ be fully sanitized, and a kill-decision on it must keep hitting the
20341
+ REPORT-3 throw. Nodes detached by hooks are the hook's
20342
+ responsibility: they are not recorded in DOMPurify.removed and are
20343
+ not neutralized by the post-walk IN_PLACE pass. */
20344
+ if (currentNode !== root && getParentNode(currentNode) === null) {
20345
+ return true;
20346
+ }
20199
20347
  /* Remove mXSS vectors, processing instructions and risky comments */
20200
20348
  if (_isUnsafeNode(currentNode, tagName)) {
20201
20349
  _forceRemove(currentNode);
@@ -20203,7 +20351,22 @@
20203
20351
  }
20204
20352
  /* Remove element if anything forbids its presence */
20205
20353
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
20206
- return _sanitizeDisallowedNode(currentNode, tagName);
20354
+ const removed = _sanitizeDisallowedNode(currentNode, tagName);
20355
+ /* A false return means the node is a custom element kept via
20356
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
20357
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
20358
+ element-hook lifecycle matches normal allowlisted elements: a
20359
+ security policy applied in this hook (e.g. stripping an attribute
20360
+ from every surviving element) must not silently skip kept custom
20361
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
20362
+ tail below - the hook runs, then the walker's subsequent
20363
+ _sanitizeAttributes pass sanitizes the element's attributes. The
20364
+ deliberately skipped namespace and fallback-tag removal checks stay
20365
+ skipped; they are removal decisions, not the hook contract. */
20366
+ if (removed === false) {
20367
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
20368
+ }
20369
+ return removed;
20207
20370
  }
20208
20371
  /* Check whether element has a valid namespace.
20209
20372
  Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
@@ -20250,6 +20413,33 @@
20250
20413
  if (FORBID_ATTR[lcName]) {
20251
20414
  return false;
20252
20415
  }
20416
+ /* Reject declarative-partial-updates patch-linkage attributes
20417
+ (https://github.com/WICG/declarative-partial-updates).
20418
+ Empirical note (Chrome 150, verified — see
20419
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
20420
+ sanitization. For the string path it fires during sanitize()'s own
20421
+ parse, so the walk sees and sanitizes the fully materialized expanded
20422
+ tree — teleports into MathML/SVG integration points included; a
20423
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
20424
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
20425
+ Either way DOMPurify is NOT blind to the patch.
20426
+ This removal is therefore defense-in-depth rather than the sole barrier:
20427
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
20428
+ in the caller's context, and keeps behaviour deterministic if a future
20429
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
20430
+ anywhere else (notably <template for>) it links the element to a patch
20431
+ target and teleports or removes an arbitrary DOM range by id/marker name.
20432
+ `patchsrc` fetches remote markup and is treated as a script-loading
20433
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
20434
+ other structural-threat checks and stays overridable, consistent with
20435
+ the rest of the codebase. PI range markers are already removed by
20436
+ _isUnsafeNode. */
20437
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
20438
+ return false;
20439
+ }
20440
+ if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
20441
+ return false;
20442
+ }
20253
20443
  /* Make sure attribute cannot clobber */
20254
20444
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
20255
20445
  return false;
@@ -20459,7 +20649,7 @@
20459
20649
  /* Execute a hook if present */
20460
20650
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
20461
20651
  /* Sanitize tags and elements */
20462
- _sanitizeElements(shadowNode);
20652
+ _sanitizeElements(shadowNode, fragment);
20463
20653
  /* Check attributes next */
20464
20654
  _sanitizeAttributes(shadowNode);
20465
20655
  /* Deep shadow DOM detected.
@@ -20650,6 +20840,11 @@
20650
20840
  keep using and whose return value they ignore — unsanitized. REPORT-2. */
20651
20841
  const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
20652
20842
  if (inPlace) {
20843
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
20844
+ linkage across the live tree BEFORE the walk, so no patch can fire
20845
+ mid-walk and inject into an already-processed region. Runs first, so
20846
+ it also covers the forbidden/clobbered roots that throw below. */
20847
+ _neutralizePatchLinkage(dirty);
20653
20848
  /* Do some early pre-sanitization to avoid unsafe root nodes.
20654
20849
  Read nodeName through the cached prototype getter — a clobbering
20655
20850
  child named "nodeName" on the form root would otherwise shadow
@@ -20659,6 +20854,9 @@
20659
20854
  if (typeof nn === 'string') {
20660
20855
  const tagName = transformCaseFunc(nn);
20661
20856
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
20857
+ /* Fail closed on a live root: neutralize handlers/children before
20858
+ throwing, exactly as the mid-walk abort path does. */
20859
+ _neutralizeRoot(dirty);
20662
20860
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
20663
20861
  }
20664
20862
  }
@@ -20673,6 +20871,10 @@
20673
20871
  the application unsanitized. Refuse to sanitize such a root
20674
20872
  the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
20675
20873
  if (_isClobbered(dirty)) {
20874
+ /* Fail closed on a live clobbered root before throwing.
20875
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
20876
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
20877
+ _neutralizeRoot(dirty);
20676
20878
  throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
20677
20879
  }
20678
20880
  /* Sanitize attached shadow roots before the main iterator runs.
@@ -20725,7 +20927,8 @@
20725
20927
  _forceRemove(body.firstChild);
20726
20928
  }
20727
20929
  /* Get node iterator */
20728
- const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
20930
+ const walkRoot = inPlace ? dirty : body;
20931
+ const nodeIterator = _createNodeIterator(walkRoot);
20729
20932
  /* Now start iterating over the created document.
20730
20933
  The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
20731
20934
  engine/custom-element mutation can detach a node mid-walk so
@@ -20738,7 +20941,7 @@
20738
20941
  try {
20739
20942
  while (currentNode = nodeIterator.nextNode()) {
20740
20943
  /* Sanitize tags and elements */
20741
- _sanitizeElements(currentNode);
20944
+ _sanitizeElements(currentNode, walkRoot);
20742
20945
  /* Check attributes next */
20743
20946
  _sanitizeAttributes(currentNode);
20744
20947
  /* Shadow DOM detected, sanitize it.
@@ -20752,6 +20955,14 @@
20752
20955
  } catch (error) {
20753
20956
  if (inPlace) {
20754
20957
  _neutralizeRoot(dirty);
20958
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
20959
+ detached from the root, so _neutralizeRoot's subtree pass does not
20960
+ reach them. Defuse them too, mirroring the success-path loop below. */
20961
+ arrayForEach(DOMPurify.removed, entry => {
20962
+ if (entry.element) {
20963
+ _neutralizeSubtree(entry.element);
20964
+ }
20965
+ });
20755
20966
  }
20756
20967
  throw error;
20757
20968
  }
@@ -21349,44 +21560,22 @@
21349
21560
  const bogus = get$a(element, 'data-mce-bogus');
21350
21561
  if (!isInternalElement && isString(bogus)) {
21351
21562
  if (bogus === 'all') {
21352
- // Empty before removing so a detached raw-text element (e.g. script/style) can't carry
21353
- // markup-like content into DOMPurify's unsafe-node check, which would then throw trying
21354
- // to _forceRemove the now-parentless node.
21355
- empty(element);
21356
21563
  remove$8(element);
21357
21564
  }
21358
21565
  else {
21359
21566
  unwrap(element);
21360
21567
  }
21361
- // We detach the element above; DOMPurify throws if it then tries to _forceRemove the
21362
- // now-parentless node, so mark the tag allowed to stop that second removal.
21363
- if (isNonNullable(evt)) {
21364
- evt.allowedTags[lcTagName] = true;
21365
- }
21366
21568
  return;
21367
21569
  }
21368
21570
  // Determine if the schema allows the element and either add it or remove it
21369
21571
  const rule = schema.getElementRule(lcTagName);
21370
21572
  if (validate && !rule) {
21371
- // Don't detach invalid special / non-HTML-namespace elements: DOMPurify throws if it later
21372
- // _forceRemoves a parentless node (its unsafe-content and namespace checks do this regardless
21373
- // of allowedTags). Empty and leave attached instead, so DOMPurify removes it without hoisting content.
21374
- if (settings.sanitize && (has$2(specialElements, lcTagName) || scope !== 'html')) {
21375
- empty(element);
21573
+ // If a special element is invalid, then remove the entire element instead of unwrapping
21574
+ if (has$2(specialElements, lcTagName)) {
21575
+ remove$8(element);
21376
21576
  }
21377
21577
  else {
21378
- // No DOMPurify to remove it (sanitize disabled), or a plain invalid HTML element: detach it ourselves.
21379
- if (has$2(specialElements, lcTagName)) {
21380
- remove$8(element);
21381
- }
21382
- else {
21383
- unwrap(element);
21384
- }
21385
- // The element is now detached; mark the tag allowed so DOMPurify won't _forceRemove the
21386
- // parentless node and throw. Safe here: it's empty and HTML-namespaced, so no earlier check fires.
21387
- if (isNonNullable(evt)) {
21388
- evt.allowedTags[lcTagName] = true;
21389
- }
21578
+ unwrap(element);
21390
21579
  }
21391
21580
  return;
21392
21581
  }
@@ -21569,9 +21758,7 @@
21569
21758
  evt.allowedTags[lcTagName] = keepElement;
21570
21759
  if (!keepElement && settings.sanitize) {
21571
21760
  if (isElement$7(node)) {
21572
- // Empty the node and leave it attached so DOMPurify removes it (allowedTags is false).
21573
- // Detaching it would make DOMPurify throw when it _forceRemoves the parentless node.
21574
- empty(SugarElement.fromDom(node));
21761
+ node.remove();
21575
21762
  }
21576
21763
  }
21577
21764
  });
@@ -23193,14 +23380,21 @@
23193
23380
  }
23194
23381
  });
23195
23382
  };
23196
- // TODO: TINY-9142: Remove this to make nested noneditable formatting work
23197
23383
  const targetNode = isNode(node) ? node : selection.getNode();
23198
23384
  if (dom.getContentEditable(targetNode) === 'false' && !isWrappableNoneditable(ed, targetNode)) {
23199
23385
  // node variable is used by other functions above in the same scope so need to set it here
23200
23386
  node = targetNode;
23201
23387
  applyNodeStyle(formatList, node);
23202
- if (isBlockFormat(format) && !dom.isBlock(targetNode)) {
23203
- const parentBlock = dom.getParent(targetNode, dom.isBlock);
23388
+ if (hasEditableDescendants(dom, node)) {
23389
+ each$e(getEditableDescendants(dom, node), (island) => {
23390
+ const rng = dom.createRng();
23391
+ rng.selectNodeContents(island);
23392
+ applyRngStyle(dom, rng, true);
23393
+ });
23394
+ return;
23395
+ }
23396
+ if (isBlockFormat(format) && !dom.isBlock(node)) {
23397
+ const parentBlock = dom.getParent(node, dom.isBlock);
23204
23398
  if (dom.isEditable(parentBlock)) {
23205
23399
  const wrapperElementName = format.block;
23206
23400
  if (parentBlock.nodeName.toLowerCase() === wrapperElementName.toLowerCase()) {
@@ -23212,6 +23406,12 @@
23212
23406
  }
23213
23407
  }
23214
23408
  }
23409
+ else {
23410
+ const selectorFormats = filter$5(formatList, isSelectorFormat);
23411
+ Optional.from(dom.getParent(node, (candidate) => exists(selectorFormats, (fmt) => dom.is(candidate, fmt.selector))))
23412
+ .filter(dom.isEditable)
23413
+ .each((parent) => applyNodeStyle(formatList, parent));
23414
+ }
23215
23415
  fireFormatApply(ed, name, node, vars);
23216
23416
  return;
23217
23417
  }
@@ -23245,7 +23445,7 @@
23245
23445
  applyCaretFormat(ed, name, vars);
23246
23446
  }
23247
23447
  getExpandedListItemFormat(ed.formatter, name).each((liFmt) => {
23248
- const list = getFullySelectedListItems(ed.selection);
23448
+ const list = getFullySelectedListItems(ed);
23249
23449
  each$e(list, (li) => applyStyles(dom, li, liFmt, vars));
23250
23450
  });
23251
23451
  }
@@ -41882,14 +42082,14 @@
41882
42082
  * @property minorVersion
41883
42083
  * @type String
41884
42084
  */
41885
- minorVersion: '8.0',
42085
+ minorVersion: '8.2',
41886
42086
  /**
41887
42087
  * Release date of TinyMCE build.
41888
42088
  *
41889
42089
  * @property releaseDate
41890
42090
  * @type String
41891
42091
  */
41892
- releaseDate: '2026-07-15',
42092
+ releaseDate: '2026-07-27',
41893
42093
  /**
41894
42094
  * Collection of language pack data.
41895
42095
  *
@@ -1,6 +1,6 @@
1
1
  module TinyMCE
2
2
  module Rails
3
- VERSION = "8.8.0"
4
- TINYMCE_VERSION = "8.8.0"
3
+ VERSION = "8.8.2"
4
+ TINYMCE_VERSION = "8.8.2"
5
5
  end
6
6
  end