129 lines
3.6 KiB
JavaScript
129 lines
3.6 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const { visit } = require('../lib/xast.js');
|
||
|
const { inheritableAttrs, pathElems } = require('./_collections.js');
|
||
|
|
||
|
exports.name = 'moveElemsAttrsToGroup';
|
||
|
exports.description = 'Move common attributes of group children to the group';
|
||
|
|
||
|
/**
|
||
|
* Move common attributes of group children to the group
|
||
|
*
|
||
|
* @example
|
||
|
* <g attr1="val1">
|
||
|
* <g attr2="val2">
|
||
|
* text
|
||
|
* </g>
|
||
|
* <circle attr2="val2" attr3="val3"/>
|
||
|
* </g>
|
||
|
* ⬇
|
||
|
* <g attr1="val1" attr2="val2">
|
||
|
* <g>
|
||
|
* text
|
||
|
* </g>
|
||
|
* <circle attr3="val3"/>
|
||
|
* </g>
|
||
|
*
|
||
|
* @author Kir Belevich
|
||
|
*
|
||
|
* @type {import('./plugins-types').Plugin<'moveElemsAttrsToGroup'>}
|
||
|
*/
|
||
|
exports.fn = (root) => {
|
||
|
// find if any style element is present
|
||
|
let deoptimizedWithStyles = false;
|
||
|
visit(root, {
|
||
|
element: {
|
||
|
enter: (node) => {
|
||
|
if (node.name === 'style') {
|
||
|
deoptimizedWithStyles = true;
|
||
|
}
|
||
|
},
|
||
|
},
|
||
|
});
|
||
|
|
||
|
return {
|
||
|
element: {
|
||
|
exit: (node) => {
|
||
|
// process only groups with more than 1 children
|
||
|
if (node.name !== 'g' || node.children.length <= 1) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// deoptimize the plugin when style elements are present
|
||
|
// selectors may rely on id, classes or tag names
|
||
|
if (deoptimizedWithStyles) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* find common attributes in group children
|
||
|
* @type {Map<string, string>}
|
||
|
*/
|
||
|
const commonAttributes = new Map();
|
||
|
let initial = true;
|
||
|
let everyChildIsPath = true;
|
||
|
for (const child of node.children) {
|
||
|
if (child.type === 'element') {
|
||
|
if (pathElems.includes(child.name) === false) {
|
||
|
everyChildIsPath = false;
|
||
|
}
|
||
|
if (initial) {
|
||
|
initial = false;
|
||
|
// collect all inheritable attributes from first child element
|
||
|
for (const [name, value] of Object.entries(child.attributes)) {
|
||
|
// consider only inheritable attributes
|
||
|
if (inheritableAttrs.includes(name)) {
|
||
|
commonAttributes.set(name, value);
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
// exclude uncommon attributes from initial list
|
||
|
for (const [name, value] of commonAttributes) {
|
||
|
if (child.attributes[name] !== value) {
|
||
|
commonAttributes.delete(name);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// preserve transform on children when group has clip-path or mask
|
||
|
if (
|
||
|
node.attributes['clip-path'] != null ||
|
||
|
node.attributes.mask != null
|
||
|
) {
|
||
|
commonAttributes.delete('transform');
|
||
|
}
|
||
|
|
||
|
// preserve transform when all children are paths
|
||
|
// so the transform could be applied to path data by other plugins
|
||
|
if (everyChildIsPath) {
|
||
|
commonAttributes.delete('transform');
|
||
|
}
|
||
|
|
||
|
// add common children attributes to group
|
||
|
for (const [name, value] of commonAttributes) {
|
||
|
if (name === 'transform') {
|
||
|
if (node.attributes.transform != null) {
|
||
|
node.attributes.transform = `${node.attributes.transform} ${value}`;
|
||
|
} else {
|
||
|
node.attributes.transform = value;
|
||
|
}
|
||
|
} else {
|
||
|
node.attributes[name] = value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// delete common attributes from children
|
||
|
for (const child of node.children) {
|
||
|
if (child.type === 'element') {
|
||
|
for (const [name] of commonAttributes) {
|
||
|
delete child.attributes[name];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
},
|
||
|
};
|
||
|
};
|