UNPKG

dom-to-svg

Version:
245 lines 10.1 kB
import cssValueParser from 'postcss-value-parser'; import { parseCSSLength } from './css.js'; import { isElement, isSVGAnchorElement, isSVGElement, isSVGGraphicsElement, isSVGSVGElement, isSVGTextContentElement, isTextNode, svgNamespace, } from './dom.js'; import { copyTextStyles } from './text.js'; import { assert, diagonale } from './util.js'; /** * Recursively clone an `<svg>` element, inlining it into the output SVG document with the necessary transforms. */ export function handleSvgNode(node, context) { if (isElement(node)) { if (!isSVGElement(node)) { return; } handleSvgElement(node, context); } else if (isTextNode(node)) { const clonedTextNode = node.cloneNode(true); context.currentSvgParent.append(clonedTextNode); } } const ignoredElements = new Set(['script', 'style', 'foreignElement']); const URL_ID_REFERENCE_REGEX = /\burl\(["']?#/; export function handleSvgElement(element, context) { var _a, _b, _c, _d; if (ignoredElements.has(element.tagName)) { return; } let elementToAppend; if (isSVGSVGElement(element)) { const contentContainer = context.svgDocument.createElementNS(svgNamespace, 'g'); elementToAppend = contentContainer; contentContainer.classList.add('svg-content', ...element.classList); contentContainer.dataset.viewBox = (_a = element.getAttribute('viewBox')) !== null && _a !== void 0 ? _a : ''; contentContainer.dataset.width = (_b = element.getAttribute('width')) !== null && _b !== void 0 ? _b : ''; contentContainer.dataset.height = (_c = element.getAttribute('height')) !== null && _c !== void 0 ? _c : ''; // Since the SVG is getting inlined into the output SVG, we need to transform its contents according to its // viewBox, width, height and preserveAspectRatio. We can use getScreenCTM() for this on one of its // SVGGraphicsElement children (in Chrome calling it on the <svg> works too, but not in Firefox: // https://bugzilla.mozilla.org/show_bug.cgi?id=873106). for (const child of element.children) { if (!isSVGGraphicsElement(child)) { continue; } let viewBoxTransformMatrix = // When this function is called on an inline <svg> element in the original DOM, we want // getScreenCTM() to map it to the DOM coordinate system. When this function is called from // inlineResources() the <svg> is already embedded into the output <svg>. In that case the output // SVG already has a viewBox, and the coordinate system of the SVG is not equal to the coordinate // system of the screen, therefor we need to use getCTM() to map it into the output SVG's // coordinate system. child.ownerDocument !== context.svgDocument && // When we inline an SVG, we put a transform on it for the getScreenCTM(). When that SVG also // contains another SVG, the inner SVG should just get transformed relative to the outer SVG, not // relative to the screen, because the transforms will stack in the output SVG. !((_d = element.parentElement) === null || _d === void 0 ? void 0 : _d.closest('svg')) ? child.getScreenCTM() : child.getCTM(); // This should only be null if the <svg> is `display: none` if (!viewBoxTransformMatrix) { break; } // Make sure to handle a child that already has a transform. That transform should only apply to the // child, not to the entire SVG contents, so we need to calculate it out. if (child.transform.baseVal.numberOfItems > 0) { child.transform.baseVal.consolidate(); const existingTransform = child.transform.baseVal.getItem(0).matrix; viewBoxTransformMatrix = viewBoxTransformMatrix.multiply(existingTransform.inverse()); } contentContainer.transform.baseVal.appendItem(contentContainer.transform.baseVal.createSVGTransformFromMatrix(viewBoxTransformMatrix)); break; } } else { // Clone element if (isSVGAnchorElement(element) && !context.options.keepLinks) { elementToAppend = context.svgDocument.createElementNS(svgNamespace, 'g'); } else { elementToAppend = element.cloneNode(false); } // Remove event handlers for (const attribute of elementToAppend.attributes) { if (attribute.localName.startsWith('on')) { elementToAppend.attributes.removeNamedItemNS(attribute.namespaceURI, attribute.localName); } else if (attribute.localName === 'href' && attribute.value.startsWith('javascript:')) { elementToAppend.attributes.removeNamedItemNS(attribute.namespaceURI, attribute.localName); } } const window = element.ownerDocument.defaultView; assert(window, "Element's ownerDocument has no defaultView"); const svgViewportElement = element.ownerSVGElement; assert(svgViewportElement, 'Expected element to have ownerSVGElement'); const styles = window.getComputedStyle(element); if (isSVGGraphicsElement(element)) { copyGraphicalPresentationAttributes(styles, elementToAppend, svgViewportElement.viewBox.animVal); if (isSVGTextContentElement(element)) { 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)); } } } // Make sure all IDs are unique if (elementToAppend.id) { elementToAppend.id = context.idPrefix + elementToAppend.id; } context.currentSvgParent.append(elementToAppend); for (const child of element.childNodes) { handleSvgNode(child, { ...context, currentSvgParent: elementToAppend }); } } const graphicalPresentationAttributes = [ 'alignment-baseline', 'baseline-shift', // 'clip', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', // 'color-profile', 'color-rendering', // 'cursor', 'direction', // 'display', // 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'flood-color', 'flood-opacity', 'image-rendering', 'lighting-color', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', // 'overflow', 'pointer-events', 'shape-rendering', // 'solid-color', // 'solid-opacity', 'stop-color', 'stop-opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'transform', 'vector-effect', 'visibility', ]; const defaults = { 'alignment-baseline': 'auto', 'baseline-shift': '0px', 'clip-path': 'none', 'clip-rule': 'nonzero', 'color-interpolation-filters': 'linearrgb', 'color-interpolation': 'srgb', 'color-rendering': 'auto', 'fill-opacity': '1', 'fill-rule': 'nonzero', 'flood-color': 'rgb(0, 0, 0)', 'flood-opacity': '1', 'image-rendering': 'auto', 'lighting-color': 'rgb(255, 255, 255)', 'marker-end': 'none', 'marker-mid': 'none', 'marker-start': 'none', 'pointer-events': 'auto', 'shape-rendering': 'auto', 'stop-color': 'rgb(0, 0, 0)', 'stop-opacity': '1', 'stroke-dasharray': 'none', 'stroke-dashoffset': '0px', 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter', 'stroke-miterlimit': '4', 'stroke-opacity': '1', 'stroke-width': '1px', 'vector-effect': 'none', color: '', direction: 'ltr', fill: '', filter: 'none', mask: 'none', opacity: '1', stroke: '', transform: 'none', visibility: 'visible', }; /** * Prefixes all ID references of the form `url(#id)` in the given string. */ function rewriteUrlIdReferences(value, { idPrefix }) { 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, target, viewBox) { var _a; for (const attribute of graphicalPresentationAttributes) { let value = styles.getPropertyValue(attribute); if (value && value !== defaults[attribute]) { if (value.endsWith('%')) { // E.g. https://svgwg.org/svg2-draft/painting.html#StrokeWidth // Percentages: refer to the normalized diagonal of the current SVG viewport (see Units) value = (_a = parseCSSLength(value, diagonale(viewBox))) !== null && _a !== void 0 ? _a : 0; } target.setAttribute(attribute, value.toString()); } } } //# sourceMappingURL=svg.js.map