diff --git a/docs/03-plugins/remove-deprecated-attrs.mdx b/docs/03-plugins/remove-deprecated-attrs.mdx
new file mode 100644
index 000000000..a294f2006
--- /dev/null
+++ b/docs/03-plugins/remove-deprecated-attrs.mdx
@@ -0,0 +1,27 @@
+---
+title: Remove Deprecated Attributes
+svgo:
+ pluginId: removeDeprecatedAttrs
+ defaultPlugin: true
+ parameters:
+ removeAny:
+ description: By default, this plugin only removes safe deprecated attributes that do not change the rendered image. Enabling this will remove all deprecated attributes which may impact rendering.
+ type: boolean
+ default: false
+---
+
+Removes deprecated attributes from elements in the document.
+
+This plugin does not remove attributes from the deprecated XLink namespace. To remove them, use the [Remove XLink](/docs/plugins/remove-xlink/) plugin.
+
+## Usage
+
+
+
+## Demo
+
+
+
+## Implementation
+
+- https://github.com/svg/svgo/blob/main/plugins/removeDeprecatedAttrs.js
diff --git a/lib/builtin.js b/lib/builtin.js
index 7e7639758..57e8bae64 100644
--- a/lib/builtin.js
+++ b/lib/builtin.js
@@ -24,6 +24,7 @@ import * as prefixIds from '../plugins/prefixIds.js';
import * as removeAttributesBySelector from '../plugins/removeAttributesBySelector.js';
import * as removeAttrs from '../plugins/removeAttrs.js';
import * as removeComments from '../plugins/removeComments.js';
+import * as removeDeprecatedAttrs from '../plugins/removeDeprecatedAttrs.js';
import * as removeDesc from '../plugins/removeDesc.js';
import * as removeDimensions from '../plugins/removeDimensions.js';
import * as removeDoctype from '../plugins/removeDoctype.js';
@@ -79,6 +80,7 @@ export const builtin = [
removeAttributesBySelector,
removeAttrs,
removeComments,
+ removeDeprecatedAttrs,
removeDesc,
removeDimensions,
removeDoctype,
diff --git a/plugins/_collections.js b/plugins/_collections.js
index 832907103..fdae3558a 100644
--- a/plugins/_collections.js
+++ b/plugins/_collections.js
@@ -375,11 +375,35 @@ export const attrsGroupsDefaults = {
},
};
+/**
+ * @type {Record, unsafe?: Set }>}
+ * @see https://www.w3.org/TR/SVG11/intro.html#Definitions
+ */
+export const attrsGroupsDeprecated = {
+ animationAttributeTarget: { unsafe: new Set(['attributeType']) },
+ conditionalProcessing: { unsafe: new Set(['requiredFeatures']) },
+ core: { unsafe: new Set(['xml:base', 'xml:lang', 'xml:space']) },
+ presentation: {
+ unsafe: new Set([
+ 'clip',
+ 'color-profile',
+ 'enable-background',
+ 'glyph-orientation-horizontal',
+ 'glyph-orientation-vertical',
+ 'kerning',
+ ]),
+ },
+};
+
/**
* @type {Record,
* attrs?: Set,
* defaults?: Record,
+ * deprecated?: {
+ * safe?: Set,
+ * unsafe?: Set,
+ * },
* contentGroups?: Set,
* content?: Set,
* }>}
@@ -574,6 +598,7 @@ export const elems = {
name: 'sRGB',
'rendering-intent': 'auto',
},
+ deprecated: { unsafe: new Set(['name']) },
contentGroups: new Set(['descriptive']),
},
cursor: {
@@ -958,6 +983,7 @@ export const elems = {
width: '120%',
height: '120%',
},
+ deprecated: { unsafe: new Set(['filterRes']) },
contentGroups: new Set(['descriptive', 'filterPrimitive']),
content: new Set(['animate', 'set']),
},
@@ -978,6 +1004,15 @@ export const elems = {
'horiz-origin-x': '0',
'horiz-origin-y': '0',
},
+ deprecated: {
+ unsafe: new Set([
+ 'horiz-origin-x',
+ 'horiz-origin-y',
+ 'vert-adv-y',
+ 'vert-origin-x',
+ 'vert-origin-y',
+ ]),
+ },
contentGroups: new Set(['descriptive']),
content: new Set(['font-face', 'glyph', 'hkern', 'missing-glyph', 'vkern']),
},
@@ -1028,6 +1063,31 @@ export const elems = {
'panose-1': '0 0 0 0 0 0 0 0 0 0',
slope: '0',
},
+ deprecated: {
+ unsafe: new Set([
+ 'accent-height',
+ 'alphabetic',
+ 'ascent',
+ 'bbox',
+ 'cap-height',
+ 'descent',
+ 'hanging',
+ 'ideographic',
+ 'mathematical',
+ 'panose-1',
+ 'slope',
+ 'stemh',
+ 'stemv',
+ 'unicode-range',
+ 'units-per-em',
+ 'v-alphabetic',
+ 'v-hanging',
+ 'v-ideographic',
+ 'v-mathematical',
+ 'widths',
+ 'x-height',
+ ]),
+ },
contentGroups: new Set(['descriptive']),
content: new Set([
// TODO: "at most one 'font-face-src' element"
@@ -1038,10 +1098,12 @@ export const elems = {
'font-face-format': {
attrsGroups: new Set(['core']),
attrs: new Set(['string']),
+ deprecated: { unsafe: new Set(['string']) },
},
'font-face-name': {
attrsGroups: new Set(['core']),
attrs: new Set(['name']),
+ deprecated: { unsafe: new Set(['name']) },
},
'font-face-src': {
attrsGroups: new Set(['core']),
@@ -1134,6 +1196,18 @@ export const elems = {
defaults: {
'arabic-form': 'initial',
},
+ deprecated: {
+ unsafe: new Set([
+ 'arabic-form',
+ 'glyph-name',
+ 'horiz-adv-x',
+ 'orientation',
+ 'unicode',
+ 'vert-adv-y',
+ 'vert-origin-x',
+ 'vert-origin-y',
+ ]),
+ },
contentGroups: new Set([
'animation',
'descriptive',
@@ -1173,6 +1247,14 @@ export const elems = {
'vert-origin-x',
'vert-origin-y',
]),
+ deprecated: {
+ unsafe: new Set([
+ 'horiz-adv-x',
+ 'vert-adv-y',
+ 'vert-origin-x',
+ 'vert-origin-y',
+ ]),
+ },
contentGroups: new Set([
'animation',
'descriptive',
@@ -1236,6 +1318,7 @@ export const elems = {
hkern: {
attrsGroups: new Set(['core']),
attrs: new Set(['u1', 'g1', 'u2', 'g2', 'k']),
+ deprecated: { unsafe: new Set(['g1', 'g2', 'k', 'u1', 'u2']) },
},
image: {
attrsGroups: new Set([
@@ -1430,6 +1513,14 @@ export const elems = {
'vert-origin-x',
'vert-origin-y',
]),
+ deprecated: {
+ unsafe: new Set([
+ 'horiz-adv-x',
+ 'vert-adv-y',
+ 'vert-origin-x',
+ 'vert-origin-y',
+ ]),
+ },
contentGroups: new Set([
'animation',
'descriptive',
@@ -1710,6 +1801,15 @@ export const elems = {
contentScriptType: 'application/ecmascript',
contentStyleType: 'text/css',
},
+ deprecated: {
+ safe: new Set(['version']),
+ unsafe: new Set([
+ 'baseProfile',
+ 'contentScriptType',
+ 'contentStyleType',
+ 'zoomAndPan',
+ ]),
+ },
contentGroups: new Set([
'animation',
'descriptive',
@@ -1956,11 +2056,13 @@ export const elems = {
'viewTarget',
'zoomAndPan',
]),
+ deprecated: { unsafe: new Set(['viewTarget', 'zoomAndPan']) },
contentGroups: new Set(['descriptive']),
},
vkern: {
attrsGroups: new Set(['core']),
attrs: new Set(['u1', 'g1', 'u2', 'g2', 'k']),
+ deprecated: { unsafe: new Set(['g1', 'g2', 'k', 'u1', 'u2']) },
},
};
diff --git a/plugins/plugins-types.d.ts b/plugins/plugins-types.d.ts
index 71a4a1286..6ebf847ef 100644
--- a/plugins/plugins-types.d.ts
+++ b/plugins/plugins-types.d.ts
@@ -143,6 +143,9 @@ type DefaultPlugins = {
removeComments: {
preservePatterns: Array | false;
};
+ removeDeprecatedAttrs: {
+ removeUnsafe?: boolean;
+ };
removeDesc: {
removeAny?: boolean;
};
diff --git a/plugins/preset-default.js b/plugins/preset-default.js
index a6742b13e..5291ab6c2 100644
--- a/plugins/preset-default.js
+++ b/plugins/preset-default.js
@@ -2,6 +2,7 @@ import { createPreset } from '../lib/svgo/plugins.js';
import * as removeDoctype from './removeDoctype.js';
import * as removeXMLProcInst from './removeXMLProcInst.js';
import * as removeComments from './removeComments.js';
+import * as removeDeprecatedAttrs from './removeDeprecatedAttrs.js';
import * as removeMetadata from './removeMetadata.js';
import * as removeEditorsNSData from './removeEditorsNSData.js';
import * as cleanupAttrs from './cleanupAttrs.js';
@@ -41,6 +42,7 @@ const presetDefault = createPreset({
removeDoctype,
removeXMLProcInst,
removeComments,
+ removeDeprecatedAttrs,
removeMetadata,
removeEditorsNSData,
cleanupAttrs,
diff --git a/plugins/removeDeprecatedAttrs.js b/plugins/removeDeprecatedAttrs.js
new file mode 100644
index 000000000..1894ca230
--- /dev/null
+++ b/plugins/removeDeprecatedAttrs.js
@@ -0,0 +1,120 @@
+import * as csswhat from 'css-what';
+import { attrsGroupsDeprecated, elems } from './_collections.js';
+import { collectStylesheet } from '../lib/style.js';
+
+export const name = 'removeDeprecatedAttrs';
+export const description = 'removes deprecated attributes';
+
+/**
+ * @typedef {{ safe?: Set; unsafe?: Set }} DeprecatedAttrs
+ * @typedef {import('../lib/types.js').XastElement} XastElement
+ */
+
+/**
+ * @param {import('../lib/types.js').Stylesheet} stylesheet
+ * @returns {Set}
+ */
+function extractAttributesInStylesheet(stylesheet) {
+ const attributesInStylesheet = new Set();
+
+ stylesheet.rules.forEach((rule) => {
+ const selectors = csswhat.parse(rule.selector);
+ selectors.forEach((subselector) => {
+ subselector.forEach((segment) => {
+ if (segment.type !== 'attribute') {
+ return;
+ }
+
+ attributesInStylesheet.add(segment.name);
+ });
+ });
+ });
+
+ return attributesInStylesheet;
+}
+
+/**
+ * @param {XastElement} node
+ * @param {DeprecatedAttrs | undefined} deprecatedAttrs
+ * @param {import('./plugins-types.js').DefaultPlugins['removeDeprecatedAttrs']} params
+ * @param {Set} attributesInStylesheet
+ */
+function processAttributes(
+ node,
+ deprecatedAttrs,
+ params,
+ attributesInStylesheet,
+) {
+ if (!deprecatedAttrs) {
+ return;
+ }
+
+ if (deprecatedAttrs.safe) {
+ deprecatedAttrs.safe.forEach((name) => {
+ if (attributesInStylesheet.has(name)) {
+ return;
+ }
+ delete node.attributes[name];
+ });
+ }
+
+ if (params.removeUnsafe && deprecatedAttrs.unsafe) {
+ deprecatedAttrs.unsafe.forEach((name) => {
+ if (attributesInStylesheet.has(name)) {
+ return;
+ }
+ delete node.attributes[name];
+ });
+ }
+}
+
+/**
+ * Remove deprecated attributes.
+ *
+ * @type {import('./plugins-types.js').Plugin<'removeDeprecatedAttrs'>}
+ */
+export function fn(root, params) {
+ const stylesheet = collectStylesheet(root);
+ const attributesInStylesheet = extractAttributesInStylesheet(stylesheet);
+
+ return {
+ element: {
+ enter: (node) => {
+ const elemConfig = elems[node.name];
+ if (!elemConfig) {
+ return;
+ }
+
+ // Special cases
+
+ // Removing deprecated xml:lang is safe when the lang attribute exists.
+ if (
+ elemConfig.attrsGroups.has('core') &&
+ node.attributes['xml:lang'] &&
+ !attributesInStylesheet.has('xml:lang') &&
+ node.attributes['lang']
+ ) {
+ delete node.attributes['xml:lang'];
+ }
+
+ // General cases
+
+ elemConfig.attrsGroups.forEach((attrsGroup) => {
+ processAttributes(
+ node,
+ attrsGroupsDeprecated[attrsGroup],
+ params,
+ attributesInStylesheet,
+ );
+ });
+
+ processAttributes(
+ node,
+ elemConfig.deprecated,
+ params,
+ attributesInStylesheet,
+ );
+ },
+ },
+ };
+}
diff --git a/test/plugins/_collections.test.js b/test/plugins/_collections.test.js
new file mode 100644
index 000000000..0dea420eb
--- /dev/null
+++ b/test/plugins/_collections.test.js
@@ -0,0 +1,24 @@
+import { elems } from '../../plugins/_collections.js';
+
+describe('elems.deprecated', () => {
+ Object.entries(elems).forEach(([tagName, elemConfig]) => {
+ const deprecated = elemConfig.deprecated;
+ if (!deprecated) {
+ return;
+ }
+
+ test(`${tagName} deprecated attributes are all known attributes`, () => {
+ if (deprecated.safe) {
+ deprecated.safe.forEach((attr) => {
+ expect(elemConfig.attrs).toContain(attr);
+ });
+ }
+
+ if (deprecated.unsafe) {
+ deprecated.unsafe.forEach((attr) => {
+ expect(elemConfig.attrs).toContain(attr);
+ });
+ }
+ });
+ });
+});
diff --git a/test/plugins/removeDeprecatedAttrs.01.svg b/test/plugins/removeDeprecatedAttrs.01.svg
new file mode 100644
index 000000000..d4bcf872d
--- /dev/null
+++ b/test/plugins/removeDeprecatedAttrs.01.svg
@@ -0,0 +1,13 @@
+Removes safe deprecated version attribute from svg node.
+
+===
+
+
+
+@@@
+
+
diff --git a/test/plugins/removeDeprecatedAttrs.02.svg b/test/plugins/removeDeprecatedAttrs.02.svg
new file mode 100644
index 000000000..4a8b693ab
--- /dev/null
+++ b/test/plugins/removeDeprecatedAttrs.02.svg
@@ -0,0 +1,13 @@
+Does not remove unsafe deprecated viewTarget attribute from view node by default.
+
+===
+
+
+
+@@@
+
+
diff --git a/test/plugins/removeDeprecatedAttrs.03.svg b/test/plugins/removeDeprecatedAttrs.03.svg
new file mode 100644
index 000000000..af835184f
--- /dev/null
+++ b/test/plugins/removeDeprecatedAttrs.03.svg
@@ -0,0 +1,17 @@
+Remove unsafe deprecated viewTarget attribute from view node with param.
+
+===
+
+
+
+@@@
+
+
+
+@@@
+
+{ "removeUnsafe": true }
diff --git a/test/plugins/removeDeprecatedAttrs.04.svg b/test/plugins/removeDeprecatedAttrs.04.svg
new file mode 100644
index 000000000..37158db23
--- /dev/null
+++ b/test/plugins/removeDeprecatedAttrs.04.svg
@@ -0,0 +1,27 @@
+Removes deprecated presentation group attribute enable-background.
+
+===
+
+
+
+@@@
+
+
+
+@@@
+
+{ "removeUnsafe": true }
diff --git a/test/plugins/removeDeprecatedAttrs.05.svg b/test/plugins/removeDeprecatedAttrs.05.svg
new file mode 100644
index 000000000..ff762fde9
--- /dev/null
+++ b/test/plugins/removeDeprecatedAttrs.05.svg
@@ -0,0 +1,13 @@
+Removes deprecated xml:lang attribute when lang attribute exists.
+
+===
+
+
+
+@@@
+
+
diff --git a/test/plugins/removeDeprecatedAttrs.06.svg b/test/plugins/removeDeprecatedAttrs.06.svg
new file mode 100644
index 000000000..86dcb2768
--- /dev/null
+++ b/test/plugins/removeDeprecatedAttrs.06.svg
@@ -0,0 +1,13 @@
+Keeps xml:lang attribute when lang attribute doesn't exist.
+
+===
+
+
+
+@@@
+
+
diff --git a/test/plugins/removeDeprecatedAttrs.07.svg b/test/plugins/removeDeprecatedAttrs.07.svg
new file mode 100644
index 000000000..685f98eef
--- /dev/null
+++ b/test/plugins/removeDeprecatedAttrs.07.svg
@@ -0,0 +1,17 @@
+Removes unsafe xml:lang attribute when lang attribute doesn't exist with removeUnsafe param.
+
+===
+
+
+
+@@@
+
+
+
+@@@
+
+{ "removeUnsafe": true }
diff --git a/test/plugins/removeDeprecatedAttrs.08.svg b/test/plugins/removeDeprecatedAttrs.08.svg
new file mode 100644
index 000000000..5b5499f4e
--- /dev/null
+++ b/test/plugins/removeDeprecatedAttrs.08.svg
@@ -0,0 +1,23 @@
+Keeps deprecated version attribute when it is a CSS selectors
+
+===
+
+
+
+@@@
+
+
+
+@@@
+
+{ "removeUnsafe": true }