Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dereferenceUses plugin #1279

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
782a87b
Add plugin file and tests.
strarsis Sep 2, 2020
8594b3a
Add code to replace the `<symbol>` element with configurable wrapper …
strarsis Sep 4, 2020
3bd002b
Fix test.
strarsis Sep 4, 2020
cb915dc
Add to plugins.js.
strarsis Mar 19, 2021
48d7c21
Apply linter fix to dereferenceUses plugin.
strarsis Mar 19, 2021
2de6eaa
Resolve package lock conflict with upstream.
strarsis Mar 22, 2021
c4397c7
Add new test for plugin (1279#issuecomment-803706532).
strarsis Mar 22, 2021
fa93ae0
Apply styles of <use/> element on top of styles of referenced element.
strarsis Mar 22, 2021
137341e
Fix expected result in test.
strarsis Mar 22, 2021
4d658eb
Merge branch 'master' of https://github.com/svg/svgo into dereference…
strarsis Mar 27, 2021
8fd5222
Apply recommended code style.
strarsis Mar 27, 2021
7d9135a
Improve constant and variable names.
strarsis Mar 27, 2021
0e426f6
Improve comment.
strarsis Mar 27, 2021
a37eb7b
Fix attribute override logic.
strarsis Mar 27, 2021
45244fc
Merge branch 'master' into dereferenceUses-plugin
strarsis Mar 29, 2021
60088b0
Simplify plugin test description.
strarsis Mar 30, 2021
8a497ef
Simplify plugin tests descriptions.
strarsis Mar 30, 2021
5dc8c65
Use href attribute names array for finding href attribute.
strarsis Mar 30, 2021
246d861
Remove large, rather redundant plugin test.
strarsis Mar 30, 2021
454e338
Rename options argument/variable to params.
strarsis Mar 30, 2021
96d0f7e
Use destructuring for plugin params.
strarsis Mar 30, 2021
470ab35
Merge branch 'main' into dereferenceUses-plugin
strarsis Mar 23, 2023
bb15cbe
Add param typings for dereferenceUses plugin.
strarsis Mar 23, 2023
e7226cb
Rewrite dereferenceUses plugin for new `svgo` architecture.
strarsis Mar 24, 2023
8db961d
Improve test comments.
strarsis Mar 24, 2023
cd30067
Increase test coverage.
strarsis Mar 24, 2023
92dcf8e
Fix `yarn` lockfile.
strarsis Mar 24, 2023
5712fee
Appy prettier.
strarsis Mar 24, 2023
314dd8e
Add `structuredClone` polyfill for older `node`s.
strarsis Mar 24, 2023
869c113
Add TS definitions for `@ungap/structured-clone`.
strarsis Mar 24, 2023
e797bb0
Merge remote-tracking branch 'origin' into dereferenceUses-plugin
strarsis Aug 29, 2024
99a01d4
Update imports and plugin declaration for current `svgo`.
strarsis Aug 29, 2024
886ade6
Add plugin documentation in new `svgo` documentation format.
strarsis Aug 29, 2024
bdde2ef
Update test fixture files to new `svgo` conventions.
strarsis Aug 29, 2024
bb4adde
Add `dereferenceUses` plugin as built-in `svgo` plugin.
strarsis Aug 29, 2024
58697fd
Remove npm lockfile.
strarsis Aug 29, 2024
eed23f1
Fix code lint errors in plugin code.
strarsis Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/04-plugins/dereferenceUses.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: dereferenceUses
svgo:
pluginId: dereferenceUses
defaultPlugin: false
parameters:
keepHref:
description: keep (xlink:)href attributes
default: false
symbolContainer:
description: browsers use <svg/> as container of <symbol/> content (e.g. <g> could also be used)
default: 'svg'
---

Dereferences <use/> elements.
2 changes: 2 additions & 0 deletions lib/builtin.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as convertPathData from '../plugins/convertPathData.js';
import * as convertShapeToPath from '../plugins/convertShapeToPath.js';
import * as convertStyleToAttrs from '../plugins/convertStyleToAttrs.js';
import * as convertTransform from '../plugins/convertTransform.js';
import * as dereferenceUses from '../plugins/dereferenceUses.js';
import * as mergeStyles from '../plugins/mergeStyles.js';
import * as inlineStyles from '../plugins/inlineStyles.js';
import * as mergePaths from '../plugins/mergePaths.js';
Expand Down Expand Up @@ -70,6 +71,7 @@ export const builtin = Object.freeze([
convertShapeToPath,
convertStyleToAttrs,
convertTransform,
dereferenceUses,
inlineStyles,
mergePaths,
mergeStyles,
Expand Down
172 changes: 172 additions & 0 deletions plugins/dereferenceUses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import * as csstree from 'css-tree';

import { visitSkip, querySelector } from '../lib/xast.js';

/**
* @typedef {import('../lib/types.js').XastElement} XastElement
* @typedef {import('../lib/types.js').XastParent} XastParent
*/

export const name = 'dereferenceUses';
export const description = 'dereferences <use/> elements';

export const params = {
keepHref: false, // keep (xlink:)href attributes
symbolContainer: 'svg', // browsers use <svg/> as container of <symbol/> content (e.g. <g> could also be used)
};

const OverridingUseAttributeNames = [
'x',
'y',
'width',
'height',
'href',
'xlink:href',
];

const HrefAttributeNames = [
'href', // By spec, `href` has precedence over (deprecated) `xlink:href`
'xlink:href',
];

/**
* Dereferences <use> elements
*
* @author strarsis <strarsis@gmail.com>
*
* @type {import('./plugins-types.js').Plugin<'dereferenceUses'>}
*/
export const fn = (root, params) => {
const { keepHref = false, symbolContainer = 'svg' } = params;

/**
* @type {Array<{ node: XastElement, parentNode: XastParent, targetNode?: XastElement }>}
*/
let useElements = [];

return {
element: {
enter: (node, parentNode) => {
if (node.name === 'use') {
useElements.push({
node,
parentNode,
});
return visitSkip;
}
},
},

root: {
exit: (root) => {
// replace each <use/> with its referenced node
for (const useElement of useElements) {
// `href`/`xlink:href` value
let href = '';
for (let hrefAttributeName of HrefAttributeNames) {
href = useElement.node.attributes[hrefAttributeName];
if (href) {
break; // The first occurence is to be used (spec).
}
}
if (!href) {
continue;
}

// look up referenced element
const targetElement = querySelector(root, href);
if (!targetElement || targetElement.type !== 'element') {
continue;
}

// clone referenced element for insertion
const insertElement = structuredClone(targetElement);

// Attribute inheritance of the referenced element
// @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use
// "Only the attributes x, y, width, height and href on the use element will override those set on the referenced element.
// However, any other attributes not set on the referenced element will be applied to the use element."
const insertElementAttributeNames = Object.keys(
insertElement.attributes,
);
for (const attributeName in useElement.node.attributes) {
// don't remove attributes from the referenced element that, by spec, override the one of the <use> element
if (
insertElementAttributeNames.includes(attributeName) &&
!OverridingUseAttributeNames.includes(attributeName)
) {
continue;
}

// don't remove href attribute with keepHref option turned on
if (!keepHref && HrefAttributeNames.includes(attributeName)) {
continue;
}

// styles must be appended, not overriden
if (attributeName === 'style') {
continue;
}

// set overriding attributes from referenced node
insertElement.attributes[attributeName] =
useElement.node.attributes[attributeName];
}

// only the original node is allowed to have this ID (IDs must be unique)
delete insertElement.attributes['id']; // remove ID attribute

// <symbol/> elements are template elements (hence not visible),
// browsers would place a <symbol/> element as a different element
if (insertElement.name === 'symbol') {
insertElement.name = symbolContainer;
}

// apply styles of <use/> element also on top of the referenced element
const useElementStyles = useElement.node.attributes.style;
const insertElementStyles = insertElement.attributes.style;
const styleParseOpts = { context: 'declarationList' };
if (useElementStyles) {
const useElementStylesAst = csstree.parse(
useElementStyles,
styleParseOpts,
);
if (useElementStylesAst.type !== 'DeclarationList') {
continue;
}

const insertElementStylesAst = csstree.parse(
insertElementStyles,
styleParseOpts,
);
if (insertElementStylesAst.type !== 'DeclarationList') {
continue;
}

insertElementStylesAst.children.appendList(
useElementStylesAst.children,
);

const insertElementStylesAppended = csstree.generate(
insertElementStylesAst,
);
insertElement.attributes.style = insertElementStylesAppended; // append styles (styles from <use/> have higher priority)
}

// replace the <use/> element with the referenced, resolved element

// position of <use/> in parent
const useElementPosition = useElement.parentNode.children.indexOf(
useElement.node,
);

useElement.parentNode.children.splice(
useElementPosition,
1,
insertElement,
);
}
},
},
};
};
4 changes: 4 additions & 0 deletions plugins/plugins-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ type DefaultPlugins = {
xmlnsOrder?: 'front' | 'alphabetical';
};
sortDefsChildren: void;
dereferenceUses: {
keepHref?: boolean;
symbolContainer?: 'svg';
};
};

type PresetDefaultOverrides = {
Expand Down
21 changes: 21 additions & 0 deletions test/plugins/dereferenceUses.01.svg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Dereference multiple <use/> elements while keeping the href attributes.

===

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle id="myCircle" cx="5" cy="5" r="4" />
<use xlink:href="#myCircle" />
<use href="#myCircle" />
</svg>

@@@

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle id="myCircle" cx="5" cy="5" r="4"/>
<circle cx="5" cy="5" r="4" xlink:href="#myCircle"/>
<circle cx="5" cy="5" r="4" href="#myCircle"/>
</svg>

@@@

{"keepHref":true}
19 changes: 19 additions & 0 deletions test/plugins/dereferenceUses.02.svg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Works with <use/> elements that reference to IDs without element, while keeping the href attributes.

===

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg">
<circle id="myCircle" cx="5" cy="5" r="4" />
<use href="#nonExisting" />
</svg>

@@@

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg">
<circle id="myCircle" cx="5" cy="5" r="4"/>
<use href="#nonExisting"/>
</svg>

@@@

{"keepHref":true}
21 changes: 21 additions & 0 deletions test/plugins/dereferenceUses.03.svg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Works with dereferencing multiple <use/> elements that reference the same element, while keeping the href attributes.

===

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle id="myCircle" cx="5" cy="5" r="4" x="5" />
<use href="#myCircle" x="10" />
<use href="#myCircle" x="20" />
</svg>

@@@

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle id="myCircle" cx="5" cy="5" r="4" x="5"/>
<circle cx="5" cy="5" r="4" x="10" href="#myCircle"/>
<circle cx="5" cy="5" r="4" x="20" href="#myCircle"/>
</svg>

@@@

{"keepHref":true}
21 changes: 21 additions & 0 deletions test/plugins/dereferenceUses.04.svg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Correctly keeps and overrides attributes of <use/> and referenced elements adhering to spec, while keeping the href attributes.

===

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg">
<circle id="myCircle" cx="5" cy="5" r="4" stroke="blue"/>
<use href="#myCircle" x="10" fill="blue"/>
<use href="#myCircle" x="20" fill="white" stroke="red"/>
</svg>

@@@

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg">
<circle id="myCircle" cx="5" cy="5" r="4" stroke="blue"/>
<circle cx="5" cy="5" r="4" stroke="blue" href="#myCircle" x="10" fill="blue"/>
<circle cx="5" cy="5" r="4" stroke="blue" href="#myCircle" x="20" fill="white"/>
</svg>

@@@

{"keepHref":true}
17 changes: 17 additions & 0 deletions test/plugins/dereferenceUses.05.svg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Correctly keeps and overrides attributes that exist on both, the <use/> element and the references element, without keeping the href attributes.

===

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg">
<circle id="myCircle" cx="5" cy="5" r="4" stroke="blue"/>
<use href="#myCircle" x="10" fill="blue"/>
<use href="#myCircle" x="20" fill="white" stroke="red"/>
</svg>

@@@

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg">
<circle id="myCircle" cx="5" cy="5" r="4" stroke="blue"/>
<circle cx="5" cy="5" r="4" stroke="blue" x="10" fill="blue"/>
<circle cx="5" cy="5" r="4" stroke="blue" x="20" fill="white"/>
</svg>
29 changes: 29 additions & 0 deletions test/plugins/dereferenceUses.06.svg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Use a symbol container explicitly passed as option and handle more complex SVGs.

===

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="423" height="75" viewBox="0 0 317 56">
<defs>
<symbol overflow="visible" id="a">
<path d="M9.281-8.438c0-.03-.219-.25-.328-.25a.701.701 0 00-.328.172l-.672.75c0 .016-.672-.921-2-.921-2.672 0-5.562 2.765-5.562 5.546C.39-1.17 2.016.266 3.844.266c1.031 0 2.078-.516 2.703-1.063C7.657-1.78 7.922-2.969 7.922-3c0-.125-.203-.031-.203-.031l-.125-.219c-.078 0-.344.156-.36.25-.109.344-.328 1.094-1.156 1.797-.812.672-1.422.828-2.047.828-1.078 0-2.156-.484-2.156-2.344 0-.687.188-2.531 1.39-3.922C4-7.5 4.986-8.062 6.048-8.062c1.219 0 1.719.796 1.719 2.187 0 .469-.032.484-.032.61 0 .109.329.25.375.25.157 0 .36-.157.407-.376zm0 0"/>
</symbol>
</defs>
<use xlink:href="#a" x="-.475" y="30.513"/>
</svg>

@@@

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="423" height="75" viewBox="0 0 317 56">
<defs>
<symbol overflow="visible" id="a">
<path d="M9.281-8.438c0-.03-.219-.25-.328-.25a.701.701 0 00-.328.172l-.672.75c0 .016-.672-.921-2-.921-2.672 0-5.562 2.765-5.562 5.546C.39-1.17 2.016.266 3.844.266c1.031 0 2.078-.516 2.703-1.063C7.657-1.78 7.922-2.969 7.922-3c0-.125-.203-.031-.203-.031l-.125-.219c-.078 0-.344.156-.36.25-.109.344-.328 1.094-1.156 1.797-.812.672-1.422.828-2.047.828-1.078 0-2.156-.484-2.156-2.344 0-.687.188-2.531 1.39-3.922C4-7.5 4.986-8.062 6.048-8.062c1.219 0 1.719.796 1.719 2.187 0 .469-.032.484-.032.61 0 .109.329.25.375.25.157 0 .36-.157.407-.376zm0 0"/>
</symbol>
</defs>
<g overflow="visible" x="-.475" y="30.513">
<path d="M9.281-8.438c0-.03-.219-.25-.328-.25a.701.701 0 00-.328.172l-.672.75c0 .016-.672-.921-2-.921-2.672 0-5.562 2.765-5.562 5.546C.39-1.17 2.016.266 3.844.266c1.031 0 2.078-.516 2.703-1.063C7.657-1.78 7.922-2.969 7.922-3c0-.125-.203-.031-.203-.031l-.125-.219c-.078 0-.344.156-.36.25-.109.344-.328 1.094-1.156 1.797-.812.672-1.422.828-2.047.828-1.078 0-2.156-.484-2.156-2.344 0-.687.188-2.531 1.39-3.922C4-7.5 4.986-8.062 6.048-8.062c1.219 0 1.719.796 1.719 2.187 0 .469-.032.484-.032.61 0 .109.329.25.375.25.157 0 .36-.157.407-.376zm0 0"/>
</g>
</svg>

@@@

{"symbolContainer":"g"}
29 changes: 29 additions & 0 deletions test/plugins/dereferenceUses.07.svg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Correctly overrides attributes of referenced elements adhering to spec, while keeping the href attributes.

===

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1 1" height="64" width="64">
<defs>
<symbol id="a" overflow="visible">
<path/>
</symbol>
</defs>
<use xlink:href="#a" style="fill:#000"/>
</svg>

@@@

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1 1" height="64" width="64">
<defs>
<symbol id="a" overflow="visible">
<path/>
</symbol>
</defs>
<svg overflow="visible" xlink:href="#a" style="fill:#000">
<path/>
</svg>
</svg>

@@@

{"keepHref":true}
13 changes: 13 additions & 0 deletions test/plugins/dereferenceUses.08.svg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Does not break when there are actually no <use/> elements at all.

===

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle id="myCircle" cx="5" cy="5" r="4" x="5" />
</svg>

@@@

<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle id="myCircle" cx="5" cy="5" r="4" x="5"/>
</svg>
Loading
Loading