Browse Source

fix path parsing (#1145)

tags/3.1.0
Ulrich-Matthias Schäfer 3 years ago
parent
commit
3f78cb8197

+ 1
- 0
CHANGELOG.md View File

@@ -38,6 +38,7 @@ The document follows the conventions described in [“Keep a CHANGELOG”](http:
- fixed animate attr which is also retargetable now
- fixed internals of ObjectBag which can hold other Morphable values now
- fixed animate transform which didnt change its origin on retarget for declaritive animations
- fixed path parsing (#1145)

### Added
- added second Parameter to `SVG(el, isHTML)` which allows to explicitely create elements in the HTML namespace (#1058)

+ 0
- 30
spec/spec/modules/core/regex.js View File

@@ -200,23 +200,6 @@ describe('regex.js', () => {
})
})

describe('hyphen', () => {
it('matches all hyphens which are preceeded by something but not an exponent', () => {
expect(regex.hyphen.test('a-')).toBe(true)
expect(regex.hyphen.test('e-')).toBe(false)
})

it('replaces all hyphens and their preceeding char which is not an exponent', () => {
expect(' - e- ---E-'.replace(regex.hyphen, '.')).toBe(' . e-..E-')
})
})

describe('pathLetters', () => {
it('replaces all path letters', () => {
expect('MLHVCSQTAZBImlhvcsqtazbi'.replace(regex.pathLetters, '.')).toBe('..........BI..........bi')
})
})

describe('isPathLetter', () => {
it('returns true if something is a path letter', () => {
'MLHVCSQTAZmlhvcsqtaz'.split('').forEach((l) => {
@@ -230,17 +213,4 @@ describe('regex.js', () => {
})
})
})

describe('numbersWithDots', () => {
it('matches numbers of the form 3.13.123', () => {
const match = '-123.23.45 +123.324 43.43.54.65 123 33 .24 .34.34 -.54 .54-.65 123.34e34.21'.match(regex.numbersWithDots)
expect(match).toEqual([ '3.23.45', '3.43.54.65', '.34.34', '3.34e34.21' ])
})
})

describe('dots', () => {
it('replaces all dots', () => {
expect('..asd..'.replace(regex.dots, 'a')).toBe('aaasdaa')
})
})
})

+ 1
- 26
spec/spec/types/PathArray.js View File

@@ -3,29 +3,12 @@
import { PathArray, Box } from '../../../src/main.js'

describe('PathArray.js', () => {
let p1, p2, p3, p4, p5, p6, p7
let p1, p2, p3

beforeEach(() => {
p1 = new PathArray('m10 10 h 80 v 80 h -80 l 300 400 z')
p2 = new PathArray('m10 80 c 40 10 65 10 95 80 s 150 150 180 80 t 300 300 q 52 10 95 80 z')
p3 = new PathArray('m80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 z')
p4 = new PathArray('M215.458,245.23c0,0,77.403,0,94.274,0S405,216.451,405,138.054S329.581,15,287.9,15c-41.68,0-139.924,0-170.688,0C86.45,15,15,60.65,15,134.084c0,73.434,96.259,112.137,114.122,112.137C146.984,246.221,215.458,245.23,215.458,245.23z')
p5 = new PathArray('M10 10-45-30.5.5 .89L2e-2.5.5.5-.5C.5.5.5.5.5.5L-3-4z')
p6 = new PathArray('m 0,0 0,3189 2209,0 0,-3189 -2209,0 z m 154,154 1901,0 0,2881 -1901,0 0,-2881 z')
p7 = new PathArray('m 0,0 a 45 45, 0, 0, 0, 125 125')
})

it('converts to absolute values', () => {
expect(p1.toString()).toBe('M10 10H90V90H10L310 490Z ')
expect(p2.toString()).toBe('M10 80C50 90 75 90 105 160S255 310 285 240T585 540Q637 550 680 620Z ')
expect(p3.toString()).toBe('M80 80A45 45 0 0 0 125 125L125 80Z ')
expect(p4.toString()).toBe('M215.458 245.23C215.458 245.23 292.861 245.23 309.73199999999997 245.23S405 216.451 405 138.054S329.581 15 287.9 15C246.21999999999997 15 147.97599999999997 15 117.21199999999999 15C86.45 15 15 60.65 15 134.084C15 207.518 111.259 246.221 129.122 246.221C146.984 246.221 215.458 245.23 215.458 245.23Z ')
expect(p6.toString()).toBe('M0 0L0 3189L2209 3189L2209 0L0 0ZM154 154L2055 154L2055 3035L154 3035L154 154Z ')
expect(p7.toString()).toBe('M0 0A45 45 0 0 0 125 125 ')
})

it('parses difficult syntax correctly', () => {
expect(p5.toString()).toBe('M10 10L-45 -30.5L0.5 0.89L0.02 0.5L0.5 -0.5C0.5 0.5 0.5 0.5 0.5 0.5L-3 -4Z ')
})

it('parses flat arrays correctly', () => {
@@ -45,14 +28,6 @@ describe('PathArray.js', () => {
expect((new PathArray(p))).toEqual(p)
})

it('can handle all formats which can be used', () => {
// when no command is specified after move, line is used automatically (specs say so)
expect(new PathArray('M10 10 80 80 30 30 Z').toString()).toBe('M10 10L80 80L30 30Z ')

// parsing can handle 0.5.3.3.2 stuff
expect(new PathArray('M10 10L.5.5.3.3Z').toString()).toBe('M10 10L0.5 0.5L0.3 0.3Z ')
})

describe('move()', () => {
it('moves all points in a straight path', () => {
expect(p1.move(100, 200).toString()).toBe('M100 200H180V280H100L400 680Z ')

+ 107
- 0
spec/spec/utils/pathParser.js View File

@@ -0,0 +1,107 @@
/* globals describe expect it */

import { pathParser } from '../../../src/utils/pathParser.js'

describe('pathParser.js', () => {
describe('pathParser()', () => {
it('parses all paths correctly', () => {
expect(pathParser('M2,0a2 2 0 00-2 2a2 2 0 002 2a.5.5 0 011 0z')).toEqual([
[ 'M', 2, 0 ],
[ 'A', 2, 2, 0, 0, 0, 0, 2 ],
[ 'A', 2, 2, 0, 0, 0, 2, 4 ],
[ 'A', 0.5, 0.5, 0, 0, 1, 3, 4 ],
[ 'Z' ]
])

expect(pathParser('M2,0a2 2 0 00-2 2a2 2 0 002 2a.5.5 0 111 0z')).toEqual([
[ 'M', 2, 0 ],
[ 'A', 2, 2, 0, 0, 0, 0, 2 ],
[ 'A', 2, 2, 0, 0, 0, 2, 4 ],
[ 'A', 0.5, 0.5, 0, 1, 1, 3, 4 ],
[ 'Z' ]
])

expect(pathParser('m10 10 h 80 v 80 h -80 l 300 400 z')).toEqual([
[ 'M', 10, 10 ],
[ 'H', 90 ],
[ 'V', 90 ],
[ 'H', 10 ],
[ 'L', 310, 490 ],
[ 'Z' ]
])

expect(pathParser('m10 80 c 40 10 65 10 95 80 s 150 150 180 80 t 300 300 q 52 10 95 80 z')).toEqual([
[ 'M', 10, 80 ],
[ 'C', 50, 90, 75, 90, 105, 160 ],
[ 'S', 255, 310, 285, 240 ],
[ 'T', 585, 540 ],
[ 'Q', 637, 550, 680, 620 ],
[ 'Z' ]
])

expect(pathParser('m80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 z')).toEqual([
[ 'M', 80, 80 ],
[ 'A', 45, 45, 0, 0, 0, 125, 125 ],
[ 'L', 125, 80 ],
[ 'Z' ]
])

expect(pathParser('M215.458,245.23c0,0,77.403,0,94.274,0S405,216.451,405,138.054S329.581,15,287.9,15c-41.68,0-139.924,0-170.688,0C86.45,15,15,60.65,15,134.084c0,73.434,96.259,112.137,114.122,112.137C146.984,246.221,215.458,245.23,215.458,245.23z')).toEqual([
[ 'M', 215.458, 245.23 ],
[ 'C', 215.458, 245.23, 292.861, 245.23, 309.73199999999997, 245.23 ],
[ 'S', 405, 216.451, 405, 138.054 ],
[ 'S', 329.581, 15, 287.9, 15 ],
[ 'C', 246.21999999999997, 15, 147.97599999999997, 15, 117.21199999999999, 15 ],
[ 'C', 86.45, 15, 15, 60.65, 15, 134.084 ],
[ 'C', 15, 207.518, 111.259, 246.221, 129.122, 246.221 ],
[ 'C', 146.984, 246.221, 215.458, 245.23, 215.458, 245.23 ],
[ 'Z' ]
])

expect(pathParser('M10 10-45-30.5.5 .89L2e-2.5.5-.5C.5.5.5.5.5.5L-3-4z')).toEqual([
[ 'M', 10, 10 ],
[ 'L', -45, -30.5 ],
[ 'L', 0.5, 0.89 ],
[ 'L', 0.02, 0.5 ],
[ 'L', 0.5, -0.5 ],
[ 'C', 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 ],
[ 'L', -3, -4 ],
[ 'Z' ]
])

expect(pathParser('m 0,0 0,3189 2209,0 0,-3189 -2209,0 z m 154,154 1901,0 0,2881 -1901,0 0,-2881 z')).toEqual([
[ 'M', 0, 0 ],
[ 'L', 0, 3189 ],
[ 'L', 2209, 3189 ],
[ 'L', 2209, 0 ],
[ 'L', 0, 0 ],
[ 'Z' ],
[ 'M', 154, 154 ],
[ 'L', 2055, 154 ],
[ 'L', 2055, 3035 ],
[ 'L', 154, 3035 ],
[ 'L', 154, 154 ],
[ 'Z' ]
])

expect(pathParser('m 0,0 a 45 45, 0, 0, 0, 125 125')).toEqual([
[ 'M', 0, 0 ],
[ 'A', 45, 45, 0, 0, 0, 125, 125 ]
])

expect(pathParser('M10 10 80 80 30 30 Z')).toEqual([
[ 'M', 10, 10 ],
[ 'L', 80, 80 ],
[ 'L', 30, 30 ],
[ 'Z' ]
])

expect(pathParser('M10 10L.5.5.3.3Z')).toEqual([
[ 'M', 10, 10 ],
[ 'L', 0.5, 0.5 ],
[ 'L', 0.3, 0.3 ],
[ 'Z' ]
])
})
})
})

+ 2
- 2
src/animation/Morphable.js View File

@@ -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)) {

+ 1
- 15
src/modules/core/regex.js View File

@@ -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

+ 5
- 128
src/types/PathArray.js View File

@@ -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

+ 234
- 0
src/utils/pathParser.js View File

@@ -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

}

Loading…
Cancel
Save