'use strict'; // Parse SVG PathData // http://www.w3.org/TR/SVG/paths.html#PathDataBNF // Access to SVGPathData constructor var SVGPathData = require('./SVGPathData.js'); // TransformStream inherance required modules var TransformStream = require('readable-stream').Transform; var util = require('util'); // Private consts : Char groups var WSP = [' ', '\t', '\r', '\n']; var DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; var SIGNS = ['-', '+']; var EXPONENTS = ['e', 'E']; var DECPOINT = ['.']; var FLAGS = ['0', '1']; var COMMA = [',']; var COMMANDS = [ 'm', 'M', 'z', 'Z', 'l', 'L', 'h', 'H', 'v', 'V', 'c', 'C', 's', 'S', 'q', 'Q', 't', 'T', 'a', 'A', ]; // Inherit of transform stream util.inherits(SVGPathDataParser, TransformStream); // Constructor function SVGPathDataParser(options) { // Ensure new were used if(!(this instanceof SVGPathDataParser)) { return new SVGPathDataParser(options); } // Parent constructor TransformStream.call(this, { objectMode: true, }); // Setting objectMode separately this._writableState.objectMode = false; this._readableState.objectMode = true; // Parsing vars this.state = SVGPathDataParser.STATE_COMMAS_WSPS; this.curNumber = ''; this.curCommand = null; this._flush = function(callback) { this._transform(new Buffer(' '), 'utf-8', function() {}); // Adding residual command if(null !== this.curCommand) { if(this.curCommand.invalid) { this.emit('error', new SyntaxError('Unterminated command at the path end.')); } this.push(this.curCommand); this.curCommand = null; this.state ^= this.state & SVGPathDataParser.STATE_COMMANDS_MASK; } callback(); }; this._transform = function(chunk, encoding, callback) { var str = chunk.toString('buffer' !== encoding ? encoding : 'utf8'); var i; var j; for(i = 0, j = str.length; i < j; i++) { // White spaces parsing if(this.state & SVGPathDataParser.STATE_WSP || this.state & SVGPathDataParser.STATE_WSPS) { if(-1 !== WSP.indexOf(str[i])) { this.state ^= this.state & SVGPathDataParser.STATE_WSP; // any space stops current number parsing if('' !== this.curNumber) { this.state ^= this.state & SVGPathDataParser.STATE_NUMBER_MASK; } else { continue; } } } // Commas parsing if(this.state & SVGPathDataParser.STATE_COMMA || this.state & SVGPathDataParser.STATE_COMMAS) { if(-1 !== COMMA.indexOf(str[i])) { this.state ^= this.state & SVGPathDataParser.STATE_COMMA; // any comma stops current number parsing if('' !== this.curNumber) { this.state ^= this.state & SVGPathDataParser.STATE_NUMBER_MASK; } else { continue; } } } // Numbers parsing : -125.25e-125 if(this.state & SVGPathDataParser.STATE_NUMBER) { // Reading the sign if((this.state & SVGPathDataParser.STATE_NUMBER_MASK) === SVGPathDataParser.STATE_NUMBER) { this.state |= SVGPathDataParser.STATE_NUMBER_INT | SVGPathDataParser.STATE_NUMBER_DIGITS; if(-1 !== SIGNS.indexOf(str[i])) { this.curNumber += str[i]; continue; } } // Reading the exponent sign if(this.state & SVGPathDataParser.STATE_NUMBER_EXPSIGN) { this.state ^= SVGPathDataParser.STATE_NUMBER_EXPSIGN; this.state |= SVGPathDataParser.STATE_NUMBER_DIGITS; if(-1 !== SIGNS.indexOf(str[i])) { this.curNumber += str[i]; continue; } } // Reading digits if(this.state & SVGPathDataParser.STATE_NUMBER_DIGITS) { if(-1 !== DIGITS.indexOf(str[i])) { this.curNumber += str[i]; continue; } this.state ^= SVGPathDataParser.STATE_NUMBER_DIGITS; } // Ended reading left side digits if(this.state & SVGPathDataParser.STATE_NUMBER_INT) { this.state ^= SVGPathDataParser.STATE_NUMBER_INT; // if got a point, reading right side digits if(-1 !== DECPOINT.indexOf(str[i])) { this.curNumber += str[i]; this.state |= SVGPathDataParser.STATE_NUMBER_FLOAT | SVGPathDataParser.STATE_NUMBER_DIGITS; continue; // if got e/E, reading the exponent } else if(-1 !== EXPONENTS.indexOf(str[i])) { this.curNumber += str[i]; this.state |= SVGPathDataParser.STATE_NUMBER_EXP | SVGPathDataParser.STATE_NUMBER_EXPSIGN; continue; } // else we're done with that number this.state ^= this.state & SVGPathDataParser.STATE_NUMBER_MASK; } // Ended reading decimal digits if(this.state & SVGPathDataParser.STATE_NUMBER_FLOAT) { this.state ^= SVGPathDataParser.STATE_NUMBER_FLOAT; // if got e/E, reading the exponent if(-1 !== EXPONENTS.indexOf(str[i])) { this.curNumber += str[i]; this.state |= SVGPathDataParser.STATE_NUMBER_EXP | SVGPathDataParser.STATE_NUMBER_EXPSIGN; continue; } // else we're done with that number this.state ^= this.state & SVGPathDataParser.STATE_NUMBER_MASK; } // Ended reading exponent digits if(this.state & SVGPathDataParser.STATE_NUMBER_EXP) { // we're done with that number this.state ^= this.state & SVGPathDataParser.STATE_NUMBER_MASK; } } // New number if(this.curNumber) { // Horizontal move to command (x) if(this.state & SVGPathDataParser.STATE_HORIZ_LINE_TO) { if(null === this.curCommand) { this.push({ type: SVGPathData.HORIZ_LINE_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), x: Number(this.curNumber), }); } else { this.curCommand.x = Number(this.curNumber); delete this.curCommand.invalid; this.push(this.curCommand); this.curCommand = null; } this.state |= SVGPathDataParser.STATE_NUMBER; // Vertical move to command (y) } else if(this.state & SVGPathDataParser.STATE_VERT_LINE_TO) { if(null === this.curCommand) { this.push({ type: SVGPathData.VERT_LINE_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), y: Number(this.curNumber), }); } else { this.curCommand.y = Number(this.curNumber); delete this.curCommand.invalid; this.push(this.curCommand); this.curCommand = null; } this.state |= SVGPathDataParser.STATE_NUMBER; // Move to / line to / smooth quadratic curve to commands (x, y) } else if(this.state & SVGPathDataParser.STATE_MOVE_TO || this.state & SVGPathDataParser.STATE_LINE_TO || this.state & SVGPathDataParser.STATE_SMOOTH_QUAD_TO) { if(null === this.curCommand) { this.curCommand = { type: (this.state & SVGPathDataParser.STATE_MOVE_TO ? SVGPathData.MOVE_TO : (this.state & SVGPathDataParser.STATE_LINE_TO ? SVGPathData.LINE_TO : SVGPathData.SMOOTH_QUAD_TO ) ), relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), x: Number(this.curNumber), }; } else if('undefined' === typeof this.curCommand.x) { this.curCommand.x = Number(this.curNumber); } else { delete this.curCommand.invalid; this.curCommand.y = Number(this.curNumber); this.push(this.curCommand); this.curCommand = null; // Switch to line to state if(this.state & SVGPathDataParser.STATE_MOVE_TO) { this.state ^= SVGPathDataParser.STATE_MOVE_TO; this.state |= SVGPathDataParser.STATE_LINE_TO; } } this.state |= SVGPathDataParser.STATE_NUMBER; // Curve to commands (x1, y1, x2, y2, x, y) } else if(this.state & SVGPathDataParser.STATE_CURVE_TO) { if(null === this.curCommand) { this.curCommand = { type: SVGPathData.CURVE_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, x2: Number(this.curNumber), }; } else if('undefined' === typeof this.curCommand.x2) { this.curCommand.x2 = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.y2) { this.curCommand.y2 = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.x1) { this.curCommand.x1 = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.y1) { this.curCommand.y1 = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.x) { this.curCommand.x = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.y) { this.curCommand.y = Number(this.curNumber); delete this.curCommand.invalid; this.push(this.curCommand); this.curCommand = null; } this.state |= SVGPathDataParser.STATE_NUMBER; // Smooth curve to commands (x1, y1, x, y) } else if(this.state & SVGPathDataParser.STATE_SMOOTH_CURVE_TO) { if(null === this.curCommand) { this.curCommand = { type: SVGPathData.SMOOTH_CURVE_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, x2: Number(this.curNumber), }; } else if('undefined' === typeof this.curCommand.x2) { this.curCommand.x2 = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.y2) { this.curCommand.y2 = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.x) { this.curCommand.x = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.y) { this.curCommand.y = Number(this.curNumber); delete this.curCommand.invalid; this.push(this.curCommand); this.curCommand = null; } this.state |= SVGPathDataParser.STATE_NUMBER; // Quadratic bezier curve to commands (x1, y1, x, y) } else if(this.state & SVGPathDataParser.STATE_QUAD_TO) { if(null === this.curCommand) { this.curCommand = { type: SVGPathData.QUAD_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, x1: Number(this.curNumber), }; } else if('undefined' === typeof this.curCommand.x1) { this.curCommand.x1 = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.y1) { this.curCommand.y1 = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.x) { this.curCommand.x = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.y) { this.curCommand.y = Number(this.curNumber); delete this.curCommand.invalid; this.push(this.curCommand); this.curCommand = null; } this.state |= SVGPathDataParser.STATE_NUMBER; // Elliptic arc commands (rX, rY, xRot, lArcFlag, sweepFlag, x, y) } else if(this.state & SVGPathDataParser.STATE_ARC) { if(null === this.curCommand) { this.curCommand = { type: SVGPathData.ARC, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, rX: Number(this.curNumber), }; } else if('undefined' === typeof this.curCommand.rX) { if(0 > Number(this.curNumber)) { this.emit('error', new SyntaxError('Expected positive number,' + ' got "' + this.curNumber + '" at index "' + i + '"')); } this.curCommand.rX = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.rY) { if(0 > Number(this.curNumber)) { this.emit('error', new SyntaxError('Expected positive number,' + ' got "' + this.curNumber + '" at index "' + i + '"')); } this.curCommand.rY = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.xRot) { this.curCommand.xRot = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.lArcFlag) { if(-1 === FLAGS.indexOf(this.curNumber)) { this.emit('error', new SyntaxError('Expected a flag, got "' + this.curNumber + '" at index "' + i + '"')); } this.curCommand.lArcFlag = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.sweepFlag) { if('0' !== this.curNumber && '1' !== this.curNumber) { this.emit('error', new SyntaxError('Expected a flag, got "' + this.curNumber + '" at index "' + i + '"')); } this.curCommand.sweepFlag = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.x) { this.curCommand.x = Number(this.curNumber); } else if('undefined' === typeof this.curCommand.y) { this.curCommand.y = Number(this.curNumber); delete this.curCommand.invalid; this.push(this.curCommand); this.curCommand = null; } this.state |= SVGPathDataParser.STATE_NUMBER; } this.curNumber = ''; // Continue if a white space or a comma was detected if(-1 !== WSP.indexOf(str[i]) || -1 !== COMMA.indexOf(str[i])) { continue; } // if a sign is detected, then parse the new number if(-1 !== SIGNS.indexOf(str[i])) { this.curNumber = str[i]; this.state |= SVGPathDataParser.STATE_NUMBER_INT | SVGPathDataParser.STATE_NUMBER_DIGITS; continue; } // if the decpoint is detected, then parse the new number if(-1 !== DECPOINT.indexOf(str[i])) { this.curNumber = str[i]; this.state |= SVGPathDataParser.STATE_NUMBER_FLOAT | SVGPathDataParser.STATE_NUMBER_DIGITS; continue; } } // End of a command if(-1 !== COMMANDS.indexOf(str[i])) { // Adding residual command if(null !== this.curCommand) { if(this.curCommand.invalid) { this.emit('error', new SyntaxError('Unterminated command at index ' + i + '.')); } this.push(this.curCommand); this.curCommand = null; this.state ^= this.state & SVGPathDataParser.STATE_COMMANDS_MASK; } } // Detecting the next command this.state ^= this.state & SVGPathDataParser.STATE_COMMANDS_MASK; // Is the command relative if(str[i] === str[i].toLowerCase()) { this.state |= SVGPathDataParser.STATE_RELATIVE; } else { this.state ^= this.state & SVGPathDataParser.STATE_RELATIVE; } // Horizontal move to command if('z' === str[i].toLowerCase()) { this.push({ type: SVGPathData.CLOSE_PATH, }); this.state = SVGPathDataParser.STATE_COMMAS_WSPS; continue; // Horizontal move to command } else if('h' === str[i].toLowerCase()) { this.state |= SVGPathDataParser.STATE_HORIZ_LINE_TO; this.curCommand = { type: SVGPathData.HORIZ_LINE_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, }; // Vertical move to command } else if('v' === str[i].toLowerCase()) { this.state |= SVGPathDataParser.STATE_VERT_LINE_TO; this.curCommand = { type: SVGPathData.VERT_LINE_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, }; // Move to command } else if('m' === str[i].toLowerCase()) { this.state |= SVGPathDataParser.STATE_MOVE_TO; this.curCommand = { type: SVGPathData.MOVE_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, }; // Line to command } else if('l' === str[i].toLowerCase()) { this.state |= SVGPathDataParser.STATE_LINE_TO; this.curCommand = { type: SVGPathData.LINE_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, }; // Curve to command } else if('c' === str[i].toLowerCase()) { this.state |= SVGPathDataParser.STATE_CURVE_TO; this.curCommand = { type: SVGPathData.CURVE_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, }; // Smooth curve to command } else if('s' === str[i].toLowerCase()) { this.state |= SVGPathDataParser.STATE_SMOOTH_CURVE_TO; this.curCommand = { type: SVGPathData.SMOOTH_CURVE_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, }; // Quadratic bezier curve to command } else if('q' === str[i].toLowerCase()) { this.state |= SVGPathDataParser.STATE_QUAD_TO; this.curCommand = { type: SVGPathData.QUAD_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, }; // Smooth quadratic bezier curve to command } else if('t' === str[i].toLowerCase()) { this.state |= SVGPathDataParser.STATE_SMOOTH_QUAD_TO; this.curCommand = { type: SVGPathData.SMOOTH_QUAD_TO, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, }; // Elliptic arc command } else if('a' === str[i].toLowerCase()) { this.state |= SVGPathDataParser.STATE_ARC; this.curCommand = { type: SVGPathData.ARC, relative: !!(this.state & SVGPathDataParser.STATE_RELATIVE), invalid: true, }; // Unkown command } else { this.emit('error', new SyntaxError('Unexpected character "' + str[i] + '" at index ' + i + '.')); } // White spaces can follow a command this.state |= SVGPathDataParser.STATE_COMMAS_WSPS | SVGPathDataParser.STATE_NUMBER; } callback(); }; } // Static consts // Parsing states SVGPathDataParser.STATE_WSP = 1; SVGPathDataParser.STATE_WSPS = 2; SVGPathDataParser.STATE_COMMA = 4; SVGPathDataParser.STATE_COMMAS = 8; SVGPathDataParser.STATE_COMMAS_WSPS = SVGPathDataParser.STATE_WSP | SVGPathDataParser.STATE_WSPS | SVGPathDataParser.STATE_COMMA | SVGPathDataParser.STATE_COMMAS; SVGPathDataParser.STATE_NUMBER = 16; SVGPathDataParser.STATE_NUMBER_DIGITS = 32; SVGPathDataParser.STATE_NUMBER_INT = 64; SVGPathDataParser.STATE_NUMBER_FLOAT = 128; SVGPathDataParser.STATE_NUMBER_EXP = 256; SVGPathDataParser.STATE_NUMBER_EXPSIGN = 512; SVGPathDataParser.STATE_NUMBER_MASK = SVGPathDataParser.STATE_NUMBER | SVGPathDataParser.STATE_NUMBER_DIGITS | SVGPathDataParser.STATE_NUMBER_INT | SVGPathDataParser.STATE_NUMBER_EXP | SVGPathDataParser.STATE_NUMBER_FLOAT; SVGPathDataParser.STATE_RELATIVE = 1024; SVGPathDataParser.STATE_CLOSE_PATH = 2048; // Close path command (z/Z) SVGPathDataParser.STATE_MOVE_TO = 4096; // Move to command (m/M) SVGPathDataParser.STATE_LINE_TO = 8192; // Line to command (l/L=) SVGPathDataParser.STATE_HORIZ_LINE_TO = 16384; // Horizontal line to command (h/H) SVGPathDataParser.STATE_VERT_LINE_TO = 32768; // Vertical line to command (v/V) SVGPathDataParser.STATE_CURVE_TO = 65536; // Curve to command (c/C) SVGPathDataParser.STATE_SMOOTH_CURVE_TO = 131072; // Smooth curve to command (s/S) SVGPathDataParser.STATE_QUAD_TO = 262144; // Quadratic bezier curve to command (q/Q) SVGPathDataParser.STATE_SMOOTH_QUAD_TO = 524288; // Smooth quadratic bezier curve to command (t/T) SVGPathDataParser.STATE_ARC = 1048576; // Elliptic arc command (a/A) SVGPathDataParser.STATE_COMMANDS_MASK = SVGPathDataParser.STATE_CLOSE_PATH | SVGPathDataParser.STATE_MOVE_TO | SVGPathDataParser.STATE_LINE_TO | SVGPathDataParser.STATE_HORIZ_LINE_TO | SVGPathDataParser.STATE_VERT_LINE_TO | SVGPathDataParser.STATE_CURVE_TO | SVGPathDataParser.STATE_SMOOTH_CURVE_TO | SVGPathDataParser.STATE_QUAD_TO | SVGPathDataParser.STATE_SMOOTH_QUAD_TO | SVGPathDataParser.STATE_ARC; module.exports = SVGPathDataParser;