├── README.md ├── demo ├── App.jsx ├── Controls.jsx ├── Icon.jsx ├── entry.js ├── icons.js ├── index.html └── webpack.config.js ├── index.js ├── lib ├── constants.js ├── expand-stroke.js ├── get-center.js ├── keys.js ├── parse.js ├── reflect-x.js ├── reflect-y.js ├── rotate.js ├── scale.js ├── stringify.js ├── to-absolute.js └── translate.js ├── package.json └── test ├── fixtures.js └── index.js /README.md: -------------------------------------------------------------------------------- 1 | # path-ast 2 | 3 | SVG path element command parser/stringifier 4 | 5 | ## Usage 6 | 7 | ```bash 8 | npm i path-ast 9 | ``` 10 | 11 | ```js 12 | var pathAst = require('path-ast') 13 | 14 | var pathString = 'M8 48 L56 48 L32 12 Z' 15 | 16 | var ast = pathAst.parse(pathString) 17 | // Returns a path AST 18 | 19 | var pathData = pathAst.stringify(ast) 20 | // Returns a string for use in the element’s d attribute. 21 | ``` 22 | 23 | ### Transform Methods 24 | 25 | These methods are experimental and mutate the AST. They work best with absolute coordinates. Use the `toAbsolute()` method to convert relative coordinates to absolute. 26 | The raw string for each command is not altered by these methods and can be used to inspect differences after transformation. 27 | 28 | #### `scale(n, cx, cy)` 29 | 30 | Scales a path by the ratio `n` where `1` is 100%, centered at `cx` and `cy`. 31 | If `cx` and `cy` are not provided, the method will attempt to find the center of the path. 32 | 33 | #### `translate(x, y)` 34 | 35 | Translates a path by `x` and `y`. 36 | 37 | #### `rotate(angle, cx, cy)` 38 | 39 | Rotates a path by `angle` in degrees, centered at `cx` and `cy`. 40 | If `cx` and `cy` are not provided, the method will attempt to find the center of the path. 41 | 42 | #### `reflectX(cx)` 43 | 44 | Flips the path horizontally across the axis `cx`. 45 | If `cx` is not provided, the method will attempt to find the horizontal center of the path. 46 | 47 | #### `reflectY(cy)` 48 | 49 | Flips the path vertically across the axis `cy`. 50 | If `cy` is not provided, the method will attempt to find the vertical center of the path. 51 | 52 | #### `toAbsolute()` 53 | 54 | Converts relative command coordinates to absolute. 55 | 56 | #### `getCenter()` 57 | 58 | Used internally to determine the center of a path. Returns an object with x and y coordinates. Arc and curve commands that extend beyond the outer edges of points are not factored into calculating the center. 59 | 60 | MIT License 61 | 62 | -------------------------------------------------------------------------------- /demo/App.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import Icon from './Icon' 4 | import Controls from './Controls' 5 | import icons from './icons' 6 | 7 | export default class App extends React.Component { 8 | 9 | constructor () { 10 | super() 11 | this.state = { 12 | rotation: 90, 13 | scale: 1, 14 | translateX: 0, 15 | translateY: 0 16 | } 17 | this.handleChange = this.handleChange.bind(this) 18 | } 19 | 20 | handleChange (e) { 21 | this.setState({ [e.target.name]: parseFloat(e.target.value) }) 22 | } 23 | 24 | render () { 25 | const styles = { 26 | root: { 27 | fontFamily: 'sans-serif' 28 | }, 29 | header: { 30 | position: 'fixed', 31 | zIndex: 2, 32 | backgroundColor: 'white' 33 | }, 34 | body: { 35 | paddingTop: 128 36 | } 37 | } 38 | 39 | return ( 40 |
41 |
42 | 45 |
46 |
47 | {Object.keys(icons).map((key, i) => { 48 | return ( 49 | 53 | ) 54 | })} 55 |
56 |
57 | ) 58 | } 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /demo/Controls.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | class Input extends React.Component { 5 | 6 | render () { 7 | const { name, label } = this.props 8 | const styles = { 9 | root: { 10 | marginBottom: 8 11 | }, 12 | label: { 13 | fontSize: 14, 14 | fontWeight: 'bold', 15 | display: 'block' 16 | }, 17 | input: { 18 | fontFamily: 'inherit', 19 | fontSize: 'inherit', 20 | display: 'block', 21 | width: '100%', 22 | padding: 4, 23 | borderStyle: 'solid', 24 | borderWidth: '1px', 25 | borderColor: '#bbb' 26 | } 27 | } 28 | 29 | return ( 30 |
31 | 35 | 37 |
38 | ) 39 | } 40 | 41 | } 42 | 43 | Input.propTypes = { 44 | name: React.PropTypes.string, 45 | label: React.PropTypes.string 46 | } 47 | 48 | class Range extends React.Component { 49 | 50 | render () { 51 | const { name, label, value } = this.props 52 | const styles = { 53 | root: { 54 | marginBottom: 8 55 | }, 56 | label: { 57 | fontSize: 14, 58 | fontWeight: 'bold', 59 | display: 'block' 60 | }, 61 | input: { 62 | display: 'block', 63 | width: '100%' 64 | } 65 | } 66 | 67 | return ( 68 |
69 | 73 | 76 |
77 | ) 78 | } 79 | 80 | } 81 | 82 | Range.propTypes = { 83 | name: React.PropTypes.string, 84 | label: React.PropTypes.string, 85 | value: React.PropTypes.number 86 | } 87 | 88 | class Col extends React.Component { 89 | 90 | render () { 91 | const { children } = this.props 92 | const style = { 93 | display: 'inline-block', 94 | paddingLeft: 16, 95 | paddingRight: 16, 96 | width: 256, 97 | maxWidth: '100%' 98 | } 99 | 100 | return ( 101 |
102 | ) 103 | } 104 | 105 | } 106 | 107 | Col.propTypes = { 108 | children: React.PropTypes.element 109 | } 110 | 111 | export default class Controls extends React.Component { 112 | 113 | render () { 114 | const { 115 | handleChange, 116 | rotation, 117 | scale, 118 | translateX, 119 | translateY 120 | } = this.props 121 | 122 | return ( 123 |
124 | 125 | 131 | 132 | 133 | 138 | 139 | 140 | 147 | 148 | 149 | 155 | 156 | 157 | 164 | 165 | 166 | 172 | 173 | 174 | 181 | 182 | 183 | 189 | 190 |
191 | ) 192 | } 193 | 194 | } 195 | 196 | Controls.propTypes = { 197 | handleChange: React.PropTypes.func, 198 | rotation: React.PropTypes.number, 199 | scale: React.PropTypes.number, 200 | translateX: React.PropTypes.number, 201 | translateY: React.PropTypes.number 202 | } 203 | 204 | -------------------------------------------------------------------------------- /demo/Icon.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import icons from './icons' 4 | import { cloneDeep } from 'lodash' 5 | import { stringify } from '..' 6 | 7 | export default class Icon extends React.Component { 8 | 9 | render () { 10 | const { 11 | name, 12 | size, 13 | scale, 14 | rotation, 15 | translateX, 16 | translateY 17 | } = this.props 18 | const styles = { 19 | svg: { 20 | overflow: 'visible' 21 | }, 22 | path: { 23 | opacity: 0.25, 24 | fill: 'red' 25 | }, 26 | guide: { 27 | fill: 'none', 28 | strokeWidth: 0.25, 29 | stroke: 'cyan', 30 | opacity: 0.25 31 | }, 32 | ghost: { 33 | fill: 'blue', 34 | opacity: 0.25 35 | } 36 | } 37 | const icon = icons[name] || '' 38 | const d = stringify(icon) 39 | const i2 = cloneDeep(icon) 40 | .toAbsolute() 41 | .rotate(rotation, 16, 16) 42 | .scale(scale, 16, 16) 43 | .translate(translateX, translateY) 44 | const d2 = stringify(i2) 45 | 46 | return ( 47 |
48 | 53 | 56 | 62 | 63 | 64 | 65 |
66 | ) 67 | } 68 | 69 | } 70 | 71 | Icon.propTypes = { 72 | name: React.PropTypes.string, 73 | size: React.PropTypes.number, 74 | scale: React.PropTypes.number, 75 | rotation: React.PropTypes.number, 76 | translateX: React.PropTypes.number, 77 | translateY: React.PropTypes.number 78 | } 79 | 80 | Icon.defaultProps = { 81 | name: 'bookmark', 82 | size: 32 83 | } 84 | 85 | -------------------------------------------------------------------------------- /demo/entry.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import App from './App' 4 | 5 | React.render(, document.getElementById('app')) 6 | 7 | -------------------------------------------------------------------------------- /demo/icons.js: -------------------------------------------------------------------------------- 1 | 2 | import { parse } from '..' 3 | import geomicons from 'geomicons-open/src/js/paths' 4 | 5 | let asts = {} 6 | 7 | Object.keys(geomicons).forEach((key) => { 8 | asts[key] = parse(geomicons[key]) 9 | }) 10 | 11 | asts.r2 = parse('m0 16 c1 4 4 14 16 14 h14 v-28 l-16 0z') 12 | asts.r3 = parse('m0 16 l16 14 h14 v-28 l-16 0z') 13 | 14 | export default asts 15 | 16 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | 6 | entry: [ 7 | 'webpack-dev-server/client?http://localhost:8080/', 8 | 'webpack/hot/only-dev-server', 9 | './demo/entry' 10 | ], 11 | 12 | output: { 13 | path: __dirname + '/demo', 14 | filename: 'bundle.js' 15 | }, 16 | 17 | resolve: { 18 | extensions: ['', '.js', '.jsx'] 19 | }, 20 | 21 | module: { 22 | loaders: [ 23 | { 24 | test: /\.jsx?$/, 25 | exclude: /node_modules/, 26 | loaders: ['react-hot', 'babel'] 27 | }, 28 | { 29 | test: /\.css$/, 30 | loaders: ['style', 'raw'] 31 | } 32 | ] 33 | }, 34 | 35 | plugins: [ 36 | new webpack.HotModuleReplacementPlugin(), 37 | new webpack.NoErrorsPlugin() 38 | ], 39 | 40 | devServer: { 41 | contentBase: './demo', 42 | historyApiFallback: true, 43 | hot: true 44 | } 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | parse: require('./lib/parse'), 4 | stringify: require('./lib/stringify') 5 | } 6 | 7 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | X_REGEX: /^x[0-9]?$/, 4 | Y_REGEX: /^y[0-9]?$/, 5 | COMMAND_SPLIT_REGEX: /([a-zA-Z][0-9\-\.\s\,]*)/ 6 | } 7 | -------------------------------------------------------------------------------- /lib/expand-stroke.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (strokeWidth) { 3 | 4 | return this 5 | 6 | } 7 | 8 | -------------------------------------------------------------------------------- /lib/get-center.js: -------------------------------------------------------------------------------- 1 | 2 | // - [ ] account for arc edges 3 | 4 | var _ = require('lodash') 5 | 6 | module.exports = function () { 7 | 8 | var xValues = [] 9 | var yValues = [] 10 | 11 | this.commands.forEach(function (command) { 12 | if (typeof command.params.x !== 'undefined') { 13 | xValues.push(command.params.x) 14 | } 15 | if (typeof command.params.y !== 'undefined') { 16 | yValues.push(command.params.y) 17 | } 18 | }) 19 | 20 | var minX = _.min(xValues) 21 | var maxX = _.max(xValues) 22 | var minY = _.min(yValues) 23 | var maxY = _.max(yValues) 24 | return { 25 | x: (maxX - minX) / 2 + minX, 26 | y: (maxY - minY) / 2 + minY 27 | } 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /lib/keys.js: -------------------------------------------------------------------------------- 1 | 2 | // Reference object for parsing and stringifying 3 | 4 | module.exports = { 5 | M: ['x', 'y'], 6 | m: ['x', 'y'], 7 | L: ['x', 'y'], 8 | l: ['x', 'y'], 9 | H: ['x'], 10 | h: ['x'], 11 | V: ['y'], 12 | v: ['y'], 13 | C: ['x1', 'y1', 'x2', 'y2', 'x', 'y'], 14 | c: ['x1', 'y1', 'x2', 'y2', 'x', 'y'], 15 | S: ['x2', 'y2', 'x', 'y'], 16 | s: ['x2', 'y2', 'x', 'y'], 17 | Q: ['x1', 'y1', 'x', 'y'], 18 | q: ['x1', 'y1', 'x', 'y'], 19 | T: ['x', 'y'], 20 | t: ['x', 'y'], 21 | A: ['rx', 'ry', 'xAxisRotation', 'largeArcFlag', 'sweepFlag', 'x', 'y'], 22 | a: ['rx', 'ry', 'xAxisRotation', 'largeArcFlag', 'sweepFlag', 'x', 'y'], 23 | Z: [], 24 | z: [] 25 | } 26 | 27 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | 2 | var keys = require('./keys') 3 | var constants = require('./constants') 4 | var scale = require('./scale') 5 | var translate = require('./translate') 6 | var rotate = require('./rotate') 7 | var reflectX = require('./reflect-x') 8 | var reflectY = require('./reflect-y') 9 | var toAbsolute = require('./to-absolute') 10 | var getCenter = require('./get-center') 11 | 12 | module.exports = function (string, options) { 13 | 14 | var ast = {} 15 | ast.raw = string.trim() 16 | ast.scale = scale 17 | ast.translate = translate 18 | ast.rotate = rotate 19 | ast.reflectX = reflectX 20 | ast.reflectY = reflectY 21 | ast.toAbsolute = toAbsolute 22 | ast.getCenter = getCenter 23 | ast.commands = [] 24 | 25 | function parseRawString (str) { 26 | var arr = str.split(constants.COMMAND_SPLIT_REGEX) 27 | .map(function (str) { 28 | return str.trim() 29 | }) 30 | .filter(function (str) { 31 | return str !== '' 32 | }) 33 | return arr 34 | } 35 | 36 | function parseCommandString (str) { 37 | var type = str.substring(0, 1) 38 | var params = str.substring(1) 39 | var arr = params.split(/\s|\,/) 40 | .filter(function (param) { 41 | return param !== '' 42 | }) 43 | .map(function (param) { 44 | return parseFloat(param) 45 | }) 46 | if (!str.substring(0, 1).match(/[a-zA-Z]/)) { 47 | console.error('First character is not a letter') 48 | } 49 | if (!params.match(/[A-Za-z0-9]/)) { 50 | params = '' 51 | arr = [] 52 | } 53 | return { 54 | raw: str, 55 | type: type, 56 | params: { 57 | raw: params, 58 | arr: arr 59 | } 60 | } 61 | } 62 | 63 | function parseParameters (command) { 64 | keys[command.type].forEach(function (key, i) { 65 | if (typeof command.params.arr[i] === 'undefined') { 66 | console.log('Missing parameter', command) 67 | return false 68 | } 69 | command.params[key] = command.params.arr[i] 70 | }) 71 | delete command.params.arr 72 | delete command.params.raw 73 | return command 74 | } 75 | 76 | ast.commands = parseRawString(ast.raw) 77 | .map(parseCommandString) 78 | .map(parseParameters) 79 | .filter(function (command) { 80 | // Remove syntax errors 81 | return command 82 | }) 83 | 84 | return ast 85 | 86 | } 87 | 88 | -------------------------------------------------------------------------------- /lib/reflect-x.js: -------------------------------------------------------------------------------- 1 | 2 | var keys = require('./keys') 3 | var constants = require('./constants') 4 | 5 | module.exports = function reflectX (cx) { 6 | 7 | cx = typeof cx !== 'undefined' ? cx : this.getCenter().x 8 | 9 | function flipParam (commandType, center, param) { 10 | if (commandType.match(/[a-z]/)) { 11 | return param * -1 12 | } else { 13 | return center + center - param 14 | } 15 | } 16 | 17 | this.commands = this.commands.map(function (command) { 18 | keys[command.type].forEach(function (key, i) { 19 | var param = command.params[key] 20 | if (key.match(constants.X_REGEX) && key !== 'rx') { 21 | command.params[key] = flipParam(command.type, cx, param) 22 | } 23 | if (key === 'sweepFlag') { 24 | command.params[key] = 1 - param 25 | } 26 | }) 27 | return command 28 | }) 29 | 30 | return this 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /lib/reflect-y.js: -------------------------------------------------------------------------------- 1 | 2 | var keys = require('./keys') 3 | var constants = require('./constants') 4 | 5 | module.exports = function reflectY (cy) { 6 | 7 | cy = typeof cy !== 'undefined' ? cy : this.getCenter().y 8 | 9 | function flipParam (commandType, center, param) { 10 | if (commandType.match(/[a-z]/)) { 11 | return param * -1 12 | } else { 13 | return center + center - param 14 | } 15 | } 16 | 17 | this.commands = this.commands.map(function (command) { 18 | keys[command.type].forEach(function (key, i) { 19 | var param = command.params[key] 20 | if (key.match(constants.Y_REGEX) && key !== 'ry') { 21 | command.params[key] = flipParam(command.type, cy, param) 22 | } 23 | if (key === 'sweepFlag') { 24 | command.params[key] = 1 - param 25 | } 26 | }) 27 | return command 28 | }) 29 | 30 | return this 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /lib/rotate.js: -------------------------------------------------------------------------------- 1 | 2 | // Only works with absolute commands 3 | // - [ ] handle relative commands 4 | 5 | var _ = require('lodash') 6 | 7 | var pairs = [ 8 | ['x1', 'y1'], 9 | ['x2', 'y2'], 10 | ['x', 'y'] 11 | ] 12 | 13 | function rad (deg) { 14 | return Math.PI * deg / 180 15 | } 16 | 17 | function deg (rad) { 18 | return rad * 180 / Math.PI 19 | } 20 | 21 | function rx (radius, angle) { 22 | return _.round(radius * Math.cos(rad(angle)), 6) 23 | } 24 | 25 | function ry (radius, angle) { 26 | return _.round(radius * Math.sin(rad(angle)), 6) 27 | } 28 | 29 | function getRadius (a, b) { 30 | return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)) 31 | } 32 | 33 | function getAngle (a, b, angle) { 34 | var n = Math.atan2(a, b) 35 | var d = 90 + deg(n) 36 | return d + angle 37 | } 38 | 39 | module.exports = function rotate (angle, cx, cy) { 40 | 41 | var lastX = 0 42 | var lastY = 0 43 | 44 | var center = this.getCenter() 45 | cx = typeof cx !== 'undefined' ? cx : center.x 46 | cy = typeof cy !== 'undefined' ? cy : center.y 47 | 48 | this.commands = this.commands.map(function (command, i, arr) { 49 | var params = command.params 50 | var xAxisRotation = command.params.xAxisRotation 51 | 52 | pairs.forEach(function (pair) { 53 | var xKey = pair[0] 54 | var yKey = pair[1] 55 | var x = params[xKey] 56 | var y = params[yKey] 57 | var radius 58 | var pointAngle 59 | 60 | if (typeof x !== 'number' && typeof y !== 'number') { 61 | return command 62 | } 63 | 64 | if (command.type.match(/[a-z]/)) { 65 | x = lastX + x || lastX 66 | y = lastY + y || lastY 67 | command.type = command.type.toUpperCase() 68 | } 69 | 70 | if (typeof x !== 'number' || isNaN(x)) { 71 | command.type = command.type.match(/[A-Z]/) ? 'L' : 'l' 72 | x = lastX 73 | } else if (typeof y !== 'number' || isNaN(y)) { 74 | command.type = command.type.match(/[A-Z]/) ? 'L' : 'l' 75 | y = lastY 76 | } 77 | 78 | lastX = typeof params.x === 'number' ? params.x : lastX 79 | lastY = typeof params.y === 'number' ? params.y : lastY 80 | radius = getRadius(cx - x, cy - y) 81 | pointAngle = getAngle(cx - x, y - cy, angle) 82 | 83 | command.params[xKey] = cx + rx(radius, pointAngle) 84 | command.params[yKey] = cy + ry(radius, pointAngle) 85 | }) 86 | 87 | if (typeof xAxisRotation !== 'undefined') { 88 | command.params.xAxisRotation += angle 89 | } 90 | 91 | return command 92 | }) 93 | 94 | return this 95 | 96 | } 97 | 98 | -------------------------------------------------------------------------------- /lib/scale.js: -------------------------------------------------------------------------------- 1 | 2 | var keys = require('./keys') 3 | var constants = require('./constants') 4 | 5 | module.exports = function scale (n, cx, cy) { 6 | 7 | var center = this.getCenter() 8 | cx = typeof cx !== 'undefined' ? cx : center.x 9 | cy = typeof cy !== 'undefined' ? cy : center.y 10 | 11 | var commands = this.commands 12 | 13 | commands = commands.map(function (command) { 14 | keys[command.type].forEach(function (key, i) { 15 | var param = command.params[key] 16 | if (key.match(constants.X_REGEX)) { 17 | command.params[key] = cx + (param - cx) * n 18 | } 19 | if (key.match(constants.Y_REGEX)) { 20 | command.params[key] = cy + (param - cy) * n 21 | } 22 | if (key.match(/^rx|^ry/)) { 23 | command.params[key] = param * n 24 | } 25 | }) 26 | return command 27 | }) 28 | 29 | return this 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /lib/stringify.js: -------------------------------------------------------------------------------- 1 | 2 | var keys = require('./keys') 3 | 4 | module.exports = function (ast, options) { 5 | 6 | var string = ast.commands.map(function (command) { 7 | 8 | var params = keys[command.type].map(function (key, i) { 9 | return command.params[key] 10 | }).join(' ') 11 | 12 | return command.type + params 13 | 14 | }).join(' ') 15 | 16 | return string.trim() 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /lib/to-absolute.js: -------------------------------------------------------------------------------- 1 | 2 | var keys = require('./keys') 3 | var constants = require('./constants') 4 | 5 | module.exports = function toAbsolute () { 6 | 7 | var lastX = 0 8 | var lastY = 0 9 | 10 | this.commands = this.commands.map(function (command) { 11 | var params = command.params 12 | if (command.type.match(/[A-Z]/)) { 13 | lastX = params.x || lastX 14 | lastY = params.y || lastY 15 | return command 16 | } else { 17 | command.type = command.type.toUpperCase() 18 | keys[command.type].forEach(function (key, i) { 19 | var param = command.params[key] 20 | if (key.match(constants.X_REGEX)) { 21 | command.params[key] = lastX + param 22 | } 23 | if (key.match(constants.Y_REGEX)) { 24 | command.params[key] = lastY + param 25 | } 26 | }) 27 | lastX = command.params.x || lastX 28 | lastY = command.params.y || lastY 29 | return command 30 | } 31 | }) 32 | 33 | return this 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /lib/translate.js: -------------------------------------------------------------------------------- 1 | 2 | var keys = require('./keys') 3 | var constants = require('./constants') 4 | 5 | module.exports = function translate (x, y) { 6 | 7 | this.commands = this.commands.map(function (command) { 8 | keys[command.type].forEach(function (key, i) { 9 | var param = command.params[key] 10 | if (!command.type.match(/[A-Z]/) || typeof command.params[key] === 'undefined') { 11 | return false 12 | } 13 | if (key.match(constants.X_REGEX)) { 14 | command.params[key] = param + x 15 | } 16 | if (key.match(constants.Y_REGEX)) { 17 | command.params[key] = param + y 18 | } 19 | }) 20 | return command 21 | }) 22 | .filter(function (command) { 23 | return command !== false 24 | }) 25 | 26 | return this 27 | 28 | } 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "path-ast", 3 | "version": "1.1.0", 4 | "description": "SVG path AST parser and stringifier", 5 | "main": "index.js", 6 | "scripts": { 7 | "standard": "standard", 8 | "dev": "webpack-dev-server --progress --colors --config demo/webpack.config.js", 9 | "test": "mocha test" 10 | }, 11 | "author": "Brent Jackson", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "assert": "^1.3.0", 15 | "babel-loader": "^5.3.2", 16 | "geomicons-open": "0.0.6", 17 | "lodash": "^3.10.1", 18 | "mocha": "^2.2.1", 19 | "react": "^0.13.3", 20 | "react-hot-loader": "^1.2.8", 21 | "standard": "^4.5.3", 22 | "webpack": "^1.11.0", 23 | "webpack-dev-server": "^1.10.1" 24 | }, 25 | "standard": { 26 | "globals": [ 27 | "describe", 28 | "it", 29 | "beforeEach" 30 | ] 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/jxnblk/path-ast.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/jxnblk/path-ast/issues" 38 | }, 39 | "homepage": "https://github.com/jxnblk/path-ast#readme" 40 | } 41 | -------------------------------------------------------------------------------- /test/fixtures.js: -------------------------------------------------------------------------------- 1 | 2 | var past = require('..') 3 | var geomicons = require('geomicons-open/src/js/paths') 4 | 5 | module.exports = { 6 | geomicons: geomicons, 7 | paths: { 8 | diamond: 'M0 16 L16 32 L32 16 L16 0z', 9 | relative: 'm0 16 l16 16 l16 -16 l-16 -16z', 10 | nospace: 'M373 434l207 207l-17 17l-207 -207zM564 240l17 16l-166 166l-17 -16l166 -166v0z' 11 | }, 12 | asts: { 13 | bookmark: past.parse(geomicons.bookmark), 14 | camera: past.parse(geomicons.camera), 15 | chat: past.parse(geomicons.chat) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | var past = require('..') 3 | var assert = require('assert') 4 | var f = require('./fixtures') 5 | 6 | describe('Parse and stringify', function () { 7 | 8 | it('should have two parameters for M', function () { 9 | var bookmarkMove = f.asts.bookmark.commands[0].params 10 | assert.equal(Object.keys(bookmarkMove).length, 2) 11 | }) 12 | 13 | it('should have seven parameters for A', function () { 14 | var cameraArc = f.asts.camera.commands[10].params 15 | assert.equal(Object.keys(cameraArc).length, 7) 16 | }) 17 | 18 | it('should have six parameters for C', function () { 19 | var chatCurve = f.asts.chat.commands[4].params 20 | assert.equal(Object.keys(chatCurve).length, 6) 21 | }) 22 | 23 | it('should handle paths with no spaces', function (done) { 24 | assert.doesNotThrow(function () { 25 | past.parse(f.paths.nospace) 26 | done() 27 | }) 28 | }) 29 | 30 | it('stringify should match input', function () { 31 | var chatString = past.stringify(f.asts.chat) 32 | assert.equal(f.geomicons.chat.trim(), chatString) 33 | }) 34 | 35 | it('should match the raw string', function () { 36 | var raw = past.parse(f.paths.nospace).raw 37 | assert.equal(raw, f.paths.nospace) 38 | }) 39 | 40 | }) 41 | 42 | describe('Transform methods', function () { 43 | 44 | var ast 45 | var relAst 46 | 47 | beforeEach(function () { 48 | ast = past.parse(f.paths.diamond) 49 | relAst = past.parse(f.paths.relative) 50 | }) 51 | 52 | it('should scale a path', function (done) { 53 | assert.doesNotThrow(function () { 54 | f.asts.bookmark.scale(2) 55 | done() 56 | }) 57 | }) 58 | 59 | it('should convert to absolute values', function (done) { 60 | assert.doesNotThrow(function () { 61 | var ast = past.parse(f.paths.nospace) 62 | ast.toAbsolute() 63 | done() 64 | }) 65 | }) 66 | 67 | it('should reflect x without error', function (done) { 68 | assert.doesNotThrow(function () { 69 | ast.reflectX(16) 70 | done() 71 | }) 72 | }) 73 | 74 | it('should reflect x', function () { 75 | var flipped = ast.reflectX(16) 76 | assert.equal(flipped.commands[0].params.x, 32) 77 | }) 78 | 79 | it('should reflect y', function () { 80 | var flipped = ast.reflectY(16) 81 | assert.equal(flipped.commands[1].params.y, 0) 82 | }) 83 | 84 | it('should scale .5x', function () { 85 | var halved = ast.scale(0.5) 86 | assert.equal(halved.commands[0].params.x, 8) 87 | }) 88 | 89 | it('should scale 2x', function () { 90 | var halved = ast.scale(2) 91 | assert.equal(halved.commands[0].params.x, -16) 92 | }) 93 | 94 | it('should translate', function () { 95 | var shifted = ast.translate(4, 4) 96 | assert.deepEqual( 97 | shifted.commands[0].params, 98 | { x: 4, y: 20 } 99 | ) 100 | }) 101 | 102 | it('should calculate the center', function () { 103 | var center = ast.getCenter() 104 | assert.deepEqual(center, { x: 16, y: 16 }) 105 | }) 106 | 107 | it('should rotate -90°', function () { 108 | var rotated = ast.rotate(-90) 109 | assert.deepEqual( 110 | rotated.commands[0].params, 111 | { x: 16, y: 32 } 112 | ) 113 | }) 114 | 115 | it('should convert relative to absolute coordinates', function (done) { 116 | var absolute = relAst.toAbsolute() 117 | assert.deepEqual( 118 | absolute.commands[0].params, 119 | ast.commands[0].params 120 | ) 121 | assert.deepEqual( 122 | absolute.commands[1].params, 123 | ast.commands[1].params 124 | ) 125 | assert.deepEqual( 126 | absolute.commands[2].params, 127 | ast.commands[2].params 128 | ) 129 | done() 130 | }) 131 | 132 | }) 133 | 134 | describe('Expand strokes', function () { 135 | 136 | it('should calculate angles') 137 | it('should expand lines') 138 | it('should expand arcs') 139 | it('should expand curves') 140 | 141 | }) 142 | 143 | --------------------------------------------------------------------------------