Skip to content

Commit

Permalink
feat: support svg <use> href
Browse files Browse the repository at this point in the history
  • Loading branch information
qq15725 committed Apr 26, 2023
1 parent f953536 commit 81a78ce
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 6 deletions.
13 changes: 12 additions & 1 deletion src/clone-element.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { isCanvasElement, isIFrameElement, isImageElement, isVideoElement } from './utils'
import { cloneSvg } from './clone-svg'
import {
isCanvasElement,
isIFrameElement,
isImageElement,
isSVGSVGElementNode,
isVideoElement,
} from './utils'
import { cloneIframe } from './clone-iframe'
import { cloneCanvas } from './clone-canvas'
import { cloneVideo } from './clone-video'
Expand All @@ -25,5 +32,9 @@ export function cloneElement<T extends HTMLElement | SVGElement>(
return cloneVideo(node)
}

if (isSVGSVGElementNode(node)) {
return cloneSvg(node, context)
}

return node.cloneNode(false) as T
}
30 changes: 30 additions & 0 deletions src/clone-svg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { cloneNode } from './clone-node'
import type { Context } from './context'

export async function cloneSvg<T extends SVGElement>(
node: T,
context: Context,
): Promise<SVGElement> {
const { ownerDocument, svgDefsElement } = context

const uses = node.querySelectorAll?.('use') ?? []

if (uses.length) {
for (let len = uses.length, i = 0; i < len; i++) {
const use = uses[i]

const id = use.getAttribute('xlink:href')
?? use.getAttribute('href')

if (!id) continue

const definition = ownerDocument?.querySelector(`svg ${ id }`)

if (!definition || svgDefsElement?.querySelector(id)) continue

svgDefsElement?.appendChild(await cloneNode(definition, context))
}
}

return node.cloneNode(false) as T
}
5 changes: 5 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export interface InternalContext<T extends Node> {
*/
svgStyleElement?: HTMLStyleElement

/**
* The `defs` element under the root `svg` element
*/
svgDefsElement?: SVGDefsElement

/**
* The `svgStyleElement` class styles
*
Expand Down
6 changes: 4 additions & 2 deletions src/converts/dom-to-canvas.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createStyleElement, orCreateContext } from '../create-context'
import { createImage, svgToDataUrl } from '../utils'
import { createImage, svgToDataUrl, xmlns } from '../utils'
import { imageToCanvas } from '../image-to-canvas'
import { domToForeignObjectSvg } from './dom-to-foreign-object-svg'
import type { Context } from '../context'

import type { Context } from '../context'
import type { Options } from '../options'

export async function domToCanvas<T extends Node>(node: T, options?: Options): Promise<HTMLCanvasElement>
Expand All @@ -14,6 +14,8 @@ export async function domToCanvas(node: any, options?: any) {
const dataUrl = svgToDataUrl(svg)
if (!context.autoDestruct) {
context.svgStyleElement = createStyleElement(context.ownerDocument)
context.svgDefsElement = context.ownerDocument?.createElementNS(xmlns, 'defs')
context.svgStyles.clear()
}
const image = createImage(dataUrl, svg.ownerDocument)
return await imageToCanvas(image, context)
Expand Down
2 changes: 2 additions & 0 deletions src/converts/dom-to-foreign-object-svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export async function domToForeignObjectSvg(node: any, options?: any) {
log,
tasks,
svgStyleElement,
svgDefsElement,
svgStyles,
font,
progress,
Expand Down Expand Up @@ -75,6 +76,7 @@ export async function domToForeignObjectSvg(node: any, options?: any) {
onEmbedNode?.(clone)

const svg = createForeignObjectSvg(clone, context)
svgDefsElement && svg.insertBefore(svgDefsElement, svg.children[0])
svgStyleElement && svg.insertBefore(svgStyleElement, svg.children[0])

autoDestruct && destroyContext(context)
Expand Down
3 changes: 2 additions & 1 deletion src/create-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
SUPPORT_WEB_WORKER,
isContext,
isElementNode,
supportWebp, waitUntilLoad,
supportWebp, waitUntilLoad, xmlns,
} from './utils'
import type { Context, Request } from './context'
import type { Options } from './options'
Expand Down Expand Up @@ -63,6 +63,7 @@ export async function createContext<T extends Node>(node: T, options?: Options &
ownerWindow,
dpi: scale === 1 ? null : 96 * scale,
svgStyleElement: createStyleElement(ownerDocument),
svgDefsElement: ownerDocument?.createElementNS(xmlns, 'defs'),
svgStyles: new Map<string, string[]>(),
defaultComputedStyles: new Map<string, Record<string, any>>(),
workers: [
Expand Down
1 change: 1 addition & 0 deletions src/destroy-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export function destroyContext(context: Context) {
context.ownerDocument = undefined
context.ownerWindow = undefined
context.svgStyleElement = undefined
context.svgDefsElement = undefined
context.svgStyles.clear()
context.defaultComputedStyles.clear()
if (context.sandbox) {
Expand Down
7 changes: 5 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export const isCSSImportRule = (rule: CSSRule): rule is CSSImportRule => rule.co
// Element
export const isElementNode = (node: Node): node is Element => node.nodeType === 1 // Node.ELEMENT_NODE
export const isSVGElementNode = (node: Element): node is SVGElement => typeof (node as SVGElement).className === 'object'
export const isSVGImageElementNode = (node: Element): node is SVGImageElement => isSVGElementNode(node) && node.tagName === 'IMAGE'
export const isSVGSVGElementNode = (node: Element): node is SVGSVGElement => isSVGElementNode(node) && node.tagName === 'svg'
export const isSVGImageElementNode = (node: Element): node is SVGImageElement => isSVGElementNode(node) && node.tagName === 'image'
export const isHTMLElementNode = (node: Node): node is HTMLElement => isElementNode(node) && typeof (node as HTMLElement).style !== 'undefined' && !isSVGElementNode(node)
export const isCommentNode = (node: Node): node is Text => node.nodeType === 8 // Node.COMMENT_NODE
export const isTextNode = (node: Node): node is Text => node.nodeType === 3 // Node.TEXT_NODE
Expand Down Expand Up @@ -87,8 +88,10 @@ export function getDocument<T extends Node>(target?: T | null): Document {
) as any
}

export const xmlns = 'http://www.w3.org/2000/svg'

export function createSvg(width: number, height: number, ownerDocument?: Document | null): SVGSVGElement {
const svg = getDocument(ownerDocument).createElementNS('http://www.w3.org/2000/svg', 'svg')
const svg = getDocument(ownerDocument).createElementNS(xmlns, 'svg')
svg.setAttributeNS(null, 'width', width.toString())
svg.setAttributeNS(null, 'height', height.toString())
svg.setAttributeNS(null, 'viewBox', `0 0 ${ width } ${ height }`)
Expand Down

1 comment on commit 81a78ce

@vercel
Copy link

@vercel vercel bot commented on 81a78ce Apr 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

modern-screenshot – ./

modern-screenshot.vercel.app
modern-screenshot-qq15725.vercel.app
modern-screenshot-git-main-qq15725.vercel.app

Please sign in to comment.