diff --git a/packages/clipper/src/clone.ts b/packages/clipper/src/clone.ts new file mode 100644 index 000000000..b906b51ed --- /dev/null +++ b/packages/clipper/src/clone.ts @@ -0,0 +1,183 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +const SVGElements = [ + "altGlyph", + "altGlyphDef", + "altGlyphItem", + "animate", + "animateColor", + "animateMotion", + "animateTransform", + "circle", + "clipPath", + "color-profile", + "cursor", + "defs", + "desc", + "ellipse", + "feBlend", + "feColorMatrix", + "feComponentTransfer", + "feComposite", + "feConvolveMatrix", + "feDiffuseLighting", + "feDisplacementMap", + "feDistantLight", + "feFlood", + "feFuncA", + "feFuncB", + "feFuncG", + "feFuncR", + "feGaussianBlur", + "feImage", + "feMerge", + "feMergeNode", + "feMorphology", + "feOffset", + "fePointLight", + "feSpecularLighting", + "feSpotLight", + "feTile", + "feTurbulence", + "filter", + "font-face", + "font-face-format", + "font-face-name", + "font-face-src", + "font-face-uri", + "foreignObject", + "g", + "glyph", + "glyphRef", + "hkern", + "image", + "line", + "linearGradient", + "marker", + "mask", + "metadata", + "missing-glyph", + "mpath", + "path", + "pattern", + "polygon", + "polyline", + "radialGradient", + "rect", + "set", + "stop", + "svg", + "switch", + "symbol", + "text", + "textPath", + "title", + "tref", + "tspan", + "use", + "view", + "vkern" +].map((a) => a.toLowerCase()); + +const INVALID_ELEMENTS = ["script"].map((a) => a.toLowerCase()); + +type CloneNodeOptions = { + images?: boolean; + styles?: boolean; +}; + +export function cloneNode(node: HTMLElement, options: CloneNodeOptions) { + const clone = node.cloneNode(true) as HTMLElement; + processNode(clone, options); + return clone; +} + +function processNode(node: HTMLElement, options: CloneNodeOptions) { + try { + if (!options.images && node instanceof HTMLImageElement) { + node.remove(); + return; + } + + if ( + !options.styles && + (node instanceof HTMLButtonElement || + node instanceof HTMLFormElement || + node instanceof HTMLSelectElement || + node instanceof HTMLInputElement || + node instanceof HTMLTextAreaElement) + ) { + node.remove(); + return; + } + + if (node.nodeType === Node.COMMENT_NODE) { + node.remove(); + return; + } + + if (isInvalidElement(node)) { + node.remove(); + return; + } + + if (node.nodeType !== Node.TEXT_NODE && !isSVGElement(node)) { + const { display, width, height } = window.getComputedStyle(node); + if (display === "none" || (width === "0px" && height === "0px")) { + node.remove(); + return; + } + + if (isCustomElement(node)) { + const isInline = display.includes("inline"); + const element = document.createElement(isInline ? "span" : "div"); + for (const attribute of node.attributes) { + element.setAttribute(attribute.name, attribute.value); + } + node.replaceWith(element); + } + } + + node.childNodes.forEach((child) => + processNode(child as HTMLElement, options) + ); + } catch (e) { + console.error("Failed to process node", e); + return null; + } +} + +function isInvalidElement(element: HTMLElement) { + if (!element || !element.tagName) return false; + return INVALID_ELEMENTS.includes(element.tagName.toLowerCase()); +} + +export function isSVGElement(element: HTMLElement) { + if (!element || !element.tagName) return false; + return SVGElements.includes(element.tagName.toLowerCase()); +} + +function isCustomElement(element: HTMLElement) { + if (!element || !element.tagName) return false; + return ( + !SVGElements.includes(element.tagName.toLowerCase()) && + element.tagName.includes("-") + ); +} diff --git a/packages/clipper/src/domtoimage.ts b/packages/clipper/src/domtoimage.ts index 43d94d03c..5ee98a83d 100644 --- a/packages/clipper/src/domtoimage.ts +++ b/packages/clipper/src/domtoimage.ts @@ -20,15 +20,9 @@ import { createImage, FetchOptions } from "./fetch.js"; import { resolveAll } from "./fontfaces.js"; import { inlineAllImages } from "./images.js"; import { Options } from "./types.js"; -import { - canvasToBlob, - delay, - escapeXhtml, - height, - width, - isSVGElement -} from "./utils.js"; +import { canvasToBlob, delay, escapeXhtml, height, width } from "./utils.js"; import { inlineStylesheets } from "./styles.js"; +import { cloneNode, isSVGElement } from "./clone.js"; // Default impl options const defaultOptions: Options = { @@ -41,15 +35,13 @@ async function getInlinedNode(node: HTMLElement, options: Options) { if (stylesheets) await inlineStylesheets(options.fetchOptions); - let clone = node.cloneNode(true) as HTMLElement; + let clone = cloneNode(node, { + images, + styles: options.styles + }); if (!clone || clone instanceof Text) return; - if (!images) { - const images = clone.querySelectorAll("img"); - images.forEach((image) => image.remove()); - } - if (fonts) clone = await embedFonts(clone, options.fetchOptions); if (inlineImages) await inlineAllImages(clone, options.fetchOptions); diff --git a/packages/clipper/src/utils.ts b/packages/clipper/src/utils.ts index 06504aa29..24978f9f4 100644 --- a/packages/clipper/src/utils.ts +++ b/packages/clipper/src/utils.ts @@ -165,90 +165,6 @@ function getRootStylesheet() { return null; } -const SVGElements = [ - "altGlyph", - "altGlyphDef", - "altGlyphItem", - "animate", - "animateColor", - "animateMotion", - "animateTransform", - "circle", - "clipPath", - "color-profile", - "cursor", - "defs", - "desc", - "ellipse", - "feBlend", - "feColorMatrix", - "feComponentTransfer", - "feComposite", - "feConvolveMatrix", - "feDiffuseLighting", - "feDisplacementMap", - "feDistantLight", - "feFlood", - "feFuncA", - "feFuncB", - "feFuncG", - "feFuncR", - "feGaussianBlur", - "feImage", - "feMerge", - "feMergeNode", - "feMorphology", - "feOffset", - "fePointLight", - "feSpecularLighting", - "feSpotLight", - "feTile", - "feTurbulence", - "filter", - "font-face", - "font-face-format", - "font-face-name", - "font-face-src", - "font-face-uri", - "foreignObject", - "g", - "glyph", - "glyphRef", - "hkern", - "image", - "line", - "linearGradient", - "marker", - "mask", - "metadata", - "missing-glyph", - "mpath", - "path", - "pattern", - "polygon", - "polyline", - "radialGradient", - "rect", - "set", - "stop", - "svg", - "switch", - "symbol", - "text", - "textPath", - "title", - "tref", - "tspan", - "use", - "view", - "vkern" -].map((a) => a.toLowerCase()); - -function isSVGElement(element: HTMLElement) { - if (!element || !element.tagName) return false; - return SVGElements.includes(element.tagName.toLowerCase()); -} - export { injectCss, escape, @@ -263,6 +179,5 @@ export { asArray, escapeXhtml, width, - height, - isSVGElement + height };