diff --git a/docs/04-plugins/removeEmptyText.mdx b/docs/04-plugins/removeEmptyText.mdx deleted file mode 100644 index 1f1efe2..0000000 --- a/docs/04-plugins/removeEmptyText.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: removeEmptyText -svgo: - pluginId: removeEmptyText - defaultPlugin: true - parameters: - text: - description: If to remove empty [``](https://developer.mozilla.org/docs/Web/SVG/Element/text) elements. - default: true - tspan: - description: If to remove empty [``](https://developer.mozilla.org/docs/Web/SVG/Element/tspan) elements. - default: true - tref: - description: If to remove empty [``](https://developer.mozilla.org/docs/Web/SVG/Element/tref) elements. - default: true ---- - -Removes empty [``](https://developer.mozilla.org/docs/Web/SVG/Element/text) and [``](https://developer.mozilla.org/docs/Web/SVG/Element/tspan) elements, and [``](https://developer.mozilla.org/docs/Web/SVG/Element/tref) elements that don't reference another node in the document. - -:::info - -No browsers supports ``, so it's best to avoid that element regardless. - -::: diff --git a/lib/builtin.js b/lib/builtin.js index cbfa502..73bb61f 100644 --- a/lib/builtin.js +++ b/lib/builtin.js @@ -38,7 +38,6 @@ import * as removeEditorsNSData from '../plugins/removeEditorsNSData.js'; import * as removeElementsByAttr from '../plugins/removeElementsByAttr.js'; import * as removeEmptyAttrs from '../plugins/removeEmptyAttrs.js'; import * as removeEmptyContainers from '../plugins/removeEmptyContainers.js'; -import * as removeEmptyText from '../plugins/removeEmptyText.js'; import * as removeHiddenElems from '../plugins/removeHiddenElems.js'; import * as removeMetadata from '../plugins/removeMetadata.js'; import * as removeNonInheritableGroupAttrs from '../plugins/removeNonInheritableGroupAttrs.js'; @@ -103,7 +102,6 @@ export const builtin = Object.freeze([ removeElementsByAttr, removeEmptyAttrs, removeEmptyContainers, - removeEmptyText, removeHiddenElems, removeMetadata, removeNonInheritableGroupAttrs, 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/_collections.js b/plugins/_collections.js index b0b7647..3c16658 100644 --- a/plugins/_collections.js +++ b/plugins/_collections.js @@ -52,19 +52,6 @@ export const elemsGroups = { 'solidColor', 'symbol', ]), - container: new Set([ - 'a', - 'defs', - 'foreignObject', - 'g', - 'marker', - 'mask', - 'missing-glyph', - 'pattern', - 'svg', - 'switch', - 'symbol', - ]), textContentChild: new Set(['altGlyph', 'textPath', 'tref', 'tspan']), lightSource: new Set([ 'feDiffuseLighting', diff --git a/plugins/plugins-types.d.ts b/plugins/plugins-types.d.ts index 23982a2..a651ba1 100644 --- a/plugins/plugins-types.d.ts +++ b/plugins/plugins-types.d.ts @@ -100,11 +100,6 @@ type DefaultPlugins = { }; removeEmptyAttrs: void; removeEmptyContainers: void; - removeEmptyText: { - text?: boolean; - tspan?: boolean; - tref?: boolean; - }; removeHiddenElems: { isHidden?: boolean; displayNone?: boolean; diff --git a/plugins/preset-default.js b/plugins/preset-default.js index 39b452c..50b8d17 100644 --- a/plugins/preset-default.js +++ b/plugins/preset-default.js @@ -20,7 +20,6 @@ import * as removeDesc from './removeDesc.js'; import * as removeDoctype from './removeDoctype.js'; import * as removeEditorsNSData from './removeEditorsNSData.js'; import * as removeEmptyContainers from './removeEmptyContainers.js'; -import * as removeEmptyText from './removeEmptyText.js'; import * as removeHiddenElems from './removeHiddenElems.js'; import * as removeMetadata from './removeMetadata.js'; import * as removeNonInheritableGroupAttrs from './removeNonInheritableGroupAttrs.js'; @@ -50,7 +49,6 @@ const presetDefault = createPreset({ removeNonInheritableGroupAttrs, removeUselessStrokeAndFill, removeHiddenElems, - removeEmptyText, minifyTransforms, convertEllipseToCircle, moveElemsStylesToGroup, diff --git a/plugins/preset-next.js b/plugins/preset-next.js index fdbf417..a565ce2 100644 --- a/plugins/preset-next.js +++ b/plugins/preset-next.js @@ -20,7 +20,6 @@ import * as removeDesc from './removeDesc.js'; import * as removeDoctype from './removeDoctype.js'; import * as removeEditorsNSData from './removeEditorsNSData.js'; import * as removeEmptyContainers from './removeEmptyContainers.js'; -import * as removeEmptyText from './removeEmptyText.js'; import * as removeHiddenElems from './removeHiddenElems.js'; import * as removeMetadata from './removeMetadata.js'; import * as removeNonInheritableGroupAttrs from './removeNonInheritableGroupAttrs.js'; @@ -50,7 +49,6 @@ const presetNext = createPreset({ removeNonInheritableGroupAttrs, removeUselessStrokeAndFill, removeHiddenElems, - removeEmptyText, minifyTransforms, convertEllipseToCircle, moveElemsStylesToGroup, diff --git a/plugins/removeEmptyContainers.js b/plugins/removeEmptyContainers.js index 907cb52..a83bdd6 100644 --- a/plugins/removeEmptyContainers.js +++ b/plugins/removeEmptyContainers.js @@ -1,101 +1,107 @@ -import { elemsGroups } from './_collections.js'; +import { + addChildToDelete, + deleteChildren, + getHrefId, +} from '../lib/svgo/tools.js'; import { detachNodeFromParent } from '../lib/xast.js'; -import { findReferences } from '../lib/svgo/tools.js'; - -/** - * @typedef {import('../lib/types.js').XastElement} XastElement - * @typedef {import('../lib/types.js').XastParent} XastParent - */ export const name = 'removeEmptyContainers'; export const description = 'removes empty container elements'; +const removableEls = new Set([ + 'a', + 'defs', + 'foreignObject', + 'g', + 'marker', + 'mask', + 'missing-glyph', + 'pattern', + 'switch', + 'symbol', + 'text', + 'tspan', +]); + /** - * Remove empty containers. + * Remove empty containers and text elements. * * @see https://www.w3.org/TR/SVG11/intro.html#TermContainerElement * - * @example - * - * - * @example - * - * - * @author Kir Belevich - * * @type {import('./plugins-types.js').Plugin<'removeEmptyContainers'>} */ export const fn = () => { const removedIds = new Set(); - /** - * @type {Map} - */ + /** @type {Map} */ const usesById = new Map(); return { element: { - enter: (node, parentNode) => { - if (node.name === 'use') { + enter: (element) => { + if (element.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 }); + const id = getHrefId(element); + if (id) { + let references = usesById.get(id); + if (references === undefined) { + references = []; + usesById.set(id, references); } + references.push(element); } } }, - exit: (node, parentNode) => { + exit: (element, parentNode) => { // remove only empty non-svg containers - if ( - node.name === 'svg' || - !elemsGroups.container.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 uses = usesById.get(id); - if (uses) { - for (const use of uses) { - detachNodeFromParent(use.node, use.parent); + const usingEls = usesById.get(id); + if (usingEls) { + for (const element of usingEls) { + addChildToDelete(childrenToDelete, element); } } } + + deleteChildren(childrenToDelete); }, }, }; diff --git a/plugins/removeEmptyText.js b/plugins/removeEmptyText.js deleted file mode 100644 index 57d72aa..0000000 --- a/plugins/removeEmptyText.js +++ /dev/null @@ -1,49 +0,0 @@ -import { detachNodeFromParent } from '../lib/xast.js'; - -export const name = 'removeEmptyText'; -export const description = 'removes empty elements'; - -/** - * Remove empty Text elements. - * - * @see https://www.w3.org/TR/SVG11/text.html - * - * @example - * Remove empty text element: - * - * - * Remove empty tspan element: - * - * - * Remove tref with empty xlink:href attribute: - * - * - * @author Kir Belevich - * - * @type {import('./plugins-types.js').Plugin<'removeEmptyText'>} - */ -export const fn = (root, params) => { - const { text = true, tspan = true, tref = true } = params; - return { - element: { - enter: (node, parentNode) => { - // Remove empty text element - if (text && node.name === 'text' && node.children.length === 0) { - detachNodeFromParent(node, parentNode); - } - // Remove empty tspan element - if (tspan && node.name === 'tspan' && node.children.length === 0) { - detachNodeFromParent(node, parentNode); - } - // Remove tref with empty xlink:href attribute - if ( - tref && - node.name === 'tref' && - node.attributes['xlink:href'] == null - ) { - detachNodeFromParent(node, parentNode); - } - }, - }, - }; -}; 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); }, }, }; diff --git a/test/plugins/removeEmptyText.01.svg.txt b/test/plugins/removeEmptyContainers.08.svg.txt similarity index 62% rename from test/plugins/removeEmptyText.01.svg.txt rename to test/plugins/removeEmptyContainers.08.svg.txt index 4855024..5de5d9b 100644 --- a/test/plugins/removeEmptyText.01.svg.txt +++ b/test/plugins/removeEmptyContainers.08.svg.txt @@ -6,6 +6,4 @@ @@@ - - - + diff --git a/test/plugins/removeEmptyContainers.09.svg.txt b/test/plugins/removeEmptyContainers.09.svg.txt new file mode 100644 index 0000000..24f9fae --- /dev/null +++ b/test/plugins/removeEmptyContainers.09.svg.txt @@ -0,0 +1,9 @@ + + + + + + +@@@ + + diff --git a/test/plugins/removeEmptyText.02.svg.txt b/test/plugins/removeEmptyText.02.svg.txt deleted file mode 100644 index 7908cd4..0000000 --- a/test/plugins/removeEmptyText.02.svg.txt +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - -@@@ - - - - diff --git a/test/plugins/removeEmptyText.03.svg.txt b/test/plugins/removeEmptyText.03.svg.txt deleted file mode 100644 index a88e6e6..0000000 --- a/test/plugins/removeEmptyText.03.svg.txt +++ /dev/null @@ -1,11 +0,0 @@ - - - ... - - - -@@@ - - - -