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,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
|
+
};
|