diff --git a/plugins/moveElemsAttrsToGroup.js b/plugins/moveElemsAttrsToGroup.js index c5a8c4efd..5012f0456 100644 --- a/plugins/moveElemsAttrsToGroup.js +++ b/plugins/moveElemsAttrsToGroup.js @@ -1,18 +1,15 @@ 'use strict'; -const { inheritableAttrs, pathElems } = require('./_collections'); +const { visit } = require('../lib/xast.js'); +const { inheritableAttrs, pathElems } = require('./_collections.js'); +exports.type = 'visitor'; exports.name = 'moveElemsAttrsToGroup'; - -exports.type = 'perItemReverse'; - exports.active = true; - -exports.description = 'moves elements attributes to the existing group wrapper'; +exports.description = 'Move common attributes of group children to the group'; /** - * Collapse content's intersected and inheritable - * attributes to the existing group wrapper. + * Move common attributes of group children to the group * * @example * @@ -29,98 +26,105 @@ exports.description = 'moves elements attributes to the existing group wrapper'; * * * - * @param {Object} item current iteration item - * @return {Boolean} if false, item will be filtered out - * * @author Kir Belevich + * + * @type {import('../lib/types').Plugin} */ -exports.fn = function (item) { - if ( - item.type === 'element' && - item.name === 'g' && - item.children.length > 1 - ) { - var intersection = {}, - hasTransform = false, - hasClip = - item.attributes['clip-path'] != null || item.attributes.mask != null, - intersected = item.children.every(function (inner) { - if ( - inner.type === 'element' && - Object.keys(inner.attributes).length !== 0 - ) { - // don't mess with possible styles (hack until CSS parsing is implemented) - if (inner.attributes.class) return false; - if (!Object.keys(intersection).length) { - intersection = inner.attributes; - } else { - intersection = intersectInheritableAttrs( - intersection, - inner.attributes - ); - - if (!intersection) return false; - } - - return true; +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; } - return false; - }), - allPath = item.children.every(function (inner) { - return inner.isElem(pathElems); - }); - - if (intersected) { - item.children.forEach(function (g) { - for (const [name, value] of Object.entries(intersection)) { - if ((!allPath && !hasClip) || name !== 'transform') { - delete g.attributes[name]; + // deoptimize the plugin when style elements are present + // selectors may rely on id, classes or tag names + if (deoptimizedWithStyles) { + return; + } - if (name === 'transform') { - if (!hasTransform) { - if (item.attributes.transform != null) { - item.attributes.transform = - item.attributes.transform + ' ' + value; - } else { - item.attributes.transform = value; + /** + * find common attributes in group children + * @type {Map} + */ + 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); } - - hasTransform = true; } } else { - item.attributes[name] = value; + // exclude uncommon attributes from initial list + for (const [name, value] of commonAttributes) { + if (child.attributes[name] !== value) { + commonAttributes.delete(name); + } + } } } } - }); - } - } -}; -/** - * Intersect inheritable attributes. - * - * @param {Object} a first attrs object - * @param {Object} b second attrs object - * - * @return {Object} intersected attrs object - */ -function intersectInheritableAttrs(a, b) { - var c = {}; + // preserve transform on children when group has clip-path or mask + if ( + node.attributes['clip-path'] != null || + node.attributes.mask != null + ) { + commonAttributes.delete('transform'); + } - for (const [name, value] of Object.entries(a)) { - if ( - // eslint-disable-next-line no-prototype-builtins - b.hasOwnProperty(name) && - inheritableAttrs.includes(name) && - value === b[name] - ) { - c[name] = value; - } - } + // preserve transform when all children are paths + // so the transform could be applied to path data by other plugins + if (everyChildIsPath) { + commonAttributes.delete('transform'); + } - if (!Object.keys(c).length) return false; + // 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; + } + } - return c; -} + // delete common attributes from children + for (const child of node.children) { + if (child.type === 'element') { + for (const [name] of commonAttributes) { + delete child.attributes[name]; + } + } + } + }, + }, + }; +}; diff --git a/test/plugins/moveElemsAttrsToGroup.01.svg b/test/plugins/moveElemsAttrsToGroup.01.svg index edd02a652..d9ab7b867 100644 --- a/test/plugins/moveElemsAttrsToGroup.01.svg +++ b/test/plugins/moveElemsAttrsToGroup.01.svg @@ -1,19 +1,31 @@ +Move common children attributes to group + +=== + - + text - + + + + + @@@ - - + + text + + + + diff --git a/test/plugins/moveElemsAttrsToGroup.02.svg b/test/plugins/moveElemsAttrsToGroup.02.svg index 794a643af..3025e6161 100644 --- a/test/plugins/moveElemsAttrsToGroup.02.svg +++ b/test/plugins/moveElemsAttrsToGroup.02.svg @@ -1,19 +1,19 @@ - - - - text - - - +Override group attributes with children attributes + +=== + + + + + + @@@ - - - - text - + + + diff --git a/test/plugins/moveElemsAttrsToGroup.03.svg b/test/plugins/moveElemsAttrsToGroup.03.svg index 32f408baa..d8ababd91 100644 --- a/test/plugins/moveElemsAttrsToGroup.03.svg +++ b/test/plugins/moveElemsAttrsToGroup.03.svg @@ -1,3 +1,7 @@ +Move to group only inheritable attributes + +=== + diff --git a/test/plugins/moveElemsAttrsToGroup.04.svg b/test/plugins/moveElemsAttrsToGroup.04.svg index d7a83238f..5bf380fd5 100644 --- a/test/plugins/moveElemsAttrsToGroup.04.svg +++ b/test/plugins/moveElemsAttrsToGroup.04.svg @@ -1,3 +1,9 @@ +Merge common group children transform attribute with the group transform + +Preserve transform on children when group has clip-path or mask + +=== + diff --git a/test/plugins/moveElemsAttrsToGroup.05.svg b/test/plugins/moveElemsAttrsToGroup.05.svg deleted file mode 100644 index 1e1873ac7..000000000 --- a/test/plugins/moveElemsAttrsToGroup.05.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - text - - - - - -@@@ - - - - - text - - - - diff --git a/test/plugins/moveElemsAttrsToGroup.06.svg b/test/plugins/moveElemsAttrsToGroup.06.svg index 65e391f09..dabfe4c22 100644 --- a/test/plugins/moveElemsAttrsToGroup.06.svg +++ b/test/plugins/moveElemsAttrsToGroup.06.svg @@ -1,3 +1,8 @@ +Preserve transform when all children are paths +so the transform could be applied to path data by other plugins + +=== + diff --git a/test/plugins/moveElemsAttrsToGroup.07.svg b/test/plugins/moveElemsAttrsToGroup.07.svg index 7ec0c040d..e228af8bc 100644 --- a/test/plugins/moveElemsAttrsToGroup.07.svg +++ b/test/plugins/moveElemsAttrsToGroup.07.svg @@ -1,3 +1,7 @@ +Plugin is deoptimized when style element is present + +=== +