From 59796a829beb4ac7c0291e10e75fa9445ae91aff Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 15 Jan 2024 13:48:14 -0800 Subject: [PATCH] feat(removeDeprecatedAttrs): new removeDeprecatedAttrs plugin (#1869) --- docs/03-plugins/remove-deprecated-attrs.mdx | 27 ++++ lib/builtin.js | 2 + plugins/_collections.js | 102 +++++++++++++++ plugins/plugins-types.d.ts | 3 + plugins/preset-default.js | 2 + plugins/removeDeprecatedAttrs.js | 120 ++++++++++++++++++ test/plugins/_collections.test.js | 24 ++++ test/plugins/removeDeprecatedAttrs.01.svg.txt | 13 ++ test/plugins/removeDeprecatedAttrs.02.svg.txt | 13 ++ test/plugins/removeDeprecatedAttrs.03.svg.txt | 17 +++ test/plugins/removeDeprecatedAttrs.04.svg.txt | 27 ++++ test/plugins/removeDeprecatedAttrs.05.svg.txt | 13 ++ test/plugins/removeDeprecatedAttrs.06.svg.txt | 13 ++ test/plugins/removeDeprecatedAttrs.07.svg.txt | 17 +++ test/plugins/removeDeprecatedAttrs.08.svg.txt | 23 ++++ 15 files changed, 416 insertions(+) create mode 100644 docs/03-plugins/remove-deprecated-attrs.mdx create mode 100644 plugins/removeDeprecatedAttrs.js create mode 100644 test/plugins/_collections.test.js create mode 100644 test/plugins/removeDeprecatedAttrs.01.svg.txt create mode 100644 test/plugins/removeDeprecatedAttrs.02.svg.txt create mode 100644 test/plugins/removeDeprecatedAttrs.03.svg.txt create mode 100644 test/plugins/removeDeprecatedAttrs.04.svg.txt create mode 100644 test/plugins/removeDeprecatedAttrs.05.svg.txt create mode 100644 test/plugins/removeDeprecatedAttrs.06.svg.txt create mode 100644 test/plugins/removeDeprecatedAttrs.07.svg.txt create mode 100644 test/plugins/removeDeprecatedAttrs.08.svg.txt 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 6b06dcb00..246ea6249 100644 --- a/plugins/plugins-types.d.ts +++ b/plugins/plugins-types.d.ts @@ -144,6 +144,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.txt b/test/plugins/removeDeprecatedAttrs.01.svg.txt new file mode 100644 index 000000000..d4bcf872d --- /dev/null +++ b/test/plugins/removeDeprecatedAttrs.01.svg.txt @@ -0,0 +1,13 @@ +Removes safe deprecated version attribute from svg node. + +=== + + + + + +@@@ + + + + diff --git a/test/plugins/removeDeprecatedAttrs.02.svg.txt b/test/plugins/removeDeprecatedAttrs.02.svg.txt new file mode 100644 index 000000000..4a8b693ab --- /dev/null +++ b/test/plugins/removeDeprecatedAttrs.02.svg.txt @@ -0,0 +1,13 @@ +Does not remove unsafe deprecated viewTarget attribute from view node by default. + +=== + + + + + +@@@ + + + + diff --git a/test/plugins/removeDeprecatedAttrs.03.svg.txt b/test/plugins/removeDeprecatedAttrs.03.svg.txt new file mode 100644 index 000000000..af835184f --- /dev/null +++ b/test/plugins/removeDeprecatedAttrs.03.svg.txt @@ -0,0 +1,17 @@ +Remove unsafe deprecated viewTarget attribute from view node with param. + +=== + + + + + +@@@ + + + + + +@@@ + +{ "removeUnsafe": true } diff --git a/test/plugins/removeDeprecatedAttrs.04.svg.txt b/test/plugins/removeDeprecatedAttrs.04.svg.txt new file mode 100644 index 000000000..37158db23 --- /dev/null +++ b/test/plugins/removeDeprecatedAttrs.04.svg.txt @@ -0,0 +1,27 @@ +Removes deprecated presentation group attribute enable-background. + +=== + + + + + + + + test + + +@@@ + + + + + + + + test + + +@@@ + +{ "removeUnsafe": true } diff --git a/test/plugins/removeDeprecatedAttrs.05.svg.txt b/test/plugins/removeDeprecatedAttrs.05.svg.txt new file mode 100644 index 000000000..ff762fde9 --- /dev/null +++ b/test/plugins/removeDeprecatedAttrs.05.svg.txt @@ -0,0 +1,13 @@ +Removes deprecated xml:lang attribute when lang attribute exists. + +=== + + + English text + + +@@@ + + + English text + diff --git a/test/plugins/removeDeprecatedAttrs.06.svg.txt b/test/plugins/removeDeprecatedAttrs.06.svg.txt new file mode 100644 index 000000000..86dcb2768 --- /dev/null +++ b/test/plugins/removeDeprecatedAttrs.06.svg.txt @@ -0,0 +1,13 @@ +Keeps xml:lang attribute when lang attribute doesn't exist. + +=== + + + English text + + +@@@ + + + English text + diff --git a/test/plugins/removeDeprecatedAttrs.07.svg.txt b/test/plugins/removeDeprecatedAttrs.07.svg.txt new file mode 100644 index 000000000..685f98eef --- /dev/null +++ b/test/plugins/removeDeprecatedAttrs.07.svg.txt @@ -0,0 +1,17 @@ +Removes unsafe xml:lang attribute when lang attribute doesn't exist with removeUnsafe param. + +=== + + + English text + + +@@@ + + + English text + + +@@@ + +{ "removeUnsafe": true } diff --git a/test/plugins/removeDeprecatedAttrs.08.svg.txt b/test/plugins/removeDeprecatedAttrs.08.svg.txt new file mode 100644 index 000000000..5b5499f4e --- /dev/null +++ b/test/plugins/removeDeprecatedAttrs.08.svg.txt @@ -0,0 +1,23 @@ +Keeps deprecated version attribute when it is a CSS selectors + +=== + + + + + + +@@@ + + + + + + +@@@ + +{ "removeUnsafe": true }