1
0
mirror of https://github.com/svg/svgo.git synced 2025-04-19 10:22:15 +03:00
svgo/plugins/mergePaths.js

172 lines
4.9 KiB
JavaScript

/**
* @typedef {import('../lib/types.js').ComputedStyles} ComputedStyles
* @typedef {import('../lib/types.js').StaticStyle} StaticStyle
* @typedef {import('../lib/types.js').DynamicStyle} DynamicStyle
* @typedef {import("../lib/types.js").PathDataItem} PathDataItem
* @typedef {import('../lib/types.js').XastChild} XastChild
* @typedef {import('../lib/types.js').XastElement} XastElement
*/
import { collectStylesheet, computeStyle } from '../lib/style.js';
import { path2js, js2path, intersects } from './_path.js';
import { includesUrlReference } from '../lib/svgo/tools.js';
export const name = 'mergePaths';
export const description = 'merges multiple paths in one if possible';
/**
* @param {ComputedStyles} computedStyle
* @param {string} attName
* @returns {boolean}
*/
function elementHasUrl(computedStyle, attName) {
const style = computedStyle[attName];
if (style?.type === 'static') {
return includesUrlReference(style.value);
}
return false;
}
/**
* Merge multiple Paths into one.
*
* @author Kir Belevich, Lev Solntsev
*
* @type {import('./plugins-types.js').Plugin<'mergePaths'>}
*/
export const fn = (root, params) => {
const {
force = false,
floatPrecision = 3,
noSpaceAfterFlags = false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20
} = params;
const stylesheet = collectStylesheet(root);
return {
element: {
enter: (node) => {
if (node.children.length <= 1) {
return;
}
/** @type {XastChild[]} */
const elementsToRemove = [];
let prevChild = node.children[0];
let prevPathData = null;
/**
* @param {XastElement} child
* @param {PathDataItem[]} pathData
*/
const updatePreviousPath = (child, pathData) => {
js2path(child, pathData, {
floatPrecision,
noSpaceAfterFlags,
});
prevPathData = null;
};
for (let i = 1; i < node.children.length; i++) {
const child = node.children[i];
if (
prevChild.type !== 'element' ||
prevChild.name !== 'path' ||
prevChild.children.length !== 0 ||
prevChild.attributes.d == null
) {
if (prevPathData && prevChild.type === 'element') {
updatePreviousPath(prevChild, prevPathData);
}
prevChild = child;
continue;
}
if (
child.type !== 'element' ||
child.name !== 'path' ||
child.children.length !== 0 ||
child.attributes.d == null
) {
if (prevPathData) {
updatePreviousPath(prevChild, prevPathData);
}
prevChild = child;
continue;
}
const computedStyle = computeStyle(stylesheet, child);
if (
computedStyle['marker-start'] ||
computedStyle['marker-mid'] ||
computedStyle['marker-end'] ||
computedStyle['clip-path'] ||
computedStyle['mask'] ||
computedStyle['mask-image'] ||
['fill', 'filter', 'stroke'].some((attName) =>
elementHasUrl(computedStyle, attName),
)
) {
if (prevPathData) {
updatePreviousPath(prevChild, prevPathData);
}
prevChild = child;
continue;
}
const childAttrs = Object.keys(child.attributes);
if (childAttrs.length !== Object.keys(prevChild.attributes).length) {
if (prevPathData) {
updatePreviousPath(prevChild, prevPathData);
}
prevChild = child;
continue;
}
const areAttrsEqual = childAttrs.some((attr) => {
return (
attr !== 'd' &&
prevChild.type === 'element' &&
prevChild.attributes[attr] !== child.attributes[attr]
);
});
if (areAttrsEqual) {
if (prevPathData) {
updatePreviousPath(prevChild, prevPathData);
}
prevChild = child;
continue;
}
const hasPrevPath = prevPathData != null;
const currentPathData = path2js(child);
prevPathData = prevPathData ?? path2js(prevChild);
if (force || !intersects(prevPathData, currentPathData)) {
prevPathData.push(...currentPathData);
elementsToRemove.push(child);
continue;
}
if (hasPrevPath) {
updatePreviousPath(prevChild, prevPathData);
}
prevChild = child;
prevPathData = null;
}
if (prevPathData && prevChild.type === 'element') {
updatePreviousPath(prevChild, prevPathData);
}
node.children = node.children.filter(
(child) => !elementsToRemove.includes(child),
);
},
},
};
};