Skip to content

Commit

Permalink
fix(svg): rewrite all IDs to make them unique within the outer SVG do…
Browse files Browse the repository at this point in the history
…cument
  • Loading branch information
felixfbecker committed Nov 29, 2020
1 parent 45acdae commit fee7e1e
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 14 deletions.
3 changes: 2 additions & 1 deletion src/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export function handleElement(element: Element, context: Readonly<TraversalConte

if (rectanglesIntersect && isHTMLImageElement(element)) {
const svgImage = context.svgDocument.createElementNS(svgNamespace, 'image')
svgImage.id = `${id}-image` // read by inlineResources()
svgImage.setAttribute('href', element.src)
const paddingLeft = parseCSSLength(styles.paddingLeft, bounds.width) ?? 0
const paddingRight = parseCSSLength(styles.paddingRight, bounds.width) ?? 0
Expand Down Expand Up @@ -199,7 +200,7 @@ export function handleElement(element: Element, context: Readonly<TraversalConte
childContext.stackingLayers.inFlowInlineLevelNonPositionedDescendants.append(svgTextElement)
}
} else if (rectanglesIntersect && isSVGSVGElement(element) && isVisible(styles)) {
handleSvgNode(element, childContext)
handleSvgNode(element, { ...childContext, idPrefix: `${id}-` })
} else {
// Walk children even if rectangles don't intersect,
// because children can overflow the parent's bounds as long as overflow: visible (default).
Expand Down
4 changes: 3 additions & 1 deletion src/inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ export async function inlineResources(element: Element): Promise<void> {
// Let handleSvgNode inline the <svg> into a simple <g>
const svgDocument = element.ownerDocument
const mount = svgDocument.createElementNS(svgNamespace, 'g')
assert(element.id, '<image> element must have ID')
handleSvgNode(svgRoot, {
currentSvgParent: mount,
svgDocument,
idPrefix: `${element.id}-`,
options: {
// SVGs embedded through <img> are never interactive.
keepLinks: false,
Expand All @@ -57,7 +59,7 @@ export async function inlineResources(element: Element): Promise<void> {
})

// Replace the <svg> element with the <g>
mount.dataset.tag = 'image'
mount.dataset.tag = 'img'
mount.setAttribute('role', 'img')
svgRoot.replaceWith(mount)
} finally {
Expand Down
67 changes: 55 additions & 12 deletions src/svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ import { TraversalContext } from './traversal'
import { assert, diagonale } from './util'
import { parseCSSLength } from './css'
import { copyTextStyles } from './text'
import cssValueParser from 'postcss-value-parser'

/**
* Recursively clone an `<svg>` element, inlining it into the output SVG document with the necessary transforms.
*/
export function handleSvgNode(
node: Node,
context: Pick<TraversalContext, 'svgDocument' | 'currentSvgParent' | 'options'>
): void {
export function handleSvgNode(node: Node, context: SvgTraversalContext): void {
if (isElement(node)) {
if (!isSVGElement(node)) {
return
Expand All @@ -33,10 +31,15 @@ export function handleSvgNode(

const ignoredElements = new Set(['script', 'style', 'foreignElement'])

export function handleSvgElement(
element: SVGElement,
context: Pick<TraversalContext, 'svgDocument' | 'currentSvgParent' | 'options'>
): void {
interface SvgTraversalContext extends Pick<TraversalContext, 'svgDocument' | 'currentSvgParent' | 'options'> {
/**
* A prefix to use for all ID to make them unique inside the output SVG document.
*/
readonly idPrefix: string
}

const URL_ID_REFERENCE_REGEX = /\burl\(["']?#/
export function handleSvgElement(element: SVGElement, context: SvgTraversalContext): void {
if (ignoredElements.has(element.tagName)) {
return
}
Expand Down Expand Up @@ -80,6 +83,11 @@ export function handleSvgElement(
)
break
}

// Make all IDs unique
for (const descendant of element.querySelectorAll('[id]')) {
descendant.id = context.idPrefix + descendant.id
}
} else {
// Clone element
if (isSVGAnchorElement(element) && !context.options.keepLinks) {
Expand All @@ -97,10 +105,6 @@ export function handleSvgElement(
}
}

if (element.id) {
elementToAppend.id = element.id
}

const window = element.ownerDocument.defaultView
assert(window, "Element's ownerDocument has no defaultView")

Expand All @@ -116,6 +120,27 @@ export function handleSvgElement(
copyTextStyles(styles, elementToAppend)
}
}

// Namespace ID references url(#...)
for (const attribute of elementToAppend.attributes) {
if (attribute.localName === 'href') {
if (attribute.value.startsWith('#')) {
attribute.value = attribute.value.replace('#', `#${context.idPrefix}`)
}
} else if (URL_ID_REFERENCE_REGEX.test(attribute.value)) {
attribute.value = rewriteUrlIdReferences(attribute.value, context)
}
}
for (const property of elementToAppend.style) {
const value = elementToAppend.style.getPropertyValue(property)
if (URL_ID_REFERENCE_REGEX.test(value)) {
elementToAppend.style.setProperty(
property,
rewriteUrlIdReferences(value, context),
elementToAppend.style.getPropertyPriority(property)
)
}
}
}

context.currentSvgParent.append(elementToAppend)
Expand Down Expand Up @@ -212,6 +237,24 @@ const defaults: Record<typeof graphicalPresentationAttributes[number], string> =
visibility: 'visible',
}

/**
* Prefixes all ID references of the form `url(#id)` in the given string.
*/
function rewriteUrlIdReferences(value: string, { idPrefix }: Pick<SvgTraversalContext, 'idPrefix'>): string {
const parsedValue = cssValueParser(value)
parsedValue.walk(node => {
if (node.type !== 'function' || node.value !== 'url') {
return
}
const urlArgument = node.nodes[0]
if (!urlArgument) {
return
}
urlArgument.value = urlArgument.value.replace('#', `#${idPrefix}`)
})
return cssValueParser.stringify(parsedValue.nodes)
}

function copyGraphicalPresentationAttributes(
styles: CSSStyleDeclaration,
target: SVGElement,
Expand Down

0 comments on commit fee7e1e

Please sign in to comment.