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,239 @@
1
+ 'use strict';
2
+
3
+ var csstree = require('css-tree-v2'),
4
+ List = csstree.List,
5
+ stable = require('stable'),
6
+ specificity = require('csso/lib/restructure/prepare/specificity');
7
+
8
+ /**
9
+ * Flatten a CSS AST to a selectors list.
10
+ *
11
+ * @param {import('css-tree-v2').CssNode} cssAst css-tree-v2 AST to flatten
12
+ * @return {Array} selectors
13
+ */
14
+ function flattenToSelectors(cssAst) {
15
+ var selectors = [];
16
+
17
+ csstree.walk(cssAst, {
18
+ visit: 'Rule',
19
+ enter: function (node) {
20
+ if (node.type !== 'Rule') {
21
+ return;
22
+ }
23
+
24
+ var atrule = this.atrule;
25
+ var rule = node;
26
+
27
+ node.prelude.children.each(function (selectorNode, selectorItem) {
28
+ var selector = {
29
+ item: selectorItem,
30
+ atrule: atrule,
31
+ rule: rule,
32
+ pseudos: /** @type {{item: any; list: any[]}[]} */ ([]),
33
+ };
34
+
35
+ selectorNode.children.each(function (
36
+ selectorChildNode,
37
+ selectorChildItem,
38
+ selectorChildList
39
+ ) {
40
+ if (
41
+ selectorChildNode.type === 'PseudoClassSelector' ||
42
+ selectorChildNode.type === 'PseudoElementSelector'
43
+ ) {
44
+ selector.pseudos.push({
45
+ item: selectorChildItem,
46
+ list: selectorChildList,
47
+ });
48
+ }
49
+ });
50
+
51
+ selectors.push(selector);
52
+ });
53
+ },
54
+ });
55
+
56
+ return selectors;
57
+ }
58
+
59
+ /**
60
+ * Filter selectors by Media Query.
61
+ *
62
+ * @param {Array} selectors to filter
63
+ * @param {Array} useMqs Array with strings of media queries that should pass (<name> <expression>)
64
+ * @return {Array} Filtered selectors that match the passed media queries
65
+ */
66
+ function filterByMqs(selectors, useMqs) {
67
+ return selectors.filter(function (selector) {
68
+ if (selector.atrule === null) {
69
+ return ~useMqs.indexOf('');
70
+ }
71
+
72
+ var mqName = selector.atrule.name;
73
+ var mqStr = mqName;
74
+ if (
75
+ selector.atrule.expression &&
76
+ selector.atrule.expression.children.first().type === 'MediaQueryList'
77
+ ) {
78
+ var mqExpr = csstree.generate(selector.atrule.expression);
79
+ mqStr = [mqName, mqExpr].join(' ');
80
+ }
81
+
82
+ return ~useMqs.indexOf(mqStr);
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Filter selectors by the pseudo-elements and/or -classes they contain.
88
+ *
89
+ * @param {Array} selectors to filter
90
+ * @param {Array} usePseudos Array with strings of single or sequence of pseudo-elements and/or -classes that should pass
91
+ * @return {Array} Filtered selectors that match the passed pseudo-elements and/or -classes
92
+ */
93
+ function filterByPseudos(selectors, usePseudos) {
94
+ return selectors.filter(function (selector) {
95
+ var pseudoSelectorsStr = csstree.generate({
96
+ type: 'Selector',
97
+ children: new List().fromArray(
98
+ selector.pseudos.map(function (pseudo) {
99
+ return pseudo.item.data;
100
+ })
101
+ ),
102
+ });
103
+ return ~usePseudos.indexOf(pseudoSelectorsStr);
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Remove pseudo-elements and/or -classes from the selectors for proper matching.
109
+ *
110
+ * @param {Array} selectors to clean
111
+ * @return {void}
112
+ */
113
+ function cleanPseudos(selectors) {
114
+ selectors.forEach(function (selector) {
115
+ selector.pseudos.forEach(function (pseudo) {
116
+ pseudo.list.remove(pseudo.item);
117
+ });
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Compares two selector specificities.
123
+ * extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211
124
+ *
125
+ * @param {Array} aSpecificity Specificity of selector A
126
+ * @param {Array} bSpecificity Specificity of selector B
127
+ * @return {number} Score of selector specificity A compared to selector specificity B
128
+ */
129
+ function compareSpecificity(aSpecificity, bSpecificity) {
130
+ for (var i = 0; i < 4; i += 1) {
131
+ if (aSpecificity[i] < bSpecificity[i]) {
132
+ return -1;
133
+ } else if (aSpecificity[i] > bSpecificity[i]) {
134
+ return 1;
135
+ }
136
+ }
137
+
138
+ return 0;
139
+ }
140
+
141
+ /**
142
+ * Compare two simple selectors.
143
+ *
144
+ * @param {Object} aSimpleSelectorNode Simple selector A
145
+ * @param {Object} bSimpleSelectorNode Simple selector B
146
+ * @return {number} Score of selector A compared to selector B
147
+ */
148
+ function compareSimpleSelectorNode(aSimpleSelectorNode, bSimpleSelectorNode) {
149
+ var aSpecificity = specificity(aSimpleSelectorNode),
150
+ bSpecificity = specificity(bSimpleSelectorNode);
151
+ return compareSpecificity(aSpecificity, bSpecificity);
152
+ }
153
+
154
+ function _bySelectorSpecificity(selectorA, selectorB) {
155
+ return compareSimpleSelectorNode(selectorA.item.data, selectorB.item.data);
156
+ }
157
+
158
+ /**
159
+ * Sort selectors stably by their specificity.
160
+ *
161
+ * @param {Array} selectors to be sorted
162
+ * @return {Array} Stable sorted selectors
163
+ */
164
+ function sortSelectors(selectors) {
165
+ return stable(selectors, _bySelectorSpecificity);
166
+ }
167
+
168
+ /**
169
+ * Convert a css-tree-v2 AST style declaration to CSSStyleDeclaration property.
170
+ *
171
+ * @param {import('css-tree-v2').CssNode} declaration css-tree-v2 style declaration
172
+ * @return {Object} CSSStyleDeclaration property
173
+ */
174
+ function csstreeToStyleDeclaration(declaration) {
175
+ var propertyName = declaration.property,
176
+ propertyValue = csstree.generate(declaration.value),
177
+ propertyPriority = declaration.important ? 'important' : '';
178
+ return {
179
+ name: propertyName,
180
+ value: propertyValue,
181
+ priority: propertyPriority,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Gets the CSS string of a style element
187
+ *
188
+ * @param {Object} elem style element
189
+ * @return {string} CSS string or empty array if no styles are set
190
+ */
191
+ function getCssStr(elem) {
192
+ if (
193
+ elem.children.length > 0 &&
194
+ (elem.children[0].type === 'text' || elem.children[0].type === 'cdata')
195
+ ) {
196
+ return elem.children[0].value;
197
+ }
198
+ return '';
199
+ }
200
+
201
+ /**
202
+ * Sets the CSS string of a style element
203
+ *
204
+ * @param {Object} elem style element
205
+ * @param {string} css string to be set
206
+ * @return {string} reference to field with CSS
207
+ */
208
+ function setCssStr(elem, css) {
209
+ if (elem.children.length === 0) {
210
+ elem.children.push({
211
+ type: 'text',
212
+ value: '',
213
+ });
214
+ }
215
+
216
+ if (elem.children[0].type !== 'text' && elem.children[0].type !== 'cdata') {
217
+ return css;
218
+ }
219
+
220
+ elem.children[0].value = css;
221
+
222
+ return css;
223
+ }
224
+
225
+ module.exports.flattenToSelectors = flattenToSelectors;
226
+
227
+ module.exports.filterByMqs = filterByMqs;
228
+ module.exports.filterByPseudos = filterByPseudos;
229
+ module.exports.cleanPseudos = cleanPseudos;
230
+
231
+ module.exports.compareSpecificity = compareSpecificity;
232
+ module.exports.compareSimpleSelectorNode = compareSimpleSelectorNode;
233
+
234
+ module.exports.sortSelectors = sortSelectors;
235
+
236
+ module.exports.csstreeToStyleDeclaration = csstreeToStyleDeclaration;
237
+
238
+ module.exports.getCssStr = getCssStr;
239
+ module.exports.setCssStr = setCssStr;
package/lib/parser.js ADDED
@@ -0,0 +1,259 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @typedef {import('./types').XastNode} XastNode
5
+ * @typedef {import('./types').XastInstruction} XastInstruction
6
+ * @typedef {import('./types').XastDoctype} XastDoctype
7
+ * @typedef {import('./types').XastComment} XastComment
8
+ * @typedef {import('./types').XastRoot} XastRoot
9
+ * @typedef {import('./types').XastElement} XastElement
10
+ * @typedef {import('./types').XastCdata} XastCdata
11
+ * @typedef {import('./types').XastText} XastText
12
+ * @typedef {import('./types').XastParent} XastParent
13
+ */
14
+
15
+ // @ts-ignore sax will be replaced with something else later
16
+ const SAX = require('@trysound/sax');
17
+ const JSAPI = require('./svgo/jsAPI.js');
18
+ const { textElems } = require('../plugins/_collections.js');
19
+
20
+ class SvgoParserError extends Error {
21
+ /**
22
+ * @param message {string}
23
+ * @param line {number}
24
+ * @param column {number}
25
+ * @param source {string}
26
+ * @param file {void | string}
27
+ */
28
+ constructor(message, line, column, source, file) {
29
+ super(message);
30
+ this.name = 'SvgoParserError';
31
+ this.message = `${file || '<input>'}:${line}:${column}: ${message}`;
32
+ this.reason = message;
33
+ this.line = line;
34
+ this.column = column;
35
+ this.source = source;
36
+ if (Error.captureStackTrace) {
37
+ Error.captureStackTrace(this, SvgoParserError);
38
+ }
39
+ }
40
+ toString() {
41
+ const lines = this.source.split(/\r?\n/);
42
+ const startLine = Math.max(this.line - 3, 0);
43
+ const endLine = Math.min(this.line + 2, lines.length);
44
+ const lineNumberWidth = String(endLine).length;
45
+ const startColumn = Math.max(this.column - 54, 0);
46
+ const endColumn = Math.max(this.column + 20, 80);
47
+ const code = lines
48
+ .slice(startLine, endLine)
49
+ .map((line, index) => {
50
+ const lineSlice = line.slice(startColumn, endColumn);
51
+ let ellipsisPrefix = '';
52
+ let ellipsisSuffix = '';
53
+ if (startColumn !== 0) {
54
+ ellipsisPrefix = startColumn > line.length - 1 ? ' ' : '…';
55
+ }
56
+ if (endColumn < line.length - 1) {
57
+ ellipsisSuffix = '…';
58
+ }
59
+ const number = startLine + 1 + index;
60
+ const gutter = ` ${number.toString().padStart(lineNumberWidth)} | `;
61
+ if (number === this.line) {
62
+ const gutterSpacing = gutter.replace(/[^|]/g, ' ');
63
+ const lineSpacing = (
64
+ ellipsisPrefix + line.slice(startColumn, this.column - 1)
65
+ ).replace(/[^\t]/g, ' ');
66
+ const spacing = gutterSpacing + lineSpacing;
67
+ return `>${gutter}${ellipsisPrefix}${lineSlice}${ellipsisSuffix}\n ${spacing}^`;
68
+ }
69
+ return ` ${gutter}${ellipsisPrefix}${lineSlice}${ellipsisSuffix}`;
70
+ })
71
+ .join('\n');
72
+ return `${this.name}: ${this.message}\n\n${code}\n`;
73
+ }
74
+ }
75
+
76
+ const entityDeclaration = /<!ENTITY\s+(\S+)\s+(?:'([^']+)'|"([^"]+)")\s*>/g;
77
+
78
+ const config = {
79
+ strict: true,
80
+ trim: false,
81
+ normalize: false,
82
+ lowercase: true,
83
+ xmlns: true,
84
+ position: true,
85
+ };
86
+
87
+ /**
88
+ * Convert SVG (XML) string to SVG-as-JS object.
89
+ *
90
+ * @type {(data: string, from?: string) => XastRoot}
91
+ */
92
+ const parseSvg = (data, from) => {
93
+ const sax = SAX.parser(config.strict, config);
94
+ /**
95
+ * @type {XastRoot}
96
+ */
97
+ const root = new JSAPI({ type: 'root', children: [] });
98
+ /**
99
+ * @type {XastParent}
100
+ */
101
+ let current = root;
102
+ /**
103
+ * @type {Array<XastParent>}
104
+ */
105
+ const stack = [root];
106
+
107
+ /**
108
+ * @type {<T extends XastNode>(node: T) => T}
109
+ */
110
+ const pushToContent = (node) => {
111
+ const wrapped = new JSAPI(node, current);
112
+ current.children.push(wrapped);
113
+ return wrapped;
114
+ };
115
+
116
+ /**
117
+ * @type {(doctype: string) => void}
118
+ */
119
+ sax.ondoctype = (doctype) => {
120
+ /**
121
+ * @type {XastDoctype}
122
+ */
123
+ const node = {
124
+ type: 'doctype',
125
+ // TODO parse doctype for name, public and system to match xast
126
+ name: 'svg',
127
+ data: {
128
+ doctype,
129
+ },
130
+ };
131
+ pushToContent(node);
132
+ const subsetStart = doctype.indexOf('[');
133
+ if (subsetStart >= 0) {
134
+ entityDeclaration.lastIndex = subsetStart;
135
+ let entityMatch = entityDeclaration.exec(data);
136
+ while (entityMatch != null) {
137
+ sax.ENTITIES[entityMatch[1]] = entityMatch[2] || entityMatch[3];
138
+ entityMatch = entityDeclaration.exec(data);
139
+ }
140
+ }
141
+ };
142
+
143
+ /**
144
+ * @type {(data: { name: string, body: string }) => void}
145
+ */
146
+ sax.onprocessinginstruction = (data) => {
147
+ /**
148
+ * @type {XastInstruction}
149
+ */
150
+ const node = {
151
+ type: 'instruction',
152
+ name: data.name,
153
+ value: data.body,
154
+ };
155
+ pushToContent(node);
156
+ };
157
+
158
+ /**
159
+ * @type {(comment: string) => void}
160
+ */
161
+ sax.oncomment = (comment) => {
162
+ /**
163
+ * @type {XastComment}
164
+ */
165
+ const node = {
166
+ type: 'comment',
167
+ value: comment.trim(),
168
+ };
169
+ pushToContent(node);
170
+ };
171
+
172
+ /**
173
+ * @type {(cdata: string) => void}
174
+ */
175
+ sax.oncdata = (cdata) => {
176
+ /**
177
+ * @type {XastCdata}
178
+ */
179
+ const node = {
180
+ type: 'cdata',
181
+ value: cdata,
182
+ };
183
+ pushToContent(node);
184
+ };
185
+
186
+ /**
187
+ * @type {(data: { name: string, attributes: Record<string, { value: string }>}) => void}
188
+ */
189
+ sax.onopentag = (data) => {
190
+ /**
191
+ * @type {XastElement}
192
+ */
193
+ let element = {
194
+ type: 'element',
195
+ name: data.name,
196
+ attributes: {},
197
+ children: [],
198
+ };
199
+ for (const [name, attr] of Object.entries(data.attributes)) {
200
+ element.attributes[name] = attr.value;
201
+ }
202
+ element = pushToContent(element);
203
+ current = element;
204
+ stack.push(element);
205
+ };
206
+
207
+ /**
208
+ * @type {(text: string) => void}
209
+ */
210
+ sax.ontext = (text) => {
211
+ if (current.type === 'element') {
212
+ // prevent trimming of meaningful whitespace inside textual tags
213
+ if (textElems.includes(current.name)) {
214
+ /**
215
+ * @type {XastText}
216
+ */
217
+ const node = {
218
+ type: 'text',
219
+ value: text,
220
+ };
221
+ pushToContent(node);
222
+ } else if (/\S/.test(text)) {
223
+ /**
224
+ * @type {XastText}
225
+ */
226
+ const node = {
227
+ type: 'text',
228
+ value: text.trim(),
229
+ };
230
+ pushToContent(node);
231
+ }
232
+ }
233
+ };
234
+
235
+ sax.onclosetag = () => {
236
+ stack.pop();
237
+ current = stack[stack.length - 1];
238
+ };
239
+
240
+ /**
241
+ * @type {(e: any) => void}
242
+ */
243
+ sax.onerror = (e) => {
244
+ const error = new SvgoParserError(
245
+ e.reason,
246
+ e.line + 1,
247
+ e.column,
248
+ data,
249
+ from
250
+ );
251
+ if (e.message.indexOf('Unexpected end') === -1) {
252
+ throw error;
253
+ }
254
+ };
255
+
256
+ sax.write(data).close();
257
+ return root;
258
+ };
259
+ exports.parseSvg = parseSvg;