clipper: remove clone related functions

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
01zulfi
2025-06-11 11:40:11 +05:00
committed by Abdullah Atta
parent 4fb056095c
commit 4fbaecc24e
4 changed files with 97 additions and 351 deletions

View File

@@ -1,346 +0,0 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
import { createImage, FetchOptions } from "./fetch.js";
import { Filter } from "./types.js";
import { uid } from "./utils.js";
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 CloneProps = {
filter?: Filter;
root: boolean;
vector: boolean;
styles?: boolean;
getElementStyles?: (element: HTMLElement) => CSSStyleDeclaration | undefined;
getPseudoElementStyles?: (
element: HTMLElement,
pseudoElement: string
) => CSSStyleDeclaration | undefined;
fetchOptions?: FetchOptions;
images?: boolean;
};
export async function cloneNode(node: HTMLElement, options: CloneProps) {
const { root, filter } = options;
if (!root && filter && !filter(node)) return null;
let clone = await makeNodeCopy(node, options);
if (!clone) return null;
clone = await cloneChildren(node, clone, options);
const processed = processClone(node, clone, options);
return processed;
}
function makeNodeCopy(original: HTMLElement, options?: CloneProps) {
try {
if (original instanceof HTMLCanvasElement && options?.images)
return createImage(original.toDataURL(), options?.fetchOptions);
if (!options?.images && original instanceof HTMLImageElement) return null;
if (
!options?.styles &&
(original instanceof HTMLButtonElement ||
original instanceof HTMLFormElement ||
original instanceof HTMLSelectElement ||
original instanceof HTMLInputElement ||
original instanceof HTMLTextAreaElement)
)
return null;
if (original.nodeType === Node.COMMENT_NODE) return null;
if (isInvalidElement(original)) return null;
if (original.nodeType !== Node.TEXT_NODE && !isSVGElement(original)) {
const { display, width, height } = window.getComputedStyle(original);
if (display === "none" || (width === "0px" && height === "0px"))
return null;
if (isCustomElement(original)) {
const isInline = display.includes("inline");
const element = document.createElement(isInline ? "span" : "div");
for (const attribute of original.attributes) {
element.setAttribute(attribute.name, attribute.value);
}
return element;
}
}
return original.cloneNode(false) as HTMLElement;
} catch (e) {
console.error("Failed to clone element", e);
return null;
}
}
function isCustomElement(element: HTMLElement) {
if (!element || !element.tagName) return false;
return (
!SVGElements.includes(element.tagName.toLowerCase()) &&
element.tagName.includes("-")
);
}
export function isSVGElement(element: HTMLElement) {
if (!element || !element.tagName) return false;
return SVGElements.includes(element.tagName.toLowerCase());
}
function isInvalidElement(element: HTMLElement) {
if (!element || !element.tagName) return false;
return INVALID_ELEMENTS.includes(element.tagName.toLowerCase());
}
async function cloneChildren(
original: HTMLElement,
clone: HTMLElement,
options: CloneProps
) {
const children = original.childNodes;
if (children.length === 0) return clone;
await cloneChildrenInOrder(clone, children, options);
return clone;
}
async function cloneChildrenInOrder(
parent: HTMLElement,
childs: NodeListOf<ChildNode>,
options: CloneProps
) {
for (const node of childs) {
const childClone = await cloneNode(node as HTMLElement, {
...options,
root: false
});
if (childClone) parent.appendChild(childClone);
}
}
function processClone(
original: HTMLElement,
clone: HTMLElement,
options: CloneProps
) {
if (!(clone instanceof Element)) return clone;
// if (clone instanceof HTMLElement) removeAttributes(clone);
if (options.styles) {
copyStyle(original, clone, options);
clonePseudoElements(original, clone, options);
}
fixRelativeUrl(clone);
copyUserInput(original, clone);
fixSvg(clone);
return clone;
}
function fixRelativeUrl(node: HTMLElement) {
const attributes = ["href", "src"];
const baseUrl = window.location.href;
for (const attribute of attributes) {
const url = node.getAttribute(attribute);
const relativeUrl = url?.startsWith("http") ? undefined : url;
if (relativeUrl) {
const absoluteUrl = new URL(relativeUrl, baseUrl).href;
node.setAttribute(attribute, absoluteUrl);
}
}
}
function copyFont(source: CSSStyleDeclaration, target: CSSStyleDeclaration) {
target.font = source.font;
target.fontFamily = source.fontFamily;
target.fontFeatureSettings = source.fontFeatureSettings;
target.fontKerning = source.fontKerning;
target.fontSize = source.fontSize;
target.fontStretch = source.fontStretch;
target.fontStyle = source.fontStyle;
target.fontVariant = source.fontVariant;
target.fontVariantCaps = source.fontVariantCaps;
target.fontVariantEastAsian = source.fontVariantEastAsian;
target.fontVariantLigatures = source.fontVariantLigatures;
target.fontVariantNumeric = source.fontVariantNumeric;
target.fontVariationSettings = source.fontVariationSettings;
target.fontWeight = source.fontWeight;
}
function copyStyle(
sourceElement: HTMLElement,
targetElement: HTMLElement,
options: CloneProps
) {
const { getElementStyles } = options;
const sourceComputedStyles =
getElementStyles && getElementStyles(sourceElement);
if (!sourceComputedStyles) return;
targetElement.style.cssText = sourceComputedStyles.cssText;
if (sourceElement.tagName.toLowerCase() === "body") {
copyFont(getComputedStyle(sourceElement), targetElement.style);
}
const styles = targetElement.getAttribute("style");
if (styles) targetElement.setAttribute("style", minifyStyles(styles));
}
function clonePseudoElements(
original: HTMLElement,
clone: HTMLElement,
options: CloneProps
) {
const { getPseudoElementStyles } = options;
let hasPseudoElements = false;
const styleElement = document.createElement("style");
const className = `pseudo--${uid()}`;
for (const element of [":before", ":after"]) {
const style =
(getPseudoElementStyles && getPseudoElementStyles(original, element)) ||
getComputedStyle(original, element);
if (!style.cssText) continue;
const selector = `.${className}:${element} {
${style.cssText}
}`;
styleElement.appendChild(document.createTextNode(selector));
hasPseudoElements = true;
}
if (hasPseudoElements) {
clone.className = className;
clone.appendChild(styleElement);
}
return hasPseudoElements;
}
function copyUserInput(original: HTMLElement, clone: HTMLElement) {
if (
original instanceof HTMLInputElement ||
original instanceof HTMLTextAreaElement
)
clone.setAttribute("value", original.value);
}
function fixSvg(clone: Element) {
if (!(clone instanceof SVGElement)) return;
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
// if (!(clone instanceof SVGRectElement)) return;
["width", "height"].forEach(function (attribute) {
const value = clone.getAttribute(attribute);
if (!value || !!clone.style.getPropertyValue(attribute)) return;
clone.style.setProperty(attribute, value);
});
}
function minifyStyles(text: string) {
return text.replace(/(:?[:;])(:? +)/gm, (_full, sep) => {
return sep;
});
}

View File

@@ -16,12 +16,18 @@ 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 <http://www.gnu.org/licenses/>.
*/
import { isSVGElement } from "./clone.js";
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 } from "./utils.js";
import {
canvasToBlob,
delay,
escapeXhtml,
height,
width,
isSVGElement
} from "./utils.js";
import { inlineStylesheets } from "./styles.js";
// Default impl options

View File

@@ -455,9 +455,10 @@ async function getPage(
config?: Config,
onlyVisible = false
) {
const fetchOptions = resolveFetchOptions(config);
const body = await getInlinedNode(document.body, {
raster: true,
fetchOptions: resolveFetchOptions(config),
fetchOptions,
inlineOptions: {
fonts: false,
inlineImages: config?.inlineImages,
@@ -478,7 +479,7 @@ async function getPage(
head.appendChild(title);
if (config?.styles) {
await addStylesToHead(head, resolveFetchOptions(config));
await addStylesToHead(head, fetchOptions);
}
return {

View File

@@ -173,6 +173,90 @@ function safeQuerySelectorAll(root: Node, selector: string) {
}
}
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,
@@ -188,5 +272,6 @@ export {
escapeXhtml,
width,
height,
safeQuerySelectorAll
safeQuerySelectorAll,
isSVGElement
};