diff --git a/lib/svgo/tools.js b/lib/svgo/tools.js index b906b32..c3b7178 100644 --- a/lib/svgo/tools.js +++ b/lib/svgo/tools.js @@ -65,6 +65,19 @@ export const decodeSVGDatauri = (str) => { return str; }; +/** + * @param {Map>} childrenToDeleteByParent + * @param {import('../types.js').XastChild} child + */ +export function addChildToDelete(childrenToDeleteByParent, child) { + let childrenToDelete = childrenToDeleteByParent.get(child.parentNode); + if (!childrenToDelete) { + childrenToDelete = new Set(); + childrenToDeleteByParent.set(child.parentNode, childrenToDelete); + } + childrenToDelete.add(child); +} + /** * @typedef {{ * noSpaceAfterFlags?: boolean, @@ -124,6 +137,16 @@ export const cleanupOutData = (data, params, command) => { return str; }; +/** + * @param {Map>} childrenToDeleteByParent + */ +export function deleteChildren(childrenToDeleteByParent) { + // For each parent, delete no longer needed children. + for (const [parent, childrenToDelete] of childrenToDeleteByParent) { + parent.children = parent.children.filter((c) => !childrenToDelete.has(c)); + } +} + /** * @param {number} n * @param {number} m diff --git a/plugins/removeEmptyContainers.js b/plugins/removeEmptyContainers.js index a4005fc..a83bdd6 100644 --- a/plugins/removeEmptyContainers.js +++ b/plugins/removeEmptyContainers.js @@ -1,4 +1,8 @@ -import { getHrefId } from '../lib/svgo/tools.js'; +import { + addChildToDelete, + deleteChildren, + getHrefId, +} from '../lib/svgo/tools.js'; import { detachNodeFromParent } from '../lib/xast.js'; export const name = 'removeEmptyContainers'; @@ -28,9 +32,7 @@ const removableEls = new Set([ */ export const fn = () => { const removedIds = new Set(); - /** - * @type {Map} - */ + /** @type {Map} */ const usesById = new Map(); return { @@ -49,48 +51,57 @@ export const fn = () => { } } }, - exit: (node, parentNode) => { + exit: (element, parentNode) => { // remove only empty non-svg containers - if (!removableEls.has(node.name) || node.children.length !== 0) { + if (!removableEls.has(element.name) || element.children.length !== 0) { return; } // empty patterns may contain reusable configuration if ( - node.name === 'pattern' && - Object.keys(node.attributes).length !== 0 + element.name === 'pattern' && + Object.keys(element.attributes).length !== 0 ) { return; } // The may not have content, but the filter may cause a rectangle // to be created and filled with pattern. - if (node.name === 'g' && node.attributes.filter != null) { + if (element.name === 'g' && element.attributes.filter != null) { return; } // empty hides masked element - if (node.name === 'mask' && node.attributes.id != null) { + if (element.name === 'mask' && element.attributes.id != null) { return; } if (parentNode.type === 'element' && parentNode.name === 'switch') { return; } - detachNodeFromParent(node, parentNode); - if (node.attributes.id) { - removedIds.add(node.attributes.id); + // TODO: Change the way this works so that parent removes empty children. We can't queue them for deletion in + // root exit; this is running in element exit so that nested empty elements are removed from bottom up, the nesting + // would be hard to detect in root exit. + detachNodeFromParent(element, parentNode); + if (element.attributes.id) { + removedIds.add(element.attributes.id); } }, }, root: { exit: () => { // Remove any elements that referenced an empty container. + + /** @type {Map>} */ + const childrenToDelete = new Map(); + for (const id of removedIds) { const usingEls = usesById.get(id); if (usingEls) { for (const element of usingEls) { - detachNodeFromParent(element); + addChildToDelete(childrenToDelete, element); } } } + + deleteChildren(childrenToDelete); }, }, }; diff --git a/plugins/removeHiddenElems.js b/plugins/removeHiddenElems.js index a11f3f6..b112169 100644 --- a/plugins/removeHiddenElems.js +++ b/plugins/removeHiddenElems.js @@ -1,5 +1,9 @@ import { parsePathCommands, PathParseError } from '../lib/pathutils.js'; -import { getEllipseProperties } from '../lib/svgo/tools.js'; +import { + addChildToDelete, + deleteChildren, + getEllipseProperties, +} from '../lib/svgo/tools.js'; import { elemsGroups } from './_collections.js'; export const name = 'removeHiddenElems'; @@ -44,12 +48,7 @@ export const fn = (root, params, info) => { * @param {import('../lib/types.js').XastElement} element */ function removeElement(element) { - let childrenToDelete = childrenToDeleteByParent.get(element.parentNode); - if (!childrenToDelete) { - childrenToDelete = new Set(); - childrenToDeleteByParent.set(element.parentNode, childrenToDelete); - } - childrenToDelete.add(element); + addChildToDelete(childrenToDeleteByParent, element); } /** @@ -200,12 +199,7 @@ export const fn = (root, params, info) => { }, root: { exit: () => { - // For each parent, delete no longer needed children. - for (const [parent, childrenToDelete] of childrenToDeleteByParent) { - parent.children = parent.children.filter( - (c) => !childrenToDelete.has(c), - ); - } + deleteChildren(childrenToDeleteByParent); }, }, };