Files
rspade_system/node_modules/svgo/plugins/removeHiddenElems.js
root d523f0f600 Fix code quality violations and exclude Manifest from checks
Document application modes (development/debug/production)
Add global file drop handler, order column normalization, SPA hash fix
Serve CDN assets via /_vendor/ URLs instead of merging into bundles
Add production minification with license preservation
Improve JSON formatting for debugging and production optimization
Add CDN asset caching with CSS URL inlining for production builds
Add three-mode system (development, debug, production)
Update Manifest CLAUDE.md to reflect helper class architecture
Refactor Manifest.php into helper classes for better organization
Pre-manifest-refactor checkpoint: Add app_mode documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-14 10:38:22 +00:00

460 lines
13 KiB
JavaScript
Executable File

import { elemsGroups } from './_collections.js';
import { detachNodeFromParent, querySelector } from '../lib/xast.js';
import { visit, visitSkip } from '../lib/util/visit.js';
import { collectStylesheet, computeStyle } from '../lib/style.js';
import { parsePathData } from '../lib/path.js';
import { findReferences, hasScripts } from '../lib/svgo/tools.js';
/**
* @typedef RemoveHiddenElemsParams
* @property {boolean=} isHidden
* @property {boolean=} displayNone
* @property {boolean=} opacity0
* @property {boolean=} circleR0
* @property {boolean=} ellipseRX0
* @property {boolean=} ellipseRY0
* @property {boolean=} rectWidth0
* @property {boolean=} rectHeight0
* @property {boolean=} patternWidth0
* @property {boolean=} patternHeight0
* @property {boolean=} imageWidth0
* @property {boolean=} imageHeight0
* @property {boolean=} pathEmptyD
* @property {boolean=} polylineEmptyPoints
* @property {boolean=} polygonEmptyPoints
*/
const nonRendering = elemsGroups.nonRendering;
export const name = 'removeHiddenElems';
export const 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('../lib/types.js').Plugin<RemoveHiddenElemsParams>}
*/
export const 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<import('../lib/types.js').XastElement, import('../lib/types.js').XastParent>}
*/
const nonRenderedNodes = new Map();
/**
* IDs for removed hidden definitions.
*
* @type {Set<string>}
*/
const removedDefIds = new Set();
/** @type {Map<import('../lib/types.js').XastElement, import('../lib/types.js').XastParent>} */
const allDefs = new Map();
/** @type {Set<string>} */
const allReferences = new Set();
/** @type {Map<string, Array<{ node: import('../lib/types.js').XastElement, parentNode: import('../lib/types.js').XastParent }>>} */
const referencesById = new Map();
/**
* If styles are present, we can't be sure if a definition is unused or not
*/
let deoptimized = false;
/**
* Nodes can't be removed if they or any of their children have an id attribute that is referenced.
* @param {import('../lib/types.js').XastElement} node
* @returns boolean
*/
function canRemoveNonRenderingNode(node) {
if (allReferences.has(node.attributes.id)) {
return false;
}
for (const child of node.children) {
if (child.type === 'element' && !canRemoveNonRenderingNode(child)) {
return false;
}
}
return true;
}
/**
* @param {import('../lib/types.js').XastChild} node
* @param {import('../lib/types.js').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)) {
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'
) {
if (node.name === 'path') {
nonRenderedNodes.set(node, parentNode);
return visitSkip;
}
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"
//
// <circle r="0">
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"
//
// <ellipse rx="0">
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"
//
// <ellipse ry="0">
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"
//
// <rect width="0">
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"
//
// <rect height="0">
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)"
//
// <pattern width="0">
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)"
//
// <pattern height="0">
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"
//
// <image width="0">
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"
//
// <image height="0">
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
//
// <path d=""/>
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
//
// <polyline points="">
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
//
// <polygon points="">
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()) {
if (canRemoveNonRenderingNode(nonRenderedNode)) {
detachNodeFromParent(nonRenderedNode, nonRenderedParent);
}
}
}
for (const [node, parentNode] of allDefs.entries()) {
if (node.children.length === 0) {
detachNodeFromParent(node, parentNode);
}
}
},
},
};
};