svgo-v2 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +294 -0
  3. package/bin/svgo +10 -0
  4. package/dist/svgo.browser.js +1 -0
  5. package/lib/css-tools.js +239 -0
  6. package/lib/parser.js +259 -0
  7. package/lib/path.js +347 -0
  8. package/lib/stringifier.js +326 -0
  9. package/lib/style.js +283 -0
  10. package/lib/svgo/coa.js +517 -0
  11. package/lib/svgo/config.js +138 -0
  12. package/lib/svgo/css-class-list.js +72 -0
  13. package/lib/svgo/css-select-adapter.d.ts +2 -0
  14. package/lib/svgo/css-select-adapter.js +120 -0
  15. package/lib/svgo/css-style-declaration.js +232 -0
  16. package/lib/svgo/jsAPI.d.ts +2 -0
  17. package/lib/svgo/jsAPI.js +443 -0
  18. package/lib/svgo/plugins.js +109 -0
  19. package/lib/svgo/tools.js +137 -0
  20. package/lib/svgo-node.js +106 -0
  21. package/lib/svgo.js +83 -0
  22. package/lib/types.ts +172 -0
  23. package/lib/xast.js +102 -0
  24. package/package.json +130 -0
  25. package/plugins/_applyTransforms.js +335 -0
  26. package/plugins/_collections.js +2168 -0
  27. package/plugins/_path.js +816 -0
  28. package/plugins/_transforms.js +379 -0
  29. package/plugins/addAttributesToSVGElement.js +87 -0
  30. package/plugins/addClassesToSVGElement.js +87 -0
  31. package/plugins/cleanupAttrs.js +55 -0
  32. package/plugins/cleanupEnableBackground.js +75 -0
  33. package/plugins/cleanupIDs.js +297 -0
  34. package/plugins/cleanupListOfValues.js +154 -0
  35. package/plugins/cleanupNumericValues.js +113 -0
  36. package/plugins/collapseGroups.js +135 -0
  37. package/plugins/convertColors.js +152 -0
  38. package/plugins/convertEllipseToCircle.js +39 -0
  39. package/plugins/convertPathData.js +1023 -0
  40. package/plugins/convertShapeToPath.js +175 -0
  41. package/plugins/convertStyleToAttrs.js +132 -0
  42. package/plugins/convertTransform.js +432 -0
  43. package/plugins/inlineStyles.js +379 -0
  44. package/plugins/mergePaths.js +104 -0
  45. package/plugins/mergeStyles.js +93 -0
  46. package/plugins/minifyStyles.js +148 -0
  47. package/plugins/moveElemsAttrsToGroup.js +130 -0
  48. package/plugins/moveGroupAttrsToElems.js +62 -0
  49. package/plugins/plugins.js +56 -0
  50. package/plugins/prefixIds.js +241 -0
  51. package/plugins/preset-default.js +80 -0
  52. package/plugins/removeAttributesBySelector.js +99 -0
  53. package/plugins/removeAttrs.js +159 -0
  54. package/plugins/removeComments.js +31 -0
  55. package/plugins/removeDesc.js +41 -0
  56. package/plugins/removeDimensions.js +43 -0
  57. package/plugins/removeDoctype.js +42 -0
  58. package/plugins/removeEditorsNSData.js +68 -0
  59. package/plugins/removeElementsByAttr.js +78 -0
  60. package/plugins/removeEmptyAttrs.js +33 -0
  61. package/plugins/removeEmptyContainers.js +58 -0
  62. package/plugins/removeEmptyText.js +57 -0
  63. package/plugins/removeHiddenElems.js +318 -0
  64. package/plugins/removeMetadata.js +29 -0
  65. package/plugins/removeNonInheritableGroupAttrs.js +38 -0
  66. package/plugins/removeOffCanvasPaths.js +138 -0
  67. package/plugins/removeRasterImages.js +33 -0
  68. package/plugins/removeScriptElement.js +29 -0
  69. package/plugins/removeStyleElement.js +29 -0
  70. package/plugins/removeTitle.js +29 -0
  71. package/plugins/removeUnknownsAndDefaults.js +218 -0
  72. package/plugins/removeUnusedNS.js +61 -0
  73. package/plugins/removeUselessDefs.js +65 -0
  74. package/plugins/removeUselessStrokeAndFill.js +144 -0
  75. package/plugins/removeViewBox.js +51 -0
  76. package/plugins/removeXMLNS.js +30 -0
  77. package/plugins/removeXMLProcInst.js +30 -0
  78. package/plugins/reusePaths.js +113 -0
  79. package/plugins/sortAttrs.js +113 -0
  80. package/plugins/sortDefsChildren.js +60 -0
