From 27cd25bc339e7f39b91b31c95055726d46f7dbf1 Mon Sep 17 00:00:00 2001 From: Justineo Date: Fri, 22 Sep 2023 12:35:01 +0800 Subject: [PATCH] feat: add `includeStyleProperties` option * the full list of computedStyle properties can be cached * users can now manually specify which style properties are included --- src/apply-style.ts | 2 +- src/clone-node.ts | 37 ++++++++++++++++-------- src/clone-pseudos.ts | 20 ++++++++----- src/types.ts | 6 ++++ src/util.ts | 16 ++++++++++ test/resources/style/image-include-style | 1 + test/spec/options.spec.ts | 12 ++++++++ 7 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 test/resources/style/image-include-style diff --git a/src/apply-style.ts b/src/apply-style.ts index da843cf6..5c58728d 100644 --- a/src/apply-style.ts +++ b/src/apply-style.ts @@ -1,4 +1,4 @@ -import { Options } from './types' +import type { Options } from './types' export function applyStyle( node: T, diff --git a/src/clone-node.ts b/src/clone-node.ts index 500ce332..5185dd41 100644 --- a/src/clone-node.ts +++ b/src/clone-node.ts @@ -1,6 +1,11 @@ import type { Options } from './types' import { clonePseudoElements } from './clone-pseudos' -import { createImage, toArray, isInstanceOfElement } from './util' +import { + createImage, + toArray, + isInstanceOfElement, + getStyleProperties, +} from './util' import { getMimeType } from './mimes' import { resourceToDataURL } from './dataurl' @@ -29,12 +34,12 @@ async function cloneVideoElement(video: HTMLVideoElement, options: Options) { return createImage(dataURL) } -async function cloneIFrameElement(iframe: HTMLIFrameElement) { +async function cloneIFrameElement(iframe: HTMLIFrameElement, options: Options) { try { if (iframe?.contentDocument?.body) { return (await cloneNode( iframe.contentDocument.body, - {}, + options, true, )) as HTMLBodyElement } @@ -58,7 +63,7 @@ async function cloneSingleNode( } if (isInstanceOfElement(node, HTMLIFrameElement)) { - return cloneIFrameElement(node) + return cloneIFrameElement(node, options) } return node.cloneNode(false) as T @@ -107,7 +112,11 @@ async function cloneChildren( return clonedNode } -function cloneCSSStyle(nativeNode: T, clonedNode: T) { +function cloneCSSStyle( + nativeNode: T, + clonedNode: T, + options: Options, +) { const targetStyle = clonedNode.style if (!targetStyle) { return @@ -118,7 +127,7 @@ function cloneCSSStyle(nativeNode: T, clonedNode: T) { targetStyle.cssText = sourceStyle.cssText targetStyle.transformOrigin = sourceStyle.transformOrigin } else { - toArray(sourceStyle).forEach((name) => { + getStyleProperties(options).forEach((name) => { let value = sourceStyle.getPropertyValue(name) if (name === 'font-size' && value.endsWith('px')) { const reducedFont = @@ -133,11 +142,11 @@ function cloneCSSStyle(nativeNode: T, clonedNode: T) { ) { value = 'block' } - + if (name === 'd' && clonedNode.getAttribute('d')) { value = `path(${clonedNode.getAttribute('d')})` } - + targetStyle.setProperty( name, value, @@ -170,10 +179,14 @@ function cloneSelectValue(nativeNode: T, clonedNode: T) { } } -function decorate(nativeNode: T, clonedNode: T): T { +function decorate( + nativeNode: T, + clonedNode: T, + options: Options, +): T { if (isInstanceOfElement(clonedNode, Element)) { - cloneCSSStyle(nativeNode, clonedNode) - clonePseudoElements(nativeNode, clonedNode) + cloneCSSStyle(nativeNode, clonedNode, options) + clonePseudoElements(nativeNode, clonedNode, options) cloneInputValue(nativeNode, clonedNode) cloneSelectValue(nativeNode, clonedNode) } @@ -240,6 +253,6 @@ export async function cloneNode( return Promise.resolve(node) .then((clonedNode) => cloneSingleNode(clonedNode, options) as Promise) .then((clonedNode) => cloneChildren(node, clonedNode, options)) - .then((clonedNode) => decorate(node, clonedNode)) + .then((clonedNode) => decorate(node, clonedNode, options)) .then((clonedNode) => ensureSVGSymbols(clonedNode, options)) } diff --git a/src/clone-pseudos.ts b/src/clone-pseudos.ts index ea1fe656..b5af1edc 100644 --- a/src/clone-pseudos.ts +++ b/src/clone-pseudos.ts @@ -1,4 +1,5 @@ -import { uuid, toArray } from './util' +import type { Options } from './types' +import { uuid, getStyleProperties } from './util' type Pseudo = ':before' | ':after' @@ -7,8 +8,8 @@ function formatCSSText(style: CSSStyleDeclaration) { return `${style.cssText} content: '${content.replace(/'|"/g, '')}';` } -function formatCSSProperties(style: CSSStyleDeclaration) { - return toArray(style) +function formatCSSProperties(style: CSSStyleDeclaration, options: Options) { + return getStyleProperties(options) .map((name) => { const value = style.getPropertyValue(name) const priority = style.getPropertyPriority(name) @@ -22,11 +23,12 @@ function getPseudoElementStyle( className: string, pseudo: Pseudo, style: CSSStyleDeclaration, + options: Options, ): Text { const selector = `.${className}:${pseudo}` const cssText = style.cssText ? formatCSSText(style) - : formatCSSProperties(style) + : formatCSSProperties(style, options) return document.createTextNode(`${selector}{${cssText}}`) } @@ -35,6 +37,7 @@ function clonePseudoElement( nativeNode: T, clonedNode: T, pseudo: Pseudo, + options: Options, ) { const style = window.getComputedStyle(nativeNode, pseudo) const content = style.getPropertyValue('content') @@ -50,14 +53,17 @@ function clonePseudoElement( } const styleElement = document.createElement('style') - styleElement.appendChild(getPseudoElementStyle(className, pseudo, style)) + styleElement.appendChild( + getPseudoElementStyle(className, pseudo, style, options), + ) clonedNode.appendChild(styleElement) } export function clonePseudoElements( nativeNode: T, clonedNode: T, + options: Options, ) { - clonePseudoElement(nativeNode, clonedNode, ':before') - clonePseudoElement(nativeNode, clonedNode, ':after') + clonePseudoElement(nativeNode, clonedNode, ':before', options) + clonePseudoElement(nativeNode, clonedNode, ':after', options) } diff --git a/src/types.ts b/src/types.ts index b511363f..8efac519 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,12 @@ export interface Options { * An object whose properties to be copied to node's style before rendering. */ style?: Partial + /** + * An array of style properties to be copied to node's style before rendering. + * For performance-critical scenarios, users may want to specify only the + * required properties instead of all styles. + */ + includeStyleProperties?: string[] /** * A function taking DOM node as argument. Should return `true` if passed * node should be included in the output. Excluding node means excluding diff --git a/src/util.ts b/src/util.ts index d418b621..3445408d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -65,6 +65,22 @@ export function toArray(arrayLike: any): T[] { return arr } +let styleProps: string[] | null = null +export function getStyleProperties(options: Options = {}): string[] { + if (styleProps) { + return styleProps + } + + if (options.includeStyleProperties) { + styleProps = options.includeStyleProperties + return styleProps + } + + styleProps = toArray(window.getComputedStyle(document.documentElement)) + + return styleProps +} + function px(node: HTMLElement, styleProperty: string) { const win = node.ownerDocument.defaultView || window const val = win.getComputedStyle(node).getPropertyValue(styleProperty) diff --git a/test/resources/style/image-include-style b/test/resources/style/image-include-style new file mode 100644 index 00000000..5c3c0346 --- /dev/null +++ b/test/resources/style/image-include-style @@ -0,0 +1 @@ +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAXJJREFUeF7t3LENwDAMxEBp/6FlJFNcQU9AkHio897MTY8xsAVhWvwgBbF6FATrUZCCaAYwnm5IQTADGE4LKQhmAMNpIQXBDGA4LaQgmAEMp4UUBDOA4bSQgmAGMJwWUhDMAIbTQgqCGcBwWkhBMAMYTgspCGYAw2khBcEMYDgtpCCYAQynhRQEM4DhtJCCYAYwnBZSEMwAhtNCCoIZwHBaSEEwAxhOCykIZgDDaSEFwQxgOC2kIJgBDKeFFAQzgOG0kIJgBjCcnbl+lIOiFASK8aEUpCCYAQynhRQEM4DhtJCCYAYwnBZSEMwAhtNCCoIZwHBaSEEwAxhOCykIZgDDaSEFwQxgOC2kIJgBDKeFFAQzgOG0kIJgBjCcFlIQzACG00IKghnAcFpIQTADGE4LKQhmAMNpIQXBDGA4LaQgmAEMp4UUBDOA4bSQgmAGMJwWUhDMAIbTQgqCGcBwWkhBMAMYTgspCGYAw2khBcEMYDgPsePHnRi+YEEAAAAASUVORK5CYII= \ No newline at end of file diff --git a/test/spec/options.spec.ts b/test/spec/options.spec.ts index ab40ffa2..f08ce43b 100644 --- a/test/spec/options.spec.ts +++ b/test/spec/options.spec.ts @@ -67,6 +67,18 @@ describe('work with options', () => { .catch(done) }) + it('should only clone specified style properties when includeStyleProperties is provided', (done) => { + bootstrap('style/node.html', 'style/style.css', 'style/image-include-style') + .then((node) => { + return toPng(node, { + includeStyleProperties: ['width', 'height'], + }) + }) + .then(check) + .then(done) + .catch(done) + }) + it('should combine dimensions and style', (done) => { bootstrap('scale/node.html', 'scale/style.css', 'scale/image') .then((node) => {