Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move removeEmptyText functionality to removeEmptyContainers. #79

Merged
merged 4 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions docs/04-plugins/removeEmptyText.mdx

This file was deleted.

2 changes: 0 additions & 2 deletions lib/builtin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -103,7 +102,6 @@ export const builtin = Object.freeze([
removeElementsByAttr,
removeEmptyAttrs,
removeEmptyContainers,
removeEmptyText,
removeHiddenElems,
removeMetadata,
removeNonInheritableGroupAttrs,
Expand Down
23 changes: 23 additions & 0 deletions lib/svgo/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ export const decodeSVGDatauri = (str) => {
return str;
};

/**
* @param {Map<import('../types.js').XastParent,Set<import('../types.js').XastChild>>} 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,
Expand Down Expand Up @@ -124,6 +137,16 @@ export const cleanupOutData = (data, params, command) => {
return str;
};

/**
* @param {Map<import('../types.js').XastParent,Set<import('../types.js').XastChild>>} 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
Expand Down
13 changes: 0 additions & 13 deletions plugins/_collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 0 additions & 5 deletions plugins/plugins-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,6 @@ type DefaultPlugins = {
};
removeEmptyAttrs: void;
removeEmptyContainers: void;
removeEmptyText: {
text?: boolean;
tspan?: boolean;
tref?: boolean;
};
removeHiddenElems: {
isHidden?: boolean;
displayNone?: boolean;
Expand Down
2 changes: 0 additions & 2 deletions plugins/preset-default.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -50,7 +49,6 @@ const presetDefault = createPreset({
removeNonInheritableGroupAttrs,
removeUselessStrokeAndFill,
removeHiddenElems,
removeEmptyText,
minifyTransforms,
convertEllipseToCircle,
moveElemsStylesToGroup,
Expand Down
2 changes: 0 additions & 2 deletions plugins/preset-next.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -50,7 +49,6 @@ const presetNext = createPreset({
removeNonInheritableGroupAttrs,
removeUselessStrokeAndFill,
removeHiddenElems,
removeEmptyText,
minifyTransforms,
convertEllipseToCircle,
moveElemsStylesToGroup,
Expand Down
100 changes: 53 additions & 47 deletions plugins/removeEmptyContainers.js
Original file line number Diff line number Diff line change
@@ -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
* <defs/>
*
* @example
* <g><marker><a/></marker></g>
*
* @author Kir Belevich
*
* @type {import('./plugins-types.js').Plugin<'removeEmptyContainers'>}
*/
export const fn = () => {
const removedIds = new Set();
/**
* @type {Map<string, {node:XastElement,parent:XastParent}[]>}
*/
/** @type {Map<string, import('../lib/types.js').XastElement[]>} */
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 <g> 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 <mask> 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 <use> elements that referenced an empty container.

/** @type {Map<import('../lib/types.js').XastParent,Set<import('../lib/types.js').XastChild>>} */
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);
},
},
};
Expand Down
49 changes: 0 additions & 49 deletions plugins/removeEmptyText.js

This file was deleted.

20 changes: 7 additions & 13 deletions plugins/removeHiddenElems.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,4 @@

@@@

<svg xmlns="http://www.w3.org/2000/svg">
<g/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg"/>
Loading