@@ -0,0 +1,297 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @typedef {import('../lib/types').XastElement} XastElement
5
+ */
6
+
7
+ const { visitSkip } = require('../lib/xast.js');
8
+ const { referencesProps } = require('./_collections.js');
9
+
10
+ exports.type = 'visitor';
11
+ exports.name = 'cleanupIDs';
12
+ exports.active = true;
13
+ exports.description = 'removes unused IDs and minifies used';
14
+
15
+ const regReferencesUrl = /\burl\(("|')?#(.+?)\1\)/;
16
+ const regReferencesHref = /^#(.+?)$/;
17
+ const regReferencesBegin = /(\w+)\./;
18
+ const generateIDchars = [
19
+ 'a',
20
+ 'b',
21
+ 'c',
22
+ 'd',
23
+ 'e',
24
+ 'f',
25
+ 'g',
26
+ 'h',
27
+ 'i',
28
+ 'j',
29
+ 'k',
30
+ 'l',
31
+ 'm',
32
+ 'n',
33
+ 'o',
34
+ 'p',
35
+ 'q',
36
+ 'r',
37
+ 's',
38
+ 't',
39
+ 'u',
40
+ 'v',
41
+ 'w',
42
+ 'x',
43
+ 'y',
44
+ 'z',
45
+ 'A',
46
+ 'B',
47
+ 'C',
48
+ 'D',
49
+ 'E',
50
+ 'F',
51
+ 'G',
52
+ 'H',
53
+ 'I',
54
+ 'J',
55
+ 'K',
56
+ 'L',
57
+ 'M',
58
+ 'N',
59
+ 'O',
60
+ 'P',
61
+ 'Q',
62
+ 'R',
63
+ 'S',
64
+ 'T',
65
+ 'U',
66
+ 'V',
67
+ 'W',
68
+ 'X',
69
+ 'Y',
70
+ 'Z',
71
+ ];
72
+ const maxIDindex = generateIDchars.length - 1;
73
+
74
+ /**
75
+ * Check if an ID starts with any one of a list of strings.
76
+ *
77
+ * @type {(string: string, prefixes: Array<string>) => boolean}
78
+ */
79
+ const hasStringPrefix = (string, prefixes) => {
80
+ for (const prefix of prefixes) {
81
+ if (string.startsWith(prefix)) {
82
+ return true;
83
+ }
84
+ }
85
+ return false;
86
+ };
87
+
88
+ /**
89
+ * Generate unique minimal ID.
90
+ *
91
+ * @type {(currentID: null | Array<number>) => Array<number>}
92
+ */
93
+ const generateID = (currentID) => {
94
+ if (currentID == null) {
95
+ return [0];
96
+ }
97
+ currentID[currentID.length - 1] += 1;
98
+ for (let i = currentID.length - 1; i > 0; i--) {
99
+ if (currentID[i] > maxIDindex) {
100
+ currentID[i] = 0;
101
+ if (currentID[i - 1] !== undefined) {
102
+ currentID[i - 1]++;
103
+ }
104
+ }
105
+ }
106
+ if (currentID[0] > maxIDindex) {
107
+ currentID[0] = 0;
108
+ currentID.unshift(0);
109
+ }
110
+ return currentID;
111
+ };
112
+
113
+ /**
114
+ * Get string from generated ID array.
115
+ *
116
+ * @type {(arr: Array<number>, prefix: string) => string}
117
+ */
118
+ const getIDstring = (arr, prefix) => {
119
+ return prefix + arr.map((i) => generateIDchars[i]).join('');
120
+ };
121
+
122
+ /**
123
+ * Remove unused and minify used IDs
124
+ * (only if there are no any <style> or <script>).
125
+ *
126
+ * @author Kir Belevich
127
+ *
128
+ * @type {import('../lib/types').Plugin<{
129
+ * remove?: boolean,
130
+ * minify?: boolean,
131
+ * prefix?: string,
132
+ * preserve?: Array<string>,
133
+ * preservePrefixes?: Array<string>,
134
+ * force?: boolean,
135
+ * }>}
136
+ */
137
+ exports.fn = (_root, params) => {
138
+ const {
139
+ remove = true,
140
+ minify = true,
141
+ prefix = '',
142
+ preserve = [],
143
+ preservePrefixes = [],
144
+ force = false,
145
+ } = params;
146
+ const preserveIDs = new Set(
147
+ Array.isArray(preserve) ? preserve : preserve ? [preserve] : []
148
+ );
149
+ const preserveIDPrefixes = Array.isArray(preservePrefixes)
150
+ ? preservePrefixes
151
+ : preservePrefixes
152
+ ? [preservePrefixes]
153
+ : [];
154
+ /**
155
+ * @type {Map<string, XastElement>}
156
+ */
157
+ const nodeById = new Map();
158
+ /**
159
+ * @type {Map<string, Array<{element: XastElement, name: string, value: string }>>}
160
+ */
161
+ const referencesById = new Map();
162
+ let deoptimized = false;
163
+
164
+ return {
165
+ element: {
166
+ enter: (node) => {
167
+ if (force == false) {
168
+ // deoptimize if style or script elements are present
169
+ if (
170
+ (node.name === 'style' || node.name === 'script') &&
171
+ node.children.length !== 0
172
+ ) {
173
+ deoptimized = true;
174
+ return;
175
+ }
176
+
177
+ // avoid removing IDs if the whole SVG consists only of defs
178
+ if (node.name === 'svg') {
179
+ let hasDefsOnly = true;
180
+ for (const child of node.children) {
181
+ if (child.type !== 'element' || child.name !== 'defs') {
182
+ hasDefsOnly = false;
183
+ break;
184
+ }
185
+ }
186
+ if (hasDefsOnly) {
187
+ return visitSkip;
188
+ }
189
+ }
190
+ }
191
+
192
+ for (const [name, value] of Object.entries(node.attributes)) {
193
+ if (name === 'id') {
194
+ // collect all ids
195
+ const id = value;
196
+ if (nodeById.has(id)) {
197
+ delete node.attributes.id; // remove repeated id
198
+ } else {
199
+ nodeById.set(id, node);
200
+ }
201
+ } else {
202
+ // collect all references
203
+ /**
204
+ * @type {null | string}
205
+ */
206
+ let id = null;
207
+ if (referencesProps.includes(name)) {
208
+ const match = value.match(regReferencesUrl);
209
+ if (match != null) {
210
+ id = match[2]; // url() reference
211
+ }
212
+ }
213
+ if (name === 'href' || name.endsWith(':href')) {
214
+ const match = value.match(regReferencesHref);
215
+ if (match != null) {
216
+ id = match[1]; // href reference
217
+ }
218
+ }
219
+ if (name === 'begin') {
220
+ const match = value.match(regReferencesBegin);
221
+ if (match != null) {
222
+ id = match[1]; // href reference
223
+ }
224
+ }
225
+ if (id != null) {
226
+ let refs = referencesById.get(id);
227
+ if (refs == null) {
228
+ refs = [];
229
+ referencesById.set(id, refs);
230
+ }
231
+ refs.push({ element: node, name, value });
232
+ }
233
+ }
234
+ }
235
+ },
236
+ },
237
+
238
+ root: {
239
+ exit: () => {
240
+ if (deoptimized) {
241
+ return;
242
+ }
243
+ /**
244
+ * @type {(id: string) => boolean}
245
+ **/
246
+ const isIdPreserved = (id) =>
247
+ preserveIDs.has(id) || hasStringPrefix(id, preserveIDPrefixes);
248
+ /**
249
+ * @type {null | Array<number>}
250
+ */
251
+ let currentID = null;
252
+ for (const [id, refs] of referencesById) {
253
+ const node = nodeById.get(id);
254
+ if (node != null) {
255
+ // replace referenced IDs with the minified ones
256
+ if (minify && isIdPreserved(id) === false) {
257
+ /**
258
+ * @type {null | string}
259
+ */
260
+ let currentIDString = null;
261
+ do {
262
+ currentID = generateID(currentID);
263
+ currentIDString = getIDstring(currentID, prefix);
264
+ } while (isIdPreserved(currentIDString));
265
+ node.attributes.id = currentIDString;
266
+ for (const { element, name, value } of refs) {
267
+ if (value.includes('#')) {
268
+ // replace id in href and url()
269
+ element.attributes[name] = value.replace(
270
+ `#${id}`,
271
+ `#${currentIDString}`
272
+ );
273
+ } else {
274
+ // replace id in begin attribute
275
+ element.attributes[name] = value.replace(
276
+ `${id}.`,
277
+ `${currentIDString}.`
278
+ );
279
+ }
280
+ }
281
+ }
282
+ // keep referenced node
283
+ nodeById.delete(id);
284
+ }
285
+ }
286
+ // remove non-referenced IDs attributes from elements
287
+ if (remove) {
288
+ for (const [id, node] of nodeById) {
289
+ if (isIdPreserved(id) === false) {
290
+ delete node.attributes.id;
291
+ }
292
+ }
293
+ }
294
+ },
295
+ },
296
+ };
297
+ };
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ const { removeLeadingZero } = require('../lib/svgo/tools.js');
4
+
5
+ exports.name = 'cleanupListOfValues';
6
+ exports.type = 'visitor';
7
+ exports.active = false;
8
+ exports.description = 'rounds list of values to the fixed precision';
9
+
10
+ const regNumericValues =
11
+ /^([-+]?\d*\.?\d+([eE][-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/;
12
+ const regSeparator = /\s+,?\s*|,\s*/;
13
+ const absoluteLengths = {
14
+ // relative to px
15
+ cm: 96 / 2.54,
16
+ mm: 96 / 25.4,
17
+ in: 96,
18
+ pt: 4 / 3,
19
+ pc: 16,
20
+ px: 1,
21
+ };
22
+
23
+ /**
24
+ * Round list of values to the fixed precision.
25
+ *
26
+ * @example
27
+ * <svg viewBox="0 0 200.28423 200.28423" enable-background="new 0 0 200.28423 200.28423">
28
+ * ⬇
29
+ * <svg viewBox="0 0 200.284 200.284" enable-background="new 0 0 200.284 200.284">
30
+ *
31
+ * <polygon points="208.250977 77.1308594 223.069336 ... "/>
32
+ * ⬇
33
+ * <polygon points="208.251 77.131 223.069 ... "/>
34
+ *
35
+ * @author kiyopikko
36
+ *
37
+ * @type {import('../lib/types').Plugin<{
38
+ * floatPrecision?: number,
39
+ * leadingZero?: boolean,
40
+ * defaultPx?: boolean,
41
+ * convertToPx?: boolean
42
+ * }>}
43
+ */
44
+ exports.fn = (_root, params) => {
45
+ const {
46
+ floatPrecision = 3,
47
+ leadingZero = true,
48
+ defaultPx = true,
49
+ convertToPx = true,
50
+ } = params;
51
+
52
+ /**
53
+ * @type {(lists: string) => string}
54
+ */
55
+ const roundValues = (lists) => {
56
+ const roundedList = [];
57
+
58
+ for (const elem of lists.split(regSeparator)) {
59
+ const match = elem.match(regNumericValues);
60
+ const matchNew = elem.match(/new/);
61
+
62
+ // if attribute value matches regNumericValues
63
+ if (match) {
64
+ // round it to the fixed precision
65
+ let num = Number(Number(match[1]).toFixed(floatPrecision));
66
+ /**
67
+ * @type {any}
68
+ */
69
+ let matchedUnit = match[3] || '';
70
+ /**
71
+ * @type{'' | keyof typeof absoluteLengths}
72
+ */
73
+ let units = matchedUnit;
74
+
75
+ // convert absolute values to pixels
76
+ if (convertToPx && units && units in absoluteLengths) {
77
+ const pxNum = Number(
78
+ (absoluteLengths[units] * Number(match[1])).toFixed(floatPrecision)
79
+ );
80
+
81
+ if (pxNum.toString().length < match[0].length) {
82
+ num = pxNum;
83
+ units = 'px';
84
+ }
85
+ }
86
+
87
+ // and remove leading zero
88
+ let str;
89
+ if (leadingZero) {
90
+ str = removeLeadingZero(num);
91
+ } else {
92
+ str = num.toString();
93
+ }
94
+
95
+ // remove default 'px' units
96
+ if (defaultPx && units === 'px') {
97
+ units = '';
98
+ }
99
+
100
+ roundedList.push(str + units);
101
+ }
102
+ // if attribute value is "new"(only enable-background).
103
+ else if (matchNew) {
104
+ roundedList.push('new');
105
+ } else if (elem) {
106
+ roundedList.push(elem);
107
+ }
108
+ }
109
+
110
+ return roundedList.join(' ');
111
+ };
112
+
113
+ return {
114
+ element: {
115
+ enter: (node) => {
116
+ if (node.attributes.points != null) {
117
+ node.attributes.points = roundValues(node.attributes.points);
118
+ }
119
+
120
+ if (node.attributes['enable-background'] != null) {
121
+ node.attributes['enable-background'] = roundValues(
122
+ node.attributes['enable-background']
123
+ );
124
+ }
125
+
126
+ if (node.attributes.viewBox != null) {
127
+ node.attributes.viewBox = roundValues(node.attributes.viewBox);
128
+ }
129
+
130
+ if (node.attributes['stroke-dasharray'] != null) {
131
+ node.attributes['stroke-dasharray'] = roundValues(
132
+ node.attributes['stroke-dasharray']
133
+ );
134
+ }
135
+
136
+ if (node.attributes.dx != null) {
137
+ node.attributes.dx = roundValues(node.attributes.dx);
138
+ }
139
+
140
+ if (node.attributes.dy != null) {
141
+ node.attributes.dy = roundValues(node.attributes.dy);
142
+ }
143
+
144
+ if (node.attributes.x != null) {
145
+ node.attributes.x = roundValues(node.attributes.x);
146
+ }
147
+
148
+ if (node.attributes.y != null) {
149
+ node.attributes.y = roundValues(node.attributes.y);
150
+ }
151
+ },
152
+ },
153
+ };
154
+ };
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ const { removeLeadingZero } = require('../lib/svgo/tools');
4
+
5
+ exports.name = 'cleanupNumericValues';
6
+ exports.type = 'visitor';
7
+ exports.active = true;
8
+ exports.description =
9
+ 'rounds numeric values to the fixed precision, removes default ‘px’ units';
10
+
11
+ const regNumericValues =
12
+ /^([-+]?\d*\.?\d+([eE][-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/;
13
+
14
+ const absoluteLengths = {
15
+ // relative to px
16
+ cm: 96 / 2.54,
17
+ mm: 96 / 25.4,
18
+ in: 96,
19
+ pt: 4 / 3,
20
+ pc: 16,
21
+ px: 1,
22
+ };
23
+
24
+ /**
25
+ * Round numeric values to the fixed precision,
26
+ * remove default 'px' units.
27
+ *
28
+ * @author Kir Belevich
29
+ *
30
+ * @type {import('../lib/types').Plugin<{
31
+ * floatPrecision?: number,
32
+ * leadingZero?: boolean,
33
+ * defaultPx?: boolean,
34
+ * convertToPx?: boolean
35
+ * }>}
36
+ */
37
+ exports.fn = (_root, params) => {
38
+ const {
39
+ floatPrecision = 3,
40
+ leadingZero = true,
41
+ defaultPx = true,
42
+ convertToPx = true,
43
+ } = params;
44
+
45
+ return {
46
+ element: {
47
+ enter: (node) => {
48
+ if (node.attributes.viewBox != null) {
49
+ const nums = node.attributes.viewBox.split(/\s,?\s*|,\s*/g);
50
+ node.attributes.viewBox = nums
51
+ .map((value) => {
52
+ const num = Number(value);
53
+ return Number.isNaN(num)
54
+ ? value
55
+ : Number(num.toFixed(floatPrecision));
56
+ })
57
+ .join(' ');
58
+ }
59
+
60
+ for (const [name, value] of Object.entries(node.attributes)) {
61
+ // The `version` attribute is a text string and cannot be rounded
62
+ if (name === 'version') {
63
+ continue;
64
+ }
65
+
66
+ const match = value.match(regNumericValues);
67
+
68
+ // if attribute value matches regNumericValues
69
+ if (match) {
70
+ // round it to the fixed precision
71
+ let num = Number(Number(match[1]).toFixed(floatPrecision));
72
+ /**
73
+ * @type {any}
74
+ */
75
+ let matchedUnit = match[3] || '';
76
+ /**
77
+ * @type{'' | keyof typeof absoluteLengths}
78
+ */
79
+ let units = matchedUnit;
80
+
81
+ // convert absolute values to pixels
82
+ if (convertToPx && units !== '' && units in absoluteLengths) {
83
+ const pxNum = Number(
84
+ (absoluteLengths[units] * Number(match[1])).toFixed(
85
+ floatPrecision
86
+ )
87
+ );
88
+ if (pxNum.toString().length < match[0].length) {
89
+ num = pxNum;
90
+ units = 'px';
91
+ }
92
+ }
93
+
94
+ // and remove leading zero
95
+ let str;
96
+ if (leadingZero) {
97
+ str = removeLeadingZero(num);
98
+ } else {
99
+ str = num.toString();
100
+ }
101
+
102
+ // remove default 'px' units
103
+ if (defaultPx && units === 'px') {
104
+ units = '';
105
+ }
106
+
107
+ node.attributes[name] = str + units;
108
+ }
109
+ }
110
+ },
111
+ },
112
+ };
113
+ };
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @typedef {import('../lib/types').XastNode} XastNode
5
+ */
6
+
7
+ const { inheritableAttrs, elemsGroups } = require('./_collections.js');
8
+
9
+ exports.type = 'visitor';
10
+ exports.name = 'collapseGroups';
11
+ exports.active = true;
12
+ exports.description = 'collapses useless groups';
13
+
14
+ /**
15
+ * @type {(node: XastNode, name: string) => boolean}
16
+ */
17
+ const hasAnimatedAttr = (node, name) => {
18
+ if (node.type === 'element') {
19
+ if (
20
+ elemsGroups.animation.includes(node.name) &&
21
+ node.attributes.attributeName === name
22
+ ) {
23
+ return true;
24
+ }
25
+ for (const child of node.children) {
26
+ if (hasAnimatedAttr(child, name)) {
27
+ return true;
28
+ }
29
+ }
30
+ }
31
+ return false;
32
+ };
33
+
34
+ /**
35
+ * Collapse useless groups.
36
+ *
37
+ * @example
38
+ * <g>
39
+ * <g attr1="val1">
40
+ * <path d="..."/>
41
+ * </g>
42
+ * </g>
43
+ * ⬇
44
+ * <g>
45
+ * <g>
46
+ * <path attr1="val1" d="..."/>
47
+ * </g>
48
+ * </g>
49
+ * ⬇
50
+ * <path attr1="val1" d="..."/>
51
+ *
52
+ * @author Kir Belevich
53
+ *
54
+ * @type {import('../lib/types').Plugin<void>}
55
+ */
56
+ exports.fn = () => {
57
+ return {
58
+ element: {
59
+ exit: (node, parentNode) => {
60
+ if (parentNode.type === 'root' || parentNode.name === 'switch') {
61
+ return;
62
+ }
63
+ // non-empty groups
64
+ if (node.name !== 'g' || node.children.length === 0) {
65
+ return;
66
+ }
67
+
68
+ // move group attibutes to the single child element
69
+ if (
70
+ Object.keys(node.attributes).length !== 0 &&
71
+ node.children.length === 1
72
+ ) {
73
+ const firstChild = node.children[0];
74
+ // TODO untangle this mess
75
+ if (
76
+ firstChild.type === 'element' &&
77
+ firstChild.attributes.id == null &&
78
+ node.attributes.filter == null &&
79
+ (node.attributes.class == null ||
80
+ firstChild.attributes.class == null) &&
81
+ ((node.attributes['clip-path'] == null &&
82
+ node.attributes.mask == null) ||
83
+ (firstChild.name === 'g' &&
84
+ node.attributes.transform == null &&
85
+ firstChild.attributes.transform == null))
86
+ ) {
87
+ for (const [name, value] of Object.entries(node.attributes)) {
88
+ // avoid copying to not conflict with animated attribute
89
+ if (hasAnimatedAttr(firstChild, name)) {
90
+ return;
91
+ }
92
+ if (firstChild.attributes[name] == null) {
93
+ firstChild.attributes[name] = value;
94
+ } else if (name === 'transform') {
95
+ firstChild.attributes[name] =
96
+ value + ' ' + firstChild.attributes[name];
97
+ } else if (firstChild.attributes[name] === 'inherit') {
98
+ firstChild.attributes[name] = value;
99
+ } else if (
100
+ inheritableAttrs.includes(name) === false &&
101
+ firstChild.attributes[name] !== value
102
+ ) {
103
+ return;
104
+ }
105
+ delete node.attributes[name];
106
+ }
107
+ }
108
+ }
109
+
110
+ // collapse groups without attributes
111
+ if (Object.keys(node.attributes).length === 0) {
112
+ // animation elements "add" attributes to group
113
+ // group should be preserved
114
+ for (const child of node.children) {
115
+ if (
116
+ child.type === 'element' &&
117
+ elemsGroups.animation.includes(child.name)
118
+ ) {
119
+ return;
120
+ }
121
+ }
122
+ // replace current node with all its children
123
+ const index = parentNode.children.indexOf(node);
124
+ parentNode.children.splice(index, 1, ...node.children);
125
+ // TODO remove in v3
126
+ for (const child of node.children) {
127
+ // @ts-ignore parentNode is forbidden for public usage
128
+ // and will be moved in v3
129
+ child.parentNode = parentNode;
130
+ }
131
+ }
132
+ },
133
+ },
134
+ };
135
+ };