From 118bd58ce4cc2a09a1a0cde5ea444dd7007365f7 Mon Sep 17 00:00:00 2001 From: John Costa Date: Wed, 18 Mar 2026 07:53:54 -0700 Subject: [PATCH] feat(paste): support filter functions in tag-based paste config Extend the paste processing logic to check filter functions when matching pasted elements to tools. Previously, filter functions in pasteConfig.tags were only used during sanitization (via HTMLJanitor) but ignored during paste processing, causing all elements with a matching tag name to be treated as substitutable regardless of the filter. Add a filter field to TagSubstitute, store it in getTagsConfig when the sanitization config is a function, and introduce isTagSubstitutable that checks both tag name and filter. Update processHTML, processElementNode, and containsAnotherToolTags to use the helper. Closes #2959 --- src/components/modules/paste.ts | 46 ++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts index 6a8378c41..513f4518b 100644 --- a/src/components/modules/paste.ts +++ b/src/components/modules/paste.ts @@ -28,6 +28,12 @@ interface TagSubstitute { * But Tool can explicitly specify sanitizer configuration for supported tags */ sanitizationConfig?: SanitizerRule; + + /** + * Optional filter function to decide whether a specific element matches this tag substitute. + * When provided, an element must pass the filter in addition to matching the tag name. + */ + filter?: (el: Element) => boolean; } /** @@ -375,9 +381,23 @@ export default class Paste extends Module { */ const sanitizationConfig = _.isObject(tagOrSanitizeConfig) ? tagOrSanitizeConfig[tag] : null; + /** + * If the sanitization config is a function, it acts as a filter — + * only elements that pass the filter should be treated as substitutable. + * The function returns a TagConfig: false means reject, anything else means accept. + */ + const filter = _.isFunction(sanitizationConfig) + ? (el: Element): boolean => { + const result = (sanitizationConfig as (el: Element) => unknown)(el); + + return result !== false; + } + : undefined; + this.toolsTags[tag.toUpperCase()] = { tool, sanitizationConfig, + filter, }; }); }); @@ -385,6 +405,26 @@ export default class Paste extends Module { this.tagsByTool[tool.name] = toolTags.map((t) => t.toUpperCase()); } + /** + * Check if an element matches a registered tag substitute, including any filter function. + * + * @param element - the element to check + * @returns true if the element's tag is registered and passes its filter (if any) + */ + private isTagSubstitutable(element: Element): boolean { + const tagSubstitute = this.toolsTags[element.tagName]; + + if (!tagSubstitute) { + return false; + } + + if (tagSubstitute.filter) { + return tagSubstitute.filter(element); + } + + return true; + } + /** * Get files` types and extensions to substitute by Tool * @@ -612,7 +652,7 @@ export default class Paste extends Module { content = node as HTMLElement; isBlock = true; - if (this.toolsTags[content.tagName]) { + if (this.isTagSubstitutable(content)) { tool = this.toolsTags[content.tagName].tool; } break; @@ -907,12 +947,12 @@ export default class Paste extends Module { const { tool } = this.toolsTags[element.tagName] || {}; const toolTags = this.tagsByTool[tool?.name] || []; - const isSubstitutable = tags.includes(element.tagName); + const isSubstitutable = this.isTagSubstitutable(element); const isBlockElement = $.blockElements.includes(element.tagName.toLowerCase()); const containsAnotherToolTags = Array .from(element.children) .some( - ({ tagName }) => tags.includes(tagName) && !toolTags.includes(tagName) + (child) => this.isTagSubstitutable(child) && !toolTags.includes(child.tagName) ); const containsBlockElements = Array.from(element.children).some(