1
0
mirror of https://github.com/svg/svgo.git synced 2025-04-19 10:22:15 +03:00
svgo/plugins/inlineDefs.js
Bogdan Chadkin bb3f1a99ef [new plugin] reimplement inlineDefs
The code is taken from https://github.com/svg/svgo/pull/976, refactored
with new api, covered types and simplified.

Plugin has no dependencies so can be used without changing.

```
const inlineDefs = require('./inlineDefs.js');

module.exports = {
  plugins: [
    'preset-default',
    inlineDefs
  ]
};
```
2022-02-21 19:12:53 +03:00

167 lines
4.5 KiB
JavaScript

'use strict';
/**
* @typedef {import('../lib/types').XastParent} XastParent
* @typedef {import('../lib/types').XastElement} XastElement
*/
exports.name = 'inlineDefs';
exports.type = 'visitor';
exports.active = true;
exports.description = 'inlines svg definitions';
/**
* @typedef {(element: XastElement, parentNode: XastParent) => void} VisitCallback
*/
/**
* @type {(element: XastParent, fn: VisitCallback) => void}
*/
const visitElements = (node, fn) => {
for (const child of node.children) {
if (child.type === 'element') {
fn(child, node);
visitElements(child, fn);
}
}
};
/**
* Replaces use tag with the corresponding definitions
* if onlyUnique is enabled, replaces only use tags with definitions referred to only once
*
* @type {import('../lib/types').Plugin<{
* onlyUnique?: boolean
* }>}
*/
exports.fn = (root, params) => {
const { onlyUnique = true } = params;
// hacky extract JSAPI class to avoid imports from other modules
const JSAPI = root.constructor;
/**
* @type {[XastElement, XastParent][]}
*/
const uses = [];
/**
* @type {Map<string, number>}
*/
const useCounts = new Map();
/**
* @type {Map<string, XastElement>}
*/
const referencedElements = new Map();
// collect defs container and all uses
visitElements(root, (node, parentNode) => {
if (node.name === 'use') {
uses.push([node, parentNode]);
const href = node.attributes['xlink:href'] || node.attributes.href;
const count = useCounts.get(href) || 0;
useCounts.set(href, count + 1);
}
});
return {
element: {
enter: (node, parentNode) => {
// find elements referenced by all <use>
if (node.attributes.id == null) {
return;
}
const href = `#${node.attributes.id}`;
const count = useCounts.get(href);
// not referenced
if (count == null) {
return;
}
referencedElements.set(href, node);
/// remove id attribute when referenced yb <use> more than once
if (onlyUnique === false && count > 1) {
delete node.attributes.id;
}
// remove elements referenced by <use> only once
if (onlyUnique === true && count === 1) {
parentNode.children = parentNode.children.filter(
(child) => child !== node
);
}
},
exit(node, parentNode) {
// remove empty <defs> container
if (node.name === 'defs') {
if (onlyUnique === false || node.children.length === 0) {
parentNode.children = parentNode.children.filter(
(child) => child !== node
);
}
}
},
},
root: {
exit: () => {
for (const [use, useParentNode] of uses) {
const href = use.attributes['xlink:href'] || use.attributes['href'];
const count = useCounts.get(href) || 0;
const referenced = referencedElements.get(href);
if (onlyUnique === true && count > 1) {
continue;
}
if (referenced == null) {
continue;
}
// copy attrubutes from <use> to referenced element
for (const [name, value] of Object.entries(use.attributes)) {
if (
name !== 'x' &&
name !== 'y' &&
name !== 'xlink:href' &&
name !== 'href'
) {
referenced.attributes[name] = value;
}
}
const x = use.attributes.x;
const y = use.attributes.y;
let attrValue = null;
if (x != null && y != null) {
attrValue = `translate(${x}, ${y})`;
} else if (x != null) {
attrValue = `translate(${x})`;
}
let replacement = referenced;
// wrap referenced element with <g> when <use> had coordinates
if (attrValue != null) {
/**
* @type {XastElement}
*/
const g = {
type: 'element',
name: 'g',
attributes: {
transform: attrValue,
},
children: [referenced],
};
// @ts-ignore
replacement = new JSAPI(g);
}
useParentNode.children = useParentNode.children.map((child) => {
if (child === use) {
return replacement;
} else {
return child;
}
});
}
},
},
};
};