1
0
mirror of https://github.com/svg/svgo.git synced 2025-07-31 07:44:22 +03:00

Refactor adhoc plugins with visitor api (#1526)

- addAttributesToSVGElement
- addClassesToSVGElement
- removeAttributesBySelector
- removeAttrs
- removeElementsByAttr
This commit is contained in:
Bogdan Chadkin
2021-08-14 16:48:39 +03:00
committed by GitHub
parent e6b441aca0
commit 7ec255719c
5 changed files with 217 additions and 202 deletions

View File

@ -1,13 +1,8 @@
'use strict'; 'use strict';
const { closestByName } = require('../lib/xast.js');
exports.name = 'addAttributesToSVGElement'; exports.name = 'addAttributesToSVGElement';
exports.type = 'visitor';
exports.type = 'perItem';
exports.active = false; exports.active = false;
exports.description = 'adds attributes to an outer <svg> element'; exports.description = 'adds attributes to an outer <svg> element';
var ENOCLS = `Error in plugin "addAttributesToSVGElement": absent parameters. var ENOCLS = `Error in plugin "addAttributesToSVGElement": absent parameters.
@ -54,32 +49,32 @@ plugins: [
* *
* @author April Arcus * @author April Arcus
*/ */
exports.fn = (node, params) => { exports.fn = (root, params) => {
if ( if (!Array.isArray(params.attributes) && !params.attribute) {
node.type === 'element' && console.error(ENOCLS);
node.name === 'svg' && return;
closestByName(node.parentNode, 'svg') == null }
) { const attributes = params.attributes || [params.attribute];
if (!params || !(Array.isArray(params.attributes) || params.attribute)) { return {
console.error(ENOCLS); element: {
return; enter: (node, parentNode) => {
} if (node.name === 'svg' && parentNode.type === 'root') {
for (const attribute of attributes) {
const attributes = params.attributes || [params.attribute]; if (typeof attribute === 'string') {
if (node.attributes[attribute] == null) {
for (const attribute of attributes) { node.attributes[attribute] = undefined;
if (typeof attribute === 'string') { }
if (node.attributes[attribute] == null) { }
node.attributes[attribute] = undefined; if (typeof attribute === 'object') {
} for (const key of Object.keys(attribute)) {
} if (node.attributes[key] == null) {
if (typeof attribute === 'object') { node.attributes[key] = attribute[key];
for (const key of Object.keys(attribute)) { }
if (node.attributes[key] == null) { }
node.attributes[key] = attribute[key]; }
} }
} }
} },
} },
} };
}; };

View File

@ -1,57 +1,72 @@
'use strict'; 'use strict';
exports.name = 'addClassesToSVGElement'; exports.name = 'addClassesToSVGElement';
exports.type = 'visitor';
exports.type = 'full';
exports.active = false; exports.active = false;
exports.description = 'adds classnames to an outer <svg> element'; exports.description = 'adds classnames to an outer <svg> element';
var ENOCLS = `Error in plugin "addClassesToSVGElement": absent parameters. var ENOCLS = `Error in plugin "addClassesToSVGElement": absent parameters.
It should have a list of classes in "classNames" or one "className". It should have a list of classes in "classNames" or one "className".
Config example: Config example:
plugins: plugins: [
- addClassesToSVGElement: {
className: "mySvg" name: "addClassesToSVGElement",
params: {
className: "mySvg"
}
}
]
plugins: plugins: [
- addClassesToSVGElement: {
classNames: ["mySvg", "size-big"] name: "addClassesToSVGElement",
params: {
classNames: ["mySvg", "size-big"]
}
}
]
`; `;
/** /**
* Add classnames to an outer <svg> element. Example config: * Add classnames to an outer <svg> element. Example config:
* *
* plugins: * plugins: [
* - addClassesToSVGElement: * {
* className: 'mySvg' * name: "addClassesToSVGElement",
* params: {
* className: "mySvg"
* }
* }
* ]
* *
* plugins: * plugins: [
* - addClassesToSVGElement: * {
* classNames: ['mySvg', 'size-big'] * name: "addClassesToSVGElement",
* params: {
* classNames: ["mySvg", "size-big"]
* }
* }
* ]
* *
* @author April Arcus * @author April Arcus
*/ */
exports.fn = function (data, params) { exports.fn = (root, params) => {
if ( if (
!params || !(Array.isArray(params.classNames) && params.classNames.some(String)) &&
!( !params.className
(Array.isArray(params.classNames) && params.classNames.some(String)) ||
params.className
)
) { ) {
console.error(ENOCLS); console.error(ENOCLS);
return data; return;
} }
const classNames = params.classNames || [params.className];
var classNames = params.classNames || [params.className], return {
svg = data.children[0]; element: {
enter: (node, parentNode) => {
if (svg.isElem('svg')) { if (node.name === 'svg' && parentNode.type === 'root') {
svg.class.add.apply(svg.class, classNames); node.class.add(...classNames);
} }
},
return data; },
};
}; };

View File

@ -1,55 +1,71 @@
'use strict'; 'use strict';
const { querySelectorAll } = require('../lib/xast.js');
exports.name = 'removeAttributesBySelector'; exports.name = 'removeAttributesBySelector';
exports.type = 'visitor';
exports.type = 'perItem';
exports.active = false; exports.active = false;
exports.description = exports.description =
'removes attributes of elements that match a css selector'; 'removes attributes of elements that match a css selector';
/** /**
* Removes attributes of elements that match a css selector. * Removes attributes of elements that match a css selector.
* *
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
*
* @example * @example
* <caption>A selector removing a single attribute</caption> * <caption>A selector removing a single attribute</caption>
* plugins: * plugins: [
* - removeAttributesBySelector: * {
* name: "removeAttributesBySelector",
* params: {
* selector: "[fill='#00ff00']" * selector: "[fill='#00ff00']"
* attributes: "fill" * attributes: "fill"
* }
* }
* ]
* *
* <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/> * <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/>
* ↓ * ↓
* <rect x="0" y="0" width="100" height="100" stroke="#00ff00"/> * <rect x="0" y="0" width="100" height="100" stroke="#00ff00"/>
* *
* <caption>A selector removing multiple attributes</caption> * <caption>A selector removing multiple attributes</caption>
* plugins: * plugins: [
* - removeAttributesBySelector: * {
* selector: "[fill='#00ff00']" * name: "removeAttributesBySelector",
* attributes: * params: {
* - fill * selector: "[fill='#00ff00']",
* - stroke * attributes: [
* "fill",
* "stroke"
* ]
* }
* }
* ]
* *
* <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/> * <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/>
* ↓ * ↓
* <rect x="0" y="0" width="100" height="100"/> * <rect x="0" y="0" width="100" height="100"/>
* *
* <caption>Multiple selectors removing attributes</caption> * <caption>Multiple selectors removing attributes</caption>
* plugins: * plugins: [
* - removeAttributesBySelector: * {
* selectors: * name: "removeAttributesBySelector",
* - selector: "[fill='#00ff00']" * params: {
* selectors: [
* {
* selector: "[fill='#00ff00']",
* attributes: "fill" * attributes: "fill"
* * },
* - selector: "#remove" * {
* attributes: * selector: "#remove",
* - stroke * attributes: [
* - id * "stroke",
* "id"
* ]
* }
* ]
* }
* }
* ]
* *
* <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/> * <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/>
* ↓ * ↓
@ -59,18 +75,21 @@ exports.description =
* *
* @author Bradley Mease * @author Bradley Mease
*/ */
exports.fn = function (item, params) { exports.fn = (root, params) => {
var selectors = Array.isArray(params.selectors) ? params.selectors : [params]; const selectors = Array.isArray(params.selectors)
? params.selectors
selectors.map(({ selector, attributes }) => { : [params];
if (item.matches(selector)) { for (const { selector, attributes } of selectors) {
const nodes = querySelectorAll(root, selector);
for (const node of nodes) {
if (Array.isArray(attributes)) { if (Array.isArray(attributes)) {
for (const name of attributes) { for (const name of attributes) {
delete item.attributes[name]; delete node.attributes[name];
} }
} else { } else {
delete item.attributes[attributes]; delete node.attributes[attributes];
} }
} }
}); }
return {};
}; };

View File

@ -1,20 +1,11 @@
'use strict'; 'use strict';
var DEFAULT_SEPARATOR = ':';
exports.name = 'removeAttrs'; exports.name = 'removeAttrs';
exports.type = 'visitor';
exports.type = 'perItem';
exports.active = false; exports.active = false;
exports.description = 'removes specified attributes'; exports.description = 'removes specified attributes';
exports.params = { const DEFAULT_SEPARATOR = ':';
elemSeparator: DEFAULT_SEPARATOR,
preserveCurrentColor: false,
attrs: [],
};
/** /**
* Remove attributes * Remove attributes
@ -77,72 +68,69 @@ exports.params = {
* attrs: 'stroke.*' * attrs: 'stroke.*'
* *
* *
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
*
* @author Benny Schudel * @author Benny Schudel
*/ */
exports.fn = function (item, params) { exports.fn = (root, params) => {
// wrap into an array if params is not // wrap into an array if params is not
if (!Array.isArray(params.attrs)) { const elemSeparator =
params.attrs = [params.attrs]; typeof params.elemSeparator == 'string'
} ? params.elemSeparator
: DEFAULT_SEPARATOR;
const preserveCurrentColor =
typeof params.preserveCurrentColor == 'boolean'
? params.preserveCurrentColor
: false;
const attrs = Array.isArray(params.attrs) ? params.attrs : [params.attrs];
if (item.type === 'element') { return {
var elemSeparator = element: {
typeof params.elemSeparator == 'string' enter: (node) => {
? params.elemSeparator for (let pattern of attrs) {
: DEFAULT_SEPARATOR; // if no element separators (:), assume it's attribute name, and apply to all elements *regardless of value*
var preserveCurrentColor = if (pattern.includes(elemSeparator) === false) {
typeof params.preserveCurrentColor == 'boolean' pattern = ['.*', elemSeparator, pattern, elemSeparator, '.*'].join(
? params.preserveCurrentColor ''
: false; );
// if only 1 separator, assume it's element and attribute name, and apply regardless of attribute value
} else if (pattern.split(elemSeparator).length < 3) {
pattern = [pattern, elemSeparator, '.*'].join('');
}
// prepare patterns // create regexps for element, attribute name, and attribute value
var patterns = params.attrs.map(function (pattern) { const list = pattern.split(elemSeparator).map((value) => {
// if no element separators (:), assume it's attribute name, and apply to all elements *regardless of value* // adjust single * to match anything
if (pattern.indexOf(elemSeparator) === -1) { if (value === '*') {
pattern = ['.*', elemSeparator, pattern, elemSeparator, '.*'].join(''); value = '.*';
}
return new RegExp(['^', value, '$'].join(''), 'i');
});
// if only 1 separator, assume it's element and attribute name, and apply regardless of attribute value // matches element
} else if (pattern.split(elemSeparator).length < 3) { if (list[0].test(node.name)) {
pattern = [pattern, elemSeparator, '.*'].join(''); // loop attributes
} for (const [name, value] of Object.entries(node.attributes)) {
const isFillCurrentColor =
// create regexps for element, attribute name, and attribute value preserveCurrentColor &&
return pattern.split(elemSeparator).map(function (value) { name == 'fill' &&
// adjust single * to match anything value == 'currentColor';
if (value === '*') { const isStrokeCurrentColor =
value = '.*'; preserveCurrentColor &&
} name == 'stroke' &&
value == 'currentColor';
return new RegExp(['^', value, '$'].join(''), 'i'); if (
}); !isFillCurrentColor &&
}); !isStrokeCurrentColor &&
// matches attribute name
// loop patterns list[1].test(name) &&
patterns.forEach(function (pattern) { // matches attribute value
// matches element list[2].test(value)
if (pattern[0].test(item.name)) { ) {
// loop attributes delete node.attributes[name];
for (const [name, value] of Object.entries(item.attributes)) {
var isFillCurrentColor =
preserveCurrentColor && name == 'fill' && value == 'currentColor';
var isStrokeCurrentColor =
preserveCurrentColor && name == 'stroke' && value == 'currentColor';
if (!(isFillCurrentColor || isStrokeCurrentColor)) {
// matches attribute name
if (pattern[1].test(name)) {
// matches attribute value
if (pattern[2].test(value)) {
delete item.attributes[name];
} }
} }
} }
} }
} },
}); },
} };
}; };

View File

@ -1,19 +1,13 @@
'use strict'; 'use strict';
const { detachNodeFromParent } = require('../lib/xast.js');
exports.name = 'removeElementsByAttr'; exports.name = 'removeElementsByAttr';
exports.type = 'visitor';
exports.type = 'perItem';
exports.active = false; exports.active = false;
exports.description = exports.description =
'removes arbitrary elements by ID or className (disabled by default)'; 'removes arbitrary elements by ID or className (disabled by default)';
exports.params = {
id: [],
class: [],
};
/** /**
* Remove arbitrary SVG elements by ID or className. * Remove arbitrary SVG elements by ID or className.
* *
@ -47,33 +41,37 @@ exports.params = {
* - 'elementClass' * - 'elementClass'
* - 'anotherClass' * - 'anotherClass'
* *
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
*
* @author Eli Dupuis (@elidupuis) * @author Eli Dupuis (@elidupuis)
*/ */
exports.fn = function (item, params) { exports.fn = (root, params) => {
// wrap params in an array if not already const ids =
['id', 'class'].forEach(function (key) { params.id == null ? [] : Array.isArray(params.id) ? params.id : [params.id];
if (!Array.isArray(params[key])) { const classes =
params[key] = [params[key]]; params.class == null
} ? []
}); : Array.isArray(params.class)
? params.class
// abort if current item is no an element : [params.class];
if (item.type !== 'element') { return {
return; element: {
} enter: (node, parentNode) => {
// remove element if it's `id` matches configured `id` params
// remove element if it's `id` matches configured `id` params if (node.attributes.id != null && ids.length !== 0) {
if (item.attributes.id != null && params.id.length !== 0) { if (ids.includes(node.attributes.id)) {
return params.id.includes(item.attributes.id) === false; detachNodeFromParent(node, parentNode);
} }
}
// remove element if it's `class` contains any of the configured `class` params // remove element if it's `class` contains any of the configured `class` params
if (item.attributes.class && params.class.length !== 0) { if (node.attributes.class && classes.length !== 0) {
const classList = item.attributes.class.split(' '); const classList = node.attributes.class.split(' ');
return params.class.some((item) => classList.includes(item)) === false; for (const item of classes) {
} if (classList.includes(item)) {
detachNodeFromParent(node, parentNode);
break;
}
}
}
},
},
};
}; };