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,443 @@
1
+ 'use strict';
2
+
3
+ const { selectAll, selectOne, is } = require('css-select');
4
+ const svgoCssSelectAdapter = require('./css-select-adapter');
5
+ const CSSClassList = require('./css-class-list');
6
+ const CSSStyleDeclaration = require('./css-style-declaration');
7
+
8
+ /**
9
+ * @type {(name: string) => { prefix: string, local: string }}
10
+ */
11
+ const parseName = (name) => {
12
+ if (name == null) {
13
+ return {
14
+ prefix: '',
15
+ local: '',
16
+ };
17
+ }
18
+ if (name === 'xmlns') {
19
+ return {
20
+ prefix: 'xmlns',
21
+ local: '',
22
+ };
23
+ }
24
+ const chunks = name.split(':');
25
+ if (chunks.length === 1) {
26
+ return {
27
+ prefix: '',
28
+ local: chunks[0],
29
+ };
30
+ }
31
+ return {
32
+ prefix: chunks[0],
33
+ local: chunks[1],
34
+ };
35
+ };
36
+
37
+ var cssSelectOpts = {
38
+ xmlMode: true,
39
+ adapter: svgoCssSelectAdapter,
40
+ };
41
+
42
+ const attrsHandler = {
43
+ get: (attributes, name) => {
44
+ // eslint-disable-next-line no-prototype-builtins
45
+ if (attributes.hasOwnProperty(name)) {
46
+ return {
47
+ name,
48
+ get value() {
49
+ return attributes[name];
50
+ },
51
+ set value(value) {
52
+ attributes[name] = value;
53
+ },
54
+ };
55
+ }
56
+ },
57
+ set: (attributes, name, attr) => {
58
+ attributes[name] = attr.value;
59
+ return true;
60
+ },
61
+ };
62
+
63
+ var JSAPI = function (data, parentNode) {
64
+ Object.assign(this, data);
65
+ if (this.type === 'element') {
66
+ if (this.attributes == null) {
67
+ this.attributes = {};
68
+ }
69
+ if (this.children == null) {
70
+ this.children = [];
71
+ }
72
+ Object.defineProperty(this, 'class', {
73
+ writable: true,
74
+ configurable: true,
75
+ value: new CSSClassList(this),
76
+ });
77
+ Object.defineProperty(this, 'style', {
78
+ writable: true,
79
+ configurable: true,
80
+ value: new CSSStyleDeclaration(this),
81
+ });
82
+ Object.defineProperty(this, 'parentNode', {
83
+ writable: true,
84
+ value: parentNode,
85
+ });
86
+
87
+ // temporary attrs polyfill
88
+ // TODO remove after migration
89
+ const element = this;
90
+ Object.defineProperty(this, 'attrs', {
91
+ configurable: true,
92
+ get() {
93
+ return new Proxy(element.attributes, attrsHandler);
94
+ },
95
+ set(value) {
96
+ const newAttributes = {};
97
+ for (const attr of Object.values(value)) {
98
+ newAttributes[attr.name] = attr.value;
99
+ }
100
+ element.attributes = newAttributes;
101
+ },
102
+ });
103
+ }
104
+ };
105
+ module.exports = JSAPI;
106
+
107
+ /**
108
+ * Perform a deep clone of this node.
109
+ *
110
+ * @return {Object} element
111
+ */
112
+ JSAPI.prototype.clone = function () {
113
+ const { children, ...nodeData } = this;
114
+ // Deep-clone node data.
115
+ const clonedNode = new JSAPI(JSON.parse(JSON.stringify(nodeData)), null);
116
+ if (children) {
117
+ clonedNode.children = children.map((child) => {
118
+ const clonedChild = child.clone();
119
+ clonedChild.parentNode = clonedNode;
120
+ return clonedChild;
121
+ });
122
+ }
123
+ return clonedNode;
124
+ };
125
+
126
+ /**
127
+ * Determine if item is an element
128
+ * (any, with a specific name or in a names array).
129
+ *
130
+ * @param {String|Array} [param] element name or names arrays
131
+ * @return {Boolean}
132
+ */
133
+ JSAPI.prototype.isElem = function (param) {
134
+ if (this.type !== 'element') {
135
+ return false;
136
+ }
137
+ if (param == null) {
138
+ return true;
139
+ }
140
+ if (Array.isArray(param)) {
141
+ return param.includes(this.name);
142
+ }
143
+ return this.name === param;
144
+ };
145
+
146
+ /**
147
+ * Renames an element
148
+ *
149
+ * @param {String} name new element name
150
+ * @return {Object} element
151
+ */
152
+ JSAPI.prototype.renameElem = function (name) {
153
+ if (name && typeof name === 'string') this.name = name;
154
+
155
+ return this;
156
+ };
157
+
158
+ /**
159
+ * Determine if element is empty.
160
+ *
161
+ * @return {Boolean}
162
+ */
163
+ JSAPI.prototype.isEmpty = function () {
164
+ return !this.children || !this.children.length;
165
+ };
166
+
167
+ /**
168
+ * Find the closest ancestor of the current element.
169
+ * @param elemName
170
+ *
171
+ * @return {?Object}
172
+ */
173
+ JSAPI.prototype.closestElem = function (elemName) {
174
+ var elem = this;
175
+
176
+ while ((elem = elem.parentNode) && !elem.isElem(elemName));
177
+
178
+ return elem;
179
+ };
180
+
181
+ /**
182
+ * Changes children by removing elements and/or adding new elements.
183
+ *
184
+ * @param {Number} start Index at which to start changing the children.
185
+ * @param {Number} n Number of elements to remove.
186
+ * @param {Array|Object} [insertion] Elements to add to the children.
187
+ * @return {Array} Removed elements.
188
+ */
189
+ JSAPI.prototype.spliceContent = function (start, n, insertion) {
190
+ if (arguments.length < 2) return [];
191
+
192
+ if (!Array.isArray(insertion))
193
+ insertion = Array.apply(null, arguments).slice(2);
194
+
195
+ insertion.forEach(function (inner) {
196
+ inner.parentNode = this;
197
+ }, this);
198
+
199
+ return this.children.splice.apply(
200
+ this.children,
201
+ [start, n].concat(insertion)
202
+ );
203
+ };
204
+
205
+ /**
206
+ * Determine if element has an attribute
207
+ * (any, or by name or by name + value).
208
+ *
209
+ * @param {String} [name] attribute name
210
+ * @param {String} [val] attribute value (will be toString()'ed)
211
+ * @return {Boolean}
212
+ */
213
+ JSAPI.prototype.hasAttr = function (name, val) {
214
+ if (this.type !== 'element') {
215
+ return false;
216
+ }
217
+ if (Object.keys(this.attributes).length === 0) {
218
+ return false;
219
+ }
220
+ if (name == null) {
221
+ return true;
222
+ }
223
+ // eslint-disable-next-line no-prototype-builtins
224
+ if (this.attributes.hasOwnProperty(name) === false) {
225
+ return false;
226
+ }
227
+ if (val !== undefined) {
228
+ return this.attributes[name] === val.toString();
229
+ }
230
+ return true;
231
+ };
232
+
233
+ /**
234
+ * Determine if element has an attribute by local name
235
+ * (any, or by name or by name + value).
236
+ *
237
+ * @param {String} [localName] local attribute name
238
+ * @param {Number|String|RegExp|Function} [val] attribute value (will be toString()'ed or executed, otherwise ignored)
239
+ * @return {Boolean}
240
+ */
241
+ JSAPI.prototype.hasAttrLocal = function (localName, val) {
242
+ if (!this.attrs || !Object.keys(this.attrs).length) return false;
243
+
244
+ if (!arguments.length) return !!this.attrs;
245
+
246
+ var callback;
247
+
248
+ switch (val != null && val.constructor && val.constructor.name) {
249
+ case 'Number': // same as String
250
+ case 'String':
251
+ callback = stringValueTest;
252
+ break;
253
+ case 'RegExp':
254
+ callback = regexpValueTest;
255
+ break;
256
+ case 'Function':
257
+ callback = funcValueTest;
258
+ break;
259
+ default:
260
+ callback = nameTest;
261
+ }
262
+ return this.someAttr(callback);
263
+
264
+ function nameTest(attr) {
265
+ const { local } = parseName(attr.name);
266
+ return local === localName;
267
+ }
268
+
269
+ function stringValueTest(attr) {
270
+ const { local } = parseName(attr.name);
271
+ return local === localName && val == attr.value;
272
+ }
273
+
274
+ function regexpValueTest(attr) {
275
+ const { local } = parseName(attr.name);
276
+ return local === localName && val.test(attr.value);
277
+ }
278
+
279
+ function funcValueTest(attr) {
280
+ const { local } = parseName(attr.name);
281
+ return local === localName && val(attr.value);
282
+ }
283
+ };
284
+
285
+ /**
286
+ * Get a specific attribute from an element
287
+ * (by name or name + value).
288
+ *
289
+ * @param {String} name attribute name
290
+ * @param {String} [val] attribute value (will be toString()'ed)
291
+ * @return {Object|Undefined}
292
+ */
293
+ JSAPI.prototype.attr = function (name, val) {
294
+ if (this.hasAttr(name, val)) {
295
+ return this.attrs[name];
296
+ }
297
+ };
298
+
299
+ /**
300
+ * Get computed attribute value from an element
301
+ *
302
+ * @param {String} name attribute name
303
+ * @return {Object|Undefined}
304
+ */
305
+ JSAPI.prototype.computedAttr = function (name, val) {
306
+ if (!arguments.length) return;
307
+
308
+ for (
309
+ var elem = this;
310
+ elem && (!elem.hasAttr(name) || !elem.attributes[name]);
311
+ elem = elem.parentNode
312
+ );
313
+
314
+ if (val != null) {
315
+ return elem ? elem.hasAttr(name, val) : false;
316
+ } else if (elem && elem.hasAttr(name)) {
317
+ return elem.attributes[name];
318
+ }
319
+ };
320
+
321
+ /**
322
+ * Remove a specific attribute.
323
+ *
324
+ * @param {String|Array} name attribute name
325
+ * @param {String} [val] attribute value
326
+ * @return {Boolean}
327
+ */
328
+ JSAPI.prototype.removeAttr = function (name, val) {
329
+ if (this.type !== 'element') {
330
+ return false;
331
+ }
332
+ if (arguments.length === 0) {
333
+ return false;
334
+ }
335
+ if (Array.isArray(name)) {
336
+ for (const nameItem of name) {
337
+ this.removeAttr(nameItem, val);
338
+ }
339
+ return false;
340
+ }
341
+ if (this.hasAttr(name, val) === false) {
342
+ return false;
343
+ }
344
+ delete this.attributes[name];
345
+ return true;
346
+ };
347
+
348
+ /**
349
+ * Add attribute.
350
+ *
351
+ * @param {Object} [attr={}] attribute object
352
+ * @return {Object|Boolean} created attribute or false if no attr was passed in
353
+ */
354
+ JSAPI.prototype.addAttr = function (attr) {
355
+ attr = attr || {};
356
+
357
+ if (attr.name === undefined) return false;
358
+
359
+ this.attributes[attr.name] = attr.value;
360
+
361
+ if (attr.name === 'class') {
362
+ // newly added class attribute
363
+ this.class.addClassValueHandler();
364
+ }
365
+
366
+ if (attr.name === 'style') {
367
+ // newly added style attribute
368
+ this.style.addStyleValueHandler();
369
+ }
370
+
371
+ return this.attrs[attr.name];
372
+ };
373
+
374
+ /**
375
+ * Iterates over all attributes.
376
+ *
377
+ * @param {Function} callback callback
378
+ * @param {Object} [context] callback context
379
+ * @return {Boolean} false if there are no any attributes
380
+ */
381
+ JSAPI.prototype.eachAttr = function (callback, context) {
382
+ if (this.type !== 'element') {
383
+ return false;
384
+ }
385
+ if (callback == null) {
386
+ return false;
387
+ }
388
+ for (const attr of Object.values(this.attrs)) {
389
+ callback.call(context, attr);
390
+ }
391
+ return true;
392
+ };
393
+
394
+ /**
395
+ * Tests whether some attribute passes the test.
396
+ *
397
+ * @param {Function} callback callback
398
+ * @param {Object} [context] callback context
399
+ * @return {Boolean} false if there are no any attributes
400
+ */
401
+ JSAPI.prototype.someAttr = function (callback, context) {
402
+ if (this.type !== 'element') {
403
+ return false;
404
+ }
405
+
406
+ for (const attr of Object.values(this.attrs)) {
407
+ if (callback.call(context, attr)) return true;
408
+ }
409
+
410
+ return false;
411
+ };
412
+
413
+ /**
414
+ * Evaluate a string of CSS selectors against the element and returns matched elements.
415
+ *
416
+ * @param {String} selectors CSS selector(s) string
417
+ * @return {Array} null if no elements matched
418
+ */
419
+ JSAPI.prototype.querySelectorAll = function (selectors) {
420
+ var matchedEls = selectAll(selectors, this, cssSelectOpts);
421
+
422
+ return matchedEls.length > 0 ? matchedEls : null;
423
+ };
424
+
425
+ /**
426
+ * Evaluate a string of CSS selectors against the element and returns only the first matched element.
427
+ *
428
+ * @param {String} selectors CSS selector(s) string
429
+ * @return {Array} null if no element matched
430
+ */
431
+ JSAPI.prototype.querySelector = function (selectors) {
432
+ return selectOne(selectors, this, cssSelectOpts);
433
+ };
434
+
435
+ /**
436
+ * Test if a selector matches a given element.
437
+ *
438
+ * @param {String} selector CSS selector string
439
+ * @return {Boolean} true if element would be selected by selector string, false if it does not
440
+ */
441
+ JSAPI.prototype.matches = function (selector) {
442
+ return is(this, selector, cssSelectOpts);
443
+ };
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ const { visit } = require('../xast.js');
4
+
5
+ /**
6
+ * Plugins engine.
7
+ *
8
+ * @module plugins
9
+ *
10
+ * @param {Object} ast input ast
11
+ * @param {Object} info extra information
12
+ * @param {Array} plugins plugins object from config
13
+ * @return {Object} output ast
14
+ */
15
+ const invokePlugins = (ast, info, plugins, overrides, globalOverrides) => {
16
+ for (const plugin of plugins) {
17
+ const override = overrides == null ? null : overrides[plugin.name];
18
+ if (override === false) {
19
+ continue;
20
+ }
21
+ const params = { ...plugin.params, ...globalOverrides, ...override };
22
+
23
+ if (plugin.type === 'perItem') {
24
+ ast = perItem(ast, info, plugin, params);
25
+ }
26
+ if (plugin.type === 'perItemReverse') {
27
+ ast = perItem(ast, info, plugin, params, true);
28
+ }
29
+ if (plugin.type === 'full') {
30
+ if (plugin.active) {
31
+ ast = plugin.fn(ast, params, info);
32
+ }
33
+ }
34
+ if (plugin.type === 'visitor') {
35
+ if (plugin.active) {
36
+ const visitor = plugin.fn(ast, params, info);
37
+ if (visitor != null) {
38
+ visit(ast, visitor);
39
+ }
40
+ }
41
+ }
42
+ }
43
+ return ast;
44
+ };
45
+ exports.invokePlugins = invokePlugins;
46
+
47
+ /**
48
+ * Direct or reverse per-item loop.
49
+ *
50
+ * @param {Object} data input data
51
+ * @param {Object} info extra information
52
+ * @param {Array} plugins plugins list to process
53
+ * @param {boolean} [reverse] reverse pass?
54
+ * @return {Object} output data
55
+ */
56
+ function perItem(data, info, plugin, params, reverse) {
57
+ function monkeys(items) {
58
+ items.children = items.children.filter(function (item) {
59
+ // reverse pass
60
+ if (reverse && item.children) {
61
+ monkeys(item);
62
+ }
63
+ // main filter
64
+ let kept = true;
65
+ if (plugin.active) {
66
+ kept = plugin.fn(item, params, info) !== false;
67
+ }
68
+ // direct pass
69
+ if (!reverse && item.children) {
70
+ monkeys(item);
71
+ }
72
+ return kept;
73
+ });
74
+ return items;
75
+ }
76
+ return monkeys(data);
77
+ }
78
+
79
+ const createPreset = ({ name, plugins }) => {
80
+ return {
81
+ name,
82
+ type: 'full',
83
+ fn: (ast, params, info) => {
84
+ const { floatPrecision, overrides } = params;
85
+ const globalOverrides = {};
86
+ if (floatPrecision != null) {
87
+ globalOverrides.floatPrecision = floatPrecision;
88
+ }
89
+ if (overrides) {
90
+ for (const [pluginName, override] of Object.entries(overrides)) {
91
+ if (override === true) {
92
+ console.warn(
93
+ `You are trying to enable ${pluginName} which is not part of preset.\n` +
94
+ `Try to put it before or after preset, for example\n\n` +
95
+ `plugins: [\n` +
96
+ ` {\n` +
97
+ ` name: 'preset-default',\n` +
98
+ ` },\n` +
99
+ ` 'cleanupListOfValues'\n` +
100
+ `]\n`
101
+ );
102
+ }
103
+ }
104
+ }
105
+ return invokePlugins(ast, info, plugins, overrides, globalOverrides);
106
+ },
107
+ };
108
+ };
109
+ exports.createPreset = createPreset;
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @typedef {import('../types').PathDataCommand} PathDataCommand
5
+ */
6
+
7
+ /**
8
+ * Encode plain SVG data string into Data URI string.
9
+ *
10
+ * @type {(str: string, type?: 'base64' | 'enc' | 'unenc') => string}
11
+ */
12
+ exports.encodeSVGDatauri = (str, type) => {
13
+ var prefix = 'data:image/svg+xml';
14
+ if (!type || type === 'base64') {
15
+ // base64
16
+ prefix += ';base64,';
17
+ str = prefix + Buffer.from(str).toString('base64');
18
+ } else if (type === 'enc') {
19
+ // URI encoded
20
+ str = prefix + ',' + encodeURIComponent(str);
21
+ } else if (type === 'unenc') {
22
+ // unencoded
23
+ str = prefix + ',' + str;
24
+ }
25
+ return str;
26
+ };
27
+
28
+ /**
29
+ * Decode SVG Data URI string into plain SVG string.
30
+ *
31
+ * @type {(str: string) => string}
32
+ */
33
+ exports.decodeSVGDatauri = (str) => {
34
+ var regexp = /data:image\/svg\+xml(;charset=[^;,]*)?(;base64)?,(.*)/;
35
+ var match = regexp.exec(str);
36
+
37
+ // plain string
38
+ if (!match) return str;
39
+
40
+ var data = match[3];
41
+
42
+ if (match[2]) {
43
+ // base64
44
+ str = Buffer.from(data, 'base64').toString('utf8');
45
+ } else if (data.charAt(0) === '%') {
46
+ // URI encoded
47
+ str = decodeURIComponent(data);
48
+ } else if (data.charAt(0) === '<') {
49
+ // unencoded
50
+ str = data;
51
+ }
52
+ return str;
53
+ };
54
+
55
+ /**
56
+ * @typedef {{
57
+ * noSpaceAfterFlags?: boolean,
58
+ * leadingZero?: boolean,
59
+ * negativeExtraSpace?: boolean
60
+ * }} CleanupOutDataParams
61
+ */
62
+
63
+ /**
64
+ * Convert a row of numbers to an optimized string view.
65
+ *
66
+ * @example
67
+ * [0, -1, .5, .5] → "0-1 .5.5"
68
+ *
69
+ * @type {(data: Array<number>, params: CleanupOutDataParams, command?: PathDataCommand) => string}
70
+ */
71
+ exports.cleanupOutData = (data, params, command) => {
72
+ let str = '';
73
+ let delimiter;
74
+ /**
75
+ * @type {number}
76
+ */
77
+ let prev;
78
+
79
+ data.forEach((item, i) => {
80
+ // space delimiter by default
81
+ delimiter = ' ';
82
+
83
+ // no extra space in front of first number
84
+ if (i == 0) delimiter = '';
85
+
86
+ // no extra space after 'arcto' command flags(large-arc and sweep flags)
87
+ // a20 60 45 0 1 30 20 → a20 60 45 0130 20
88
+ if (params.noSpaceAfterFlags && (command == 'A' || command == 'a')) {
89
+ var pos = i % 7;
90
+ if (pos == 4 || pos == 5) delimiter = '';
91
+ }
92
+
93
+ // remove floating-point numbers leading zeros
94
+ // 0.5 → .5
95
+ // -0.5 → -.5
96
+ const itemStr = params.leadingZero
97
+ ? removeLeadingZero(item)
98
+ : item.toString();
99
+
100
+ // no extra space in front of negative number or
101
+ // in front of a floating number if a previous number is floating too
102
+ if (
103
+ params.negativeExtraSpace &&
104
+ delimiter != '' &&
105
+ (item < 0 || (itemStr.charAt(0) === '.' && prev % 1 !== 0))
106
+ ) {
107
+ delimiter = '';
108
+ }
109
+ // save prev item value
110
+ prev = item;
111
+ str += delimiter + itemStr;
112
+ });
113
+ return str;
114
+ };
115
+
116
+ /**
117
+ * Remove floating-point numbers leading zero.
118
+ *
119
+ * @example
120
+ * 0.5 → .5
121
+ *
122
+ * @example
123
+ * -0.5 → -.5
124
+ *
125
+ * @type {(num: number) => string}
126
+ */
127
+ const removeLeadingZero = (num) => {
128
+ var strNum = num.toString();
129
+
130
+ if (0 < num && num < 1 && strNum.charAt(0) === '0') {
131
+ strNum = strNum.slice(1);
132
+ } else if (-1 < num && num < 0 && strNum.charAt(1) === '0') {
133
+ strNum = strNum.charAt(0) + strNum.slice(2);
134
+ }
135
+ return strNum;
136
+ };
137
+ exports.removeLeadingZero = removeLeadingZero;