| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 | 
							- 'use strict';
 
- /**
 
-  * @typedef {import('./types').PathDataItem} PathDataItem
 
-  * @typedef {import('./types').PathDataCommand} PathDataCommand
 
-  */
 
- // Based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF
 
- const argsCountPerCommand = {
 
-   M: 2,
 
-   m: 2,
 
-   Z: 0,
 
-   z: 0,
 
-   L: 2,
 
-   l: 2,
 
-   H: 1,
 
-   h: 1,
 
-   V: 1,
 
-   v: 1,
 
-   C: 6,
 
-   c: 6,
 
-   S: 4,
 
-   s: 4,
 
-   Q: 4,
 
-   q: 4,
 
-   T: 2,
 
-   t: 2,
 
-   A: 7,
 
-   a: 7,
 
- };
 
- /**
 
-  * @type {(c: string) => c is PathDataCommand}
 
-  */
 
- const isCommand = (c) => {
 
-   return c in argsCountPerCommand;
 
- };
 
- /**
 
-  * @type {(c: string) => boolean}
 
-  */
 
- const isWsp = (c) => {
 
-   const codePoint = c.codePointAt(0);
 
-   return (
 
-     codePoint === 0x20 ||
 
-     codePoint === 0x9 ||
 
-     codePoint === 0xd ||
 
-     codePoint === 0xa
 
-   );
 
- };
 
- /**
 
-  * @type {(c: string) => boolean}
 
-  */
 
- const isDigit = (c) => {
 
-   const codePoint = c.codePointAt(0);
 
-   if (codePoint == null) {
 
-     return false;
 
-   }
 
-   return 48 <= codePoint && codePoint <= 57;
 
- };
 
- /**
 
-  * @typedef {'none' | 'sign' | 'whole' | 'decimal_point' | 'decimal' | 'e' | 'exponent_sign' | 'exponent'} ReadNumberState
 
-  */
 
- /**
 
-  * @type {(string: string, cursor: number) => [number, number | null]}
 
-  */
 
- const readNumber = (string, cursor) => {
 
-   let i = cursor;
 
-   let value = '';
 
-   let state = /** @type {ReadNumberState} */ ('none');
 
-   for (; i < string.length; i += 1) {
 
-     const c = string[i];
 
-     if (c === '+' || c === '-') {
 
-       if (state === 'none') {
 
-         state = 'sign';
 
-         value += c;
 
-         continue;
 
-       }
 
-       if (state === 'e') {
 
-         state = 'exponent_sign';
 
-         value += c;
 
-         continue;
 
-       }
 
-     }
 
-     if (isDigit(c)) {
 
-       if (state === 'none' || state === 'sign' || state === 'whole') {
 
-         state = 'whole';
 
-         value += c;
 
-         continue;
 
-       }
 
-       if (state === 'decimal_point' || state === 'decimal') {
 
-         state = 'decimal';
 
-         value += c;
 
-         continue;
 
-       }
 
-       if (state === 'e' || state === 'exponent_sign' || state === 'exponent') {
 
-         state = 'exponent';
 
-         value += c;
 
-         continue;
 
-       }
 
-     }
 
-     if (c === '.') {
 
-       if (state === 'none' || state === 'sign' || state === 'whole') {
 
-         state = 'decimal_point';
 
-         value += c;
 
-         continue;
 
-       }
 
-     }
 
-     if (c === 'E' || c == 'e') {
 
-       if (
 
-         state === 'whole' ||
 
-         state === 'decimal_point' ||
 
-         state === 'decimal'
 
-       ) {
 
-         state = 'e';
 
-         value += c;
 
-         continue;
 
-       }
 
-     }
 
-     break;
 
-   }
 
-   const number = Number.parseFloat(value);
 
-   if (Number.isNaN(number)) {
 
-     return [cursor, null];
 
-   } else {
 
-     // step back to delegate iteration to parent loop
 
-     return [i - 1, number];
 
-   }
 
- };
 
- /**
 
-  * @type {(string: string) => Array<PathDataItem>}
 
-  */
 
