import { elemsGroups } from './_collections.js'; import { detachNodeFromParent } from '../lib/xast.js'; import { collectStylesheet, computeStyle } from '../lib/style.js'; import { findReferences } from '../lib/svgo/tools.js'; export const name = 'removeEmptyContainers'; export const description = 'removes empty container elements'; /** * Remove empty containers. * * @see https://www.w3.org/TR/SVG11/intro.html#TermContainerElement * * @example * * * @example * * * @author Kir Belevich * * @type {import('../lib/types.js').Plugin} */ export const fn = (root) => { const stylesheet = collectStylesheet(root); const removedIds = new Set(); /** * @type {Map} */ const usesById = new Map(); return { element: { enter: (node, parentNode) => { if (node.name === 'use') { // Record uses so those referencing empty containers can be removed. for (const [name, value] of Object.entries(node.attributes)) { const ids = findReferences(name, value); for (const id of ids) { let references = usesById.get(id); if (references === undefined) { references = []; usesById.set(id, references); } references.push({ node: node, parent: parentNode }); } } } }, exit: (node, parentNode) => { // remove only empty non-svg containers if ( node.name === 'svg' || !elemsGroups.container.has(node.name) || node.children.length !== 0 ) { return; } // empty patterns may contain reusable configuration if ( node.name === 'pattern' && Object.keys(node.attributes).length !== 0 ) { return; } // empty hides masked element if (node.name === 'mask' && node.attributes.id != null) { return; } if (parentNode.type === 'element' && parentNode.name === 'switch') { 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 || computeStyle(stylesheet, node).filter) ) { return; } detachNodeFromParent(node, parentNode); if (node.attributes.id) { removedIds.add(node.attributes.id); } }, }, root: { exit: () => { // Remove any elements that referenced an empty container. for (const id of removedIds) { const uses = usesById.get(id); if (uses) { for (const use of uses) { detachNodeFromParent(use.node, use.parent); } } } }, }, }; };