Spaces:
Sleeping
Sleeping
| // Parse SVG PathData | |
| // http://www.w3.org/TR/SVG/paths.html#PathDataBNF | |
| import { COMMAND_ARG_COUNTS, SVGPathData } from "./SVGPathData"; | |
| import { TransformableSVG } from "./TransformableSVG"; | |
| import { SVGCommand, TransformFunction } from "./types"; | |
| // Private consts : Char groups | |
| const isWhiteSpace = (c: string) => | |
| " " === c || "\t" === c || "\r" === c || "\n" === c; | |
| const isDigit = (c: string) => | |
| "0".charCodeAt(0) <= c.charCodeAt(0) && c.charCodeAt(0) <= "9".charCodeAt(0); | |
| const COMMANDS = "mMzZlLhHvVcCsSqQtTaA"; | |
| export class SVGPathDataParser extends TransformableSVG { | |
| private curNumber: string = ""; | |
| private curCommandType: SVGCommand["type"] | -1 = -1; | |
| private curCommandRelative = false; | |
| private canParseCommandOrComma = true; | |
| private curNumberHasExp = false; | |
| private curNumberHasExpDigits = false; | |
| private curNumberHasDecimal = false; | |
| private curArgs: number[] = []; | |
| constructor() { | |
| super(); | |
| } | |
| finish(commands: SVGCommand[] = []) { | |
| this.parse(" ", commands); | |
| // Adding residual command | |
| if (0 !== this.curArgs.length || !this.canParseCommandOrComma) { | |
| throw new SyntaxError("Unterminated command at the path end."); | |
| } | |
| return commands; | |
| } | |
| parse(str: string, commands: SVGCommand[] = []) { | |
| const finishCommand = (command: SVGCommand) => { | |
| commands.push(command); | |
| this.curArgs.length = 0; | |
| this.canParseCommandOrComma = true; | |
| }; | |
| for (let i = 0; i < str.length; i++) { | |
| const c = str[i]; | |
| // White spaces parsing | |
| const isAArcFlag = this.curCommandType === SVGPathData.ARC && | |
| (this.curArgs.length === 3 || this.curArgs.length === 4) && | |
| this.curNumber.length === 1 && | |
| (this.curNumber === "0" || this.curNumber === "1"); | |
| const isEndingDigit = isDigit(c) && ( | |
| (this.curNumber === "0" && c === "0") || | |
| isAArcFlag | |
| ); | |
| if ( | |
| isDigit(c) && | |
| !isEndingDigit | |
| ) { | |
| this.curNumber += c; | |
| this.curNumberHasExpDigits = this.curNumberHasExp; | |
| continue; | |
| } | |
| if ("e" === c || "E" === c) { | |
| this.curNumber += c; | |
| this.curNumberHasExp = true; | |
| continue; | |
| } | |
| if ( | |
| ("-" === c || "+" === c) && | |
| this.curNumberHasExp && | |
| !this.curNumberHasExpDigits | |
| ) { | |
| this.curNumber += c; | |
| continue; | |
| } | |
| // if we already have a ".", it means we are starting a new number | |
| if ("." === c && !this.curNumberHasExp && !this.curNumberHasDecimal && !isAArcFlag) { | |
| this.curNumber += c; | |
| this.curNumberHasDecimal = true; | |
| continue; | |
| } | |
| // New number | |
| if (this.curNumber && -1 !== this.curCommandType) { | |
| const val = Number(this.curNumber); | |
| if (isNaN(val)) { | |
| throw new SyntaxError(`Invalid number ending at ${i}`); | |
| } | |
| if (this.curCommandType === SVGPathData.ARC) { | |
| if (0 === this.curArgs.length || 1 === this.curArgs.length) { | |
| if (0 > val) { | |
| throw new SyntaxError( | |
| `Expected positive number, got "${val}" at index "${i}"`, | |
| ); | |
| } | |
| } else if (3 === this.curArgs.length || 4 === this.curArgs.length) { | |
| if ("0" !== this.curNumber && "1" !== this.curNumber) { | |
| throw new SyntaxError( | |
| `Expected a flag, got "${this.curNumber}" at index "${i}"`, | |
| ); | |
| } | |
| } | |
| } | |
| this.curArgs.push(val); | |
| if (this.curArgs.length === COMMAND_ARG_COUNTS[this.curCommandType]) { | |
| if (SVGPathData.HORIZ_LINE_TO === this.curCommandType) { | |
| finishCommand({ | |
| type: SVGPathData.HORIZ_LINE_TO, | |
| relative: this.curCommandRelative, | |
| x: val, | |
| }); | |
| } else if (SVGPathData.VERT_LINE_TO === this.curCommandType) { | |
| finishCommand({ | |
| type: SVGPathData.VERT_LINE_TO, | |
| relative: this.curCommandRelative, | |
| y: val, | |
| }); | |
| // Move to / line to / smooth quadratic curve to commands (x, y) | |
| } else if ( | |
| this.curCommandType === SVGPathData.MOVE_TO || | |
| this.curCommandType === SVGPathData.LINE_TO || | |
| this.curCommandType === SVGPathData.SMOOTH_QUAD_TO | |
| ) { | |
| finishCommand({ | |
| type: this.curCommandType, | |
| relative: this.curCommandRelative, | |
| x: this.curArgs[0], | |
| y: this.curArgs[1], | |
| } as SVGCommand); | |
| // Switch to line to state | |
| if (SVGPathData.MOVE_TO === this.curCommandType) { | |
| this.curCommandType = SVGPathData.LINE_TO; | |
| } | |
| } else if (this.curCommandType === SVGPathData.CURVE_TO) { | |
| finishCommand({ | |
| type: SVGPathData.CURVE_TO, | |
| relative: this.curCommandRelative, | |
| x1: this.curArgs[0], | |
| y1: this.curArgs[1], | |
| x2: this.curArgs[2], | |
| y2: this.curArgs[3], | |
| x: this.curArgs[4], | |
| y: this.curArgs[5], | |
| }); | |
| } else if (this.curCommandType === SVGPathData.SMOOTH_CURVE_TO) { | |
| finishCommand({ | |
| type: SVGPathData.SMOOTH_CURVE_TO, | |
| relative: this.curCommandRelative, | |
| x2: this.curArgs[0], | |
| y2: this.curArgs[1], | |
| x: this.curArgs[2], | |
| y: this.curArgs[3], | |
| }); | |
| } else if (this.curCommandType === SVGPathData.QUAD_TO) { | |
| finishCommand({ | |
| type: SVGPathData.QUAD_TO, | |
| relative: this.curCommandRelative, | |
| x1: this.curArgs[0], | |
| y1: this.curArgs[1], | |
| x: this.curArgs[2], | |
| y: this.curArgs[3], | |
| }); | |
| } else if (this.curCommandType === SVGPathData.ARC) { | |
| finishCommand({ | |
| type: SVGPathData.ARC, | |
| relative: this.curCommandRelative, | |
| rX: this.curArgs[0], | |
| rY: this.curArgs[1], | |
| xRot: this.curArgs[2], | |
| lArcFlag: this.curArgs[3] as 0 | 1, | |
| sweepFlag: this.curArgs[4] as 0 | 1, | |
| x: this.curArgs[5], | |
| y: this.curArgs[6], | |
| }); | |
| } | |
| } | |
| this.curNumber = ""; | |
| this.curNumberHasExpDigits = false; | |
| this.curNumberHasExp = false; | |
| this.curNumberHasDecimal = false; | |
| this.canParseCommandOrComma = true; | |
| } | |
| // Continue if a white space or a comma was detected | |
| if (isWhiteSpace(c)) { | |
| continue; | |
| } | |
| if ("," === c && this.canParseCommandOrComma) { | |
| // L 0,0, H is not valid: | |
| this.canParseCommandOrComma = false; | |
| continue; | |
| } | |
| // if a sign is detected, then parse the new number | |
| if ("+" === c || "-" === c || "." === c) { | |
| this.curNumber = c; | |
| this.curNumberHasDecimal = "." === c; | |
| continue; | |
| } | |
| // if a 0 is detected, then parse the new number | |
| if (isEndingDigit) { | |
| this.curNumber = c; | |
| this.curNumberHasDecimal = false; | |
| continue; | |
| } | |
| // Adding residual command | |
| if (0 !== this.curArgs.length) { | |
| throw new SyntaxError(`Unterminated command at index ${i}.`); | |
| } | |
| if (!this.canParseCommandOrComma) { | |
| throw new SyntaxError( | |
| `Unexpected character "${c}" at index ${i}. Command cannot follow comma`, | |
| ); | |
| } | |
| this.canParseCommandOrComma = false; | |
| // Detecting the next command | |
| if ("z" === c || "Z" === c) { | |
| commands.push({ | |
| type: SVGPathData.CLOSE_PATH, | |
| }); | |
| this.canParseCommandOrComma = true; | |
| this.curCommandType = -1; | |
| continue; | |
| // Horizontal move to command | |
| } else if ("h" === c || "H" === c) { | |
| this.curCommandType = SVGPathData.HORIZ_LINE_TO; | |
| this.curCommandRelative = "h" === c; | |
| // Vertical move to command | |
| } else if ("v" === c || "V" === c) { | |
| this.curCommandType = SVGPathData.VERT_LINE_TO; | |
| this.curCommandRelative = "v" === c; | |
| // Move to command | |
| } else if ("m" === c || "M" === c) { | |
| this.curCommandType = SVGPathData.MOVE_TO; | |
| this.curCommandRelative = "m" === c; | |
| // Line to command | |
| } else if ("l" === c || "L" === c) { | |
| this.curCommandType = SVGPathData.LINE_TO; | |
| this.curCommandRelative = "l" === c; | |
| // Curve to command | |
| } else if ("c" === c || "C" === c) { | |
| this.curCommandType = SVGPathData.CURVE_TO; | |
| this.curCommandRelative = "c" === c; | |
| // Smooth curve to command | |
| } else if ("s" === c || "S" === c) { | |
| this.curCommandType = SVGPathData.SMOOTH_CURVE_TO; | |
| this.curCommandRelative = "s" === c; | |
| // Quadratic bezier curve to command | |
| } else if ("q" === c || "Q" === c) { | |
| this.curCommandType = SVGPathData.QUAD_TO; | |
| this.curCommandRelative = "q" === c; | |
| // Smooth quadratic bezier curve to command | |
| } else if ("t" === c || "T" === c) { | |
| this.curCommandType = SVGPathData.SMOOTH_QUAD_TO; | |
| this.curCommandRelative = "t" === c; | |
| // Elliptic arc command | |
| } else if ("a" === c || "A" === c) { | |
| this.curCommandType = SVGPathData.ARC; | |
| this.curCommandRelative = "a" === c; | |
| } else { | |
| throw new SyntaxError(`Unexpected character "${c}" at index ${i}.`); | |
| } | |
| } | |
| return commands; | |
| } | |
| /** | |
| * Return a wrapper around this parser which applies the transformation on parsed commands. | |
| */ | |
| transform(transform: TransformFunction) { | |
| const result = Object.create(this, { | |
| parse: { | |
| value(chunk: string, commands: SVGCommand[] = []) { | |
| const parsedCommands = Object.getPrototypeOf(this).parse.call( | |
| this, | |
| chunk, | |
| ); | |
| for (const c of parsedCommands) { | |
| const cT = transform(c); | |
| if (Array.isArray(cT)) { | |
| commands.push(...cT); | |
| } else { | |
| commands.push(cT); | |
| } | |
| } | |
| return commands; | |
| }, | |
| }, | |
| }); | |
| return result as this; | |
| } | |
| } | |