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 reuseDefs plugin #1776

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -160,7 +160,7 @@ const config = await loadConfig(configFile, cwd);
## Built-in plugins

| Plugin | Description | Default |
| ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|-------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| ------- |
| [addAttributesToSVGElement](https://github.com/svg/svgo/blob/main/plugins/addAttributesToSVGElement.js) | adds attributes to an outer `<svg>` element | |
| [addClassesToSVGElement](https://github.com/svg/svgo/blob/main/plugins/addClassesToSVGElement.js) | add classnames to an outer `<svg>` element | |
H3r3zy marked this conversation as resolved.
Show resolved Hide resolved
| [cleanupAttrs](https://github.com/svg/svgo/blob/main/plugins/cleanupAttrs.js) | cleanup attributes from newlines, trailing, and repeating spaces | Yes |
Expand Down Expand Up @@ -208,6 +208,7 @@ const config = await loadConfig(configFile, cwd);
| [removeViewBox](https://github.com/svg/svgo/blob/main/plugins/removeViewBox.js) | remove `viewBox` attribute when possible | Yes |
| [removeXMLNS](https://github.com/svg/svgo/blob/main/plugins/removeXMLNS.js) | removes the `xmlns` attribute (for inline SVG) | |
| [removeXMLProcInst](https://github.com/svg/svgo/blob/main/plugins/removeXMLProcInst.js) | remove XML processing instructions | Yes |
| [reuseDefs](https://github.com/svg/svgo/blob/main/plugins/reuseDefs.js) | Find duplicated def elements, delete them and replace by the id left | |
| [reusePaths](https://github.com/svg/svgo/blob/main/plugins/reusePaths.js) | Find duplicated <path> elements and replace them with <use> links | |
| [sortAttrs](https://github.com/svg/svgo/blob/main/plugins/sortAttrs.js) | sort element attributes for epic readability | Yes |
| [sortDefsChildren](https://github.com/svg/svgo/blob/main/plugins/sortDefsChildren.js) | sort children of `<defs>` in order to improve compression | Yes |
Expand Down
1 change: 1 addition & 0 deletions lib/builtin.js
Expand Up @@ -49,6 +49,7 @@ exports.builtin = [
require('../plugins/removeViewBox.js'),
require('../plugins/removeXMLNS.js'),
require('../plugins/removeXMLProcInst.js'),
require('../plugins/reuseDefs.js'),
require('../plugins/reusePaths.js'),
require('../plugins/sortAttrs.js'),
require('../plugins/sortDefsChildren.js'),
Expand Down
1 change: 1 addition & 0 deletions plugins/plugins-types.ts
Expand Up @@ -220,6 +220,7 @@ export type BuiltinsWithOptionalParams = DefaultPlugins & {
removeScriptElement: void;
removeStyleElement: void;
removeXMLNS: void;
reuseDefs: void;
reusePaths: void;
};

Expand Down
147 changes: 147 additions & 0 deletions plugins/reuseDefs.js
@@ -0,0 +1,147 @@
'use strict';

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

const { referencesProps } = require('./_collections');
exports.name = 'reuseDefs';
exports.description =
'Finds <defs> elements with the same def, keep only one ref,' +
'and replace id references with the new one.';

const regReferencesUrl = /\burl\((["'])?#(.+?)\1\)/;
const regReferencesHref = /^#(.+?)$/;
const regReferencesBegin = /(\D+)\./;

/**
* Finds <defs> elements with the same def, keep only one ref, and replace id
* references with the new one.
*
* @author Sahel LUCAS--SAOUDI
*
* @type {import('./plugins-types').Plugin<'reuseDefs'>}
*/
exports.fn = () => {
/**
* @type {Array<XastElement>}
*/
const defsChildren = [];
/**
* @type {Map<string, string>}
*/
const defsReused = new Map();
/**
* @type {Map<string, Array<{element: XastElement, name: string, value: string }>>}
*/
const referencesById = new Map();
/**
* @type {XastElement | null}
*/
let defsTag = null;

return {
element: {
enter: (node) => {
if (node.name === 'defs') {
node.children.forEach((child) => {
if (child.type === 'element' && child.attributes.id) {
const sameDef = defsChildren.find((def) => {
if (def.type !== 'element' || def.name !== child.name) {
return false;
}
if (
JSON.stringify({ ...def.attributes, id: undefined }) ===
JSON.stringify({ ...child.attributes, id: undefined })
) {
return (
JSON.stringify(def.children) ===
JSON.stringify(child.children)
);
}
return false;
});

if (!sameDef) {
defsChildren.push(child);
} else {
defsReused.set(child.attributes.id, sameDef.attributes.id);
// delete child
node.children = node.children.filter((c) => c !== child);
}
}
});
}

for (const [name, value] of Object.entries(node.attributes)) {
// collect all references
/**
* @type {null | string}
*/
let id = null;
if (referencesProps.includes(name)) {
const match = value.match(regReferencesUrl);
if (match != null) {
id = match[2]; // url() reference
}
}
if (name === 'href' || name.endsWith(':href')) {
const match = value.match(regReferencesHref);
if (match != null) {
id = match[1]; // href reference
}
}
if (name === 'begin') {
const match = value.match(regReferencesBegin);
if (match != null) {
id = match[1]; // href reference
}
}
if (id != null) {
let refs = referencesById.get(id);
if (refs == null) {
refs = [];
referencesById.set(id, refs);
}
refs.push({ element: node, name, value });
}
}
},

exit: (node, parentNode) => {
// remove empty defs node
if (node.name === 'defs' && node !== defsTag && node.children.length === 0) {
parentNode.children = parentNode.children.filter((c) => c !== node);
}
},
},
root: {
exit: () => {
// Replace references to reused defs
for (const [id, refs] of referencesById.entries()) {
const reusedId = defsReused.get(id);
if (reusedId) {
for (const ref of refs) {
if (ref.value.includes('#')) {
// replace id in href and url()
ref.element.attributes[ref.name] = ref.value.replace(
`#${id}`,
`#${reusedId}`
);
} else {
// replace id in begin attribute
ref.element.attributes[ref.name] = ref.value.replace(
`${id}.`,
`${reusedId}.`
);
}
}
}
}
},
},
};
};
26 changes: 26 additions & 0 deletions test/plugins/reuseDefs.01.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions test/plugins/reuseDefs.02.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions test/plugins/reuseDefs.03.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions test/plugins/reuseDefs.04.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.