source: trip-planner-front/node_modules/svgo/lib/path.js@ eed0bf8

Last change on this file since eed0bf8 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 8.2 KB
Line 
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
10const 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 */
36const isCommand = (c) => {
37 return c in argsCountPerCommand;
38};
39
40/**
41 * @type {(c: string) => boolean}
42 */
43const 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 */
56const 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 */
71const 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 */
138const 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};
241exports.parsePathData = parsePathData;
242
243/**
244 * @type {(number: number, precision?: number) => string}
245 */
246const 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 */
266const 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 */
305const 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};
347exports.stringifyPathData = stringifyPathData;
Note: See TracBrowser for help on using the repository browser.