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,379 @@
1
+ 'use strict';
2
+
3
+ const regTransformTypes = /matrix|translate|scale|rotate|skewX|skewY/;
4
+ const regTransformSplit =
5
+ /\s*(matrix|translate|scale|rotate|skewX|skewY)\s*\(\s*(.+?)\s*\)[\s,]*/;
6
+ const regNumericValues = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
7
+
8
+ /**
9
+ * @typedef {{ name: string, data: Array<number> }} TransformItem
10
+ */
11
+
12
+ /**
13
+ * Convert transform string to JS representation.
14
+ *
15
+ * @type {(transformString: string) => Array<TransformItem>}
16
+ */
17
+ exports.transform2js = (transformString) => {
18
+ // JS representation of the transform data
19
+ /**
20
+ * @type {Array<TransformItem>}
21
+ */
22
+ const transforms = [];
23
+ // current transform context
24
+ /**
25
+ * @type {null | TransformItem}
26
+ */
27
+ let current = null;
28
+ // split value into ['', 'translate', '10 50', '', 'scale', '2', '', 'rotate', '-45', '']
29
+ for (const item of transformString.split(regTransformSplit)) {
30
+ var num;
31
+ if (item) {
32
+ // if item is a translate function
33
+ if (regTransformTypes.test(item)) {
34
+ // then collect it and change current context
35
+ current = { name: item, data: [] };
36
+ transforms.push(current);
37
+ // else if item is data
38
+ } else {
39
+ // then split it into [10, 50] and collect as context.data
40
+ // eslint-disable-next-line no-cond-assign
41
+ while ((num = regNumericValues.exec(item))) {
42
+ num = Number(num);
43
+ if (current != null) {
44
+ current.data.push(num);
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ // return empty array if broken transform (no data)
51
+ return current == null || current.data.length == 0 ? [] : transforms;
52
+ };
53
+
54
+ /**
55
+ * Multiply transforms into one.
56
+ *
57
+ * @type {(transforms: Array<TransformItem>) => TransformItem}
58
+ */
59
+ exports.transformsMultiply = (transforms) => {
60
+ // convert transforms objects to the matrices
61
+ const matrixData = transforms.map((transform) => {
62
+ if (transform.name === 'matrix') {
63
+ return transform.data;
64
+ }
65
+ return transformToMatrix(transform);
66
+ });
67
+ // multiply all matrices into one
68
+ const matrixTransform = {
69
+ name: 'matrix',
70
+ data:
71
+ matrixData.length > 0 ? matrixData.reduce(multiplyTransformMatrices) : [],
72
+ };
73
+ return matrixTransform;
74
+ };
75
+
76
+ /**
77
+ * math utilities in radians.
78
+ */
79
+ const mth = {
80
+ /**
81
+ * @type {(deg: number) => number}
82
+ */
83
+ rad: (deg) => {
84
+ return (deg * Math.PI) / 180;
85
+ },
86
+
87
+ /**
88
+ * @type {(rad: number) => number}
89
+ */
90
+ deg: (rad) => {
91
+ return (rad * 180) / Math.PI;
92
+ },
93
+
94
+ /**
95
+ * @type {(deg: number) => number}
96
+ */
97
+ cos: (deg) => {
98
+ return Math.cos(mth.rad(deg));
99
+ },
100
+
101
+ /**
102
+ * @type {(val: number, floatPrecision: number) => number}
103
+ */
104
+ acos: (val, floatPrecision) => {
105
+ return Number(mth.deg(Math.acos(val)).toFixed(floatPrecision));
106
+ },
107
+
108
+ /**
109
+ * @type {(deg: number) => number}
110
+ */
111
+ sin: (deg) => {
112
+ return Math.sin(mth.rad(deg));
113
+ },
114
+
115
+ /**
116
+ * @type {(val: number, floatPrecision: number) => number}
117
+ */
118
+ asin: (val, floatPrecision) => {
119
+ return Number(mth.deg(Math.asin(val)).toFixed(floatPrecision));
120
+ },
121
+
122
+ /**
123
+ * @type {(deg: number) => number}
124
+ */
125
+ tan: (deg) => {
126
+ return Math.tan(mth.rad(deg));
127
+ },
128
+
129
+ /**
130
+ * @type {(val: number, floatPrecision: number) => number}
131
+ */
132
+ atan: (val, floatPrecision) => {
133
+ return Number(mth.deg(Math.atan(val)).toFixed(floatPrecision));
134
+ },
135
+ };
136
+
137
+ /**
138
+ * @typedef {{
139
+ * convertToShorts: boolean,
140
+ * floatPrecision: number,
141
+ * transformPrecision: number,
142
+ * matrixToTransform: boolean,
143
+ * shortTranslate: boolean,
144
+ * shortScale: boolean,
145
+ * shortRotate: boolean,
146
+ * removeUseless: boolean,
147
+ * collapseIntoOne: boolean,
148
+ * leadingZero: boolean,
149
+ * negativeExtraSpace: boolean,
150
+ * }} TransformParams
151
+ */
152
+
153
+ /**
154
+ * Decompose matrix into simple transforms. See
155
+ * https://frederic-wang.fr/decomposition-of-2d-transform-matrices.html
156
+ *
157
+ * @type {(transform: TransformItem, params: TransformParams) => Array<TransformItem>}
158
+ */
159
+ exports.matrixToTransform = (transform, params) => {
160
+ let floatPrecision = params.floatPrecision;
161
+ let data = transform.data;
162
+ let transforms = [];
163
+ let sx = Number(
164
+ Math.hypot(data[0], data[1]).toFixed(params.transformPrecision)
165
+ );
166
+ let sy = Number(
167
+ ((data[0] * data[3] - data[1] * data[2]) / sx).toFixed(
168
+ params.transformPrecision
169
+ )
170
+ );
171
+ let colsSum = data[0] * data[2] + data[1] * data[3];
172
+ let rowsSum = data[0] * data[1] + data[2] * data[3];
173
+ let scaleBefore = rowsSum != 0 || sx == sy;
174
+
175
+ // [..., ..., ..., ..., tx, ty] → translate(tx, ty)
176
+ if (data[4] || data[5]) {
177
+ transforms.push({
178
+ name: 'translate',
179
+ data: data.slice(4, data[5] ? 6 : 5),
180
+ });
181
+ }
182
+
183
+ // [sx, 0, tan(a)·sy, sy, 0, 0] → skewX(a)·scale(sx, sy)
184
+ if (!data[1] && data[2]) {
185
+ transforms.push({
186
+ name: 'skewX',
187
+ data: [mth.atan(data[2] / sy, floatPrecision)],
188
+ });
189
+
190
+ // [sx, sx·tan(a), 0, sy, 0, 0] → skewY(a)·scale(sx, sy)
191
+ } else if (data[1] && !data[2]) {
192
+ transforms.push({
193
+ name: 'skewY',
194
+ data: [mth.atan(data[1] / data[0], floatPrecision)],
195
+ });
196
+ sx = data[0];
197
+ sy = data[3];
198
+
199
+ // [sx·cos(a), sx·sin(a), sy·-sin(a), sy·cos(a), x, y] → rotate(a[, cx, cy])·(scale or skewX) or
200
+ // [sx·cos(a), sy·sin(a), sx·-sin(a), sy·cos(a), x, y] → scale(sx, sy)·rotate(a[, cx, cy]) (if !scaleBefore)
201
+ } else if (!colsSum || (sx == 1 && sy == 1) || !scaleBefore) {
202
+ if (!scaleBefore) {
203
+ sx = (data[0] < 0 ? -1 : 1) * Math.hypot(data[0], data[2]);
204
+ sy = (data[3] < 0 ? -1 : 1) * Math.hypot(data[1], data[3]);
205
+ transforms.push({ name: 'scale', data: [sx, sy] });
206
+ }
207
+ var angle = Math.min(Math.max(-1, data[0] / sx), 1),
208
+ rotate = [
209
+ mth.acos(angle, floatPrecision) *
210
+ ((scaleBefore ? 1 : sy) * data[1] < 0 ? -1 : 1),
211
+ ];
212
+
213
+ if (rotate[0]) transforms.push({ name: 'rotate', data: rotate });
214
+
215
+ if (rowsSum && colsSum)
216
+ transforms.push({
217
+ name: 'skewX',
218
+ data: [mth.atan(colsSum / (sx * sx), floatPrecision)],
219
+ });
220
+
221
+ // rotate(a, cx, cy) can consume translate() within optional arguments cx, cy (rotation point)
222
+ if (rotate[0] && (data[4] || data[5])) {
223
+ transforms.shift();
224
+ var cos = data[0] / sx,
225
+ sin = data[1] / (scaleBefore ? sx : sy),
226
+ x = data[4] * (scaleBefore ? 1 : sy),
227
+ y = data[5] * (scaleBefore ? 1 : sx),
228
+ denom =
229
+ (Math.pow(1 - cos, 2) + Math.pow(sin, 2)) *
230
+ (scaleBefore ? 1 : sx * sy);
231
+ rotate.push(((1 - cos) * x - sin * y) / denom);
232
+ rotate.push(((1 - cos) * y + sin * x) / denom);
233
+ }
234
+
235
+ // Too many transformations, return original matrix if it isn't just a scale/translate
236
+ } else if (data[1] || data[2]) {
237
+ return [transform];
238
+ }
239
+
240
+ if ((scaleBefore && (sx != 1 || sy != 1)) || !transforms.length)
241
+ transforms.push({
242
+ name: 'scale',
243
+ data: sx == sy ? [sx] : [sx, sy],
244
+ });
245
+
246
+ return transforms;
247
+ };
248
+
249
+ /**
250
+ * Convert transform to the matrix data.
251
+ *
252
+ * @type {(transform: TransformItem) => Array<number> }
253
+ */
254
+ const transformToMatrix = (transform) => {
255
+ if (transform.name === 'matrix') {
256
+ return transform.data;
257
+ }
258
+ switch (transform.name) {
259
+ case 'translate':
260
+ // [1, 0, 0, 1, tx, ty]
261
+ return [1, 0, 0, 1, transform.data[0], transform.data[1] || 0];
262
+ case 'scale':
263
+ // [sx, 0, 0, sy, 0, 0]
264
+ return [
265
+ transform.data[0],
266
+ 0,
267
+ 0,
268
+ transform.data[1] || transform.data[0],
269
+ 0,
270
+ 0,
271
+ ];
272
+ case 'rotate':
273
+ // [cos(a), sin(a), -sin(a), cos(a), x, y]
274
+ var cos = mth.cos(transform.data[0]),
275
+ sin = mth.sin(transform.data[0]),
276
+ cx = transform.data[1] || 0,
277
+ cy = transform.data[2] || 0;
278
+ return [
279
+ cos,
280
+ sin,
281
+ -sin,
282
+ cos,
283
+ (1 - cos) * cx + sin * cy,
284
+ (1 - cos) * cy - sin * cx,
285
+ ];
286
+ case 'skewX':
287
+ // [1, 0, tan(a), 1, 0, 0]
288
+ return [1, 0, mth.tan(transform.data[0]), 1, 0, 0];
289
+ case 'skewY':
290
+ // [1, tan(a), 0, 1, 0, 0]
291
+ return [1, mth.tan(transform.data[0]), 0, 1, 0, 0];
292
+ default:
293
+ throw Error(`Unknown transform ${transform.name}`);
294
+ }
295
+ };
296
+
297
+ /**
298
+ * Applies transformation to an arc. To do so, we represent ellipse as a matrix, multiply it
299
+ * by the transformation matrix and use a singular value decomposition to represent in a form
300
+ * rotate(θ)·scale(a b)·rotate(φ). This gives us new ellipse params a, b and θ.
301
+ * SVD is being done with the formulae provided by Wolffram|Alpha (svd {{m0, m2}, {m1, m3}})
302
+ *
303
+ * @type {(
304
+ * cursor: [x: number, y: number],
305
+ * arc: Array<number>,
306
+ * transform: Array<number>
307
+ * ) => Array<number>}
308
+ */
309
+ exports.transformArc = (cursor, arc, transform) => {
310
+ const x = arc[5] - cursor[0];
311
+ const y = arc[6] - cursor[1];
312
+ let a = arc[0];
313
+ let b = arc[1];
314
+ const rot = (arc[2] * Math.PI) / 180;
315
+ const cos = Math.cos(rot);
316
+ const sin = Math.sin(rot);
317
+ // skip if radius is 0
318
+ if (a > 0 && b > 0) {
319
+ let h =
320
+ Math.pow(x * cos + y * sin, 2) / (4 * a * a) +
321
+ Math.pow(y * cos - x * sin, 2) / (4 * b * b);
322
+ if (h > 1) {
323
+ h = Math.sqrt(h);
324
+ a *= h;
325
+ b *= h;
326
+ }
327
+ }
328
+ const ellipse = [a * cos, a * sin, -b * sin, b * cos, 0, 0];
329
+ const m = multiplyTransformMatrices(transform, ellipse);
330
+ // Decompose the new ellipse matrix
331
+ const lastCol = m[2] * m[2] + m[3] * m[3];
332
+ const squareSum = m[0] * m[0] + m[1] * m[1] + lastCol;
333
+ const root =
334
+ Math.hypot(m[0] - m[3], m[1] + m[2]) * Math.hypot(m[0] + m[3], m[1] - m[2]);
335
+
336
+ if (!root) {
337
+ // circle
338
+ arc[0] = arc[1] = Math.sqrt(squareSum / 2);
339
+ arc[2] = 0;
340
+ } else {
341
+ const majorAxisSqr = (squareSum + root) / 2;
342
+ const minorAxisSqr = (squareSum - root) / 2;
343
+ const major = Math.abs(majorAxisSqr - lastCol) > 1e-6;
344
+ const sub = (major ? majorAxisSqr : minorAxisSqr) - lastCol;
345
+ const rowsSum = m[0] * m[2] + m[1] * m[3];
346
+ const term1 = m[0] * sub + m[2] * rowsSum;
347
+ const term2 = m[1] * sub + m[3] * rowsSum;
348
+ arc[0] = Math.sqrt(majorAxisSqr);
349
+ arc[1] = Math.sqrt(minorAxisSqr);
350
+ arc[2] =
351
+ (((major ? term2 < 0 : term1 > 0) ? -1 : 1) *
352
+ Math.acos((major ? term1 : term2) / Math.hypot(term1, term2)) *
353
+ 180) /
354
+ Math.PI;
355
+ }
356
+
357
+ if (transform[0] < 0 !== transform[3] < 0) {
358
+ // Flip the sweep flag if coordinates are being flipped horizontally XOR vertically
359
+ arc[4] = 1 - arc[4];
360
+ }
361
+
362
+ return arc;
363
+ };
364
+
365
+ /**
366
+ * Multiply transformation matrices.
367
+ *
368
+ * @type {(a: Array<number>, b: Array<number>) => Array<number>}
369
+ */
370
+ const multiplyTransformMatrices = (a, b) => {
371
+ return [
372
+ a[0] * b[0] + a[2] * b[1],
373
+ a[1] * b[0] + a[3] * b[1],
374
+ a[0] * b[2] + a[2] * b[3],
375
+ a[1] * b[2] + a[3] * b[3],
376
+ a[0] * b[4] + a[2] * b[5] + a[4],
377
+ a[1] * b[4] + a[3] * b[5] + a[5],
378
+ ];
379
+ };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ exports.name = 'addAttributesToSVGElement';
4
+ exports.type = 'visitor';
5
+ exports.active = false;
6
+ exports.description = 'adds attributes to an outer <svg> element';
7
+
8
+ var ENOCLS = `Error in plugin "addAttributesToSVGElement": absent parameters.
9
+ It should have a list of "attributes" or one "attribute".
10
+ Config example:
11
+
12
+ plugins: [
13
+ {
14
+ name: 'addAttributesToSVGElement',
15
+ params: {
16
+ attribute: "mySvg"
17
+ }
18
+ }
19
+ ]
20
+
21
+ plugins: [
22
+ {
23
+ name: 'addAttributesToSVGElement',
24
+ params: {
25
+ attributes: ["mySvg", "size-big"]
26
+ }
27
+ }
28
+ ]
29
+
30
+ plugins: [
31
+ {
32
+ name: 'addAttributesToSVGElement',
33
+ params: {
34
+ attributes: [
35
+ {
36
+ focusable: false
37
+ },
38
+ {
39
+ 'data-image': icon
40
+ }
41
+ ]
42
+ }
43
+ }
44
+ ]
45
+ `;
46
+
47
+ /**
48
+ * Add attributes to an outer <svg> element. Example config:
49
+ *
50
+ * @author April Arcus
51
+ *
52
+ * @type {import('../lib/types').Plugin<{
53
+ * attribute?: string | Record<string, null | string>,
54
+ * attributes?: Array<string | Record<string, null | string>>
55
+ * }>}
56
+ */
57
+ exports.fn = (root, params) => {
58
+ if (!Array.isArray(params.attributes) && !params.attribute) {
59
+ console.error(ENOCLS);
60
+ return null;
61
+ }
62
+ const attributes = params.attributes || [params.attribute];
63
+ return {
64
+ element: {
65
+ enter: (node, parentNode) => {
66
+ if (node.name === 'svg' && parentNode.type === 'root') {
67
+ for (const attribute of attributes) {
68
+ if (typeof attribute === 'string') {
69
+ if (node.attributes[attribute] == null) {
70
+ // @ts-ignore disallow explicit nullable attribute value
71
+ node.attributes[attribute] = undefined;
72
+ }
73
+ }
74
+ if (typeof attribute === 'object') {
75
+ for (const key of Object.keys(attribute)) {
76
+ if (node.attributes[key] == null) {
77
+ // @ts-ignore disallow explicit nullable attribute value
78
+ node.attributes[key] = attribute[key];
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ },
85
+ },
86
+ };
87
+ };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ exports.name = 'addClassesToSVGElement';
4
+ exports.type = 'visitor';
5
+ exports.active = false;
6
+ exports.description = 'adds classnames to an outer <svg> element';
7
+
8
+ var ENOCLS = `Error in plugin "addClassesToSVGElement": absent parameters.
9
+ It should have a list of classes in "classNames" or one "className".
10
+ Config example:
11
+
12
+ plugins: [
13
+ {
14
+ name: "addClassesToSVGElement",
15
+ params: {
16
+ className: "mySvg"
17
+ }
18
+ }
19
+ ]
20
+
21
+ plugins: [
22
+ {
23
+ name: "addClassesToSVGElement",
24
+ params: {
25
+ classNames: ["mySvg", "size-big"]
26
+ }
27
+ }
28
+ ]
29
+ `;
30
+
31
+ /**
32
+ * Add classnames to an outer <svg> element. Example config:
33
+ *
34
+ * plugins: [
35
+ * {
36
+ * name: "addClassesToSVGElement",
37
+ * params: {
38
+ * className: "mySvg"
39
+ * }
40
+ * }
41
+ * ]
42
+ *
43
+ * plugins: [
44
+ * {
45
+ * name: "addClassesToSVGElement",
46
+ * params: {
47
+ * classNames: ["mySvg", "size-big"]
48
+ * }
49
+ * }
50
+ * ]
51
+ *
52
+ * @author April Arcus
53
+ *
54
+ * @type {import('../lib/types').Plugin<{
55
+ * className?: string,
56
+ * classNames?: Array<string>
57
+ * }>}
58
+ */
59
+ exports.fn = (root, params) => {
60
+ if (
61
+ !(Array.isArray(params.classNames) && params.classNames.some(String)) &&
62
+ !params.className
63
+ ) {
64
+ console.error(ENOCLS);
65
+ return null;
66
+ }
67
+ const classNames = params.classNames || [params.className];
68
+ return {
69
+ element: {
70
+ enter: (node, parentNode) => {
71
+ if (node.name === 'svg' && parentNode.type === 'root') {
72
+ const classList = new Set(
73
+ node.attributes.class == null
74
+ ? null
75
+ : node.attributes.class.split(' ')
76
+ );
77
+ for (const className of classNames) {
78
+ if (className != null) {
79
+ classList.add(className);
80
+ }
81
+ }
82
+ node.attributes.class = Array.from(classList).join(' ');
83
+ }
84
+ },
85
+ },
86
+ };
87
+ };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ exports.name = 'cleanupAttrs';
4
+ exports.type = 'visitor';
5
+ exports.active = true;
6
+ exports.description =
7
+ 'cleanups attributes from newlines, trailing and repeating spaces';
8
+
9
+ const regNewlinesNeedSpace = /(\S)\r?\n(\S)/g;
10
+ const regNewlines = /\r?\n/g;
11
+ const regSpaces = /\s{2,}/g;
12
+
13
+ /**
14
+ * Cleanup attributes values from newlines, trailing and repeating spaces.
15
+ *
16
+ * @author Kir Belevich
17
+ *
18
+ * @type {import('../lib/types').Plugin<{
19
+ * newlines?: boolean,
20
+ * trim?: boolean,
21
+ * spaces?: boolean
22
+ * }>}
23
+ */
24
+ exports.fn = (root, params) => {
25
+ const { newlines = true, trim = true, spaces = true } = params;
26
+ return {
27
+ element: {
28
+ enter: (node) => {
29
+ for (const name of Object.keys(node.attributes)) {
30
+ if (newlines) {
31
+ // new line which requires a space instead of themselve
32
+ node.attributes[name] = node.attributes[name].replace(
33
+ regNewlinesNeedSpace,
34
+ (match, p1, p2) => p1 + ' ' + p2
35
+ );
36
+ // simple new line
37
+ node.attributes[name] = node.attributes[name].replace(
38
+ regNewlines,
39
+ ''
40
+ );
41
+ }
42
+ if (trim) {
43
+ node.attributes[name] = node.attributes[name].trim();
44
+ }
45
+ if (spaces) {
46
+ node.attributes[name] = node.attributes[name].replace(
47
+ regSpaces,
48
+ ' '
49
+ );
50
+ }
51
+ }
52
+ },
53
+ },
54
+ };
55
+ };
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ const { visit } = require('../lib/xast.js');
4
+
5
+ exports.type = 'visitor';
6
+ exports.name = 'cleanupEnableBackground';
7
+ exports.active = true;
8
+ exports.description =
9
+ 'remove or cleanup enable-background attribute when possible';
10
+
11
+ /**
12
+ * Remove or cleanup enable-background attr which coincides with a width/height box.
13
+ *
14
+ * @see https://www.w3.org/TR/SVG11/filters.html#EnableBackgroundProperty
15
+ *
16
+ * @example
17
+ * <svg width="100" height="50" enable-background="new 0 0 100 50">
18
+ * ⬇
19
+ * <svg width="100" height="50">
20
+ *
21
+ * @author Kir Belevich
22
+ *
23
+ * @type {import('../lib/types').Plugin<void>}
24
+ */
25
+ exports.fn = (root) => {
26
+ const regEnableBackground =
27
+ /^new\s0\s0\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)$/;
28
+
29
+ let hasFilter = false;
30
+ visit(root, {
31
+ element: {
32
+ enter: (node) => {
33
+ if (node.name === 'filter') {
34
+ hasFilter = true;
35
+ }
36
+ },
37
+ },
38
+ });
39
+
40
+ return {
41
+ element: {
42
+ enter: (node) => {
43
+ if (node.attributes['enable-background'] == null) {
44
+ return;
45
+ }
46
+ if (hasFilter) {
47
+ if (
48
+ (node.name === 'svg' ||
49
+ node.name === 'mask' ||
50
+ node.name === 'pattern') &&
51
+ node.attributes.width != null &&
52
+ node.attributes.height != null
53
+ ) {
54
+ const match =
55
+ node.attributes['enable-background'].match(regEnableBackground);
56
+ if (
57
+ match != null &&
58
+ node.attributes.width === match[1] &&
59
+ node.attributes.height === match[3]
60
+ ) {
61
+ if (node.name === 'svg') {
62
+ delete node.attributes['enable-background'];
63
+ } else {
64
+ node.attributes['enable-background'] = 'new';
65
+ }
66
+ }
67
+ }
68
+ } else {
69
+ //we don't need 'enable-background' if we have no filters
70
+ delete node.attributes['enable-background'];
71
+ }
72
+ },
73
+ },
74
+ };
75
+ };