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.
- package/LICENSE +21 -0
- package/README.md +294 -0
- package/bin/svgo +10 -0
- package/dist/svgo.browser.js +1 -0
- package/lib/css-tools.js +239 -0
- package/lib/parser.js +259 -0
- package/lib/path.js +347 -0
- package/lib/stringifier.js +326 -0
- package/lib/style.js +283 -0
- package/lib/svgo/coa.js +517 -0
- package/lib/svgo/config.js +138 -0
- package/lib/svgo/css-class-list.js +72 -0
- package/lib/svgo/css-select-adapter.d.ts +2 -0
- package/lib/svgo/css-select-adapter.js +120 -0
- package/lib/svgo/css-style-declaration.js +232 -0
- package/lib/svgo/jsAPI.d.ts +2 -0
- package/lib/svgo/jsAPI.js +443 -0
- package/lib/svgo/plugins.js +109 -0
- package/lib/svgo/tools.js +137 -0
- package/lib/svgo-node.js +106 -0
- package/lib/svgo.js +83 -0
- package/lib/types.ts +172 -0
- package/lib/xast.js +102 -0
- package/package.json +130 -0
- package/plugins/_applyTransforms.js +335 -0
- package/plugins/_collections.js +2168 -0
- package/plugins/_path.js +816 -0
- package/plugins/_transforms.js +379 -0
- package/plugins/addAttributesToSVGElement.js +87 -0
- package/plugins/addClassesToSVGElement.js +87 -0
- package/plugins/cleanupAttrs.js +55 -0
- package/plugins/cleanupEnableBackground.js +75 -0
- package/plugins/cleanupIDs.js +297 -0
- package/plugins/cleanupListOfValues.js +154 -0
- package/plugins/cleanupNumericValues.js +113 -0
- package/plugins/collapseGroups.js +135 -0
- package/plugins/convertColors.js +152 -0
- package/plugins/convertEllipseToCircle.js +39 -0
- package/plugins/convertPathData.js +1023 -0
- package/plugins/convertShapeToPath.js +175 -0
- package/plugins/convertStyleToAttrs.js +132 -0
- package/plugins/convertTransform.js +432 -0
- package/plugins/inlineStyles.js +379 -0
- package/plugins/mergePaths.js +104 -0
- package/plugins/mergeStyles.js +93 -0
- package/plugins/minifyStyles.js +148 -0
- package/plugins/moveElemsAttrsToGroup.js +130 -0
- package/plugins/moveGroupAttrsToElems.js +62 -0
- package/plugins/plugins.js +56 -0
- package/plugins/prefixIds.js +241 -0
- package/plugins/preset-default.js +80 -0
- package/plugins/removeAttributesBySelector.js +99 -0
- package/plugins/removeAttrs.js +159 -0
- package/plugins/removeComments.js +31 -0
- package/plugins/removeDesc.js +41 -0
- package/plugins/removeDimensions.js +43 -0
- package/plugins/removeDoctype.js +42 -0
- package/plugins/removeEditorsNSData.js +68 -0
- package/plugins/removeElementsByAttr.js +78 -0
- package/plugins/removeEmptyAttrs.js +33 -0
- package/plugins/removeEmptyContainers.js +58 -0
- package/plugins/removeEmptyText.js +57 -0
- package/plugins/removeHiddenElems.js +318 -0
- package/plugins/removeMetadata.js +29 -0
- package/plugins/removeNonInheritableGroupAttrs.js +38 -0
- package/plugins/removeOffCanvasPaths.js +138 -0
- package/plugins/removeRasterImages.js +33 -0
- package/plugins/removeScriptElement.js +29 -0
- package/plugins/removeStyleElement.js +29 -0
- package/plugins/removeTitle.js +29 -0
- package/plugins/removeUnknownsAndDefaults.js +218 -0
- package/plugins/removeUnusedNS.js +61 -0
- package/plugins/removeUselessDefs.js +65 -0
- package/plugins/removeUselessStrokeAndFill.js +144 -0
- package/plugins/removeViewBox.js +51 -0
- package/plugins/removeXMLNS.js +30 -0
- package/plugins/removeXMLProcInst.js +30 -0
- package/plugins/reusePaths.js +113 -0
- package/plugins/sortAttrs.js +113 -0
- 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
|
+
};
|