1 | 'use strict';
|
2 |
|
3 | const { removeLeadingZero, toFixed } = require('./svgo/tools');
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | const argsCountPerCommand = {
|
13 | M: 2,
|
14 | m: 2,
|
15 | Z: 0,
|
16 | z: 0,
|
17 | L: 2,
|
18 | l: 2,
|
19 | H: 1,
|
20 | h: 1,
|
21 | V: 1,
|
22 | v: 1,
|
23 | C: 6,
|
24 | c: 6,
|
25 | S: 4,
|
26 | s: 4,
|
27 | Q: 4,
|
28 | q: 4,
|
29 | T: 2,
|
30 | t: 2,
|
31 | A: 7,
|
32 | a: 7,
|
33 | };
|
34 |
|
35 |
|
36 |
|
37 |
|
38 | const isCommand = (c) => {
|
39 | return c in argsCountPerCommand;
|
40 | };
|
41 |
|
42 |
|
43 |
|
44 |
|
45 | const isWsp = (c) => {
|
46 | const codePoint = c.codePointAt(0);
|
47 | return (
|
48 | codePoint === 0x20 ||
|
49 | codePoint === 0x9 ||
|
50 | codePoint === 0xd ||
|
51 | codePoint === 0xa
|
52 | );
|
53 | };
|
54 |
|
55 |
|
56 |
|
57 |
|
58 | const isDigit = (c) => {
|
59 | const codePoint = c.codePointAt(0);
|
60 | if (codePoint == null) {
|
61 | return false;
|
62 | }
|
63 | return 48 <= codePoint && codePoint <= 57;
|
64 | };
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 | const readNumber = (string, cursor) => {
|
74 | let i = cursor;
|
75 | let value = '';
|
76 | let state = ('none');
|
77 | for (; i < string.length; i += 1) {
|
78 | const c = string[i];
|
79 | if (c === '+' || c === '-') {
|
80 | if (state === 'none') {
|
81 | state = 'sign';
|
82 | value += c;
|
83 | continue;
|
84 | }
|
85 | if (state === 'e') {
|
86 | state = 'exponent_sign';
|
87 | value += c;
|
88 | continue;
|
89 | }
|
90 | }
|
91 | if (isDigit(c)) {
|
92 | if (state === 'none' || state === 'sign' || state === 'whole') {
|
93 | state = 'whole';
|
94 | value += c;
|
95 | continue;
|
96 | }
|
97 | if (state === 'decimal_point' || state === 'decimal') {
|
98 | state = 'decimal';
|
99 | value += c;
|
100 | continue;
|
101 | }
|
102 | if (state === 'e' || state === 'exponent_sign' || state === 'exponent') {
|
103 | state = 'exponent';
|
104 | value += c;
|
105 | continue;
|
106 | }
|
107 | }
|
108 | if (c === '.') {
|
109 | if (state === 'none' || state === 'sign' || state === 'whole') {
|
110 | state = 'decimal_point';
|
111 | value += c;
|
112 | continue;
|
113 | }
|
114 | }
|
115 | if (c === 'E' || c == 'e') {
|
116 | if (
|
117 | state === 'whole' ||
|
118 | state === 'decimal_point' ||
|
119 | state === 'decimal'
|
120 | ) {
|
121 | state = 'e';
|
122 | value += c;
|
123 | continue;
|
124 | }
|
125 | }
|
126 | break;
|
127 | }
|
128 | const number = Number.parseFloat(value);
|
129 | if (Number.isNaN(number)) {
|
130 | return [cursor, null];
|
131 | } else {
|
132 |
|
133 | return [i - 1, number];
|
134 | }
|
135 | };
|
136 |
|
137 |
|
138 |
|
139 |
|
140 | const parsePathData = (string) => {
|
141 | |
142 |
|
143 |
|
144 | const pathData = [];
|
145 | |
146 |
|
147 |
|
148 | let command = null;
|
149 | let args = ([]);
|
150 | let argsCount = 0;
|
151 | let canHaveComma = false;
|
152 | let hadComma = false;
|
153 | for (let i = 0; i < string.length; i += 1) {
|
154 | const c = string.charAt(i);
|
155 | if (isWsp(c)) {
|
156 | continue;
|
157 | }
|
158 |
|
159 | if (canHaveComma && c === ',') {
|
160 | if (hadComma) {
|
161 | break;
|
162 | }
|
163 | hadComma = true;
|
164 | continue;
|
165 | }
|
166 | if (isCommand(c)) {
|
167 | if (hadComma) {
|
168 | return pathData;
|
169 | }
|
170 | if (command == null) {
|
171 |
|
172 | if (c !== 'M' && c !== 'm') {
|
173 | return pathData;
|
174 | }
|
175 | } else {
|
176 |
|
177 | if (args.length !== 0) {
|
178 | return pathData;
|
179 | }
|
180 | }
|
181 | command = c;
|
182 | args = [];
|
183 | argsCount = argsCountPerCommand[command];
|
184 | canHaveComma = false;
|
185 |
|
186 | if (argsCount === 0) {
|
187 | pathData.push({ command, args });
|
188 | }
|
189 | continue;
|
190 | }
|
191 |
|
192 | if (command == null) {
|
193 | return pathData;
|
194 | }
|
195 |
|
196 | let newCursor = i;
|
197 | let number = null;
|
198 | if (command === 'A' || command === 'a') {
|
199 | const position = args.length;
|
200 | if (position === 0 || position === 1) {
|
201 |
|
202 | if (c !== '+' && c !== '-') {
|
203 | [newCursor, number] = readNumber(string, i);
|
204 | }
|
205 | }
|
206 | if (position === 2 || position === 5 || position === 6) {
|
207 | [newCursor, number] = readNumber(string, i);
|
208 | }
|
209 | if (position === 3 || position === 4) {
|
210 |
|
211 | if (c === '0') {
|
212 | number = 0;
|
213 | }
|
214 | if (c === '1') {
|
215 | number = 1;
|
216 | }
|
217 | }
|
218 | } else {
|
219 | [newCursor, number] = readNumber(string, i);
|
220 | }
|
221 | if (number == null) {
|
222 | return pathData;
|
223 | }
|
224 | args.push(number);
|
225 | canHaveComma = true;
|
226 | hadComma = false;
|
227 | i = newCursor;
|
228 |
|
229 | if (args.length === argsCount) {
|
230 | pathData.push({ command, args });
|
231 |
|
232 | if (command === 'M') {
|
233 | command = 'L';
|
234 | }
|
235 | if (command === 'm') {
|
236 | command = 'l';
|
237 | }
|
238 | args = [];
|
239 | }
|
240 | }
|
241 | return pathData;
|
242 | };
|
243 | exports.parsePathData = parsePathData;
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 | const roundAndStringify = (number, precision) => {
|
252 | if (precision != null) {
|
253 | number = toFixed(number, precision);
|
254 | }
|
255 |
|
256 | return {
|
257 | roundedStr: removeLeadingZero(number),
|
258 | rounded: number,
|
259 | };
|
260 | };
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 | const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => {
|
274 | let result = '';
|
275 | let previous;
|
276 |
|
277 | for (let i = 0; i < args.length; i++) {
|
278 | const { roundedStr, rounded } = roundAndStringify(args[i], precision);
|
279 | if (
|
280 | disableSpaceAfterFlags &&
|
281 | (command === 'A' || command === 'a') &&
|
282 |
|
283 | (i % 7 === 4 || i % 7 === 5)
|
284 | ) {
|
285 | result += roundedStr;
|
286 | } else if (i === 0 || rounded < 0) {
|
287 |
|
288 | result += roundedStr;
|
289 | } else if (
|
290 | !Number.isInteger(previous) &&
|
291 | rounded != 0 &&
|
292 | rounded < 1 &&
|
293 | rounded > -1
|
294 | ) {
|
295 |
|
296 |
|
297 | result += roundedStr;
|
298 | } else {
|
299 | result += ` ${roundedStr}`;
|
300 | }
|
301 | previous = rounded;
|
302 | }
|
303 |
|
304 | return result;
|
305 | };
|
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 |
|
317 |
|
318 |
|
319 | const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => {
|
320 | if (pathData.length === 1) {
|
321 | const { command, args } = pathData[0];
|
322 | return (
|
323 | command + stringifyArgs(command, args, precision, disableSpaceAfterFlags)
|
324 | );
|
325 | }
|
326 |
|
327 | let result = '';
|
328 | let prev = { ...pathData[0] };
|
329 |
|
330 |
|
331 | if (pathData[1].command === 'L') {
|
332 | prev.command = 'M';
|
333 | } else if (pathData[1].command === 'l') {
|
334 | prev.command = 'm';
|
335 | }
|
336 |
|
337 | for (let i = 1; i < pathData.length; i++) {
|
338 | const { command, args } = pathData[i];
|
339 | if (
|
340 | (prev.command === command &&
|
341 | prev.command !== 'M' &&
|
342 | prev.command !== 'm') ||
|
343 |
|
344 | (prev.command === 'M' && command === 'L') ||
|
345 | (prev.command === 'm' && command === 'l')
|
346 | ) {
|
347 | prev.args = [...prev.args, ...args];
|
348 | if (i === pathData.length - 1) {
|
349 | result +=
|
350 | prev.command +
|
351 | stringifyArgs(
|
352 | prev.command,
|
353 | prev.args,
|
354 | precision,
|
355 | disableSpaceAfterFlags,
|
356 | );
|
357 | }
|
358 | } else {
|
359 | result +=
|
360 | prev.command +
|
361 | stringifyArgs(
|
362 | prev.command,
|
363 | prev.args,
|
364 | precision,
|
365 | disableSpaceAfterFlags,
|
366 | );
|
367 |
|
368 | if (i === pathData.length - 1) {
|
369 | result +=
|
370 | command +
|
371 | stringifyArgs(command, args, precision, disableSpaceAfterFlags);
|
372 | } else {
|
373 | prev = { command, args };
|
374 | }
|
375 | }
|
376 | }
|
377 |
|
378 | return result;
|
379 | };
|
380 | exports.stringifyPathData = stringifyPathData;
|