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
package/lib/path.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('./types').PathDataItem} PathDataItem
|
|
5
|
+
* @typedef {import('./types').PathDataCommand} PathDataCommand
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF
|
|
9
|
+
|
|
10
|
+
const argsCountPerCommand = {
|
|
11
|
+
M: 2,
|
|
12
|
+
m: 2,
|
|
13
|
+
Z: 0,
|
|
14
|
+
z: 0,
|
|
15
|
+
L: 2,
|
|
16
|
+
l: 2,
|
|
17
|
+
H: 1,
|
|
18
|
+
h: 1,
|
|
19
|
+
V: 1,
|
|
20
|
+
v: 1,
|
|
21
|
+
C: 6,
|
|
22
|
+
c: 6,
|
|
23
|
+
S: 4,
|
|
24
|
+
s: 4,
|
|
25
|
+
Q: 4,
|
|
26
|
+
q: 4,
|
|
27
|
+
T: 2,
|
|
28
|
+
t: 2,
|
|
29
|
+
A: 7,
|
|
30
|
+
a: 7,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @type {(c: string) => c is PathDataCommand}
|
|
35
|
+
*/
|
|
36
|
+
const isCommand = (c) => {
|
|
37
|
+
return c in argsCountPerCommand;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @type {(c: string) => boolean}
|
|
42
|
+
*/
|
|
43
|
+
const isWsp = (c) => {
|
|
44
|
+
const codePoint = c.codePointAt(0);
|
|
45
|
+
return (
|
|
46
|
+
codePoint === 0x20 ||
|
|
47
|
+
codePoint === 0x9 ||
|
|
48
|
+
codePoint === 0xd ||
|
|
49
|
+
codePoint === 0xa
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @type {(c: string) => boolean}
|
|
55
|
+
*/
|
|
56
|
+
const isDigit = (c) => {
|
|
57
|
+
const codePoint = c.codePointAt(0);
|
|
58
|
+
if (codePoint == null) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return 48 <= codePoint && codePoint <= 57;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @typedef {'none' | 'sign' | 'whole' | 'decimal_point' | 'decimal' | 'e' | 'exponent_sign' | 'exponent'} ReadNumberState
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @type {(string: string, cursor: number) => [number, number | null]}
|
|
70
|
+
*/
|
|
71
|
+
const readNumber = (string, cursor) => {
|
|
72
|
+
let i = cursor;
|
|
73
|
+
let value = '';
|
|
74
|
+
let state = /** @type {ReadNumberState} */ ('none');
|
|
75
|
+
for (; i < string.length; i += 1) {
|
|
76
|
+
const c = string[i];
|
|
77
|
+
if (c === '+' || c === '-') {
|
|
78
|
+
if (state === 'none') {
|
|
79
|
+
state = 'sign';
|
|
80
|
+
value += c;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (state === 'e') {
|
|
84
|
+
state = 'exponent_sign';
|
|
85
|
+
value += c;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (isDigit(c)) {
|
|
90
|
+
if (state === 'none' || state === 'sign' || state === 'whole') {
|
|
91
|
+
state = 'whole';
|
|
92
|
+
value += c;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (state === 'decimal_point' || state === 'decimal') {
|
|
96
|
+
state = 'decimal';
|
|
97
|
+
value += c;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (state === 'e' || state === 'exponent_sign' || state === 'exponent') {
|
|
101
|
+
state = 'exponent';
|
|
102
|
+
value += c;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (c === '.') {
|
|
107
|
+
if (state === 'none' || state === 'sign' || state === 'whole') {
|
|
108
|
+
state = 'decimal_point';
|
|
109
|
+
value += c;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (c === 'E' || c == 'e') {
|
|
114
|
+
if (
|
|
115
|
+
state === 'whole' ||
|
|
116
|
+
state === 'decimal_point' ||
|
|
117
|
+
state === 'decimal'
|
|
118
|
+
) {
|
|
119
|
+
state = 'e';
|
|
120
|
+
value += c;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
const number = Number.parseFloat(value);
|
|
127
|
+
if (Number.isNaN(number)) {
|
|
128
|
+
return [cursor, null];
|
|
129
|
+
} else {
|
|
130
|
+
// step back to delegate iteration to parent loop
|
|
131
|
+
return [i - 1, number];
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @type {(string: string) => Array<PathDataItem>}
|
|
137
|
+
*/
|
|
138
|
+
const parsePathData = (string) => {
|
|
139
|
+
/**
|
|
140
|
+
* @type {Array<PathDataItem>}
|
|
141
|
+
*/
|
|
142
|
+
const pathData = [];
|
|
143
|
+
/**
|
|
144
|
+
* @type {null | PathDataCommand}
|
|
145
|
+
*/
|
|
146
|
+
let command = null;
|
|
147
|
+
let args = /** @type {number[]} */ ([]);
|
|
148
|
+
let argsCount = 0;
|
|
149
|
+
let canHaveComma = false;
|
|
150
|
+
let hadComma = false;
|
|
151
|
+
for (let i = 0; i < string.length; i += 1) {
|
|
152
|
+
const c = string.charAt(i);
|
|
153
|
+
if (isWsp(c)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// allow comma only between arguments
|
|
157
|
+
if (canHaveComma && c === ',') {
|
|
158
|
+
if (hadComma) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
hadComma = true;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (isCommand(c)) {
|
|
165
|
+
if (hadComma) {
|
|
166
|
+
return pathData;
|
|
167
|
+
}
|
|
168
|
+
if (command == null) {
|
|
169
|
+
// moveto should be leading command
|
|
170
|
+
if (c !== 'M' && c !== 'm') {
|
|
171
|
+
return pathData;
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
// stop if previous command arguments are not flushed
|
|
175
|
+
if (args.length !== 0) {
|
|
176
|
+
return pathData;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
command = c;
|
|
180
|
+
args = [];
|
|
181
|
+
argsCount = argsCountPerCommand[command];
|
|
182
|
+
canHaveComma = false;
|
|
183
|
+
// flush command without arguments
|
|
184
|
+
if (argsCount === 0) {
|
|
185
|
+
pathData.push({ command, args });
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// avoid parsing arguments if no command detected
|
|
190
|
+
if (command == null) {
|
|
191
|
+
return pathData;
|
|
192
|
+
}
|
|
193
|
+
// read next argument
|
|
194
|
+
let newCursor = i;
|
|
195
|
+
let number = null;
|
|
196
|
+
if (command === 'A' || command === 'a') {
|
|
197
|
+
const position = args.length;
|
|
198
|
+
if (position === 0 || position === 1) {
|
|
199
|
+
// allow only positive number without sign as first two arguments
|
|
200
|
+
if (c !== '+' && c !== '-') {
|
|
201
|
+
[newCursor, number] = readNumber(string, i);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (position === 2 || position === 5 || position === 6) {
|
|
205
|
+
[newCursor, number] = readNumber(string, i);
|
|
206
|
+
}
|
|
207
|
+
if (position === 3 || position === 4) {
|
|
208
|
+
// read flags
|
|
209
|
+
if (c === '0') {
|
|
210
|
+
number = 0;
|
|
211
|
+
}
|
|
212
|
+
if (c === '1') {
|
|
213
|
+
number = 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
[newCursor, number] = readNumber(string, i);
|
|
218
|
+
}
|
|
219
|
+
if (number == null) {
|
|
220
|
+
return pathData;
|
|
221
|
+
}
|
|
222
|
+
args.push(number);
|
|
223
|
+
canHaveComma = true;
|
|
224
|
+
hadComma = false;
|
|
225
|
+
i = newCursor;
|
|
226
|
+
// flush arguments when necessary count is reached
|
|
227
|
+
if (args.length === argsCount) {
|
|
228
|
+
pathData.push({ command, args });
|
|
229
|
+
// subsequent moveto coordinates are threated as implicit lineto commands
|
|
230
|
+
if (command === 'M') {
|
|
231
|
+
command = 'L';
|
|
232
|
+
}
|
|
233
|
+
if (command === 'm') {
|
|
234
|
+
command = 'l';
|
|
235
|
+
}
|
|
236
|
+
args = [];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return pathData;
|
|
240
|
+
};
|
|
241
|
+
exports.parsePathData = parsePathData;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @type {(number: number, precision?: number) => string}
|
|
245
|
+
*/
|
|
246
|
+
const stringifyNumber = (number, precision) => {
|
|
247
|
+
if (precision != null) {
|
|
248
|
+
const ratio = 10 ** precision;
|
|
249
|
+
number = Math.round(number * ratio) / ratio;
|
|
250
|
+
}
|
|
251
|
+
// remove zero whole from decimal number
|
|
252
|
+
return number.toString().replace(/^0\./, '.').replace(/^-0\./, '-.');
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Elliptical arc large-arc and sweep flags are rendered with spaces
|
|
257
|
+
* because many non-browser environments are not able to parse such paths
|
|
258
|
+
*
|
|
259
|
+
* @type {(
|
|
260
|
+
* command: string,
|
|
261
|
+
* args: number[],
|
|
262
|
+
* precision?: number,
|
|
263
|
+
* disableSpaceAfterFlags?: boolean
|
|
264
|
+
* ) => string}
|
|
265
|
+
*/
|
|
266
|
+
const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => {
|
|
267
|
+
let result = '';
|
|
268
|
+
let prev = '';
|
|
269
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
270
|
+
const number = args[i];
|
|
271
|
+
const numberString = stringifyNumber(number, precision);
|
|
272
|
+
if (
|
|
273
|
+
disableSpaceAfterFlags &&
|
|
274
|
+
(command === 'A' || command === 'a') &&
|
|
275
|
+
// consider combined arcs
|
|
276
|
+
(i % 7 === 4 || i % 7 === 5)
|
|
277
|
+
) {
|
|
278
|
+
result += numberString;
|
|
279
|
+
} else if (i === 0 || numberString.startsWith('-')) {
|
|
280
|
+
// avoid space before first and negative numbers
|
|
281
|
+
result += numberString;
|
|
282
|
+
} else if (prev.includes('.') && numberString.startsWith('.')) {
|
|
283
|
+
// remove space before decimal with zero whole
|
|
284
|
+
// only when previous number is also decimal
|
|
285
|
+
result += numberString;
|
|
286
|
+
} else {
|
|
287
|
+
result += ` ${numberString}`;
|
|
288
|
+
}
|
|
289
|
+
prev = numberString;
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @typedef {{
|
|
296
|
+
* pathData: Array<PathDataItem>;
|
|
297
|
+
* precision?: number;
|
|
298
|
+
* disableSpaceAfterFlags?: boolean;
|
|
299
|
+
* }} StringifyPathDataOptions
|
|
300
|
+
*/
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @type {(options: StringifyPathDataOptions) => string}
|
|
304
|
+
*/
|
|
305
|
+
const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => {
|
|
306
|
+
// combine sequence of the same commands
|
|
307
|
+
let combined = [];
|
|
308
|
+
for (let i = 0; i < pathData.length; i += 1) {
|
|
309
|
+
const { command, args } = pathData[i];
|
|
310
|
+
if (i === 0) {
|
|
311
|
+
combined.push({ command, args });
|
|
312
|
+
} else {
|
|
313
|
+
/**
|
|
314
|
+
* @type {PathDataItem}
|
|
315
|
+
*/
|
|
316
|
+
const last = combined[combined.length - 1];
|
|
317
|
+
// match leading moveto with following lineto
|
|
318
|
+
if (i === 1) {
|
|
319
|
+
if (command === 'L') {
|
|
320
|
+
last.command = 'M';
|
|
321
|
+
}
|
|
322
|
+
if (command === 'l') {
|
|
323
|
+
last.command = 'm';
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (
|
|
327
|
+
(last.command === command &&
|
|
328
|
+
last.command !== 'M' &&
|
|
329
|
+
last.command !== 'm') ||
|
|
330
|
+
// combine matching moveto and lineto sequences
|
|
331
|
+
(last.command === 'M' && command === 'L') ||
|
|
332
|
+
(last.command === 'm' && command === 'l')
|
|
333
|
+
) {
|
|
334
|
+
last.args = [...last.args, ...args];
|
|
335
|
+
} else {
|
|
336
|
+
combined.push({ command, args });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
let result = '';
|
|
341
|
+
for (const { command, args } of combined) {
|
|
342
|
+
result +=
|
|
343
|
+
command + stringifyArgs(command, args, precision, disableSpaceAfterFlags);
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
};
|
|
347
|
+
exports.stringifyPathData = stringifyPathData;
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('./types').XastParent} XastParent
|
|
5
|
+
* @typedef {import('./types').XastRoot} XastRoot
|
|
6
|
+
* @typedef {import('./types').XastElement} XastElement
|
|
7
|
+
* @typedef {import('./types').XastInstruction} XastInstruction
|
|
8
|
+
* @typedef {import('./types').XastDoctype} XastDoctype
|
|
9
|
+
* @typedef {import('./types').XastText} XastText
|
|
10
|
+
* @typedef {import('./types').XastCdata} XastCdata
|
|
11
|
+
* @typedef {import('./types').XastComment} XastComment
|
|
12
|
+
* @typedef {import('./types').StringifyOptions} StringifyOptions
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { textElems } = require('../plugins/_collections.js');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {{
|
|
19
|
+
* width: void | string,
|
|
20
|
+
* height: void | string,
|
|
21
|
+
* indent: string,
|
|
22
|
+
* textContext: null | XastElement,
|
|
23
|
+
* indentLevel: number,
|
|
24
|
+
* }} State
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Required<StringifyOptions>} Options
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @type {(char: string) => string}
|
|
33
|
+
*/
|
|
34
|
+
const encodeEntity = (char) => {
|
|
35
|
+
return entities[char];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @type {Options}
|
|
40
|
+
*/
|
|
41
|
+
const defaults = {
|
|
42
|
+
doctypeStart: '<!DOCTYPE',
|
|
43
|
+
doctypeEnd: '>',
|
|
44
|
+
procInstStart: '<?',
|
|
45
|
+
procInstEnd: '?>',
|
|
46
|
+
tagOpenStart: '<',
|
|
47
|
+
tagOpenEnd: '>',
|
|
48
|
+
tagCloseStart: '</',
|
|
49
|
+
tagCloseEnd: '>',
|
|
50
|
+
tagShortStart: '<',
|
|
51
|
+
tagShortEnd: '/>',
|
|
52
|
+
attrStart: '="',
|
|
53
|
+
attrEnd: '"',
|
|
54
|
+
commentStart: '<!--',
|
|
55
|
+
commentEnd: '-->',
|
|
56
|
+
cdataStart: '<![CDATA[',
|
|
57
|
+
cdataEnd: ']]>',
|
|
58
|
+
textStart: '',
|
|
59
|
+
textEnd: '',
|
|
60
|
+
indent: 4,
|
|
61
|
+
regEntities: /[&'"<>]/g,
|
|
62
|
+
regValEntities: /[&"<>]/g,
|
|
63
|
+
encodeEntity: encodeEntity,
|
|
64
|
+
pretty: false,
|
|
65
|
+
useShortTags: true,
|
|
66
|
+
eol: 'lf',
|
|
67
|
+
finalNewline: false,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @type {Record<string, string>}
|
|
72
|
+
*/
|
|
73
|
+
const entities = {
|
|
74
|
+
'&': '&',
|
|
75
|
+
"'": ''',
|
|
76
|
+
'"': '"',
|
|
77
|
+
'>': '>',
|
|
78
|
+
'<': '<',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* convert XAST to SVG string
|
|
83
|
+
*
|
|
84
|
+
* @type {(data: XastRoot, config: StringifyOptions) => {
|
|
85
|
+
* data: string,
|
|
86
|
+
* info: {
|
|
87
|
+
* width: void | string,
|
|
88
|
+
* height: void | string
|
|
89
|
+
* }
|
|
90
|
+
* }}
|
|
91
|
+
*/
|
|
92
|
+
const stringifySvg = (data, userOptions = {}) => {
|
|
93
|
+
/**
|
|
94
|
+
* @type {Options}
|
|
95
|
+
*/
|
|
96
|
+
const config = { ...defaults, ...userOptions };
|
|
97
|
+
const indent = config.indent;
|
|
98
|
+
let newIndent = ' ';
|
|
99
|
+
if (typeof indent === 'number' && Number.isNaN(indent) === false) {
|
|
100
|
+
newIndent = indent < 0 ? '\t' : ' '.repeat(indent);
|
|
101
|
+
} else if (typeof indent === 'string') {
|
|
102
|
+
newIndent = indent;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* @type {State}
|
|
106
|
+
*/
|
|
107
|
+
const state = {
|
|
108
|
+
// TODO remove width and height in v3
|
|
109
|
+
width: undefined,
|
|
110
|
+
height: undefined,
|
|
111
|
+
indent: newIndent,
|
|
112
|
+
textContext: null,
|
|
113
|
+
indentLevel: 0,
|
|
114
|
+
};
|
|
115
|
+
const eol = config.eol === 'crlf' ? '\r\n' : '\n';
|
|
116
|
+
if (config.pretty) {
|
|
117
|
+
config.doctypeEnd += eol;
|
|
118
|
+
config.procInstEnd += eol;
|
|
119
|
+
config.commentEnd += eol;
|
|
120
|
+
config.cdataEnd += eol;
|
|
121
|
+
config.tagShortEnd += eol;
|
|
122
|
+
config.tagOpenEnd += eol;
|
|
123
|
+
config.tagCloseEnd += eol;
|
|
124
|
+
config.textEnd += eol;
|
|
125
|
+
}
|
|
126
|
+
let svg = stringifyNode(data, config, state);
|
|
127
|
+
if (config.finalNewline && svg.length > 0 && svg[svg.length - 1] !== '\n') {
|
|
128
|
+
svg += eol;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
data: svg,
|
|
132
|
+
info: {
|
|
133
|
+
width: state.width,
|
|
134
|
+
height: state.height,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
exports.stringifySvg = stringifySvg;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @type {(node: XastParent, config: Options, state: State) => string}
|
|
142
|
+
*/
|
|
143
|
+
const stringifyNode = (data, config, state) => {
|
|
144
|
+
let svg = '';
|
|
145
|
+
state.indentLevel += 1;
|
|
146
|
+
for (const item of data.children) {
|
|
147
|
+
if (item.type === 'element') {
|
|
148
|
+
svg += stringifyElement(item, config, state);
|
|
149
|
+
}
|
|
150
|
+
if (item.type === 'text') {
|
|
151
|
+
svg += stringifyText(item, config, state);
|
|
152
|
+
}
|
|
153
|
+
if (item.type === 'doctype') {
|
|
154
|
+
svg += stringifyDoctype(item, config);
|
|
155
|
+
}
|
|
156
|
+
if (item.type === 'instruction') {
|
|
157
|
+
svg += stringifyInstruction(item, config);
|
|
158
|
+
}
|
|
159
|
+
if (item.type === 'comment') {
|
|
160
|
+
svg += stringifyComment(item, config);
|
|
161
|
+
}
|
|
162
|
+
if (item.type === 'cdata') {
|
|
163
|
+
svg += stringifyCdata(item, config, state);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
state.indentLevel -= 1;
|
|
167
|
+
return svg;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* create indent string in accordance with the current node level.
|
|
172
|
+
*
|
|
173
|
+
* @type {(config: Options, state: State) => string}
|
|
174
|
+
*/
|
|
175
|
+
const createIndent = (config, state) => {
|
|
176
|
+
let indent = '';
|
|
177
|
+
if (config.pretty && state.textContext == null) {
|
|
178
|
+
indent = state.indent.repeat(state.indentLevel - 1);
|
|
179
|
+
}
|
|
180
|
+
return indent;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @type {(node: XastDoctype, config: Options) => string}
|
|
185
|
+
*/
|
|
186
|
+
const stringifyDoctype = (node, config) => {
|
|
187
|
+
return config.doctypeStart + node.data.doctype + config.doctypeEnd;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @type {(node: XastInstruction, config: Options) => string}
|
|
192
|
+
*/
|
|
193
|
+
const stringifyInstruction = (node, config) => {
|
|
194
|
+
return (
|
|
195
|
+
config.procInstStart + node.name + ' ' + node.value + config.procInstEnd
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @type {(node: XastComment, config: Options) => string}
|
|
201
|
+
*/
|
|
202
|
+
const stringifyComment = (node, config) => {
|
|
203
|
+
return config.commentStart + node.value + config.commentEnd;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @type {(node: XastCdata, config: Options, state: State) => string}
|
|
208
|
+
*/
|
|
209
|
+
const stringifyCdata = (node, config, state) => {
|
|
210
|
+
return (
|
|
211
|
+
createIndent(config, state) +
|
|
212
|
+
config.cdataStart +
|
|
213
|
+
node.value +
|
|
214
|
+
config.cdataEnd
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @type {(node: XastElement, config: Options, state: State) => string}
|
|
220
|
+
*/
|
|
221
|
+
const stringifyElement = (node, config, state) => {
|
|
222
|
+
// beautiful injection for obtaining SVG information :)
|
|
223
|
+
if (
|
|
224
|
+
node.name === 'svg' &&
|
|
225
|
+
node.attributes.width != null &&
|
|
226
|
+
node.attributes.height != null
|
|
227
|
+
) {
|
|
228
|
+
state.width = node.attributes.width;
|
|
229
|
+
state.height = node.attributes.height;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// empty element and short tag
|
|
233
|
+
if (node.children.length === 0) {
|
|
234
|
+
if (config.useShortTags) {
|
|
235
|
+
return (
|
|
236
|
+
createIndent(config, state) +
|
|
237
|
+
config.tagShortStart +
|
|
238
|
+
node.name +
|
|
239
|
+
stringifyAttributes(node, config) +
|
|
240
|
+
config.tagShortEnd
|
|
241
|
+
);
|
|
242
|
+
} else {
|
|
243
|
+
return (
|
|
244
|
+
createIndent(config, state) +
|
|
245
|
+
config.tagShortStart +
|
|
246
|
+
node.name +
|
|
247
|
+
stringifyAttributes(node, config) +
|
|
248
|
+
config.tagOpenEnd +
|
|
249
|
+
config.tagCloseStart +
|
|
250
|
+
node.name +
|
|
251
|
+
config.tagCloseEnd
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
// non-empty element
|
|
255
|
+
} else {
|
|
256
|
+
let tagOpenStart = config.tagOpenStart;
|
|
257
|
+
let tagOpenEnd = config.tagOpenEnd;
|
|
258
|
+
let tagCloseStart = config.tagCloseStart;
|
|
259
|
+
let tagCloseEnd = config.tagCloseEnd;
|
|
260
|
+
let openIndent = createIndent(config, state);
|
|
261
|
+
let closeIndent = createIndent(config, state);
|
|
262
|
+
|
|
263
|
+
if (state.textContext) {
|
|
264
|
+
tagOpenStart = defaults.tagOpenStart;
|
|
265
|
+
tagOpenEnd = defaults.tagOpenEnd;
|
|
266
|
+
tagCloseStart = defaults.tagCloseStart;
|
|
267
|
+
tagCloseEnd = defaults.tagCloseEnd;
|
|
268
|
+
openIndent = '';
|
|
269
|
+
} else if (textElems.includes(node.name)) {
|
|
270
|
+
tagOpenEnd = defaults.tagOpenEnd;
|
|
271
|
+
tagCloseStart = defaults.tagCloseStart;
|
|
272
|
+
closeIndent = '';
|
|
273
|
+
state.textContext = node;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const children = stringifyNode(node, config, state);
|
|
277
|
+
|
|
278
|
+
if (state.textContext === node) {
|
|
279
|
+
state.textContext = null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
openIndent +
|
|
284
|
+
tagOpenStart +
|
|
285
|
+
node.name +
|
|
286
|
+
stringifyAttributes(node, config) +
|
|
287
|
+
tagOpenEnd +
|
|
288
|
+
children +
|
|
289
|
+
closeIndent +
|
|
290
|
+
tagCloseStart +
|
|
291
|
+
node.name +
|
|
292
|
+
tagCloseEnd
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* @type {(node: XastElement, config: Options) => string}
|
|
299
|
+
*/
|
|
300
|
+
const stringifyAttributes = (node, config) => {
|
|
301
|
+
let attrs = '';
|
|
302
|
+
for (const [name, value] of Object.entries(node.attributes)) {
|
|
303
|
+
// TODO remove attributes without values support in v3
|
|
304
|
+
if (value !== undefined) {
|
|
305
|
+
const encodedValue = value
|
|
306
|
+
.toString()
|
|
307
|
+
.replace(config.regValEntities, config.encodeEntity);
|
|
308
|
+
attrs += ' ' + name + config.attrStart + encodedValue + config.attrEnd;
|
|
309
|
+
} else {
|
|
310
|
+
attrs += ' ' + name;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return attrs;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @type {(node: XastText, config: Options, state: State) => string}
|
|
318
|
+
*/
|
|
319
|
+
const stringifyText = (node, config, state) => {
|
|
320
|
+
return (
|
|
321
|
+
createIndent(config, state) +
|
|
322
|
+
config.textStart +
|
|
323
|
+
node.value.replace(config.regEntities, config.encodeEntity) +
|
|
324
|
+
(state.textContext ? '' : config.textEnd)
|
|
325
|
+
);
|
|
326
|
+
};
|