- const parsePathData = (string) => {
 
-   /**
 
-    * @type {Array<PathDataItem>}
 
-    */
 
-   const pathData = [];
 
-   /**
 
-    * @type {null | PathDataCommand}
 
-    */
 
-   let command = null;
 
-   let args = /** @type {number[]} */ ([]);
 
-   let argsCount = 0;
 
-   let canHaveComma = false;
 
-   let hadComma = false;
 
-   for (let i = 0; i < string.length; i += 1) {
 
-     const c = string.charAt(i);
 
-     if (isWsp(c)) {
 
-       continue;
 
-     }
 
-     // allow comma only between arguments
 
-     if (canHaveComma && c === ',') {
 
-       if (hadComma) {
 
-         break;
 
-       }
 
-       hadComma = true;
 
-       continue;
 
-     }
 
-     if (isCommand(c)) {
 
-       if (hadComma) {
 
-         return pathData;
 
-       }
 
-       if (command == null) {
 
-         // moveto should be leading command
 
-         if (c !== 'M' && c !== 'm') {
 
-           return pathData;
 
-         }
 
-       } else {
 
-         // stop if previous command arguments are not flushed
 
-         if (args.length !== 0) {
 
-           return pathData;
 
-         }
 
-       }
 
-       command = c;
 
-       args = [];
 
-       argsCount = argsCountPerCommand[command];
 
-       canHaveComma = false;
 
-       // flush command without arguments
 
-       if (argsCount === 0) {
 
-         pathData.push({ command, args });
 
-       }
 
-       continue;
 
-     }
 
-     // avoid parsing arguments if no command detected
 
-     if (command == null) {
 
-       return pathData;
 
-     }
 
-     // read next argument
 
-     let newCursor = i;
 
-     let number = null;
 
-     if (command === 'A' || command === 'a') {
 
-       const position = args.length;
 
-       if (position === 0 || position === 1) {
 
-         // allow only positive number without sign as first two arguments
 
-         if (c !== '+' && c !== '-') {
 
-           [newCursor, number] = readNumber(string, i);
 
-         }
 
-       }
 
-       if (position === 2 || position === 5 || position === 6) {
 
-         [newCursor, number] = readNumber(string, i);
 
-       }
 
-       if (position === 3 || position === 4) {
 
-         // read flags
 
-         if (c === '0') {
 
-           number = 0;
 
-         }
 
-         if (c === '1') {
 
-           number = 1;
 
-         }
 
-       }
 
-     } else {
 
-       [newCursor, number] = readNumber(string, i);
 
-     }
 
-     if (number == null) {
 
-       return pathData;
 
-     }
 
-     args.push(number);
 
-     canHaveComma = true;
 
-     hadComma = false;
 
-     i = newCursor;
 
-     // flush arguments when necessary count is reached
 
-     if (args.length === argsCount) {
 
-       pathData.push({ command, args });
 
-       // subsequent moveto coordinates are threated as implicit lineto commands
 
-       if (command === 'M') {
 
-         command = 'L';
 
-       }
 
-       if (command === 'm') {
 
-         command = 'l';
 
-       }
 
-       args = [];
 
-     }
 
-   }
 
-   return pathData;
 
- };
 
- exports.parsePathData = parsePathData;
 
- /**
 
-  * @type {(number: number, precision?: number) => string}
 
-  */
 
- const stringifyNumber = (number, precision) => {
 
-   if (precision != null) {
 
-     const ratio = 10 ** precision;
 
-     number = Math.round(number * ratio) / ratio;
 
-   }
 
-   // remove zero whole from decimal number
 
-   return number.toString().replace(/^0\./, '.').replace(/^-0\./, '-.');
 
- };
 
- /**
 
-  * Elliptical arc large-arc and sweep flags are rendered with spaces
 
-  * because many non-browser environments are not able to parse such paths
 
-  *
 
-  * @type {(
 
-  *   command: string,
 
-  *   args: number[],
 
-  *   precision?: number,
 
-  *   disableSpaceAfterFlags?: boolean
 
-  * ) => string}
 
-  */
 
- const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => {
 
-   let result = '';
 
-   let prev = '';
 
-   for (let i = 0; i < args.length; i += 1) {
 
-     const number = args[i];
 
-     const numberString = stringifyNumber(number, precision);
 
-     if (
 
-       disableSpaceAfterFlags &&
 
-       (command === 'A' || command === 'a') &&
 
-       // consider combined arcs
 
-       (i % 7 === 4 || i % 7 === 5)
 
-     ) {
 
-       result += numberString;
 
-     } else if (i === 0 || numberString.startsWith('-')) {
 
-       // avoid space before first and negative numbers
 
-       result += numberString;
 
-     } else if (prev.includes('.') && numberString.startsWith('.')) {
 
-       // remove space before decimal with zero whole
 
-       // only when previous number is also decimal
 
-       result += numberString;
 
-     } else {
 
-       result += ` ${numberString}`;
 
-     }
 
-     prev = numberString;
 
-   }
 
-   return result;
 
- };
 
- /**
 
-  * @typedef {{
 
-  *   pathData: Array<PathDataItem>;
 
-  *   precision?: number;
 
-  *   disableSpaceAfterFlags?: boolean;
 
-  * }} StringifyPathDataOptions
 
-  */
 
- /**
 
-  * @type {(options: StringifyPathDataOptions) => string}
 
-  */
 
- const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => {
 
-   // combine sequence of the same commands
 
-   let combined = [];
 
-   for (let i = 0; i < pathData.length; i += 1) {
 
-     const { command, args } = pathData[i];
 
-     if (i === 0) {
 
-       combined.push({ command, args });
 
-     } else {
 
-       /**
 
-        * @type {PathDataItem}
 
-        */
 
-       const last = combined[combined.length - 1];
 
-       // match leading moveto with following lineto
 
-       if (i === 1) {
 
-         if (command === 'L') {
 
-           last.command = 'M';
 
-         }
 
-         if (command === 'l') {
 
-           last.command = 'm';
 
-         }
 
-       }
 
-       if (
 
-         (last.command === command &&
 
-           last.command !== 'M' &&
 
-           last.command !== 'm') ||
 
-         // combine matching moveto and lineto sequences
 
-         (last.command === 'M' && command === 'L') ||
 
-         (last.command === 'm' && command === 'l')
 
-       ) {
 
-         last.args = [...last.args, ...args];
 
-       } else {
 
-         combined.push({ command, args });
 
-       }
 
-     }
 
-   }
 
-   let result = '';
 
-   for (const { command, args } of combined) {
 
-     result +=
 
-       command + stringifyArgs(command, args, precision, disableSpaceAfterFlags);
 
-   }
 
-   return result;
 
- };
 
- exports.stringifyPathData = stringifyPathData;
 
 
  |