From bb3328d5b54d7215d5f192a2e0925258dbb860b2 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 26 Feb 2021 18:06:03 +0300 Subject: [PATCH 01/12] Implement new path parser Regexps are quite leaky for complex parsing. Regression tests caught a few issues related to path parser. In this diff I implemented the new spec-compliant parser which solves 2 regression cases and covers many edge cases hard to handle with regexps. --- lib/path.js | 199 +++++++++++++++++++++++++++++++++++++++++++++++ plugins/_path.js | 99 +++++------------------ 2 files changed, 217 insertions(+), 81 deletions(-) create mode 100644 lib/path.js diff --git a/lib/path.js b/lib/path.js new file mode 100644 index 000000000..689cac68d --- /dev/null +++ b/lib/path.js @@ -0,0 +1,199 @@ +'use strict'; + +// 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 +}; + +const isCommand = c => { + return c in argsCountPerCommand; +}; + +const isWsp = c => { + const codePoint = c.codePointAt(0); + return ( + codePoint === 0x20 || + codePoint === 0x9 || + codePoint === 0xD || + codePoint === 0xA + ); +}; + +const isDigit = c => { + const codePoint = c.codePointAt(0); + return 48 <= codePoint && codePoint <= 57; +}; + +const readNumber = (string, cursor) => { + let i = cursor; + let number = ''; + // none | sign | whole | decimal_point | decimal | e | exponent_sign | exponent + let state = 'none'; + for (; i < string.length; i += 1) { + const c = string[i]; + if (c === '+' || c === '-') { + if (state === 'none') { + state = 'sign' + number += c; + continue; + } + if (state === 'e') { + state === 'exponent_sign'; + number += c; + continue; + } + } + if (isDigit(c)) { + if (state === 'none' || state === 'sign' || state === 'whole') { + state = 'whole'; + number += c; + continue; + } + if (state === 'decimal_point' || state === 'decimal') { + state = 'decimal'; + number += c; + continue; + } + if (state === 'e' || state === 'exponent_sign' || state === 'exponent') { + state = 'exponent'; + number += c; + continue; + } + } + if (c === '.') { + if (state === 'none' || state === 'sign' || state === 'whole') { + state = 'decimal_point'; + number += c; + continue; + } + } + if (c === 'E' || c == 'e') { + if (state === 'whole' || state === 'decimal_point' || state === 'decimal') { + state = 'e'; + number += c; + continue; + } + } + break; + } + if (state === 'none' || state === 'sign' || state === 'e' || state === 'exponent_sign') { + return [i - 1, null]; + } + // step back to delegate teration to parent loop + return [i - 1, Number(number)]; +}; + + +// TODO do not erase args in flushing after number reading this prevents handling commas +const parsePathData = (string) => { + const commands = []; + let i = 0; + let command = null; + let args; + let argsCount; + // moveto should be leading command + const c = string.charAt(i); + for (; i < string.length; i += 1) { + const c = string.charAt(i); + // TODO forbid comma between commands + if (isWsp(c) || c === ',') { + continue; + } + if (isCommand(c)) { + if (command == null) { + // moveto should be leading command + if (c !== 'M' && c !== 'm') { + return commands; + } + } else { + // stop if previous command arguments are miscounted + if (argsCount !== 0 && args.length % argsCount !== 0) { + return commands; + } + } + command = c; + args = []; + argsCount = argsCountPerCommand[command]; + if (argsCount === 0) { + commands.push({ name: command, args }); + } + continue; + } + // avoid parsing arguments if no command detected + if (command == null) { + return commands; + } + // read next arg + let newCursor = i; + let number = null; + if (command === 'A' || command === 'a') { + const mod = args.length % argsCount; + if (mod === 0 || mod === 1) { + // allow only positive number without sign as first two arguments + if (c !== '+' && c !== '-') { + [newCursor, number] = readNumber(string, i); + } + } + if (mod === 2) { + [newCursor, number] = readNumber(string, i); + } + if (mod === 3 || mod === 4) { + // read flags + if (c === '0') { + number = 0; + } + if (c === '1') { + number = 1; + } + } + if (mod === 5 || mod === 6) { + [newCursor, number] = readNumber(string, i); + } + } else { + [newCursor, number] = readNumber(string, i); + } + if (number == null) { + return commands; + } else { + args.push(number); + } + i = newCursor; + // flush args + if (args.length >= argsCount && args.length % argsCount === 0) { + commands.push({ name: command, args }); + // subsequent moveto coordinates are threated as implicit lineto commands + if (command === 'M') { + command = 'L'; + } + if (command === 'm') { + command = 'l'; + } + args = []; + } + } + if (args.length !== argsCount) { + return commands; + } + return commands; +}; +exports.parsePathData = parsePathData; diff --git a/plugins/_path.js b/plugins/_path.js index 01c9fa7f7..6c72fe281 100644 --- a/plugins/_path.js +++ b/plugins/_path.js @@ -1,16 +1,8 @@ 'use strict'; -var rNumber = String.raw`[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?\s*`, - rCommaWsp = String.raw`(?:\s,?\s*|,\s*)`, - rNumberCommaWsp = `(${rNumber})` + rCommaWsp, - rFlagCommaWsp = `([01])${rCommaWsp}?`, - rCoordinatePair = String.raw`(${rNumber})${rCommaWsp}?(${rNumber})`, - rArcSeq = (rNumberCommaWsp + '?').repeat(2) + rNumberCommaWsp + rFlagCommaWsp.repeat(2) + rCoordinatePair; - -var regPathInstructions = /([MmLlHhVvCcSsQqTtAaZz])\s*/, - regCoordinateSequence = new RegExp(rNumber, 'g'), - regArcArgumentSequence = new RegExp(rArcSeq, 'g'), - regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g, +const { parsePathData } = require('../lib/path.js'); + +var regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g, transform2js = require('./_transforms').transform2js, transformsMultiply = require('./_transforms').transformsMultiply, transformArc = require('./_transforms').transformArc, @@ -29,77 +21,22 @@ var regPathInstructions = /([MmLlHhVvCcSsQqTtAaZz])\s*/, * @return {Array} output array */ exports.path2js = function(path) { - if (path.pathJS) return path.pathJS; - - var paramsLength = { // Number of parameters of every path command - H: 1, V: 1, M: 2, L: 2, T: 2, Q: 4, S: 4, C: 6, A: 7, - h: 1, v: 1, m: 2, l: 2, t: 2, q: 4, s: 4, c: 6, a: 7 - }, - pathData = [], // JS representation of the path data - instruction, // current instruction context - startMoveto = false; - - // splitting path string into array like ['M', '10 50', 'L', '20 30'] - path.attr('d').value.split(regPathInstructions).forEach(function(data) { - if (!data) return; - if (!startMoveto) { - if (data == 'M' || data == 'm') { - startMoveto = true; - } else return; - } - - // instruction item - if (regPathInstructions.test(data)) { - instruction = data; - - // z - instruction w/o data - if (instruction == 'Z' || instruction == 'z') { - pathData.push({ - instruction: 'z' - }); - } - // data item - } else { - if (instruction == 'A' || instruction == 'a') { - var newData = []; - for (var args; (args = regArcArgumentSequence.exec(data));) { - for (var i = 1; i < args.length; i++) { - newData.push(args[i]); - } - } - data = newData; - } else { - data = data.match(regCoordinateSequence); - } - if (!data) return; - - data = data.map(Number); - // Subsequent moveto pairs of coordinates are threated as implicit lineto commands - // https://www.w3.org/TR/SVG11/paths.html#PathDataMovetoCommands - if (instruction == 'M' || instruction == 'm') { - pathData.push({ - instruction: pathData.length == 0 ? 'M' : instruction, - data: data.splice(0, 2) - }); - instruction = instruction == 'M' ? 'L' : 'l'; - } - - for (var pair = paramsLength[instruction]; data.length;) { - pathData.push({ - instruction: instruction, - data: data.splice(0, pair) - }); - } - } - }); - - // First moveto is actually absolute. Subsequent coordinates were separated above. - if (pathData.length && pathData[0].instruction == 'm') { - pathData[0].instruction = 'M'; + if (path.pathJS) return path.pathJS; + const pathData = []; // JS representation of the path data + const commands = parsePathData(path.attr('d').value); + for (const { name, args } of commands) { + if (name === 'Z' || name === 'z') { + pathData.push({ instruction: 'z' }); + } else { + pathData.push({ instruction: name, data: args }); } - path.pathJS = pathData; - - return pathData; + } + // First moveto is actually absolute. Subsequent coordinates were separated above. + if (pathData.length && pathData[0].instruction == 'm') { + pathData[0].instruction = 'M'; + } + path.pathJS = pathData; + return pathData; }; /** From 2061c4027bb10f9225a35adc3361a6e352ec84bb Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 26 Feb 2021 18:12:08 +0300 Subject: [PATCH 02/12] Remove old code --- lib/path.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/path.js b/lib/path.js index 689cac68d..efe3cdd3b 100644 --- a/lib/path.js +++ b/lib/path.js @@ -111,8 +111,6 @@ const parsePathData = (string) => { let command = null; let args; let argsCount; - // moveto should be leading command - const c = string.charAt(i); for (; i < string.length; i += 1) { const c = string.charAt(i); // TODO forbid comma between commands From 347d9c9bcaad6075d021a87b2c1587756799e089 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 26 Feb 2021 18:14:56 +0300 Subject: [PATCH 03/12] fix comment --- lib/path.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/path.js b/lib/path.js index efe3cdd3b..cce7da33a 100644 --- a/lib/path.js +++ b/lib/path.js @@ -99,7 +99,7 @@ const readNumber = (string, cursor) => { if (state === 'none' || state === 'sign' || state === 'e' || state === 'exponent_sign') { return [i - 1, null]; } - // step back to delegate teration to parent loop + // step back to delegate iteration to parent loop return [i - 1, Number(number)]; }; From 67b8944ce17ed19c1c1d41ccad2a4eaea9758075 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 26 Feb 2021 20:13:20 +0300 Subject: [PATCH 04/12] Rename commands to pathData --- lib/path.js | 18 +++++++++--------- plugins/_path.js | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/path.js b/lib/path.js index cce7da33a..b6bfa01ad 100644 --- a/lib/path.js +++ b/lib/path.js @@ -106,7 +106,7 @@ const readNumber = (string, cursor) => { // TODO do not erase args in flushing after number reading this prevents handling commas const parsePathData = (string) => { - const commands = []; + const pathData = []; let i = 0; let command = null; let args; @@ -121,25 +121,25 @@ const parsePathData = (string) => { if (command == null) { // moveto should be leading command if (c !== 'M' && c !== 'm') { - return commands; + return pathData; } } else { // stop if previous command arguments are miscounted if (argsCount !== 0 && args.length % argsCount !== 0) { - return commands; + return pathData; } } command = c; args = []; argsCount = argsCountPerCommand[command]; if (argsCount === 0) { - commands.push({ name: command, args }); + pathData.push({ command, args }); } continue; } // avoid parsing arguments if no command detected if (command == null) { - return commands; + return pathData; } // read next arg let newCursor = i; @@ -171,14 +171,14 @@ const parsePathData = (string) => { [newCursor, number] = readNumber(string, i); } if (number == null) { - return commands; + return pathData; } else { args.push(number); } i = newCursor; // flush args if (args.length >= argsCount && args.length % argsCount === 0) { - commands.push({ name: command, args }); + pathData.push({ command, args }); // subsequent moveto coordinates are threated as implicit lineto commands if (command === 'M') { command = 'L'; @@ -190,8 +190,8 @@ const parsePathData = (string) => { } } if (args.length !== argsCount) { - return commands; + return pathData; } - return commands; + return pathData; }; exports.parsePathData = parsePathData; diff --git a/plugins/_path.js b/plugins/_path.js index 6c72fe281..00ecf5a4e 100644 --- a/plugins/_path.js +++ b/plugins/_path.js @@ -23,12 +23,12 @@ var regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g, exports.path2js = function(path) { if (path.pathJS) return path.pathJS; const pathData = []; // JS representation of the path data - const commands = parsePathData(path.attr('d').value); - for (const { name, args } of commands) { - if (name === 'Z' || name === 'z') { + const newPathData = parsePathData(path.attr('d').value); + for (const { command, args } of newPathData) { + if (command === 'Z' || command === 'z') { pathData.push({ instruction: 'z' }); } else { - pathData.push({ instruction: name, data: args }); + pathData.push({ instruction: command, data: args }); } } // First moveto is actually absolute. Subsequent coordinates were separated above. From aff622beb9715d5375ee774f36f1586d69a17198 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 26 Feb 2021 22:10:54 +0300 Subject: [PATCH 05/12] Handle commas according to spec --- lib/path.js | 23 ++++++++++++++++++----- lib/path.test.js | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 lib/path.test.js diff --git a/lib/path.js b/lib/path.js index b6bfa01ad..0c4ece5cb 100644 --- a/lib/path.js +++ b/lib/path.js @@ -104,20 +104,31 @@ const readNumber = (string, cursor) => { }; -// TODO do not erase args in flushing after number reading this prevents handling commas const parsePathData = (string) => { const pathData = []; let i = 0; let command = null; let args; let argsCount; + let argsInSequenceCount = 0; + let hadComma = false; for (; i < string.length; i += 1) { const c = string.charAt(i); - // TODO forbid comma between commands - if (isWsp(c) || c === ',') { + if (isWsp(c)) { + continue; + } + // allow comma only between arguments + if (argsInSequenceCount !== 0 && 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') { @@ -132,6 +143,7 @@ const parsePathData = (string) => { command = c; args = []; argsCount = argsCountPerCommand[command]; + argsInSequenceCount = 0; if (argsCount === 0) { pathData.push({ command, args }); } @@ -172,9 +184,10 @@ const parsePathData = (string) => { } if (number == null) { return pathData; - } else { - args.push(number); } + args.push(number); + argsInSequenceCount += 1; + hadComma = false; i = newCursor; // flush args if (args.length >= argsCount && args.length % argsCount === 0) { diff --git a/lib/path.test.js b/lib/path.test.js new file mode 100644 index 000000000..6956749fd --- /dev/null +++ b/lib/path.test.js @@ -0,0 +1,49 @@ +'use strict'; + +const { expect } = require('chai'); +const { parsePathData } = require('./path.js'); + +describe('parse path data', () => { + it('should allow spaces between commands', () => { + expect(parsePathData('M0 10 L \n\r\t20 30')).to.deep.equal([ + { command: 'M', args: [0, 10] }, + { command: 'L', args: [20, 30] }, + ]); + }); + it('should allow spaces and commas between arguments', () => { + expect(parsePathData('M0 , 10 L 20 \n\r\t30,40,50')).to.deep.equal([ + { command: 'M', args: [0, 10] }, + { command: 'L', args: [20, 30] }, + { command: 'L', args: [40, 50] }, + ]); + }); + it('should forbid commas before commands', () => { + expect(parsePathData(', M0 10')).to.deep.equal([]); + }); + it('should forbid commas between commands', () => { + expect(parsePathData('M0,10 , L 20,30')).to.deep.equal([ + { command: 'M', args: [0, 10] }, + ]); + }); + it('should forbid commas between command name and argument', () => { + expect(parsePathData('M0,10 L,20,30')).to.deep.equal([ + { command: 'M', args: [0, 10] }, + ]); + }); + it('should forbid multipe commas in a row', () => { + expect(parsePathData('M0 , , 10')).to.deep.equal([]); + }); + it('should stop when unknown char appears', () => { + expect(parsePathData('M0 10 , L 20 #40')).to.deep.equal([ + { command: 'M', args: [0, 10] }, + ]); + }); + it('should stop when not enough arguments', () => { + expect(parsePathData('M0 10 L 20 L 30 40')).to.deep.equal([ + { command: 'M', args: [0, 10] }, + ]); + }); + it('should stop if moveto not the first command', () => { + expect(parsePathData('L 10 20')).to.deep.equal([]); + }); +}); diff --git a/package.json b/package.json index 4d19409b1..fb2f7db06 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "dist" ], "scripts": { - "test": "c8 --reporter=html --reporter=text mocha \"test/*/_index.js\"", + "test": "c8 --reporter=html --reporter=text mocha \"test/*/_index.js\" \"**/*.test.js\" --ignore=\"node_modules/**\"", "lint": "eslint .", "test-browser": "rollup -c && node ./test/browser.js", "prepublishOnly": "rm -rf dist && rollup -c" From 32c50fbe706873cfc57b59b7842d6d6ed7dfc635 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 26 Feb 2021 22:12:19 +0300 Subject: [PATCH 06/12] Group arc arguments parsing --- lib/path.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/path.js b/lib/path.js index 0c4ece5cb..d0af3d6f1 100644 --- a/lib/path.js +++ b/lib/path.js @@ -164,7 +164,7 @@ const parsePathData = (string) => { [newCursor, number] = readNumber(string, i); } } - if (mod === 2) { + if (mod === 2 || mod === 5 || mod === 6) { [newCursor, number] = readNumber(string, i); } if (mod === 3 || mod === 4) { @@ -176,9 +176,6 @@ const parsePathData = (string) => { number = 1; } } - if (mod === 5 || mod === 6) { - [newCursor, number] = readNumber(string, i); - } } else { [newCursor, number] = readNumber(string, i); } From f01fa7ba20e04837d1bfcf9dedb6464eced4a99b Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 26 Feb 2021 22:15:34 +0300 Subject: [PATCH 07/12] Fix lint --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index fb2f7db06..2512b6ce0 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,8 @@ }, { "files": [ - "test/**/*.js" + "test/**/*.js", + "**/*.test.js" ], "env": { "mocha": true From 26a3fd85ff66b7b374a2ba631d993b1e8302f31e Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 26 Feb 2021 23:49:21 +0300 Subject: [PATCH 08/12] Fix invalid number parsing --- lib/path.js | 27 +++++++++++++++------------ lib/path.test.js | 4 ++++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/path.js b/lib/path.js index d0af3d6f1..72505e544 100644 --- a/lib/path.js +++ b/lib/path.js @@ -46,7 +46,7 @@ const isDigit = c => { const readNumber = (string, cursor) => { let i = cursor; - let number = ''; + let value = ''; // none | sign | whole | decimal_point | decimal | e | exponent_sign | exponent let state = 'none'; for (; i < string.length; i += 1) { @@ -54,53 +54,55 @@ const readNumber = (string, cursor) => { if (c === '+' || c === '-') { if (state === 'none') { state = 'sign' - number += c; + value += c; continue; } if (state === 'e') { state === 'exponent_sign'; - number += c; + value += c; continue; } } if (isDigit(c)) { if (state === 'none' || state === 'sign' || state === 'whole') { state = 'whole'; - number += c; + value += c; continue; } if (state === 'decimal_point' || state === 'decimal') { state = 'decimal'; - number += c; + value += c; continue; } if (state === 'e' || state === 'exponent_sign' || state === 'exponent') { state = 'exponent'; - number += c; + value += c; continue; } } if (c === '.') { if (state === 'none' || state === 'sign' || state === 'whole') { state = 'decimal_point'; - number += c; + value += c; continue; } } if (c === 'E' || c == 'e') { if (state === 'whole' || state === 'decimal_point' || state === 'decimal') { state = 'e'; - number += c; + value += c; continue; } } break; } - if (state === 'none' || state === 'sign' || state === 'e' || state === 'exponent_sign') { - return [i - 1, null]; + 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]; } - // step back to delegate iteration to parent loop - return [i - 1, Number(number)]; }; @@ -144,6 +146,7 @@ const parsePathData = (string) => { args = []; argsCount = argsCountPerCommand[command]; argsInSequenceCount = 0; + // flush command without args if (argsCount === 0) { pathData.push({ command, args }); } diff --git a/lib/path.test.js b/lib/path.test.js index 6956749fd..66a2887b2 100644 --- a/lib/path.test.js +++ b/lib/path.test.js @@ -45,5 +45,9 @@ describe('parse path data', () => { }); it('should stop if moveto not the first command', () => { expect(parsePathData('L 10 20')).to.deep.equal([]); + expect(parsePathData('10 20')).to.deep.equal([]); + }); + it('should stop on invalid numbers', () => { + expect(parsePathData('M ...')).to.deep.equal([]); }); }); From 9bd69d35684441c4da2a6d9fd3fb481060952f7f Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sat, 27 Feb 2021 00:09:16 +0300 Subject: [PATCH 09/12] Slightly simplify arcs parsing --- lib/path.js | 10 +++++----- lib/path.test.js | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/path.js b/lib/path.js index 72505e544..9ccdde73e 100644 --- a/lib/path.js +++ b/lib/path.js @@ -156,21 +156,21 @@ const parsePathData = (string) => { if (command == null) { return pathData; } - // read next arg + // read next argument let newCursor = i; let number = null; if (command === 'A' || command === 'a') { - const mod = args.length % argsCount; - if (mod === 0 || mod === 1) { + 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 (mod === 2 || mod === 5 || mod === 6) { + if (position === 2 || position === 5 || position === 6) { [newCursor, number] = readNumber(string, i); } - if (mod === 3 || mod === 4) { + if (position === 3 || position === 4) { // read flags if (c === '0') { number = 0; diff --git a/lib/path.test.js b/lib/path.test.js index 66a2887b2..daa1bfe32 100644 --- a/lib/path.test.js +++ b/lib/path.test.js @@ -50,4 +50,27 @@ describe('parse path data', () => { it('should stop on invalid numbers', () => { expect(parsePathData('M ...')).to.deep.equal([]); }); + it('should handle arcs', () => { + expect( + parsePathData( + ` + M600,350 + l 50,-25 + a25,25 -30 0,1 50,-25 + 25,50 -30 0,1 50,-25 + 25,75 -30 0,1 50,-25 + a25,100 -30 0,1 50,-25 + l 50,-25 + ` + ) + ).to.deep.equal([ + { "command": "M", "args": [600, 350] }, + { "command": "l", "args": [50, -25] }, + { "command": "a", "args": [25, 25, -30, 0, 1, 50, -25] }, + { "command": "a", "args": [25, 50, -30, 0, 1, 50, -25] }, + { "command": "a", "args": [25, 75, -30, 0, 1, 50, -25] }, + { "command": "a", "args": [25, 100, -30, 0, 1, 50, -25] }, + { "command": "l", "args": [50, -25] }, + ]); + }); }); From 9f8675cf018dbb3ee5a4f144b896b2efe0ca3154 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sat, 27 Feb 2021 00:19:56 +0300 Subject: [PATCH 10/12] Remove useless mod --- lib/path.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/path.js b/lib/path.js index 9ccdde73e..63dfee57b 100644 --- a/lib/path.js +++ b/lib/path.js @@ -137,8 +137,8 @@ const parsePathData = (string) => { return pathData; } } else { - // stop if previous command arguments are miscounted - if (argsCount !== 0 && args.length % argsCount !== 0) { + // stop if previous command arguments are not flushed + if (args.length !== 0) { return pathData; } } @@ -146,7 +146,7 @@ const parsePathData = (string) => { args = []; argsCount = argsCountPerCommand[command]; argsInSequenceCount = 0; - // flush command without args + // flush command without arguments if (argsCount === 0) { pathData.push({ command, args }); } @@ -189,8 +189,8 @@ const parsePathData = (string) => { argsInSequenceCount += 1; hadComma = false; i = newCursor; - // flush args - if (args.length >= argsCount && args.length % argsCount === 0) { + // 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') { @@ -202,9 +202,6 @@ const parsePathData = (string) => { args = []; } } - if (args.length !== argsCount) { - return pathData; - } return pathData; }; exports.parsePathData = parsePathData; From bc23d2f108774acfb71a436994a2a57a13443cf7 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sat, 27 Feb 2021 00:24:29 +0300 Subject: [PATCH 11/12] Simplify comma detection --- lib/path.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/path.js b/lib/path.js index 63dfee57b..5b4a89665 100644 --- a/lib/path.js +++ b/lib/path.js @@ -112,7 +112,7 @@ const parsePathData = (string) => { let command = null; let args; let argsCount; - let argsInSequenceCount = 0; + let canHaveComma = false; let hadComma = false; for (; i < string.length; i += 1) { const c = string.charAt(i); @@ -120,7 +120,7 @@ const parsePathData = (string) => { continue; } // allow comma only between arguments - if (argsInSequenceCount !== 0 && c === ',') { + if (canHaveComma && c === ',') { if (hadComma) { break; } @@ -145,7 +145,7 @@ const parsePathData = (string) => { command = c; args = []; argsCount = argsCountPerCommand[command]; - argsInSequenceCount = 0; + canHaveComma = false; // flush command without arguments if (argsCount === 0) { pathData.push({ command, args }); @@ -186,7 +186,7 @@ const parsePathData = (string) => { return pathData; } args.push(number); - argsInSequenceCount += 1; + canHaveComma = true; hadComma = false; i = newCursor; // flush arguments when necessary count is reached From 651aa6918f82eb7ea16af2f5df8c32d063a28e52 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 28 Feb 2021 15:37:37 +0300 Subject: [PATCH 12/12] Fix formatting --- lib/path.js | 21 ++++++++++++--------- lib/path.test.js | 14 +++++++------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/path.js b/lib/path.js index 5b4a89665..9b79550b0 100644 --- a/lib/path.js +++ b/lib/path.js @@ -22,24 +22,24 @@ const argsCountPerCommand = { T: 2, t: 2, A: 7, - a: 7 + a: 7, }; -const isCommand = c => { +const isCommand = (c) => { return c in argsCountPerCommand; }; -const isWsp = c => { +const isWsp = (c) => { const codePoint = c.codePointAt(0); return ( codePoint === 0x20 || codePoint === 0x9 || - codePoint === 0xD || - codePoint === 0xA + codePoint === 0xd || + codePoint === 0xa ); }; -const isDigit = c => { +const isDigit = (c) => { const codePoint = c.codePointAt(0); return 48 <= codePoint && codePoint <= 57; }; @@ -53,7 +53,7 @@ const readNumber = (string, cursor) => { const c = string[i]; if (c === '+' || c === '-') { if (state === 'none') { - state = 'sign' + state = 'sign'; value += c; continue; } @@ -88,7 +88,11 @@ const readNumber = (string, cursor) => { } } if (c === 'E' || c == 'e') { - if (state === 'whole' || state === 'decimal_point' || state === 'decimal') { + if ( + state === 'whole' || + state === 'decimal_point' || + state === 'decimal' + ) { state = 'e'; value += c; continue; @@ -105,7 +109,6 @@ const readNumber = (string, cursor) => { } }; - const parsePathData = (string) => { const pathData = []; let i = 0; diff --git a/lib/path.test.js b/lib/path.test.js index daa1bfe32..bcb1dbd48 100644 --- a/lib/path.test.js +++ b/lib/path.test.js @@ -64,13 +64,13 @@ describe('parse path data', () => { ` ) ).to.deep.equal([ - { "command": "M", "args": [600, 350] }, - { "command": "l", "args": [50, -25] }, - { "command": "a", "args": [25, 25, -30, 0, 1, 50, -25] }, - { "command": "a", "args": [25, 50, -30, 0, 1, 50, -25] }, - { "command": "a", "args": [25, 75, -30, 0, 1, 50, -25] }, - { "command": "a", "args": [25, 100, -30, 0, 1, 50, -25] }, - { "command": "l", "args": [50, -25] }, + { command: 'M', args: [600, 350] }, + { command: 'l', args: [50, -25] }, + { command: 'a', args: [25, 25, -30, 0, 1, 50, -25] }, + { command: 'a', args: [25, 50, -30, 0, 1, 50, -25] }, + { command: 'a', args: [25, 75, -30, 0, 1, 50, -25] }, + { command: 'a', args: [25, 100, -30, 0, 1, 50, -25] }, + { command: 'l', args: [50, -25] }, ]); }); });