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
+ /**
4
+ * @typedef {import('../lib/types').Specificity} Specificity
5
+ * @typedef {import('../lib/types').XastElement} XastElement
6
+ * @typedef {import('../lib/types').XastParent} XastParent
7
+ */
8
+
9
+ const csstree = require('css-tree-v2');
10
+ // @ts-ignore not defined in @types/csso
11
+ const specificity = require('csso/lib/restructure/prepare/specificity');
12
+ const stable = require('stable');
13
+ const {
14
+ visitSkip,
15
+ querySelectorAll,
16
+ detachNodeFromParent,
17
+ } = require('../lib/xast.js');
18
+
19
+ exports.type = 'visitor';
20
+ exports.name = 'inlineStyles';
21
+ exports.active = true;
22
+ exports.description = 'inline styles (additional options)';
23
+
24
+ /**
25
+ * Compares two selector specificities.
26
+ * extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211
27
+ *
28
+ * @type {(a: Specificity, b: Specificity) => number}
29
+ */
30
+ const compareSpecificity = (a, b) => {
31
+ for (var i = 0; i < 4; i += 1) {
32
+ if (a[i] < b[i]) {
33
+ return -1;
34
+ } else if (a[i] > b[i]) {
35
+ return 1;
36
+ }
37
+ }
38
+ return 0;
39
+ };
40
+
41
+ /**
42
+ * Moves + merges styles from style elements to element styles
43
+ *
44
+ * Options
45
+ * onlyMatchedOnce (default: true)
46
+ * inline only selectors that match once
47
+ *
48
+ * removeMatchedSelectors (default: true)
49
+ * clean up matched selectors,
50
+ * leave selectors that hadn't matched
51
+ *
52
+ * useMqs (default: ['', 'screen'])
53
+ * what media queries to be used
54
+ * empty string element for styles outside media queries
55
+ *
56
+ * usePseudos (default: [''])
57
+ * what pseudo-classes/-elements to be used
58
+ * empty string element for all non-pseudo-classes and/or -elements
59
+ *
60
+ * @author strarsis <strarsis@gmail.com>
61
+ *
62
+ * @type {import('../lib/types').Plugin<{
63
+ * onlyMatchedOnce?: boolean,
64
+ * removeMatchedSelectors?: boolean,
65
+ * useMqs?: Array<string>,
66
+ * usePseudos?: Array<string>
67
+ * }>}
68
+ */
69
+ exports.fn = (root, params) => {
70
+ const {
71
+ onlyMatchedOnce = true,
72
+ removeMatchedSelectors = true,
73
+ useMqs = ['', 'screen'],
74
+ usePseudos = [''],
75
+ } = params;
76
+
77
+ /**
78
+ * @type {Array<{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }>}
79
+ */
80
+ const styles = [];
81
+ /**
82
+ * @type {Array<{
83
+ * node: csstree.Selector,
84
+ * item: csstree.ListItem<csstree.CssNode>,
85
+ * rule: csstree.Rule,
86
+ * matchedElements?: Array<XastElement>
87
+ * }>}
88
+ */
89
+ let selectors = [];
90
+
91
+ return {
92
+ element: {
93
+ enter: (node, parentNode) => {
94
+ // skip <foreignObject /> content
95
+ if (node.name === 'foreignObject') {
96
+ return visitSkip;
97
+ }
98
+ // collect only non-empty <style /> elements
99
+ if (node.name !== 'style' || node.children.length === 0) {
100
+ return;
101
+ }
102
+ // values other than the empty string or text/css are not used
103
+ if (
104
+ node.attributes.type != null &&
105
+ node.attributes.type !== '' &&
106
+ node.attributes.type !== 'text/css'
107
+ ) {
108
+ return;
109
+ }
110
+ // parse css in style element
111
+ let cssText = '';
112
+ for (const child of node.children) {
113
+ if (child.type === 'text' || child.type === 'cdata') {
114
+ cssText += child.value;
115
+ }
116
+ }
117
+ /**
118
+ * @type {null | csstree.CssNode}
119
+ */
120
+ let cssAst = null;
121
+ try {
122
+ cssAst = csstree.parse(cssText, {
123
+ parseValue: false,
124
+ parseCustomProperty: false,
125
+ });
126
+ } catch {
127
+ return;
128
+ }
129
+ if (cssAst.type === 'StyleSheet') {
130
+ styles.push({ node, parentNode, cssAst });
131
+ }
132
+
133
+ // collect selectors
134
+ csstree.walk(cssAst, {
135
+ visit: 'Selector',
136
+ enter(node, item) {
137
+ const atrule = this.atrule;
138
+ const rule = this.rule;
139
+ if (rule == null) {
140
+ return;
141
+ }
142
+
143
+ // skip media queries not included into useMqs param
144
+ let mq = '';
145
+ if (atrule != null) {
146
+ mq = atrule.name;
147
+ if (atrule.prelude != null) {
148
+ mq += ` ${csstree.generate(atrule.prelude)}`;
149
+ }
150
+ }
151
+ if (useMqs.includes(mq) === false) {
152
+ return;
153
+ }
154
+
155
+ /**
156
+ * @type {Array<{
157
+ * item: csstree.ListItem<csstree.CssNode>,
158
+ * list: csstree.List<csstree.CssNode>
159
+ * }>}
160
+ */
161
+ const pseudos = [];
162
+ if (node.type === 'Selector') {
163
+ node.children.each((childNode, childItem, childList) => {
164
+ if (
165
+ childNode.type === 'PseudoClassSelector' ||
166
+ childNode.type === 'PseudoElementSelector'
167
+ ) {
168
+ pseudos.push({ item: childItem, list: childList });
169
+ }
170
+ });
171
+ }
172
+
173
+ // skip pseudo classes and pseudo elements not includes into usePseudos param
174
+ const pseudoSelectors = csstree.generate({
175
+ type: 'Selector',
176
+ children: new csstree.List().fromArray(
177
+ pseudos.map((pseudo) => pseudo.item.data)
178
+ ),
179
+ });
180
+ if (usePseudos.includes(pseudoSelectors) === false) {
181
+ return;
182
+ }
183
+
184
+ // remove pseudo classes and elements to allow querySelector match elements
185
+ // TODO this is not very accurate since some pseudo classes like first-child
186
+ // are used for selection
187
+ for (const pseudo of pseudos) {
188
+ pseudo.list.remove(pseudo.item);
189
+ }
190
+
191
+ selectors.push({ node, item, rule });
192
+ },
193
+ });
194
+ },
195
+ },
196
+
197
+ root: {
198
+ exit: () => {
199
+ if (styles.length === 0) {
200
+ return;
201
+ }
202
+ // stable sort selectors
203
+ const sortedSelectors = stable(selectors, (a, b) => {
204
+ const aSpecificity = specificity(a.item.data);
205
+ const bSpecificity = specificity(b.item.data);
206
+ return compareSpecificity(aSpecificity, bSpecificity);
207
+ }).reverse();
208
+
209
+ for (const selector of sortedSelectors) {
210
+ // match selectors
211
+ const selectorText = csstree.generate(selector.item.data);
212
+ /**
213
+ * @type {Array<XastElement>}
214
+ */
215
+ const matchedElements = [];
216
+ try {
217
+ for (const node of querySelectorAll(root, selectorText)) {
218
+ if (node.type === 'element') {
219
+ matchedElements.push(node);
220
+ }
221
+ }
222
+ } catch (selectError) {
223
+ continue;
224
+ }
225
+ // nothing selected
226
+ if (matchedElements.length === 0) {
227
+ continue;
228
+ }
229
+
230
+ // apply styles to matched elements
231
+ // skip selectors that match more than once if option onlyMatchedOnce is enabled
232
+ if (onlyMatchedOnce && matchedElements.length > 1) {
233
+ continue;
234
+ }
235
+
236
+ // apply <style/> to matched elements
237
+ for (const selectedEl of matchedElements) {
238
+ const styleDeclarationList = csstree.parse(
239
+ selectedEl.attributes.style == null
240
+ ? ''
241
+ : selectedEl.attributes.style,
242
+ {
243
+ context: 'declarationList',
244
+ parseValue: false,
245
+ }
246
+ );
247
+ if (styleDeclarationList.type !== 'DeclarationList') {
248
+ continue;
249
+ }
250
+ const styleDeclarationItems = new Map();
251
+ csstree.walk(styleDeclarationList, {
252
+ visit: 'Declaration',
253
+ enter(node, item) {
254
+ styleDeclarationItems.set(node.property, item);
255
+ },
256
+ });
257
+ // merge declarations
258
+ csstree.walk(selector.rule, {
259
+ visit: 'Declaration',
260
+ enter(ruleDeclaration) {
261
+ // existing inline styles have higher priority
262
+ // no inline styles, external styles, external styles used
263
+ // inline styles, external styles same priority as inline styles, inline styles used
264
+ // inline styles, external styles higher priority than inline styles, external styles used
265
+ const matchedItem = styleDeclarationItems.get(
266
+ ruleDeclaration.property
267
+ );
268
+ const ruleDeclarationItem =
269
+ styleDeclarationList.children.createItem(ruleDeclaration);
270
+ if (matchedItem == null) {
271
+ styleDeclarationList.children.append(ruleDeclarationItem);
272
+ } else if (
273
+ matchedItem.data.important !== true &&
274
+ ruleDeclaration.important === true
275
+ ) {
276
+ styleDeclarationList.children.replace(
277
+ matchedItem,
278
+ ruleDeclarationItem
279
+ );
280
+ styleDeclarationItems.set(
281
+ ruleDeclaration.property,
282
+ ruleDeclarationItem
283
+ );
284
+ }
285
+ },
286
+ });
287
+ selectedEl.attributes.style =
288
+ csstree.generate(styleDeclarationList);
289
+ }
290
+
291
+ if (
292
+ removeMatchedSelectors &&
293
+ matchedElements.length !== 0 &&
294
+ selector.rule.prelude.type === 'SelectorList'
295
+ ) {
296
+ // clean up matching simple selectors if option removeMatchedSelectors is enabled
297
+ selector.rule.prelude.children.remove(selector.item);
298
+ }
299
+ selector.matchedElements = matchedElements;
300
+ }
301
+
302
+ // no further processing required
303
+ if (removeMatchedSelectors === false) {
304
+ return;
305
+ }
306
+
307
+ // clean up matched class + ID attribute values
308
+ for (const selector of sortedSelectors) {
309
+ if (selector.matchedElements == null) {
310
+ continue;
311
+ }
312
+
313
+ if (onlyMatchedOnce && selector.matchedElements.length > 1) {
314
+ // skip selectors that match more than once if option onlyMatchedOnce is enabled
315
+ continue;
316
+ }
317
+
318
+ for (const selectedEl of selector.matchedElements) {
319
+ // class
320
+ const classList = new Set(
321
+ selectedEl.attributes.class == null
322
+ ? null
323
+ : selectedEl.attributes.class.split(' ')
324
+ );
325
+ const firstSubSelector = selector.node.children.first();
326
+ if (
327
+ firstSubSelector != null &&
328
+ firstSubSelector.type === 'ClassSelector'
329
+ ) {
330
+ classList.delete(firstSubSelector.name);
331
+ }
332
+ if (classList.size === 0) {
333
+ delete selectedEl.attributes.class;
334
+ } else {
335
+ selectedEl.attributes.class = Array.from(classList).join(' ');
336
+ }
337
+
338
+ // ID
339
+ if (
340
+ firstSubSelector != null &&
341
+ firstSubSelector.type === 'IdSelector'
342
+ ) {
343
+ if (selectedEl.attributes.id === firstSubSelector.name) {
344
+ delete selectedEl.attributes.id;
345
+ }
346
+ }
347
+ }
348
+ }
349
+
350
+ for (const style of styles) {
351
+ csstree.walk(style.cssAst, {
352
+ visit: 'Rule',
353
+ enter: function (node, item, list) {
354
+ // clean up <style/> rulesets without any css selectors left
355
+ if (
356
+ node.type === 'Rule' &&
357
+ node.prelude.type === 'SelectorList' &&
358
+ node.prelude.children.isEmpty()
359
+ ) {
360
+ list.remove(item);
361
+ }
362
+ },
363
+ });
364
+
365
+ if (style.cssAst.children.isEmpty()) {
366
+ // remove emtpy style element
367
+ detachNodeFromParent(style.node, style.parentNode);
368
+ } else {
369
+ // update style element if any styles left
370
+ const firstChild = style.node.children[0];
371
+ if (firstChild.type === 'text' || firstChild.type === 'cdata') {
372
+ firstChild.value = csstree.generate(style.cssAst);
373
+ }
374
+ }
375
+ }
376
+ },
377
+ },
378
+ };
379
+ };
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ const { detachNodeFromParent } = require('../lib/xast.js');
4
+ const { collectStylesheet, computeStyle } = require('../lib/style.js');
5
+ const { path2js, js2path, intersects } = require('./_path.js');
6
+
7
+ exports.type = 'visitor';
8
+ exports.name = 'mergePaths';
9
+ exports.active = true;
10
+ exports.description = 'merges multiple paths in one if possible';
11
+
12
+ /**
13
+ * Merge multiple Paths into one.
14
+ *
15
+ * @author Kir Belevich, Lev Solntsev
16
+ *
17
+ * @type {import('../lib/types').Plugin<{
18
+ * force?: boolean,
19
+ * floatPrecision?: number,
20
+ * noSpaceAfterFlags?: boolean
21
+ * }>}
22
+ */
23
+ exports.fn = (root, params) => {
24
+ const {
25
+ force = false,
26
+ floatPrecision,
27
+ noSpaceAfterFlags = false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20
28
+ } = params;
29
+ const stylesheet = collectStylesheet(root);
30
+
31
+ return {
32
+ element: {
33
+ enter: (node) => {
34
+ let prevChild = null;
35
+
36
+ for (const child of node.children) {
37
+ // skip if previous element is not path or contains animation elements
38
+ if (
39
+ prevChild == null ||
40
+ prevChild.type !== 'element' ||
41
+ prevChild.name !== 'path' ||
42
+ prevChild.children.length !== 0 ||
43
+ prevChild.attributes.d == null
44
+ ) {
45
+ prevChild = child;
46
+ continue;
47
+ }
48
+
49
+ // skip if element is not path or contains animation elements
50
+ if (
51
+ child.type !== 'element' ||
52
+ child.name !== 'path' ||
53
+ child.children.length !== 0 ||
54
+ child.attributes.d == null
55
+ ) {
56
+ prevChild = child;
57
+ continue;
58
+ }
59
+
60
+ // preserve paths with markers
61
+ const computedStyle = computeStyle(stylesheet, child);
62
+ if (
63
+ computedStyle['marker-start'] ||
64
+ computedStyle['marker-mid'] ||
65
+ computedStyle['marker-end']
66
+ ) {
67
+ prevChild = child;
68
+ continue;
69
+ }
70
+
71
+ const prevChildAttrs = Object.keys(prevChild.attributes);
72
+ const childAttrs = Object.keys(child.attributes);
73
+ let attributesAreEqual = prevChildAttrs.length === childAttrs.length;
74
+ for (const name of childAttrs) {
75
+ if (name !== 'd') {
76
+ if (
77
+ prevChild.attributes[name] == null ||
78
+ prevChild.attributes[name] !== child.attributes[name]
79
+ ) {
80
+ attributesAreEqual = false;
81
+ }
82
+ }
83
+ }
84
+ const prevPathJS = path2js(prevChild);
85
+ const curPathJS = path2js(child);
86
+
87
+ if (
88
+ attributesAreEqual &&
89
+ (force || !intersects(prevPathJS, curPathJS))
90
+ ) {
91
+ js2path(prevChild, prevPathJS.concat(curPathJS), {
92
+ floatPrecision,
93
+ noSpaceAfterFlags,
94
+ });
95
+ detachNodeFromParent(child, node);
96
+ continue;
97
+ }
98
+
99
+ prevChild = child;
100
+ }
101
+ },
102
+ },
103
+ };
104
+ };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @typedef {import('../lib/types').XastElement} XastElement
5
+ */
6
+
7
+ const { visitSkip, detachNodeFromParent } = require('../lib/xast.js');
8
+ const JSAPI = require('../lib/svgo/jsAPI.js');
9
+
10
+ exports.name = 'mergeStyles';
11
+ exports.type = 'visitor';
12
+ exports.active = true;
13
+ exports.description = 'merge multiple style elements into one';
14
+
15
+ /**
16
+ * Merge multiple style elements into one.
17
+ *
18
+ * @author strarsis <strarsis@gmail.com>
19
+ *
20
+ * @type {import('../lib/types').Plugin<void>}
21
+ */
22
+ exports.fn = () => {
23
+ /**
24
+ * @type {null | XastElement}
25
+ */
26
+ let firstStyleElement = null;
27
+ let collectedStyles = '';
28
+ let styleContentType = 'text';
29
+
30
+ return {
31
+ element: {
32
+ enter: (node, parentNode) => {
33
+ // skip <foreignObject> content
34
+ if (node.name === 'foreignObject') {
35
+ return visitSkip;
36
+ }
37
+
38
+ // collect style elements
39
+ if (node.name !== 'style') {
40
+ return;
41
+ }
42
+
43
+ // skip <style> with invalid type attribute
44
+ if (
45
+ node.attributes.type != null &&
46
+ node.attributes.type !== '' &&
47
+ node.attributes.type !== 'text/css'
48
+ ) {
49
+ return;
50
+ }
51
+
52
+ // extract style element content
53
+ let css = '';
54
+ for (const child of node.children) {
55
+ if (child.type === 'text') {
56
+ css += child.value;
57
+ }
58
+ if (child.type === 'cdata') {
59
+ styleContentType = 'cdata';
60
+ css += child.value;
61
+ }
62
+ }
63
+
64
+ // remove empty style elements
65
+ if (css.trim().length === 0) {
66
+ detachNodeFromParent(node, parentNode);
67
+ return;
68
+ }
69
+
70
+ // collect css and wrap with media query if present in attribute
71
+ if (node.attributes.media == null) {
72
+ collectedStyles += css;
73
+ } else {
74
+ collectedStyles += `@media ${node.attributes.media}{${css}}`;
75
+ delete node.attributes.media;
76
+ }
77
+
78
+ // combine collected styles in the first style element
79
+ if (firstStyleElement == null) {
80
+ firstStyleElement = node;
81
+ } else {
82
+ detachNodeFromParent(node, parentNode);
83
+ firstStyleElement.children = [
84
+ new JSAPI(
85
+ { type: styleContentType, value: collectedStyles },
86
+ firstStyleElement
87
+ ),
88
+ ];
89
+ }
90
+ },
91
+ },
92
+ };
93
+ };