'use strict'; /** * @typedef {import('../lib/types').XastChild} XastChild * @typedef {import('../lib/types').XastElement} XastElement * @typedef {import('../lib/types').XastParent} XastParent */ const { elemsGroups } = require('./_collections.js'); const { visit, visitSkip, querySelector, detachNodeFromParent, } = require('../lib/xast.js'); const { collectStylesheet, computeStyle } = require('../lib/style.js'); const { parsePathData } = require('../lib/path.js'); const { hasScripts, findReferences } = require('../lib/svgo/tools.js'); const nonRendering = elemsGroups.nonRendering; exports.name = 'removeHiddenElems'; exports.description = 'removes hidden elements (zero sized, with absent attributes)'; /** * Remove hidden elements with disabled rendering: * - display="none" * - opacity="0" * - circle with zero radius * - ellipse with zero x-axis or y-axis radius * - rectangle with zero width or height * - pattern with zero width or height * - image with zero width or height * - path with empty data * - polyline with empty points * - polygon with empty points * * @author Kir Belevich * * @type {import('./plugins-types').Plugin<'removeHiddenElems'>} */ exports.fn = (root, params) => { const { isHidden = true, displayNone = true, opacity0 = true, circleR0 = true, ellipseRX0 = true, ellipseRY0 = true, rectWidth0 = true, rectHeight0 = true, patternWidth0 = true, patternHeight0 = true, imageWidth0 = true, imageHeight0 = true, pathEmptyD = true, polylineEmptyPoints = true, polygonEmptyPoints = true, } = params; const stylesheet = collectStylesheet(root); /** * Skip non-rendered nodes initially, and only detach if they have no ID, or * their ID is not referenced by another node. * * @type {Map} */ const nonRenderedNodes = new Map(); /** * IDs for removed hidden definitions. * * @type {Set} */ const removedDefIds = new Set(); /** * @type {Map} */ const allDefs = new Map(); /** @type {Set} */ const allReferences = new Set(); /** * @type {Map>} */ const referencesById = new Map(); /** * If styles are present, we can't be sure if a definition is unused or not */ let deoptimized = false; /** * @param {XastChild} node * @param {XastParent} parentNode */ function removeElement(node, parentNode) { if ( node.type === 'element' && node.attributes.id != null && parentNode.type === 'element' && parentNode.name === 'defs' ) { removedDefIds.add(node.attributes.id); } detachNodeFromParent(node, parentNode); } visit(root, { element: { enter: (node, parentNode) => { // transparent non-rendering elements still apply where referenced if (nonRendering.has(node.name)) { if (node.attributes.id == null) { detachNodeFromParent(node, parentNode); return visitSkip; } nonRenderedNodes.set(node, parentNode); return visitSkip; } const computedStyle = computeStyle(stylesheet, node); // opacity="0" // // https://www.w3.org/TR/SVG11/masking.html#ObjectAndGroupOpacityProperties if ( opacity0 && computedStyle.opacity && computedStyle.opacity.type === 'static' && computedStyle.opacity.value === '0' ) { removeElement(node, parentNode); } }, }, }); return { element: { enter: (node, parentNode) => { if ( (node.name === 'style' && node.children.length !== 0) || hasScripts(node) ) { deoptimized = true; return; } if (node.name === 'defs') { allDefs.set(node, parentNode); } if (node.name === 'use') { for (const attr of Object.keys(node.attributes)) { if (attr !== 'href' && !attr.endsWith(':href')) continue; const value = node.attributes[attr]; const id = value.slice(1); let refs = referencesById.get(id); if (!refs) { refs = []; referencesById.set(id, refs); } refs.push({ node, parentNode }); } } // Removes hidden elements // https://www.w3schools.com/cssref/pr_class_visibility.asp const computedStyle = computeStyle(stylesheet, node); if ( isHidden && computedStyle.visibility && computedStyle.visibility.type === 'static' && computedStyle.visibility.value === 'hidden' && // keep if any descendant enables visibility querySelector(node, '[visibility=visible]') == null ) { removeElement(node, parentNode); return; } // display="none" // // https://www.w3.org/TR/SVG11/painting.html#DisplayProperty // "A value of display: none indicates that the given element // and its children shall not be rendered directly" if ( displayNone && computedStyle.display && computedStyle.display.type === 'static' && computedStyle.display.value === 'none' && // markers with display: none still rendered node.name !== 'marker' ) { removeElement(node, parentNode); return; } // Circles with zero radius // // https://www.w3.org/TR/SVG11/shapes.html#CircleElementRAttribute // "A value of zero disables rendering of the element" // // if ( circleR0 && node.name === 'circle' && node.children.length === 0 && node.attributes.r === '0' ) { removeElement(node, parentNode); return; } // Ellipse with zero x-axis radius // // https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRXAttribute // "A value of zero disables rendering of the element" // // if ( ellipseRX0 && node.name === 'ellipse' && node.children.length === 0 && node.attributes.rx === '0' ) { removeElement(node, parentNode); return; } // Ellipse with zero y-axis radius // // https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRYAttribute // "A value of zero disables rendering of the element" // // if ( ellipseRY0 && node.name === 'ellipse' && node.children.length === 0 && node.attributes.ry === '0' ) { removeElement(node, parentNode); return; } // Rectangle with zero width // // https://www.w3.org/TR/SVG11/shapes.html#RectElementWidthAttribute // "A value of zero disables rendering of the element" // // if ( rectWidth0 && node.name === 'rect' && node.children.length === 0 && node.attributes.width === '0' ) { removeElement(node, parentNode); return; } // Rectangle with zero height // // https://www.w3.org/TR/SVG11/shapes.html#RectElementHeightAttribute // "A value of zero disables rendering of the element" // // if ( rectHeight0 && rectWidth0 && node.name === 'rect' && node.children.length === 0 && node.attributes.height === '0' ) { removeElement(node, parentNode); return; } // Pattern with zero width // // https://www.w3.org/TR/SVG11/pservers.html#PatternElementWidthAttribute // "A value of zero disables rendering of the element (i.e., no paint is applied)" // // if ( patternWidth0 && node.name === 'pattern' && node.attributes.width === '0' ) { removeElement(node, parentNode); return; } // Pattern with zero height // // https://www.w3.org/TR/SVG11/pservers.html#PatternElementHeightAttribute // "A value of zero disables rendering of the element (i.e., no paint is applied)" // // if ( patternHeight0 && node.name === 'pattern' && node.attributes.height === '0' ) { removeElement(node, parentNode); return; } // Image with zero width // // https://www.w3.org/TR/SVG11/struct.html#ImageElementWidthAttribute // "A value of zero disables rendering of the element" // // if ( imageWidth0 && node.name === 'image' && node.attributes.width === '0' ) { removeElement(node, parentNode); return; } // Image with zero height // // https://www.w3.org/TR/SVG11/struct.html#ImageElementHeightAttribute // "A value of zero disables rendering of the element" // // if ( imageHeight0 && node.name === 'image' && node.attributes.height === '0' ) { removeElement(node, parentNode); return; } // Path with empty data // // https://www.w3.org/TR/SVG11/paths.html#DAttribute // // if (pathEmptyD && node.name === 'path') { if (node.attributes.d == null) { removeElement(node, parentNode); return; } const pathData = parsePathData(node.attributes.d); if (pathData.length === 0) { removeElement(node, parentNode); return; } // keep single point paths for markers if ( pathData.length === 1 && computedStyle['marker-start'] == null && computedStyle['marker-end'] == null ) { removeElement(node, parentNode); return; } } // Polyline with empty points // // https://www.w3.org/TR/SVG11/shapes.html#PolylineElementPointsAttribute // // if ( polylineEmptyPoints && node.name === 'polyline' && node.attributes.points == null ) { removeElement(node, parentNode); return; } // Polygon with empty points // // https://www.w3.org/TR/SVG11/shapes.html#PolygonElementPointsAttribute // // if ( polygonEmptyPoints && node.name === 'polygon' && node.attributes.points == null ) { removeElement(node, parentNode); return; } for (const [name, value] of Object.entries(node.attributes)) { const ids = findReferences(name, value); for (const id of ids) { allReferences.add(id); } } }, }, root: { exit: () => { for (const id of removedDefIds) { const refs = referencesById.get(id); if (refs) { for (const { node, parentNode } of refs) { detachNodeFromParent(node, parentNode); } } } if (!deoptimized) { for (const [ nonRenderedNode, nonRenderedParent, ] of nonRenderedNodes.entries()) { const id = nonRenderedNode.attributes.id; if (!allReferences.has(id)) { detachNodeFromParent(nonRenderedNode, nonRenderedParent); } } } for (const [node, parentNode] of allDefs.entries()) { if (node.children.length === 0) { detachNodeFromParent(node, parentNode); } } }, }, }; };