From fee7e1e7b63c888bc1c5205126b05c63073ebdd3 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sun, 29 Nov 2020 22:06:13 +0100 Subject: [PATCH] fix(svg): rewrite all IDs to make them unique within the outer SVG document --- src/element.ts | 3 ++- src/inline.ts | 4 ++- src/svg.ts | 67 +++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/element.ts b/src/element.ts index fb5bac0e..f7fa61ab 100644 --- a/src/element.ts +++ b/src/element.ts @@ -167,6 +167,7 @@ export function handleElement(element: Element, context: Readonly { // Let handleSvgNode inline the into a simple const svgDocument = element.ownerDocument const mount = svgDocument.createElementNS(svgNamespace, 'g') + assert(element.id, ' element must have ID') handleSvgNode(svgRoot, { currentSvgParent: mount, svgDocument, + idPrefix: `${element.id}-`, options: { // SVGs embedded through are never interactive. keepLinks: false, @@ -57,7 +59,7 @@ export async function inlineResources(element: Element): Promise { }) // Replace the element with the - mount.dataset.tag = 'image' + mount.dataset.tag = 'img' mount.setAttribute('role', 'img') svgRoot.replaceWith(mount) } finally { diff --git a/src/svg.ts b/src/svg.ts index c38a5c08..1e5033bc 100644 --- a/src/svg.ts +++ b/src/svg.ts @@ -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 `` element, inlining it into the output SVG document with the necessary transforms. */ -export function handleSvgNode( - node: Node, - context: Pick -): void { +export function handleSvgNode(node: Node, context: SvgTraversalContext): void { if (isElement(node)) { if (!isSVGElement(node)) { return @@ -33,10 +31,15 @@ export function handleSvgNode( const ignoredElements = new Set(['script', 'style', 'foreignElement']) -export function handleSvgElement( - element: SVGElement, - context: Pick -): void { +interface SvgTraversalContext extends Pick { + /** + * 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 } @@ -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) { @@ -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") @@ -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) @@ -212,6 +237,24 @@ const defaults: Record = visibility: 'visible', } +/** + * Prefixes all ID references of the form `url(#id)` in the given string. + */ +function rewriteUrlIdReferences(value: string, { idPrefix }: Pick): 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,