diff options
author | Ulrich-Matthias Schäfer <ulima.ums@googlemail.com> | 2020-09-05 23:06:25 +1000 |
---|---|---|
committer | Ulrich-Matthias Schäfer <ulima.ums@googlemail.com> | 2020-09-05 23:06:25 +1000 |
commit | 3f78cb81973f9c6c9a0754a555c939e81d24a1ff (patch) | |
tree | 3733c22051219b118ad5a09aff15838081be2820 /src | |
parent | cfa5fd5b4138bdaea7b7afae3ccadac3735a3e7b (diff) | |
download | svg.js-3f78cb81973f9c6c9a0754a555c939e81d24a1ff.tar.gz svg.js-3f78cb81973f9c6c9a0754a555c939e81d24a1ff.zip |
fix path parsing (#1145)
Diffstat (limited to 'src')
-rw-r--r-- | src/animation/Morphable.js | 4 | ||||
-rw-r--r-- | src/modules/core/regex.js | 16 | ||||
-rw-r--r-- | src/types/PathArray.js | 133 | ||||
-rw-r--r-- | src/utils/pathParser.js | 234 |
4 files changed, 242 insertions, 145 deletions
diff --git a/src/animation/Morphable.js b/src/animation/Morphable.js index 46dd166..3344480 100644 --- a/src/animation/Morphable.js +++ b/src/animation/Morphable.js @@ -2,7 +2,7 @@ import { Ease } from './Controller.js' import { delimiter, numberAndUnit, - pathLetters + isPathLetter } from '../modules/core/regex.js' import { extend } from '../utils/adopter.js' import Color from '../types/Color.js' @@ -19,7 +19,7 @@ const getClassForType = (value) => { if (Color.isColor(value)) { return Color } else if (delimiter.test(value)) { - return pathLetters.test(value) + return isPathLetter.test(value) ? PathArray : SVGArray } else if (numberAndUnit.test(value)) { diff --git a/src/modules/core/regex.js b/src/modules/core/regex.js index c32ac93..a18c692 100644 --- a/src/modules/core/regex.js +++ b/src/modules/core/regex.js @@ -34,19 +34,5 @@ export const isImage = /\.(jpg|jpeg|png|gif|svg)(\?[^=]+.*)?/i // split at whitespace and comma export const delimiter = /[\s,]+/ -// The following regex are used to parse the d attribute of a path - -// Matches all hyphens which are preceeded by something but not an exponent -export const hyphen = /([^e])-/gi - -// Replaces and tests for all path letters -export const pathLetters = /[MLHVCSQTAZ]/gi - -// yes we need this one, too +// Test for path letter export const isPathLetter = /[MLHVCSQTAZ]/i - -// matches 0.154.23.45 -export const numbersWithDots = /((\d?\.\d+(?:e[+-]?\d+)?)((?:\.\d+(?:e[+-]?\d+)?)+))+/gi - -// matches . -export const dots = /\./g diff --git a/src/types/PathArray.js b/src/types/PathArray.js index b7c3c33..e021238 100644 --- a/src/types/PathArray.js +++ b/src/types/PathArray.js @@ -1,19 +1,7 @@ -import { - delimiter, - dots, - hyphen, - isPathLetter, - numbersWithDots, - pathLetters -} from '../modules/core/regex.js' -import Point from './Point.js' import SVGArray from './SVGArray.js' import parser from '../modules/core/parser.js' import Box from './Box.js' - -export function pathRegReplace (a, b, c, d) { - return c + d.replace(dots, ' .') -} +import { pathParser } from '../utils/pathParser.js' function arrayToString (a) { for (var i = 0, il = a.length, s = ''; i < il; i++) { @@ -51,79 +39,6 @@ function arrayToString (a) { return s + ' ' } -const pathHandlers = { - M: function (c, p, p0) { - p.x = p0.x = c[0] - p.y = p0.y = c[1] - - return [ 'M', p.x, p.y ] - }, - L: function (c, p) { - p.x = c[0] - p.y = c[1] - return [ 'L', c[0], c[1] ] - }, - H: function (c, p) { - p.x = c[0] - return [ 'H', c[0] ] - }, - V: function (c, p) { - p.y = c[0] - return [ 'V', c[0] ] - }, - C: function (c, p) { - p.x = c[4] - p.y = c[5] - return [ 'C', c[0], c[1], c[2], c[3], c[4], c[5] ] - }, - S: function (c, p) { - p.x = c[2] - p.y = c[3] - return [ 'S', c[0], c[1], c[2], c[3] ] - }, - Q: function (c, p) { - p.x = c[2] - p.y = c[3] - return [ 'Q', c[0], c[1], c[2], c[3] ] - }, - T: function (c, p) { - p.x = c[0] - p.y = c[1] - return [ 'T', c[0], c[1] ] - }, - Z: function (c, p, p0) { - p.x = p0.x - p.y = p0.y - return [ 'Z' ] - }, - A: function (c, p) { - p.x = c[5] - p.y = c[6] - return [ 'A', c[0], c[1], c[2], c[3], c[4], c[5], c[6] ] - } -} - -const mlhvqtcsaz = 'mlhvqtcsaz'.split('') - -for (var i = 0, il = mlhvqtcsaz.length; i < il; ++i) { - pathHandlers[mlhvqtcsaz[i]] = (function (i) { - return function (c, p, p0) { - if (i === 'H') c[0] = c[0] + p.x - else if (i === 'V') c[0] = c[0] + p.y - else if (i === 'A') { - c[5] = c[5] + p.x - c[6] = c[6] + p.y - } else { - for (var j = 0, jl = c.length; j < jl; ++j) { - c[j] = c[j] + (j % 2 ? p.y : p.x) - } - } - - return pathHandlers[i](c, p, p0) - } - })(mlhvqtcsaz[i].toUpperCase()) -} - export default class PathArray extends SVGArray { // Get bounding box of path bbox () { @@ -173,50 +88,12 @@ export default class PathArray extends SVGArray { } // Absolutize and parse path to array - parse (array = [ 'M', 0, 0 ]) { - // prepare for parsing - var s - var paramCnt = { M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2, A: 7, Z: 0 } - - if (typeof array === 'string') { - array = array - .replace(numbersWithDots, pathRegReplace) // convert 45.123.123 to 45.123 .123 - .replace(pathLetters, ' $& ') // put some room between letters and numbers - .replace(hyphen, '$1 -') // add space before hyphen - .trim() // trim - .split(delimiter) // split into array - } else { - // Flatten array - array = Array.prototype.concat.apply([], array) + parse (d = 'M0 0') { + if (Array.isArray(d)) { + d = Array.prototype.concat.apply([], d).toString() } - // array now is an array containing all parts of a path e.g. ['M', '0', '0', 'L', '30', '30' ...] - var result = [] - var p = new Point() - var p0 = new Point() - var index = 0 - var len = array.length - - do { - // Test if we have a path letter - if (isPathLetter.test(array[index])) { - s = array[index] - ++index - // If last letter was a move command and we got no new, it defaults to [L]ine - } else if (s === 'M') { - s = 'L' - } else if (s === 'm') { - s = 'l' - } - - result.push(pathHandlers[s].call(null, - array.slice(index, (index = index + paramCnt[s.toUpperCase()])).map(parseFloat), - p, p0 - ) - ) - } while (len > index) - - return result + return pathParser(d) } // Resize path string diff --git a/src/utils/pathParser.js b/src/utils/pathParser.js new file mode 100644 index 0000000..1ab90a3 --- /dev/null +++ b/src/utils/pathParser.js @@ -0,0 +1,234 @@ +import { isPathLetter } from '../modules/core/regex.js' +import Point from '../types/Point.js' + +const segmentParameters = { M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2, A: 7, Z: 0 } + +const pathHandlers = { + M: function (c, p, p0) { + p.x = p0.x = c[0] + p.y = p0.y = c[1] + + return [ 'M', p.x, p.y ] + }, + L: function (c, p) { + p.x = c[0] + p.y = c[1] + return [ 'L', c[0], c[1] ] + }, + H: function (c, p) { + p.x = c[0] + return [ 'H', c[0] ] + }, + V: function (c, p) { + p.y = c[0] + return [ 'V', c[0] ] + }, + C: function (c, p) { + p.x = c[4] + p.y = c[5] + return [ 'C', c[0], c[1], c[2], c[3], c[4], c[5] ] + }, + S: function (c, p) { + p.x = c[2] + p.y = c[3] + return [ 'S', c[0], c[1], c[2], c[3] ] + }, + Q: function (c, p) { + p.x = c[2] + p.y = c[3] + return [ 'Q', c[0], c[1], c[2], c[3] ] + }, + T: function (c, p) { + p.x = c[0] + p.y = c[1] + return [ 'T', c[0], c[1] ] + }, + Z: function (c, p, p0) { + p.x = p0.x + p.y = p0.y + return [ 'Z' ] + }, + A: function (c, p) { + p.x = c[5] + p.y = c[6] + return [ 'A', c[0], c[1], c[2], c[3], c[4], c[5], c[6] ] + } +} + +const mlhvqtcsaz = 'mlhvqtcsaz'.split('') + +for (var i = 0, il = mlhvqtcsaz.length; i < il; ++i) { + pathHandlers[mlhvqtcsaz[i]] = (function (i) { + return function (c, p, p0) { + if (i === 'H') c[0] = c[0] + p.x + else if (i === 'V') c[0] = c[0] + p.y + else if (i === 'A') { + c[5] = c[5] + p.x + c[6] = c[6] + p.y + } else { + for (var j = 0, jl = c.length; j < jl; ++j) { + c[j] = c[j] + (j % 2 ? p.y : p.x) + } + } + + return pathHandlers[i](c, p, p0) + } + })(mlhvqtcsaz[i].toUpperCase()) +} + +function makeAbsolut (parser) { + const command = parser.segment[0] + return pathHandlers[command](parser.segment.slice(1), parser.p, parser.p0) +} + +function segmentComplete (parser) { + return parser.segment.length && parser.segment.length - 1 === segmentParameters[parser.segment[0].toUpperCase()] +} + +function startNewSegment (parser, token) { + parser.inNumber && finalizeNumber(parser, false) + const pathLetter = isPathLetter.test(token) + + if (pathLetter) { + parser.segment = [ token ] + } else { + const lastCommand = parser.lastCommand + const small = lastCommand.toLowerCase() + const isSmall = lastCommand === small + parser.segment = [ small === 'm' ? (isSmall ? 'l' : 'L') : lastCommand ] + } + + parser.inSegment = true + parser.lastCommand = parser.segment[0] + + return pathLetter +} + +function finalizeNumber (parser, inNumber) { + if (!parser.inNumber) throw new Error('Parser Error') + parser.number && parser.segment.push(parseFloat(parser.number)) + parser.inNumber = inNumber + parser.number = '' + parser.pointSeen = false + parser.hasExponent = false + + if (segmentComplete(parser)) { + finalizeSegment(parser) + } +} + +function finalizeSegment (parser) { + parser.inSegment = false + if (parser.absolute) { + parser.segment = makeAbsolut(parser) + } + parser.segments.push(parser.segment) +} + +function isArcFlag (parser) { + if (!parser.segment.length) return false + const isArc = parser.segment[0].toUpperCase() === 'A' + const length = parser.segment.length + + return isArc && (length === 4 || length === 5) +} + +function isExponential (parser) { + return parser.lastToken.toUpperCase() === 'E' +} + +export function pathParser (d, toAbsolute = true) { + + let index = 0 + let token = '' + const parser = { + segment: [], + inNumber: false, + number: '', + lastToken: '', + inSegment: false, + segments: [], + pointSeen: false, + hasExponent: false, + absolute: toAbsolute, + p0: new Point(), + p: new Point() + } + + while ((parser.lastToken = token, token = d.charAt(index++))) { + if (!parser.inSegment) { + if (startNewSegment(parser, token)) { + continue + } + } + + if (token === '.') { + if (parser.pointSeen || parser.hasExponent) { + finalizeNumber(parser, false) + --index + continue + } + parser.inNumber = true + parser.pointSeen = true + parser.number += token + continue + } + + if (!isNaN(parseInt(token))) { + + if (parser.number === '0' || (parser.inNumber && isArcFlag(parser))) { + finalizeNumber(parser, true) + } + + parser.inNumber = true + parser.number += token + continue + } + + if (token === ' ' || token === ',') { + if (parser.inNumber) { + finalizeNumber(parser, false) + } + continue + } + + if (token === '-') { + if (parser.inNumber && !isExponential(parser)) { + finalizeNumber(parser, false) + --index + continue + } + parser.number += token + parser.inNumber = true + continue + } + + if (token.toUpperCase() === 'E') { + parser.number += token + parser.hasExponent = true + continue + } + + if (isPathLetter.test(token)) { + if (parser.inNumber) { + finalizeNumber(parser, false) + } else if (!segmentComplete(parser)) { + throw new Error('parser Error') + } else { + finalizeSegment(parser) + } + --index + } + } + + if (parser.inNumber) { + finalizeNumber(parser, false) + } + + if (parser.inSegment && segmentComplete(parser)) { + finalizeSegment(parser) + } + + return parser.segments + +} |