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

feat(moveElemsAttrsToGroup): improve attribute flow #1895

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 4 additions & 1 deletion docs/03-plugins/move-elems-attrs-to-group.mdx
Expand Up @@ -5,7 +5,10 @@ svgo:
defaultPlugin: true
---

Move an elements attributes to their enclosing group.
Optimize the flow of attributes for a group by:

- Removing default attributes that are overriden by children
- Analyzing the attributes children have, and setting a group default based on them

## Usage

Expand Down
156 changes: 115 additions & 41 deletions plugins/moveElemsAttrsToGroup.js
Expand Up @@ -44,46 +44,61 @@ exports.fn = (root) => {
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;
}

const children = node.children.filter(
/**
* @type {(c: import('../lib/types.js').XastChild) => c is import('../lib/types.js').XastElement}
*/ (c) => c.type === 'element',
);

// process only groups with more than 1 children
if (node.name !== 'g' || children.length <= 1) {
if (
children.length > 0 &&
(node.name === 'g' || node.name === 'svg')
) {
// remove attributes that all children override
for (const a of Object.keys(node.attributes)) {
if (!inheritableAttrs.has(a)) continue;
if (a === 'transform') continue;
const isOverriden = children.every(
(child) =>
child.attributes[a] && child.attributes[a] !== 'inherit',
);
if (isOverriden) {
delete node.attributes[a];
}
}
}
return;
}

/**
* find common attributes in group children
* @type {Map<string, string>}
* attributes in group children
* @type {Map<string, string[]>}
*/
const commonAttributes = new Map();
let initial = true;
const attributes = new Map();
let everyChildIsPath = true;
for (const child of node.children) {
if (child.type === 'element') {
if (!pathElems.has(child.name)) {
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.has(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);
}
}
for (const child of children) {
if (!pathElems.has(child.name)) {
everyChildIsPath = false;
}
// collect all inheritable attributes from first child element
for (const [name, value] of Object.entries(child.attributes)) {
// consider only inheritable attributes
if (!inheritableAttrs.has(name)) continue;

let list = attributes.get(name);
if (!list) {
list = [];
attributes.set(name, list);
}
list.push(value);
}
}

Expand All @@ -92,35 +107,94 @@ exports.fn = (root) => {
node.attributes['clip-path'] != null ||
node.attributes.mask != null
) {
commonAttributes.delete('transform');
attributes.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');
attributes.delete('transform');
}

// add common children attributes to group
for (const [name, value] of commonAttributes) {
for (const [name, values] of attributes) {
if (values.includes('inherit')) continue;

if (name === 'transform') {
const value = values[0];
if (values.length != children.length) continue;
if (values.some((v) => v !== value)) continue;
if (node.attributes.transform != null) {
node.attributes.transform = `${node.attributes.transform} ${value}`;
} else {
node.attributes.transform = value;
}
} else {
node.attributes[name] = value;
for (const child of children) {
delete child.attributes.transform;
}
continue;
}
}
if (
(name === 'fill' || name === 'stroke') &&
values.some((v) => v.includes('url'))
)
continue;

// delete common attributes from children
for (const child of node.children) {
if (child.type === 'element') {
for (const [name] of commonAttributes) {
delete child.attributes[name];
const unsetValues = children.length - values.length;
const defaultV = node.attributes[name];
const assignmentCost = name.length + 4;
if (unsetValues && !defaultV) continue;

/**
* @type {Map<string, {chars: number, count: number}>}
*/
const counts = new Map();
values.forEach((v) => {
let count = counts.get(v);
if (!count) {
count = {
chars: 0,
count: 0,
};
counts.set(v, count);
}
count.chars += v.length + assignmentCost;
count.count++;
});
if (unsetValues) {
let count = counts.get(defaultV);
if (!count) {
count = {
chars: 0,
count: 0,
};
counts.set(defaultV, count);
}
count.chars += unsetValues * (defaultV.length + assignmentCost);
count.count += unsetValues;
}

const [preferred, preferredInfo] = Array.from(
counts.entries(),
).reduce((a, b) => (a[1].chars > b[1].chars ? a : b));
if (preferredInfo.count === 1) {
children.forEach((c) => {
if (!c.attributes[name]) c.attributes[name] = defaultV;
});
delete node.attributes[name];
continue;
}
if (preferred === defaultV) continue;

children.forEach((c) => {
if (!c.attributes[name]) {
c.attributes[name] = defaultV;
}
if (c.attributes[name] === preferred) {
delete c.attributes[name];
}
});
node.attributes[name] = preferred;
}
},
},
Expand Down
15 changes: 15 additions & 0 deletions test/plugins/moveElemsAttrsToGroup.08.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions test/plugins/moveElemsAttrsToGroup.09.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions test/plugins/moveElemsAttrsToGroup.10.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.