- 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)
})
})
- 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) => {
})
})
})
-
- 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')
- })
- })
})
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', () => {
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 ')
--- /dev/null
+/* 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' ]
+ ])
+ })
+ })
+})
import {
delimiter,
numberAndUnit,
- pathLetters
+ isPathLetter
} from '../modules/core/regex.js'
import { extend } from '../utils/adopter.js'
import Color from '../types/Color.js'
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)) {
// 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
-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++) {
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 () {
}
// 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
--- /dev/null
+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
+
+}