diff --git a/lib/docdata.js b/lib/docdata.js index b737330..69a097d 100644 --- a/lib/docdata.js +++ b/lib/docdata.js @@ -2,6 +2,7 @@ import * as csso from 'csso'; import { attrsGroups, elemsGroups, + geometryProperties, inheritableAttrs, } from '../plugins/_collections.js'; import { parseStylesheet } from './style-css-tree.js'; @@ -97,6 +98,12 @@ export class StyleData { for (const [name, value] of Object.entries(element.attributes)) { if (attrsGroups.presentation.has(name)) { computedStyles.set(name, value); + } else { + // See if it is a geometry property which applies to this element. + const els = geometryProperties[name]; + if (els && els.has(element.name)) { + computedStyles.set(name, value); + } } } @@ -112,6 +119,9 @@ export class StyleData { const hasVars = VAR_REGEXP.test(value.value); if (hasVars) { computedStyles.set(name, null); + } else if (geometryProperties[name]) { + // Support for geometry properties is inconsistent. Avoid changing these. + computedStyles.set(name, null); } else { computedStyles.set(name, value.value); if (value.important) { @@ -129,7 +139,10 @@ export class StyleData { } if (declarations) { declarations.forEach((value, name) => { - if (value.important || !importantProperties.has(name)) { + if (geometryProperties[name]) { + // Support for geometry properties is inconsistent. Avoid changing these. + computedStyles.set(name, null); + } else if (value.important || !importantProperties.has(name)) { computedStyles.set(name, value.value); } }); diff --git a/lib/parser.js b/lib/parser.js index 8d156c6..44a420e 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -167,7 +167,7 @@ export const parseSvg = (data, from) => { const node = { type: 'comment', parentNode: current, - value: foreignLevel > 0 ? comment : comment.trim(), + value: comment, }; pushToContent(node); }; diff --git a/lib/path.js b/lib/path.js index 6ea533c..0cb2a5a 100644 --- a/lib/path.js +++ b/lib/path.js @@ -128,6 +128,7 @@ const readNumber = (string, cursor) => { /** * @param {string} string * @returns {PathDataItem[]} + * @deprecated */ export const parsePathData = (string) => { /** @type {PathDataItem[]} */ diff --git a/lib/svgo/tools.js b/lib/svgo/tools.js index feab6b0..aefbc8d 100644 --- a/lib/svgo/tools.js +++ b/lib/svgo/tools.js @@ -147,6 +147,28 @@ export function exactMul(n, m) { return toFixed(n * m, d1 + d2); } +/** + * @param {Map} properties + * @returns {{rx:string,ry:string}|undefined} + */ +export function getEllipseProperties(properties) { + let rx = properties.get('rx'); + let ry = properties.get('ry'); + if (rx === undefined) { + if (ry === undefined) { + rx = ry = '0'; + } else { + rx = ry; + } + } else if (ry === undefined) { + ry = rx; + } + if (rx === null || ry === null) { + return; + } + return { rx: rx, ry: ry }; +} + /** * @param {number|string} str */ diff --git a/lib/xast.js b/lib/xast.js index a947f9e..64786a6 100644 --- a/lib/xast.js +++ b/lib/xast.js @@ -100,6 +100,7 @@ const visitChild = (node, visitor, parents) => { /** * @param {XastChild} node * @param {XastParent} [parentNode] + * @deprecated */ // Disable no-unused-vars until all calls to detachNodeFromParent() are updated. // eslint-disable-next-line no-unused-vars diff --git a/plugins/_collections.js b/plugins/_collections.js index 54933a0..88a5802 100644 --- a/plugins/_collections.js +++ b/plugins/_collections.js @@ -2381,6 +2381,21 @@ export const colorsProps = new Set([ 'stroke', ]); +// See https://www.w3.org/TR/SVG2/geometry.html#geometry-properties +/** @type {Object>} */ +export const geometryProperties = { + cx: new Set(['circle', 'ellipse']), + cy: new Set(['circle', 'ellipse']), + d: new Set(['path']), + height: new Set(['foreignObject', 'image', 'rect', 'svg', 'symbol', 'use']), + r: new Set(['circle']), + rx: new Set(['ellipse', 'rect']), + ry: new Set(['ellipse', 'rect']), + width: new Set(['foreignObject', 'image', 'rect', 'svg', 'symbol', 'use']), + x: new Set(['foreignObject', 'image', 'rect', 'svg', 'symbol', 'use']), + y: new Set(['foreignObject', 'image', 'rect', 'svg', 'symbol', 'use']), +}; + /** @see https://developer.mozilla.org/docs/Web/CSS/Pseudo-classes */ export const pseudoClasses = { displayState: new Set(['fullscreen', 'modal', 'picture-in-picture']), diff --git a/plugins/cleanupStyleAttributes.js b/plugins/cleanupStyleAttributes.js index 4fffffb..d428b06 100644 --- a/plugins/cleanupStyleAttributes.js +++ b/plugins/cleanupStyleAttributes.js @@ -1,7 +1,11 @@ import { getStyleDeclarations } from '../lib/css-tools.js'; import { writeStyleAttribute } from '../lib/css.js'; import { visitSkip } from '../lib/xast.js'; -import { elemsGroups, uselessShapeProperties } from './_collections.js'; +import { + elemsGroups, + geometryProperties, + uselessShapeProperties, +} from './_collections.js'; export const name = 'cleanupStyleAttributes'; export const description = 'removes invalid properties from style attributes'; @@ -76,20 +80,6 @@ const presentationProperties = new Set([ 'writing-mode', ]); -/** @type {Object>} */ -const limitedProperties = { - cx: new Set(['circle', 'ellipse']), - cy: new Set(['circle', 'ellipse']), - d: new Set(['path']), - height: new Set(['foreignObject', 'image', 'rect', 'svg', 'symbol', 'use']), - r: new Set(['circle']), - rx: new Set(['ellipse', 'rect']), - ry: new Set(['ellipse', 'rect']), - width: new Set(['foreignObject', 'image', 'rect', 'svg', 'symbol', 'use']), - x: new Set(['foreignObject', 'image', 'rect', 'svg', 'symbol', 'use']), - y: new Set(['foreignObject', 'image', 'rect', 'svg', 'symbol', 'use']), -}; - /** * @type {import('./plugins-types.js').Plugin<'cleanupStyleAttributes'>} */ @@ -135,7 +125,7 @@ export const fn = (root, params, info) => { } // See if it is allowed for this element. - const allowedElements = limitedProperties[propName]; + const allowedElements = geometryProperties[propName]; return allowedElements && allowedElements.has(elName); } diff --git a/plugins/combinePaths.js b/plugins/combinePaths.js index e36a676..cfd01f0 100644 --- a/plugins/combinePaths.js +++ b/plugins/combinePaths.js @@ -122,6 +122,7 @@ function allStylesAreMergeable(styles) { continue; } break; + case 'd': case 'fill-opacity': case 'opacity': case 'stroke-linecap': diff --git a/plugins/minifyPathData.js b/plugins/minifyPathData.js index 7365b48..7ec3946 100644 --- a/plugins/minifyPathData.js +++ b/plugins/minifyPathData.js @@ -787,9 +787,10 @@ function stringifyCommand(cmdCode, prevCmdChar, lastNumber, args) { /** * @param {string} path + * @param {number} [maxCommands] * @returns {PathCommand[]} */ -export function parsePathCommands(path) { +export function parsePathCommands(path, maxCommands = Number.MAX_SAFE_INTEGER) { function addArg() { if (currentArg !== '') { args.push(currentArg); @@ -831,6 +832,9 @@ export function parsePathCommands(path) { switch (getCharType(c)) { case 'command': addCurrentCommand(c); + if (commands.length >= maxCommands) { + return commands; + } continue; case '.': if (currentCommand === '') { @@ -1088,7 +1092,7 @@ class ExactNum { } } -class PathParseError extends Error { +export class PathParseError extends Error { /** * @param {string} msg */ diff --git a/plugins/preset-default.js b/plugins/preset-default.js index c6313f4..7145db2 100644 --- a/plugins/preset-default.js +++ b/plugins/preset-default.js @@ -26,7 +26,6 @@ import * as removeMetadata from './removeMetadata.js'; import * as removeNonInheritableGroupAttrs from './removeNonInheritableGroupAttrs.js'; import * as removeUnknownsAndDefaults from './removeUnknownsAndDefaults.js'; import * as removeUnusedNS from './removeUnusedNS.js'; -import * as removeUselessDefs from './removeUselessDefs.js'; import * as removeUselessStrokeAndFill from './removeUselessStrokeAndFill.js'; import * as removeXMLProcInst from './removeXMLProcInst.js'; @@ -45,7 +44,6 @@ const presetDefault = createPreset({ inlineStyles, minifyStyles, cleanupIds, - removeUselessDefs, convertColors, removeUnknownsAndDefaults, removeNonInheritableGroupAttrs, diff --git a/plugins/preset-next.js b/plugins/preset-next.js index 3f05717..df3a768 100644 --- a/plugins/preset-next.js +++ b/plugins/preset-next.js @@ -26,7 +26,6 @@ import * as removeMetadata from './removeMetadata.js'; import * as removeNonInheritableGroupAttrs from './removeNonInheritableGroupAttrs.js'; import * as removeUnknownsAndDefaults from './removeUnknownsAndDefaults.js'; import * as removeUnusedNS from './removeUnusedNS.js'; -import * as removeUselessDefs from './removeUselessDefs.js'; import * as removeUselessStrokeAndFill from './removeUselessStrokeAndFill.js'; import * as removeXMLProcInst from './removeXMLProcInst.js'; @@ -45,7 +44,6 @@ const presetNext = createPreset({ inlineStyles, minifyStyles, cleanupIds, - removeUselessDefs, convertColors, removeUnknownsAndDefaults, removeNonInheritableGroupAttrs, diff --git a/plugins/removeHiddenElems.js b/plugins/removeHiddenElems.js index 42891d9..30475fe 100644 --- a/plugins/removeHiddenElems.js +++ b/plugins/removeHiddenElems.js @@ -1,56 +1,15 @@ -/** - * @typedef {import('../lib/types.js').XastChild} XastChild - * @typedef {import('../lib/types.js').XastElement} XastElement - * @typedef {import('../lib/types.js').XastParent} XastParent - */ - +import { getEllipseProperties } from '../lib/svgo/tools.js'; import { elemsGroups } from './_collections.js'; -import { querySelector, detachNodeFromParent } from '../lib/xast.js'; -import { parsePathData } from '../lib/path.js'; -import { findReferences } from '../lib/svgo/tools.js'; - -const nonRendering = elemsGroups.nonRendering; +import { parsePathCommands, PathParseError } from './minifyPathData.js'; export const name = 'removeHiddenElems'; export const description = - 'removes hidden elements (zero sized, with absent attributes)'; + 'removes non-rendered elements that are not referenced'; /** - * 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.js').Plugin<'removeHiddenElems'>} */ export const fn = (root, params, info) => { - 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 styleData = info.docData.getStyles(); if ( info.docData.hasScripts() || @@ -60,455 +19,250 @@ export const fn = (root, params, info) => { return; } - let inNonRenderingNode = 0; - - /** - * Skip non-rendered nodes initially, and only detach if they have no ID, or - * their ID is not referenced by another node. - * - * @type {Set} - */ - const nonRenderedNodes = new Set(); - - /** - * IDs for removed hidden definitions. - * - * @type {Set} - */ - const removedDefIds = new Set(); - - /** @type {Set} */ - const defNodesToRemove = new Set(); - - /** - * @type {Set} - */ - const allDefs = new Set(); - - /** @type {Map>} */ - const allReferences = new Map(); - - /** @type {Map} */ - const referencedIdsByNode = new Map(); + // Record which elements to delete, sorted by parent. + /** @type {Map>} */ + const childrenToDeleteByParent = new Map(); - /** - * @type {Map} - */ - const referencesById = new Map(); + /** @type {import('../lib/types.js').XastElement[]} */ + const nonRenderingStack = []; /** - * Nodes can't be removed if they or any of their children have an id attribute that is referenced. - * @param {XastElement} node - * @returns boolean + * @param {import('../lib/types.js').XastElement} element */ - function canRemoveNonRenderingNode(node) { - const refs = allReferences.get(node.attributes.id); - if (refs && refs.size) { - return false; - } - for (const child of node.children) { - if (child.type === 'element' && !canRemoveNonRenderingNode(child)) { - return false; - } + function convertToDefs(element) { + element.name = 'defs'; + element.attributes = {}; + processDefsChildren(element); + if (element.children.length === 0) { + // If there are no children, delete the element; otherwise it may limit opportunities for compression of siblings. + removeElement(element); } - return true; } /** - * Retrieve information about all IDs referenced by an element and its children. - * @param {XastElement} node - * @returns {XastElement[]} + * @param {import('../lib/types.js').XastElement} element */ - function getNodesReferencedByBranch(node) { - const allIds = []; - const thisNodeIds = referencedIdsByNode.get(node); - if (thisNodeIds) { - allIds.push(node); + function removeElement(element) { + let childrenToDelete = childrenToDeleteByParent.get(element.parentNode); + if (!childrenToDelete) { + childrenToDelete = new Set(); + childrenToDeleteByParent.set(element.parentNode, childrenToDelete); } - for (const child of node.children) { - if (child.type === 'element') { - allIds.push(...getNodesReferencedByBranch(child)); - } - } - return allIds; + childrenToDelete.add(element); } /** - * @param {string} referencedId - * @param {XastElement} referencingElement + * @param {import('../lib/types.js').XastElement} element */ - function recordReference(referencedId, referencingElement) { - const refs = allReferences.get(referencedId); - if (refs) { - refs.add(referencingElement); + function removeUndisplayedElement(element) { + if (element.name === 'g') { + // It may contain referenced elements; treat it as . + convertToDefs(element); } else { - allReferences.set(referencedId, new Set([referencingElement])); + removeElement(element); } } /** - * @param {XastElement} node - * @param {XastParent} parentNode + * @param {import('../lib/types.js').XastElement} element + * @param {Map} properties + * @returns {boolean} */ - function removeElement(node, parentNode) { - if ( - node.type === 'element' && - node.attributes.id != null && - parentNode.type === 'element' && - parentNode.name === 'defs' - ) { - removedDefIds.add(node.attributes.id); - } - - defNodesToRemove.add(node); - } - - // Record all references in the style element. - const styleElement = styleData.getFirstStyleElement(); - if (styleElement) { - for (const id of styleData.getIdsReferencedByProperties()) { - recordReference(id, styleElement); - } - } - - return { - element: { - enter: (node, parentNode, parentInfo) => { - if (nonRendering.has(node.name)) { - nonRenderedNodes.add(node); - inNonRenderingNode++; + function removeEmptyShapes(element, properties) { + switch (element.name) { + case 'circle': + if (properties.get('r') === '0' && !isAnimated(element)) { + removeElement(element); + return true; } - const computedStyle = styleData.computeStyle(node, parentInfo); - const opacity = computedStyle.get('opacity'); - // https://www.w3.org/TR/SVG11/masking.html#ObjectAndGroupOpacityProperties - if (opacity0 && opacity === '0') { - if (!inNonRenderingNode) { - if (node.name === 'path') { - // It's possible this will be referenced in a . - nonRenderedNodes.add(node); - } else { - removeElement(node, parentNode); - return; - } + return false; + case 'ellipse': + { + // Ellipse with zero radius -- https://svgwg.org/svg2-draft/geometry.html#RxProperty + const props = getEllipseProperties(properties); + if (props === undefined) { + return false; + } + if ( + element.children.length === 0 && + (props.rx === '0' || props.ry === '0') + ) { + removeElement(element); + return true; } } - - if (node.name === 'defs') { - allDefs.add(node); + return false; + case 'path': { + const d = properties.get('d'); + if (d === null) { + return false; } - - 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); + if (!d) { + removeElement(element); + return true; + } + try { + const commands = parsePathCommands(d, 2); + if (commands.length === 1) { + if (properties.get('marker-end') !== undefined) { + return false; } - refs.push(node); + removeElement(element); + return true; } + } catch (error) { + if (error instanceof PathParseError) { + console.warn(error.message); + return false; + } + throw error; } - - // Removes hidden elements - // https://www.w3schools.com/cssref/pr_class_visibility.asp - const visibility = computedStyle.get('visibility'); - if ( - isHidden && - visibility === '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" - const display = computedStyle.get('display'); - if ( - displayNone && - display === '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" - // - // + return false; + } + case 'rect': + // https://svgwg.org/svg2-draft/shapes.html#RectElement if ( - circleR0 && - node.name === 'circle' && - node.children.length === 0 && - node.attributes.r === '0' + element.children.length === 0 && + (!element.attributes.width || + !element.attributes.height || + element.attributes.width === '0' || + element.attributes.height === '0') ) { - removeElement(node, parentNode); - return; + removeElement(element); + return true; } + break; + } - // 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; - } + return false; + } - // 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 { + element: { + enter: (element, parentNode, parentInfo) => { + if (element.name === 'defs') { + processDefsChildren(element); 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); + // Process non-rendering elements. + if (elemsGroups.nonRendering.has(element.name)) { + if (!element.attributes.id) { + // If the element doesn't have an id, it can't be referenced; but it may contain referenced elements. Change it to . + convertToDefs(element); + } else { + nonRenderingStack.push(element); + } 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); + if (element.attributes.id) { + // Never delete elements with an id. 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); + const properties = styleData.computeStyle(element, parentInfo); + if (!properties) { 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); + if (removeEmptyShapes(element, properties)) { 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; - } + // Remove any rendering elements which are not visible. - // Image with zero height - // - // https://www.w3.org/TR/SVG11/struct.html#ImageElementHeightAttribute - // "A value of zero disables rendering of the element" - // - // + const display = properties.get('display'); if ( - imageHeight0 && - node.name === 'image' && - node.attributes.height === '0' + display === 'none' && + // markers with display: none still rendered + element.name !== 'marker' ) { - removeElement(node, parentNode); + removeUndisplayedElement(element); 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); + if (nonRenderingStack.length === 0) { + // Don't delete elements with opacity 0 which are in a non-rendering element. + const opacity = properties.get('opacity'); + if (opacity === '0') { + removeUndisplayedElement(element); return; } - // keep single point paths for markers - if ( - pathData.length === 1 && - !computedStyle.has('marker-start') && - !computedStyle.has('marker-end') - ) { - 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; - } - - const allIds = []; - for (const [name, value] of Object.entries(node.attributes)) { - const ids = findReferences(name, value); - - // Record which other nodes are referenced by this node. - for (const id of ids) { - allIds.push(id); - recordReference(id, node); - } - } - - // Record which ids are referenced by this node. - if (allIds.length) { - referencedIdsByNode.set(node, allIds); } }, - exit: (node) => { - if (nonRendering.has(node.name)) { - inNonRenderingNode--; + exit: (element) => { + if (elemsGroups.nonRendering.has(element.name)) { + nonRenderingStack.pop(); } }, }, root: { exit: () => { - for (const child of defNodesToRemove) { - detachNodeFromParent(child); - nonRenderedNodes.delete(child); - } - - for (const id of removedDefIds) { - const refs = referencesById.get(id); - if (refs) { - for (const node of refs) { - detachNodeFromParent(node); - } - } - } - - let tryAgain; - do { - tryAgain = false; - for (const nonRenderedNode of nonRenderedNodes) { - if (canRemoveNonRenderingNode(nonRenderedNode)) { - detachNodeFromParent(nonRenderedNode); - nonRenderedNodes.delete(nonRenderedNode); - - // For any elements referenced by the just-deleted node and its children, remove the node from the list of referencing nodes. - const deletedReferenceNodes = - getNodesReferencedByBranch(nonRenderedNode); - for (const deletedNode of deletedReferenceNodes) { - const referencedIds = referencedIdsByNode.get(deletedNode); - if (referencedIds) { - for (const id of referencedIds) { - const referencingNodes = allReferences.get(id); - if (referencingNodes) { - referencingNodes.delete(deletedNode); - if (referencingNodes.size === 0) { - tryAgain = true; - } - } - } - } - } - } - } - } while (tryAgain); - - for (const node of allDefs) { - if (node.children.length === 0) { - detachNodeFromParent(node); - } + // For each parent, delete no longer needed children. + for (const [parent, childrenToDelete] of childrenToDeleteByParent) { + parent.children = parent.children.filter( + (c) => !childrenToDelete.has(c), + ); } }, }, }; }; + +/** + * @param {import('../lib/types.js').XastChild} child + * @returns {import('../lib/types.js').XastChild[]} + */ +function getChildrenWithIds(child) { + switch (child.type) { + case 'comment': + return [child]; + case 'element': + if (child.attributes.id) { + return [child]; + } + break; + default: + return []; + } + + // Preserve styles and scripts with no id. + switch (child.name) { + case 'script': + case 'style': + return [child]; + } + + // It's an element with no id; return its children which have ids. + const children = []; + for (const grandchild of child.children) { + children.push(...getChildrenWithIds(grandchild)); + } + return children; +} + +/** + * @param {import('../lib/types.js').XastElement} element + */ +function isAnimated(element) { + // TODO: fix this - it doesn't really tell whether the element is animated, and doesn't handle the case of the animation being somewhere + // besdides within the element. + return element.children.length !== 0; +} + +/** + * @param {import('../lib/types.js').XastElement} element + */ +function processDefsChildren(element) { + /** @type {import('../lib/types.js').XastChild[]} */ + const children = []; + + // Make sure all children of have an id; otherwise they can't be rendered. If a child doesn't have an id, delete it and move up + // its children so they are immediate children of the . + for (const child of element.children) { + children.push(...getChildrenWithIds(child)); + } + children.forEach((c) => (c.parentNode = element)); + element.children = children; +} diff --git a/plugins/removeUselessDefs.js b/plugins/removeUselessDefs.js index 5da285f..cc2351e 100644 --- a/plugins/removeUselessDefs.js +++ b/plugins/removeUselessDefs.js @@ -8,14 +8,23 @@ import { elemsGroups } from './_collections.js'; export const name = 'removeUselessDefs'; export const description = 'removes elements in without id'; +let deprecationWarning = true; + /** * Removes content of defs and properties that aren't rendered directly without ids. * * @author Lev Solntsev * * @type {import('./plugins-types.js').Plugin<'removeUselessDefs'>} + * @deprecated */ export const fn = () => { + if (deprecationWarning) { + console.warn( + 'The moveGroupAttrsToElems plugin is deprecated and will be removed in a future release.', + ); + deprecationWarning = false; + } return { element: { enter: (node, parentNode) => { diff --git a/test/fixtures/files/ellipse.1.svg.txt b/test/fixtures/files/ellipse.1.svg.txt new file mode 100644 index 0000000..0919a82 --- /dev/null +++ b/test/fixtures/files/ellipse.1.svg.txt @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/files/marker.1.svg b/test/fixtures/files/marker.1.svg new file mode 100644 index 0000000..7b8a20c --- /dev/null +++ b/test/fixtures/files/marker.1.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/plugins/_index.test.js b/test/plugins/_index.test.js index 475aebd..43b7654 100644 --- a/test/plugins/_index.test.js +++ b/test/plugins/_index.test.js @@ -43,6 +43,7 @@ describe('plugins tests', function () { // @ts-ignore plugins: [plugin], js2svg: { pretty: true }, + maxPasses: 1, }); lastResultData = result.data; expect(normalize(result.data)).toStrictEqual(should); diff --git a/test/plugins/inlineStyles.03.svg.txt b/test/plugins/inlineStyles.03.svg.txt index eb5ef2d..b3bd74a 100644 --- a/test/plugins/inlineStyles.03.svg.txt +++ b/test/plugins/inlineStyles.03.svg.txt @@ -16,7 +16,7 @@ @@@ - + + diff --git a/test/plugins/removeHiddenElems.19.svg.txt b/test/plugins/removeHiddenElems.19.obsolete.svg.txt similarity index 100% rename from test/plugins/removeHiddenElems.19.svg.txt rename to test/plugins/removeHiddenElems.19.obsolete.svg.txt diff --git a/test/plugins/removeHiddenElems.20.svg.txt b/test/plugins/removeHiddenElems.20.obsolete.svg.txt similarity index 98% rename from test/plugins/removeHiddenElems.20.svg.txt rename to test/plugins/removeHiddenElems.20.obsolete.svg.txt index 524807d..76c84af 100644 --- a/test/plugins/removeHiddenElems.20.svg.txt +++ b/test/plugins/removeHiddenElems.20.obsolete.svg.txt @@ -20,5 +20,6 @@ Remove all unreferenced elements which are only referenced by other unreferenced @@@ + diff --git a/test/plugins/removeHiddenElems.23.svg.txt b/test/plugins/removeHiddenElems.23.svg.txt new file mode 100644 index 0000000..9c92196 --- /dev/null +++ b/test/plugins/removeHiddenElems.23.svg.txt @@ -0,0 +1,47 @@ +Don't delete non-rendered nodes that are referenced from within another non-rendered node. + +=== + + + + + + + + + + + + + + + + + + + + + + +@@@ + + + + + + + + + + + + + + + + + + + + + diff --git a/test/plugins/removeHiddenElems.24.svg.txt b/test/plugins/removeHiddenElems.24.svg.txt new file mode 100644 index 0000000..b2bd059 --- /dev/null +++ b/test/plugins/removeHiddenElems.24.svg.txt @@ -0,0 +1,123 @@ +Test with ellipses. + +=== + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@@@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/plugins/removeHiddenElems.25.svg.txt b/test/plugins/removeHiddenElems.25.svg.txt new file mode 100644 index 0000000..c3d0d8d --- /dev/null +++ b/test/plugins/removeHiddenElems.25.svg.txt @@ -0,0 +1,27 @@ +Preserve referenced elements within display:none or opacity:0 elements. + +=== + + + + + + + + + + + + +@@@ + + + + + + + + + + + diff --git a/test/plugins/removeHiddenElems.28.svg.txt b/test/plugins/removeHiddenElems.28.svg.txt new file mode 100644 index 0000000..a45ce8c --- /dev/null +++ b/test/plugins/removeHiddenElems.28.svg.txt @@ -0,0 +1,37 @@ +If a non-rendering element doesn't have an id, treat it the same as - remove any unreferenced children. + +=== + + + + + + + + + + + + + + + + + +@@@ + + + + + + + + + + + + + + + + diff --git a/test/plugins/removeHiddenElems.29.svg.txt b/test/plugins/removeHiddenElems.29.svg.txt new file mode 100644 index 0000000..21967b1 --- /dev/null +++ b/test/plugins/removeHiddenElems.29.svg.txt @@ -0,0 +1,41 @@ +Hoist children of so all children have id attributes. + +=== + + + + + + + + + + + + + xxx + + + + + + + + +@@@ + + + + + + + + + + + + + + + + diff --git a/test/plugins/removeHiddenElems.30.svg.txt b/test/plugins/removeHiddenElems.30.svg.txt new file mode 100644 index 0000000..ac72e7c --- /dev/null +++ b/test/plugins/removeHiddenElems.30.svg.txt @@ -0,0 +1,91 @@ +Test with markers. + +=== + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@@@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/plugins/removeHiddenElems.31.svg.txt b/test/plugins/removeHiddenElems.31.svg.txt new file mode 100644 index 0000000..b5883d2 --- /dev/null +++ b/test/plugins/removeHiddenElems.31.svg.txt @@ -0,0 +1,29 @@ +Remove empty shapes in a non-rendered element. + +=== + + + + + + + + + + + + + + + +@@@ + + + + + + + + + + diff --git a/test/svg2js/_index.test.js b/test/svg2js/_index.test.js index bb859a2..8ef386b 100644 --- a/test/svg2js/_index.test.js +++ b/test/svg2js/_index.test.js @@ -64,7 +64,7 @@ describe('svg2js', function () { expect(root.children[1]).toMatchObject({ type: 'comment', value: - 'Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)', + ' Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) ', }); }); diff --git a/test/svgo/entities.svg.txt b/test/svgo/entities.svg.txt index cc85db3..0d7a02a 100644 --- a/test/svgo/entities.svg.txt +++ b/test/svgo/entities.svg.txt @@ -15,7 +15,7 @@ @@@ - +