├── .gitignore ├── .npmignore ├── README.md ├── package.json ├── src ├── bin.ts ├── d-path-parser.d.ts ├── index.ts ├── jsx.ts ├── parse.ts ├── plugins │ ├── applyTransforms.ts │ ├── camelCaseAttributes.ts │ ├── changeRootTag.ts │ ├── gatherCommonAttributes.ts │ ├── index.ts │ ├── inlineDefinitions.ts │ ├── moveGroupAttributesDown.ts │ ├── numberValues.ts │ ├── removeEmptyGroups.ts │ ├── removeIds.ts │ ├── removeSvgAttributes.ts │ ├── removeTags.ts │ ├── setJSXProps.ts │ └── trimText.ts ├── serialize.ts └── utils.ts ├── test └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | tsconfig.json 4 | yarn.lock 5 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Vectorinox 2 | ========== 3 | 4 | Cleans up and compresses SVG files exported from Sketch (and other design tools). Optionally formats SVG as JSX for inlining into React projects. 5 | 6 | Install 7 | ------- 8 | 9 | ```bash 10 | > npm install -g vectorinox 11 | ``` 12 | 13 | Example Usage 14 | -------------- 15 | 16 | To optimize an SVG file in place: 17 | 18 | ```bash 19 | > vectorinox vector-file-to-optimize.svg 20 | ``` 21 | 22 | To output the optimized SVG to stdout: 23 | 24 | ```bash 25 | > vectorinox --stdout vector-file-to-optimize.svg 26 | ``` 27 | 28 | Full Usage 29 | ---------- 30 | ``` 31 | vectorinox [options] svgfile ... svgfile 32 | 33 | Output: 34 | --stdout, -o Print results to stdout instead of writing to file [boolean] [default: false] 35 | 36 | JSX Options: 37 | --jsx Convert to JSX [boolean] [default: false] 38 | --jsx-extension File extension to use when converting to JSX (ignored with --stdout) [default: "js"] 39 | --jsx-tag The name of the top level tag to use when converting to JSX [default: "svg"] 40 | --jsx-prop Add a prop and value to add to the top level tag, in the format prop=value (can be used 41 | multiple times) [default: []] 42 | --jsx-inherit-prop A prop name to pass through to the root tag, i.e. prop={prop} (can be used multiple 43 | times) [default: []] 44 | --jsx-splice-prop A prop name to splice into the root tag, i.e. {...prop} (can be used multiple times) 45 | [default: []] 46 | --jsx-template A file containing the template to use when converting to a JSX component 47 | [default: ".svgTemplate.js"] 48 | 49 | Options: 50 | --version Show version number [boolean] 51 | --help Show help [boolean] 52 | 53 | Examples: 54 | vectorinox image.svg Optimize an image in place 55 | vectorinox --stdout image.svg | pbcopy Optimize an image and copy the SVG code to the 56 | clipboard 57 | vectorinox --jsx image.svg Optimize a file and convert it to a React module 58 | with a .js extension using the default template 59 | vectorinox --jsx image.svg --jsx-template Optimize a file and convert it to a React module 60 | mySvgTemplate.js with a .js extension using the provided template 61 | vectorinox --jsx --jsx-extension tsx image.svg Optimize a file and convert it to a React module 62 | with a .tsx extension 63 | ``` 64 | 65 | React/JSX 66 | --------- 67 | 68 | To prep and format SVG for inlining in your React codebase, use the `--jsx` option. 69 | 70 | ```bash 71 | > vectorinox --jsx --stdout vector-file-to-optimize.svg 72 | ``` 73 | ```xml 74 | 75 | 76 | 77 | 78 | 79 | ``` 80 | 81 | You can pipe the output to `pbcopy` (Mac) or `clip` (Windows) for easy pasting into your code: 82 | 83 | ```bash 84 | > vectorinox --jsx --stdout vector-file-to-optimize.svg | pbcopy 85 | ``` 86 | 87 | You can customize the root tag and props assigned to it. For example, if you use [JSXStyle](https://github.com/smyte/jsxstyle), you can output `` instead of ``: 88 | 89 | ```bash 90 | > vectorinox --jsx --stdout vector-file-to-optimize.svg --jsxTag View --jsxProp component=svg 91 | ``` 92 | ```xml 93 | 94 | 95 | 96 | 97 | ``` 98 | 99 | You can even specify which props to pass through from the parent: 100 | 101 | ```bash 102 | > vectorinox --jsx --stdout vector-file-to-optimize.svg --jsxTag View --jsxProp component=svg --jsxInheritProp stroke --jsxInheritProp fill 103 | ``` 104 | ```xml 105 | 106 | 107 | 108 | 109 | ``` 110 | 111 | Or which props object to splice in: 112 | ```bash 113 | > vectorinox --jsx --stdout vector-file-to-optimize.svg --jsxTag View --jsxProp component=svg --jsxSpliceProp props 114 | ``` 115 | ```xml 116 | 117 | 118 | 119 | 120 | ``` 121 | 122 | If you use the `--jsx` option without `--stdout`, a `.js` file with the same name as the `.svg` file will be created. To customize the extension used, use `--jsxExtension tsx`. 123 | 124 | React Component Templates 125 | ------------------------- 126 | 127 | It usually takes a small amount of boilerplate to convert an SVG image to a valid React component. Vectorinox comes with a default template used when converting to JSX. It looks like this: 128 | 129 | ```js 130 | import * as React from 'react'; 131 | 132 | const %NAME% = (%PROPS%) => 133 | %SVG%; 134 | 135 | export default %NAME%; 136 | ``` 137 | 138 | You can customize the template by providing a `--jsxTemplate template.js` option, or by creating a file called `.svgTemplate.js` in the current directory. 139 | 140 | Available placeholders are: 141 | 142 | - `%NAME%`: a CamelCased version of the SVG file name. 143 | - `%PROPS%`: an object destructuring of props, inferred from the `--jsxInheritProp` and `--jsxSpliceProp` options provided. For example, with `--jsxInheritProp color --jsxInheritProp fill --jsxSpliceProp props`, this token gets replaced with `{color, fill, ...props}`. 144 | - `%SVG%`: the actual converted SVG markup. Put this on its own line with space in front of it to indent the code accordingly. 145 | 146 | 147 | Known Issues 148 | ------------ 149 | 150 | Not all of SVG is currently supported. Specifically masks and transforms on paths that use arc segments are known to be broken. 151 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vectorinox", 3 | "version": "0.0.7", 4 | "description": "Cleans up and compresses SVG files exported from Sketch (and other design tools). Optionally formats SVG for inlining into React projects.", 5 | "author": "Stefano J. Attardi (http://github.com/steadicat)", 6 | "homepage": "https://github.com/steadicat/vectorinox", 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com:steadicat/vectorinox.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/steadicat/vectorinox/issues" 13 | }, 14 | "keywords": [ 15 | "svg", 16 | "vector", 17 | "image", 18 | "graphics", 19 | "optimizer", 20 | "minifier", 21 | "compress", 22 | "minify", 23 | "optimize", 24 | "jsx", 25 | "react", 26 | "format", 27 | "sketch", 28 | "export" 29 | ], 30 | "bin": { 31 | "vectorinox": "./dist/bin.js" 32 | }, 33 | "scripts": { 34 | "build": "./node_modules/.bin/tsc", 35 | "prepublish": "npm run build", 36 | "test": "./node_modules/.bin/mocha -r ts-node/register test/**/*.ts", 37 | "test:watch": "./node_modules/.bin/mocha --watch --watch-extensions ts -r ts-node/register test/**/*.ts src/plugins/*.ts" 38 | }, 39 | "dependencies": { 40 | "chai": "^4.1.2", 41 | "d-path-parser": "^1.0.0", 42 | "ltx": "^2.7.1", 43 | "yargs": "^10.0.3" 44 | }, 45 | "devDependencies": { 46 | "@types/chai": "^4.0.4", 47 | "@types/ltx": "^2.6.2", 48 | "@types/mocha": "^2.2.44", 49 | "@types/yargs": "^8.0.2", 50 | "expect": "^21.2.1", 51 | "mocha": "^4.0.1", 52 | "ts-node": "^3.3.0", 53 | "typescript": "^2.5.3" 54 | }, 55 | "license": "MIT" 56 | } 57 | -------------------------------------------------------------------------------- /src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import * as yargs from 'yargs'; 6 | import {clean, cleanJSX} from './index'; 7 | 8 | const DEFAULT_JSX_TEMPLATE = `import * as React from 'react'; 9 | 10 | const %NAME% = (%PROPS%) => 11 | %SVG%; 12 | 13 | export default %NAME%; 14 | `; 15 | 16 | const options = (yargs 17 | .usage('$0 [options] svgfile ... svgfile') 18 | .option('stdout', { 19 | group: 'Output:', 20 | type: 'boolean', 21 | default: false, 22 | alias: 'o', 23 | describe: 'Print results to stdout instead of writing to file', 24 | }) 25 | .option('jsx', { 26 | group: 'JSX Options:', 27 | type: 'boolean', 28 | default: false, 29 | describe: 'Convert to JSX', 30 | }) 31 | .option('jsx-extension', { 32 | group: 'JSX Options:', 33 | default: 'js', 34 | describe: 'File extension to use when converting to JSX (ignored with --stdout)', 35 | }) 36 | .option('jsx-tag', { 37 | group: 'JSX Options:', 38 | default: 'svg', 39 | describe: 'The name of the top level tag to use when converting to JSX', 40 | }) 41 | .option('jsx-prop', { 42 | group: 'JSX Options:', 43 | default: [], 44 | describe: 45 | 'Add a prop and value to add to the top level tag, in the format prop=value (can be used multiple times)', 46 | coerce: toDict, 47 | }) 48 | .option('jsx-inherit-prop', { 49 | group: 'JSX Options:', 50 | default: [], 51 | describe: 'A prop name to pass through to the root tag, i.e. prop={prop} (can be used multiple times)', 52 | coerce: toArray, 53 | }) 54 | .option('jsx-splice-prop', { 55 | group: 'JSX Options:', 56 | default: [], 57 | describe: 'A prop name to splice into the root tag, i.e. {...prop} (can be used multiple times)', 58 | coerce: toArray, 59 | }) 60 | .option('jsx-template', { 61 | group: 'JSX Options:', 62 | default: '.svgTemplate.js', 63 | describe: 'A file containing the template to use when converting to a JSX component', 64 | }) 65 | .example('$0 image.svg', 'Optimize an image in place') 66 | .example('$0 --stdout image.svg | pbcopy', 'Optimize an image and copy the SVG code to the clipboard') 67 | .example( 68 | '$0 --jsx image.svg', 69 | 'Optimize a file and convert it to a React module with a .js extension using the default template', 70 | ) 71 | .example( 72 | '$0 --jsx image.svg --jsx-template mySvgTemplate.js', 73 | 'Optimize a file and convert it to a React module with a .js extension using the provided template', 74 | ) 75 | .example( 76 | '$0 --jsx --jsx-extension tsx image.svg', 77 | 'Optimize a file and convert it to a React module with a .tsx extension', 78 | ) 79 | .strict() 80 | .wrap(Math.min(120, yargs.terminalWidth() - 2)) 81 | .help().argv as object) as { 82 | jsx: boolean; 83 | stdout: boolean; 84 | jsxTag: string; 85 | jsxExtension: string; 86 | jsxProp: {[name: string]: string}; 87 | jsxInheritProp: string[]; 88 | jsxSpliceProp: string[]; 89 | jsxTemplate: string; 90 | _: string[]; 91 | }; 92 | 93 | function indent(s: string, prefix: string) { 94 | return s.split('\n').join(`\n${prefix}`); 95 | } 96 | 97 | function wrapJSX(pathName: string, cleaned: string, template: string) { 98 | const name = path 99 | .basename(pathName, path.extname(pathName)) 100 | .replace(/(^|-)([a-z])/g, (_, _1, c) => c.toUpperCase()); 101 | const propsDefinition = [...options.jsxInheritProp, ...options.jsxSpliceProp.map(p => `...${p}`)].join( 102 | ', ', 103 | ); 104 | return template 105 | .replace(/%NAME%/g, name) 106 | .replace(/%PROPS%/g, `{${propsDefinition}}`) 107 | .replace( 108 | /(\n([ \t]*))?%SVG%/g, 109 | (_, before, whitespace) => (before || '') + indent(cleaned, whitespace || ' '), 110 | ); 111 | } 112 | 113 | function toDict(options: string[] | string): {[name: string]: string} { 114 | if (typeof options === 'string') options = [options]; 115 | return options.map(s => s.split('=')).reduce((obj: {[name: string]: string}, [k, v]) => { 116 | obj[k] = v; 117 | return obj; 118 | }, {}); 119 | } 120 | 121 | function toArray(options: string[] | string): string[] { 122 | return typeof options === 'string' ? [options] : options; 123 | } 124 | 125 | for (let f of options._) { 126 | const data = fs.readFileSync(f).toString(); 127 | let cleaned = options.jsx 128 | ? cleanJSX(data, options.jsxTag, options.jsxProp, options.jsxInheritProp, options.jsxSpliceProp) 129 | : clean(data); 130 | 131 | if (options.jsx) { 132 | const template = fs.existsSync(options.jsxTemplate) 133 | ? fs.readFileSync(options.jsxTemplate).toString() 134 | : DEFAULT_JSX_TEMPLATE; 135 | cleaned = wrapJSX(f, cleaned, template); 136 | } 137 | 138 | if (options.stdout) { 139 | console.log(cleaned); 140 | } else { 141 | if (options.jsx) { 142 | const sourceName = f; 143 | f = f.replace(/\.svg$/, `.${options.jsxExtension}`); 144 | console.error(`${sourceName} -> ${f}`); 145 | } else { 146 | console.error(f); 147 | } 148 | 149 | fs.writeFileSync(f, cleaned); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/d-path-parser.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'd-path-parser' { 2 | export = parse; 3 | function parse(d: string): parse.Command[]; 4 | namespace parse { 5 | export type Point = {x: number; y: number}; 6 | 7 | export type Command = 8 | | { 9 | code: 'M' | 'L'; 10 | relative: false; 11 | end: Point; 12 | } 13 | | { 14 | code: 'm' | 'l'; 15 | relative: true; 16 | end: Point; 17 | } 18 | | {code: 'H' | 'V'; relative: false; value: number} 19 | | {code: 'h' | 'v'; relative: true; value: number} 20 | | { 21 | code: 'C'; 22 | relative: false; 23 | cp1: Point; 24 | cp2: Point; 25 | end: Point; 26 | } 27 | | { 28 | code: 'c'; 29 | relative: true; 30 | cp1: Point; 31 | cp2: Point; 32 | end: Point; 33 | } 34 | | { 35 | code: 'S' | 'Q'; 36 | relative: false; 37 | cp: Point; 38 | end: Point; 39 | } 40 | | { 41 | code: 's' | 'q'; 42 | relative: true; 43 | cp: Point; 44 | end: Point; 45 | } 46 | | { 47 | code: 'T'; 48 | relative: false; 49 | end: Point; 50 | } 51 | | { 52 | code: 'T'; 53 | relative: true; 54 | end: Point; 55 | } 56 | | { 57 | code: 'A'; 58 | relative: false; 59 | radii: Point; 60 | rotation: number; 61 | large: boolean; 62 | clockwise: boolean; 63 | end: Point; 64 | } 65 | | { 66 | code: 'a'; 67 | relative: true; 68 | radii: Point; 69 | rotation: number; 70 | large: boolean; 71 | clockwise: boolean; 72 | end: Point; 73 | } 74 | | { 75 | code: 'Z'; 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as plugins from './plugins'; 2 | import {cleanup} from './utils'; 3 | import {parse} from './parse'; 4 | import {serialize} from './serialize'; 5 | 6 | const processors = [ 7 | plugins.trimText, 8 | plugins.removeSvgAttributes('xmlns:xlink', 'version'), 9 | plugins.inlineDefinitions, 10 | plugins.removeTags('desc', 'title', 'defs'), 11 | plugins.moveGroupAttributesDown, 12 | plugins.removeEmptyGroups, 13 | plugins.applyTransforms, 14 | plugins.removeIds, 15 | plugins.gatherCommonAttributes, 16 | ]; 17 | 18 | const jsxProcessors = [ 19 | plugins.removeSvgAttributes('xmlns'), 20 | plugins.camelCaseAttributes, 21 | plugins.numberValues, 22 | ]; 23 | 24 | export function clean(svgString: string): string { 25 | let svg = parse(svgString); 26 | svg = processors.reduce((svg, p) => cleanup(p(svg)), svg); 27 | return serialize(svg); 28 | } 29 | 30 | export function cleanJSX( 31 | svgString: string, 32 | jsxTag = 'svg', 33 | jsxProps: Attributes = {}, 34 | jsxInheritProps: string[] = [], 35 | jsxSpliceProps: string[] = [], 36 | ): string { 37 | let svg = parse(svgString); 38 | svg = processors.reduce((svg, p) => cleanup(p(svg)), svg); 39 | svg = jsxProcessors.reduce((svg, p) => cleanup(p(svg)), svg); 40 | svg = plugins.changeRootTag(jsxTag, jsxProps)(svg); 41 | svg = plugins.setJSXProps(jsxProps, jsxInheritProps, jsxSpliceProps)(svg); 42 | return serialize(svg); 43 | } 44 | -------------------------------------------------------------------------------- /src/jsx.ts: -------------------------------------------------------------------------------- 1 | export class InheritedProp { 2 | name: string; 3 | constructor(name: string) { 4 | this.name = name; 5 | } 6 | } 7 | 8 | export class SplicedProp { 9 | name: string; 10 | constructor(name: string) { 11 | this.name = name; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/parse.ts: -------------------------------------------------------------------------------- 1 | import * as ltx from 'ltx'; 2 | import * as dPathParser from 'd-path-parser'; 3 | 4 | export const parseD = dPathParser; 5 | 6 | export function parse(svgString: string): Element { 7 | return ltx.parse(svgString) as Element; 8 | } 9 | -------------------------------------------------------------------------------- /src/plugins/applyTransforms.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | import {parseD} from '../parse'; 3 | import {serializeD, formatNumber} from '../serialize'; 4 | 5 | type Vector = [number, number, number]; 6 | type Matrix = [number, number, number, number, number, number, number, number, number]; 7 | 8 | function multiply(a: Matrix, b: Matrix): Matrix { 9 | return [ 10 | a[0] * b[0] + a[1] * b[3] + a[2] * b[6], 11 | a[0] * b[1] + a[1] * b[4] + a[2] * b[7], 12 | a[0] * b[2] + a[1] * b[5] + a[2] * b[8], 13 | a[3] * b[0] + a[4] * b[3] + a[5] * b[6], 14 | a[3] * b[1] + a[4] * b[4] + a[5] * b[7], 15 | a[3] * b[2] + a[4] * b[5] + a[5] * b[8], 16 | a[6] * b[0] + a[7] * b[3] + a[8] * b[6], 17 | a[6] * b[1] + a[7] * b[4] + a[8] * b[7], 18 | a[6] * b[2] + a[7] * b[5] + a[8] * b[8], 19 | ]; 20 | } 21 | 22 | function multiplyVector(a: Matrix, v: Vector): Vector { 23 | return [ 24 | a[0] * v[0] + a[1] * v[1] + a[2] * v[2], 25 | a[3] * v[0] + a[4] * v[1] + a[5] * v[2], 26 | a[6] * v[0] + a[7] * v[1] + a[8] * v[2], 27 | ]; 28 | } 29 | 30 | function transformX(matrix: Matrix, x: number) { 31 | const [nx] = multiplyVector(matrix, [x, 0, 1]); 32 | return nx; 33 | } 34 | 35 | function transformY(matrix: Matrix, y: number) { 36 | const [, ny] = multiplyVector(matrix, [0, y, 1]); 37 | return ny; 38 | } 39 | 40 | function transformXY(matrix: Matrix, x: number, y: number) { 41 | return multiplyVector(matrix, [x, y, 1]); 42 | } 43 | 44 | function transformDX(matrix: Matrix, dx: number) { 45 | const [x0] = multiplyVector(matrix, [0, 0, 1]); 46 | const [x1] = multiplyVector(matrix, [dx, 0, 1]); 47 | return x1 - x0; 48 | } 49 | function transformDY(matrix: Matrix, dy: number) { 50 | const [, y0] = multiplyVector(matrix, [0, 0, 1]); 51 | const [, y1] = multiplyVector(matrix, [0, dy, 1]); 52 | return y1 - y0; 53 | } 54 | 55 | function parseTransform(transform: string): [Matrix, string] { 56 | let matrix: Matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1]; 57 | let match; 58 | const re = /(\w+)\(([-0-9e.]+\s*([,\s]\s*[-0-9e.]+)*)\)/g; 59 | const remainingTransforms = []; 60 | while ((match = re.exec(transform))) { 61 | const [, op, paramsString] = match; 62 | const params = paramsString.split(/[,\s]+/).map(parseFloat); 63 | let m: Matrix; 64 | switch (op) { 65 | case 'translate': 66 | let [tx, ty] = params; 67 | m = [1, 0, tx, 0, 1, ty, 0, 0, 1]; 68 | matrix = multiply(matrix, m); 69 | break; 70 | case 'scale': 71 | let [sx, sy] = params; 72 | m = [sx, 0, 0, 0, sy, 0, 0, 0, 1]; 73 | matrix = multiply(matrix, m); 74 | break; 75 | case 'rotate': 76 | let [a] = params; 77 | a = a * Math.PI / 180; 78 | m = [Math.cos(a), -Math.sin(a), 0, Math.sin(a), Math.cos(a), 0, 0, 0, 1]; 79 | matrix = multiply(matrix, m); 80 | break; 81 | default: 82 | console.warn(`Unknown transform ${op}`); 83 | remainingTransforms.push(match[0]); 84 | } 85 | } 86 | return [matrix, remainingTransforms.join(' ')]; 87 | } 88 | 89 | function applyMatrixToD(matrix: Matrix, d: string): string { 90 | const segments = parseD(d); 91 | for (let segment of segments) { 92 | switch (segment.code) { 93 | case 'M': 94 | case 'L': 95 | const [mx, my] = transformXY(matrix, segment.end.x, segment.end.y); 96 | segment.end.x = mx; 97 | segment.end.y = my; 98 | break; 99 | case 'm': 100 | case 'l': 101 | segment.end.x = transformDX(matrix, segment.end.x); 102 | segment.end.y = transformDY(matrix, segment.end.y); 103 | break; 104 | case 'H': 105 | segment.value = transformX(matrix, segment.value); 106 | break; 107 | case 'h': 108 | segment.value = transformDX(matrix, segment.value); 109 | break; 110 | case 'V': 111 | segment.value = transformY(matrix, segment.value); 112 | break; 113 | case 'v': 114 | segment.value = transformDY(matrix, segment.value); 115 | break; 116 | case 'C': 117 | const [cp1x, cp1y] = transformXY(matrix, segment.cp1.x, segment.cp1.y); 118 | const [cp2x, cp2y] = transformXY(matrix, segment.cp2.x, segment.cp2.y); 119 | const [endx, endy] = transformXY(matrix, segment.end.x, segment.end.y); 120 | segment.cp1.x = cp1x; 121 | segment.cp1.y = cp1y; 122 | segment.cp2.x = cp2x; 123 | segment.cp2.y = cp2y; 124 | segment.end.x = endx; 125 | segment.end.y = endy; 126 | break; 127 | case 'c': 128 | segment.cp1.x = transformDX(matrix, segment.cp1.x); 129 | segment.cp1.y = transformDY(matrix, segment.cp1.y); 130 | segment.cp2.x = transformDX(matrix, segment.cp2.x); 131 | segment.cp2.y = transformDY(matrix, segment.cp2.x); 132 | segment.end.x = transformDX(matrix, segment.end.x); 133 | segment.end.y = transformDY(matrix, segment.end.y); 134 | break; 135 | case 'S': 136 | const [cpx, cpy] = transformXY(matrix, segment.cp.x, segment.cp.y); 137 | const [sendx, sendy] = transformXY(matrix, segment.end.x, segment.end.y); 138 | segment.cp.x = cpx; 139 | segment.cp.y = cpy; 140 | segment.end.x = sendx; 141 | segment.end.y = sendy; 142 | break; 143 | case 's': 144 | segment.cp.x = transformDX(matrix, segment.cp.x); 145 | segment.cp.y = transformDY(matrix, segment.cp.y); 146 | segment.end.x = transformDX(matrix, segment.cp.x); 147 | segment.end.y = transformDY(matrix, segment.cp.y); 148 | break; 149 | case 'A': 150 | const [aendx, aendy] = transformXY(matrix, segment.end.x, segment.end.y); 151 | segment.end.x = aendx; 152 | segment.end.y = aendy; 153 | segment.radii.x = transformDX(matrix, segment.radii.x); 154 | segment.radii.y = transformDY(matrix, segment.radii.y); 155 | if (hasRotation(matrix)) { 156 | console.warn(`Rotating of arc path segments is not yet implemented: ${d}`); 157 | } 158 | break; 159 | case 'a': 160 | segment.end.x = transformDX(matrix, segment.end.x); 161 | segment.end.y = transformDY(matrix, segment.end.y); 162 | segment.radii.x = transformDX(matrix, segment.radii.x); 163 | segment.radii.y = transformDY(matrix, segment.radii.y); 164 | if (hasRotation(matrix)) { 165 | console.warn(`Rotating of arc path segments is not yet implemented: ${d}`); 166 | } 167 | break; 168 | case 'Z': 169 | break; 170 | default: 171 | console.warn(`Segment transform not implemented: ${segment.code}`); 172 | } 173 | } 174 | return serializeD(segments); 175 | } 176 | 177 | function applyMatrixToPoints(matrix: Matrix, points: string): string { 178 | const p = points.split(' '); 179 | const newPoints = []; 180 | for (let i = 0; i < p.length; i += 2) { 181 | const x = parseFloat(p[i]); 182 | const y = parseFloat(p[i + 1]); 183 | const vector: Vector = [x, y, 1]; 184 | const [nx, ny] = multiplyVector(matrix, vector); 185 | newPoints.push(formatNumber(nx)); 186 | newPoints.push(formatNumber(ny)); 187 | } 188 | return newPoints.join(' '); 189 | } 190 | 191 | function applyMatrixToX(matrix: Matrix, x: string): string { 192 | return formatNumber(transformX(matrix, parseFloat(x))); 193 | } 194 | 195 | function applyMatrixToY(matrix: Matrix, y: string): string { 196 | return formatNumber(transformY(matrix, parseFloat(y))); 197 | } 198 | 199 | function applyMatrixToWidth(matrix: Matrix, width: string): string { 200 | return formatNumber(transformDX(matrix, parseFloat(width))); 201 | } 202 | 203 | function applyMatrixToHeight(matrix: Matrix, height: string): string { 204 | return formatNumber(transformDY(matrix, parseFloat(height))); 205 | } 206 | 207 | function applyMatrixToR(matrix: Matrix, r: string): string { 208 | const rx = applyMatrixToWidth(matrix, r); 209 | const ry = applyMatrixToHeight(matrix, r); 210 | if (rx !== ry) { 211 | console.warn('Circle transformed into an ellipsis... not implemeted yet.'); 212 | } 213 | return rx; 214 | } 215 | 216 | function hasRotation(matrix: Matrix): boolean { 217 | return matrix[1] !== 0 || matrix[3] !== 0; 218 | } 219 | 220 | function hasScale(matrix: Matrix): boolean { 221 | return matrix[0] !== 1 || matrix[4] !== 1; 222 | } 223 | 224 | function convertRectToPolygon(el: Element) { 225 | // Punt on rects with corner radii 226 | if (el.attrs['rx']) return; 227 | el.name = 'polygon'; 228 | const x = parseFloat(el.attrs['x'] as string); 229 | const y = parseFloat(el.attrs['y'] as string); 230 | const width = parseFloat(el.attrs['width'] as string); 231 | const height = parseFloat(el.attrs['height'] as string); 232 | const points = [x, y, x + width, y, x + width, y + height, x, y + height]; 233 | el.attrs['points'] = points.map(formatNumber).join(' '); 234 | delete el.attrs['x']; 235 | delete el.attrs['y']; 236 | delete el.attrs['width']; 237 | delete el.attrs['height']; 238 | } 239 | 240 | function convertPolygonToRect(el: Element) { 241 | const [x0, y0, x1, y1, x2, y2, x3, y3, ...rest] = (el.attrs['points'] as string).split(' ').map(parseFloat); 242 | if (rest.length > 0) { 243 | if (rest.length !== 2) return; 244 | const [x4, y4] = rest; 245 | if (x0 !== x4 || y0 !== y4) return; 246 | } 247 | let width = 0; 248 | let height = 0; 249 | if (y0 === y1 && x1 === x2 && y2 === y3 && x3 === x0) { 250 | width = Math.abs(x1 - x0); 251 | height = Math.abs(y2 - y1); 252 | } else if (x0 === x1 && y1 === y2 && x2 === x3 && y3 === y0) { 253 | width = Math.abs(x2 - x1); 254 | height = Math.abs(y1 - y0); 255 | } else { 256 | return; 257 | } 258 | el.name = 'rect'; 259 | el.attrs['x'] = formatNumber(Math.min(x0, x1, x2, x3)); 260 | el.attrs['y'] = formatNumber(Math.min(y0, y1, y2, y3)); 261 | el.attrs['width'] = formatNumber(width); 262 | el.attrs['height'] = formatNumber(height); 263 | delete el.attrs['points']; 264 | } 265 | 266 | const transformsByElement = { 267 | rect(el: Element, matrix: Matrix) { 268 | if (hasRotation(matrix) || hasScale(matrix)) { 269 | convertRectToPolygon(el); 270 | if (el.name !== 'polygon') return false; 271 | transformsByElement.polygon(el, matrix); 272 | convertPolygonToRect(el); 273 | return true; 274 | } else { 275 | el.attrs['x'] = applyMatrixToX(matrix, el.attrs['x'] as string); 276 | el.attrs['y'] = applyMatrixToY(matrix, el.attrs['y'] as string); 277 | el.attrs['width'] = applyMatrixToWidth(matrix, el.attrs['width'] as string); 278 | el.attrs['height'] = applyMatrixToHeight(matrix, el.attrs['height'] as string); 279 | return true; 280 | } 281 | }, 282 | 283 | polygon(el: Element, matrix: Matrix) { 284 | el.attrs['points'] = applyMatrixToPoints(matrix, el.attrs['points'] as string); 285 | return true; 286 | }, 287 | 288 | polyline(el: Element, matrix: Matrix) { 289 | el.attrs['points'] = applyMatrixToPoints(matrix, el.attrs['points'] as string); 290 | return true; 291 | }, 292 | 293 | path(el: Element, matrix: Matrix) { 294 | el.attrs['d'] = applyMatrixToD(matrix, el.attrs['d'] as string); 295 | return true; 296 | }, 297 | 298 | circle(el: Element, matrix: Matrix) { 299 | el.attrs['cx'] = applyMatrixToX(matrix, el.attrs['cx'] as string); 300 | el.attrs['cy'] = applyMatrixToY(matrix, el.attrs['cy'] as string); 301 | el.attrs['r'] = applyMatrixToR(matrix, el.attrs['r'] as string); 302 | return true; 303 | }, 304 | }; 305 | 306 | // if (el.attrs['x']) el.attrs['x'] = applyMatrixToX(matrix, el.attrs['x'] as string); 307 | // if (el.attrs['y']) el.attrs['y'] = applyMatrixToY(matrix, el.attrs['y'] as string); 308 | 309 | export default function applyTransforms(el: Element): Element { 310 | if (el.attrs['transform']) { 311 | const [matrix] = parseTransform(el.attrs['transform'] as string); 312 | if (matrix) { 313 | const transform = transformsByElement[el.name as keyof typeof transformsByElement]; 314 | if (transform) { 315 | const success = transform(el, matrix); 316 | if (success) { 317 | delete el.attrs['transform']; 318 | } 319 | } 320 | } 321 | } 322 | if (el.children) { 323 | el.children = el.children.map(applyTransforms); 324 | } 325 | return el; 326 | } 327 | 328 | describe('applyTransforms', () => { 329 | it('should translate rects', () => { 330 | expect( 331 | applyTransforms, 332 | ``, 333 | ``, 334 | ); 335 | }); 336 | 337 | it('should scale rects', () => { 338 | expect( 339 | applyTransforms, 340 | ``, 341 | ``, 342 | ); 343 | }); 344 | 345 | it('should rotate rects 90 degrees', () => { 346 | expect( 347 | applyTransforms, 348 | ``, 349 | ``, 350 | ); 351 | }); 352 | 353 | it('should rotate rects 45 degrees', () => { 354 | expect( 355 | applyTransforms, 356 | ``, 357 | ``, 358 | ); 359 | }); 360 | 361 | it('should mix simple transforms with complex ones', () => { 362 | expect( 363 | applyTransforms, 364 | ``, 365 | ``, 366 | ); 367 | }); 368 | 369 | it('should translate circles', () => { 370 | expect( 371 | applyTransforms, 372 | ``, 373 | ``, 374 | ); 375 | }); 376 | 377 | it('should scale circles', () => { 378 | expect( 379 | applyTransforms, 380 | ``, 381 | ``, 382 | ); 383 | }); 384 | 385 | it('should translate polygons', () => { 386 | expect( 387 | applyTransforms, 388 | ``, 389 | ``, 390 | ); 391 | }); 392 | 393 | it('should translate polylines', () => { 394 | expect( 395 | applyTransforms, 396 | ``, 397 | ``, 398 | ); 399 | }); 400 | 401 | it('should scale polylines', () => { 402 | expect( 403 | applyTransforms, 404 | ``, 405 | ``, 406 | ); 407 | }); 408 | 409 | it('should rotate polylines', () => { 410 | expect( 411 | applyTransforms, 412 | ``, 413 | ``, 414 | ); 415 | }); 416 | 417 | it('should rotate paths', () => { 418 | expect( 419 | applyTransforms, 420 | ``, 421 | ``, 422 | ); 423 | }); 424 | 425 | it('should preserve transforms it cannot handle', () => { 426 | expect( 427 | applyTransforms, 428 | ``, 430 | ``, 431 | ); 432 | }); 433 | 434 | it('should apply translates to rects with radii', () => { 435 | expect( 436 | applyTransforms, 437 | ``, 438 | ``, 439 | ); 440 | }); 441 | 442 | xit('should support rotations around on a non-zero point', () => { 443 | expect( 444 | applyTransforms, 445 | ``, 447 | ``, 448 | ); 449 | }); 450 | }); 451 | -------------------------------------------------------------------------------- /src/plugins/camelCaseAttributes.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | 3 | export default function camelCaseAttributes(el: Element): Element { 4 | const newAttrs: Attributes = {}; 5 | for (let k of Object.keys(el.attrs)) { 6 | newAttrs[k.replace(/-([a-z])/g, (_, s) => s.toUpperCase())] = el.attrs[k]; 7 | } 8 | el.attrs = newAttrs; 9 | el.children = el.children.map(camelCaseAttributes); 10 | return el; 11 | } 12 | 13 | describe('camelCaseAttributes', () => { 14 | it('should camel case dashed attributes', () => { 15 | expect( 16 | camelCaseAttributes, 17 | ``, 18 | ``, 19 | ); 20 | }); 21 | }); -------------------------------------------------------------------------------- /src/plugins/changeRootTag.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | 3 | export default function changeRootTag(name: string, attrs: Attributes = {}) { 4 | return (el: Element) => { 5 | el.name = name; 6 | el.attrs = {...el.attrs, ...attrs}; 7 | return el; 8 | }; 9 | } 10 | 11 | describe('changeRootTag', () => { 12 | it('should change the tag', () => { 13 | expect( 14 | changeRootTag('View'), 15 | ``, 16 | ` 17 | 18 | `, 19 | ); 20 | }); 21 | 22 | it('should set attributes', () => { 23 | expect( 24 | changeRootTag('svg', {id: 'svg'}), 25 | ``, 26 | ` 27 | 28 | `, 29 | ); 30 | }); 31 | 32 | it('should override attributes', () => { 33 | expect( 34 | changeRootTag('View', {component: 'svg', viewBox: '0 0 10 10'}), 35 | ``, 36 | ` 37 | 38 | `, 39 | ); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/plugins/gatherCommonAttributes.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | import {serializeAttributes} from '../serialize'; 3 | 4 | const BLACKLIST = /^(x|y|width|height|points|d|cx|cy|r|rx|transform)=/; 5 | 6 | export default function gatherCommonAttributes(el: Element): Element { 7 | if (typeof el === 'string') return el; 8 | const childElements = el.children.filter(el => typeof el !== 'string') as Element[]; 9 | if (childElements.length > 0) { 10 | const attrs: {[name: string]: number} = {}; 11 | for (let child of childElements) { 12 | for (let attr of serializeAttributes(child.attrs || {})) { 13 | if (BLACKLIST.test(attr)) continue; 14 | if (!attrs[attr]) attrs[attr] = 0; 15 | attrs[attr]++; 16 | } 17 | } 18 | const attrsToHoist = Object.keys(attrs).filter(k => attrs[k] > 1 && attrs[k] >= childElements.length / 2); 19 | for (let attr of attrsToHoist) { 20 | const [name, value] = attr.split('='); 21 | el.attrs[name] = value.substring(1, value.length - 1); 22 | } 23 | for (let child of childElements) { 24 | for (let k in child.attrs) { 25 | if (el.attrs[k] === child.attrs[k]) { 26 | delete child.attrs[k]; 27 | } 28 | } 29 | } 30 | } 31 | if (el.children) { 32 | el.children = el.children.map(gatherCommonAttributes); 33 | } 34 | return el; 35 | } 36 | 37 | describe('gatherCommonAttributes', () => { 38 | it('should gather attributes that have quorum', () => { 39 | expect( 40 | gatherCommonAttributes, 41 | ` 42 | 43 | 44 | 45 | 46 | 47 | `, 48 | ` 49 | 50 | 51 | 52 | 53 | 54 | `, 55 | ); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export {default as applyTransforms} from './applyTransforms'; 2 | export {default as camelCaseAttributes} from './camelCaseAttributes'; 3 | export {default as changeRootTag} from './changeRootTag'; 4 | export {default as gatherCommonAttributes} from './gatherCommonAttributes'; 5 | export {default as inlineDefinitions} from './inlineDefinitions'; 6 | export {default as moveGroupAttributesDown} from './moveGroupAttributesDown'; 7 | export {default as numberValues} from './numberValues'; 8 | export {default as removeEmptyGroups} from './removeEmptyGroups'; 9 | export {default as removeIds} from './removeIds'; 10 | export {default as removeSvgAttributes} from './removeSvgAttributes'; 11 | export {default as removeTags} from './removeTags'; 12 | export {default as setJSXProps} from './setJSXProps'; 13 | export {default as trimText} from './trimText'; 14 | -------------------------------------------------------------------------------- /src/plugins/inlineDefinitions.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | 3 | type Definitions = {[id: string]: Element}; 4 | 5 | function findDefinitions(el: Element): Definitions { 6 | if (typeof el === 'string') return null; 7 | const childElements = el.children.filter(el => typeof el !== 'string') as Element[]; 8 | if (el.name !== 'defs') { 9 | for (const child of childElements) { 10 | const definitions = findDefinitions(child); 11 | if (definitions) return definitions; 12 | } 13 | return null; 14 | } 15 | const definitions: Definitions = {}; 16 | for (const child of childElements) { 17 | definitions[child.attrs['id'] as string] = child; 18 | } 19 | return definitions; 20 | } 21 | 22 | function insertDefinitions(el: Element | string, definitions: Definitions): Element | string { 23 | if (typeof el === 'string') return el; 24 | if (el.name === 'use') { 25 | const ref = (el.attrs['xlink:href'] as string).replace(/^#/, ''); 26 | const def = definitions[ref]; 27 | if (def) { 28 | el.name = def.name; 29 | el.attrs = {...def.attrs, ...el.attrs}; 30 | delete el.attrs['xlink:href']; 31 | delete el.attrs['id']; 32 | } 33 | } 34 | if (el.children) { 35 | el.children = el.children.map(child => insertDefinitions(child, definitions)); 36 | } 37 | return el; 38 | } 39 | 40 | export default function inlineDefinitions(el: Element): Element { 41 | const definitions = findDefinitions(el); 42 | return insertDefinitions(el, definitions) as Element; 43 | } 44 | 45 | describe('inlineDefinitions', () => { 46 | it('should inline definitions', () => { 47 | expect( 48 | inlineDefinitions, 49 | ` 50 | 51 | 52 | 53 | 54 | `, 55 | ` 56 | 57 | 58 | 59 | 60 | `, 61 | ); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/plugins/moveGroupAttributesDown.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | 3 | export default function moveGroupAttributesDown(el: Element): Element { 4 | if (el.name === 'g') { 5 | el.children = el.children.map(child => { 6 | if (typeof child === 'string') return child; 7 | const newAttrs = {...el.attrs, ...child.attrs}; 8 | if (el.attrs['transform'] && child.attrs['transform']) { 9 | newAttrs['transform'] = el.attrs['transform'] + ' ' + child.attrs['transform']; 10 | } 11 | child.attrs = newAttrs; 12 | return moveGroupAttributesDown(child); 13 | }); 14 | el.attrs = {}; 15 | } else if (el.children) { 16 | el.children = el.children.map(moveGroupAttributesDown); 17 | } 18 | return el; 19 | } 20 | 21 | describe('moveGroupAttributesDown', () => { 22 | it('should move attributes from group to children', () => { 23 | expect( 24 | moveGroupAttributesDown, 25 | ` 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | `, 34 | ` 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | `, 43 | ); 44 | }); 45 | 46 | it('should concatenate transforms', () => { 47 | expect( 48 | moveGroupAttributesDown, 49 | ` 50 | 51 | 52 | 53 | 54 | 55 | `, 56 | ` 57 | 58 | 59 | 60 | 61 | 62 | `, 63 | ); 64 | }); 65 | }); -------------------------------------------------------------------------------- /src/plugins/numberValues.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | 3 | export default function numberValues(el: Element): Element { 4 | for (let k of Object.keys(el.attrs)) { 5 | const val = el.attrs[k]; 6 | if (typeof val === 'string' && /^[-0-9.e]+(px)?$/.test(val)) { 7 | el.attrs[k] = parseFloat(val); 8 | } 9 | } 10 | el.children = el.children.map(numberValues); 11 | return el; 12 | } 13 | 14 | describe('numberValues', () => { 15 | it('should convert strings to numbers', () => { 16 | expect( 17 | numberValues, 18 | ``, 19 | ``, 20 | ); 21 | }); 22 | 23 | }); -------------------------------------------------------------------------------- /src/plugins/removeEmptyGroups.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | 3 | export default function removeEmptyGroups(el: Element): Element { 4 | if (typeof el === 'string') return el; 5 | const children = el.children.map(removeEmptyGroups); 6 | let newChildren: (Element | string)[] = []; 7 | for (let child of children) { 8 | if (typeof child !== 'string' && child.name === 'g' && (Object.keys(child.attrs).length === 0 || child.children.length === 0)) { 9 | newChildren = [...newChildren, ...child.children]; 10 | } else { 11 | newChildren.push(child); 12 | } 13 | } 14 | el.children = newChildren; 15 | return el; 16 | } 17 | 18 | describe('removeEmptyGroups', () => { 19 | it('should remove groups with no attributes', () => { 20 | expect( 21 | removeEmptyGroups, 22 | ``, 23 | ` 24 | 25 | `, 26 | ); 27 | }); 28 | 29 | it('should remove groups with no children', () => { 30 | expect( 31 | removeEmptyGroups, 32 | ``, 33 | ` 34 | 35 | `, 36 | ); 37 | }); 38 | 39 | }); -------------------------------------------------------------------------------- /src/plugins/removeIds.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | 3 | export default function removeIds(el: Element): Element { 4 | if (typeof el === 'string') return el; 5 | delete el.attrs['id']; 6 | el.children = el.children.map(removeIds); 7 | return el; 8 | } 9 | 10 | describe('removeIds', () => { 11 | it('should remove ids', () => { 12 | expect( 13 | removeIds, 14 | ``, 15 | ` 16 | 17 | 18 | 19 | `, 20 | ); 21 | }); 22 | 23 | }); -------------------------------------------------------------------------------- /src/plugins/removeSvgAttributes.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | 3 | export default function removeSvgAttributes(...attrs: string[]) { 4 | return (el: Element) => { 5 | for (let attr of attrs) { 6 | delete el.attrs[attr]; 7 | } 8 | return el; 9 | }; 10 | } 11 | 12 | 13 | describe('removeSvgAttributes', () => { 14 | it('should remove ids', () => { 15 | expect( 16 | removeSvgAttributes('viewport', 'id'), 17 | ``, 18 | ` 19 | 20 | 21 | 22 | `, 23 | ); 24 | }); 25 | 26 | }); -------------------------------------------------------------------------------- /src/plugins/removeTags.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | 3 | export default function removeTags(...tags: string[]) { 4 | return function remove(el: Element) { 5 | if (tags.indexOf(el.name) >= 0) return null; 6 | if (el.children) { 7 | el.children = el.children.map(remove); 8 | } 9 | return el; 10 | } 11 | } 12 | 13 | describe('removeTags', () => { 14 | it('should remove the tags', () => { 15 | expect( 16 | removeTags('defs', 'title'), 17 | `title`, 18 | ` 19 | 20 | `, 21 | ); 22 | }); 23 | 24 | }); -------------------------------------------------------------------------------- /src/plugins/setJSXProps.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | import {InheritedProp, SplicedProp} from '../jsx'; 3 | 4 | export default function setJSXProps( 5 | props: Attributes = {}, 6 | inheritedProps: string[] = [], 7 | splicedProps: string[] = [], 8 | ) { 9 | return (el: Element) => { 10 | el.attrs = {...el.attrs, ...props}; 11 | for (let prop of inheritedProps) { 12 | el.attrs[prop] = new InheritedProp(prop); 13 | } 14 | for (let prop of splicedProps) { 15 | el.attrs[prop] = new SplicedProp(prop); 16 | } 17 | return el; 18 | }; 19 | } 20 | 21 | describe('setJSXProps', () => { 22 | it('should add constant props', () => { 23 | expect( 24 | setJSXProps({fill: '#000', stroke: '#ccc'}), 25 | ``, 26 | ` 27 | 28 | `, 29 | ); 30 | }); 31 | 32 | it('should add inherited props', () => { 33 | expect( 34 | setJSXProps({}, ['fill', 'stroke']), 35 | ``, 36 | ` 37 | 38 | `, 39 | ); 40 | }); 41 | 42 | it('should add spliced props', () => { 43 | expect( 44 | setJSXProps({}, ['fill', 'stroke'], ['props']), 45 | ``, 46 | ` 47 | 48 | `, 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/plugins/trimText.ts: -------------------------------------------------------------------------------- 1 | import {expect, describe} from '../utils'; 2 | 3 | export default function trimText(el: Element): Element { 4 | el.children = el.children.map(child => { 5 | if (typeof child !== 'string') return trimText(child); 6 | child = child.replace(/\s+/g, ''); 7 | if (!child) return null; 8 | return child; 9 | }) 10 | return el; 11 | } 12 | 13 | describe('trimText', () => { 14 | it('should trim text', () => { 15 | expect(trimText, ` 16 | text 17 | `, ` 18 | text 19 | `); 20 | }); 21 | }); -------------------------------------------------------------------------------- /src/serialize.ts: -------------------------------------------------------------------------------- 1 | import {InheritedProp, SplicedProp} from './jsx'; 2 | import {Command} from 'd-path-parser'; 3 | 4 | export function formatNumber(n: number, precision = 3): string { 5 | n = parseFloat(n.toFixed(precision)); 6 | if (isNaN(n)) console.warn('Found invalid number: ', n); 7 | return n.toString(); 8 | } 9 | 10 | function float(n: number): string { 11 | return formatNumber(n); 12 | } 13 | 14 | function floatPair(x: number, y: number): string { 15 | return `${float(x)},${float(y)}`; 16 | } 17 | 18 | function boolean(x: boolean): string { 19 | return x ? '1' : '0'; 20 | } 21 | 22 | export function serializeD(segments: Command[]): string { 23 | return segments 24 | .map(segment => { 25 | switch (segment.code) { 26 | case 'M': 27 | case 'm': 28 | case 'L': 29 | case 'l': 30 | return `${segment.code}${floatPair(segment.end.x, segment.end.y)}`; 31 | case 'H': 32 | case 'h': 33 | case 'V': 34 | case 'v': 35 | return `${segment.code}${float(segment.value)}`; 36 | case 'C': 37 | case 'c': 38 | const {code, cp1, cp2, end} = segment; 39 | return `${code}${floatPair(cp1.x, cp1.y)} ${floatPair(cp2.x, cp2.y)} ${floatPair(end.x, end.y)}`; 40 | case 'S': 41 | case 's': 42 | const {code: codes, cp: cps, end: ends} = segment; 43 | return `${codes}${floatPair(cps.x, cps.y)} ${floatPair(ends.x, ends.y)}`; 44 | case 'A': 45 | case 'a': 46 | const {code: codea, radii, rotation, large, clockwise, end: enda} = segment; 47 | return `${codea}${floatPair(radii.x, radii.y)} ${float(rotation)} ${boolean(large)} ${boolean( 48 | clockwise, 49 | )} ${floatPair(enda.x, enda.y)}`; 50 | default: 51 | return `${segment.code}`; 52 | } 53 | }) 54 | .join(' '); 55 | } 56 | 57 | export function serializeAttributes(attrs: Attributes): string[] { 58 | return Object.keys(attrs).map(name => { 59 | const value = attrs[name]; 60 | if (value instanceof InheritedProp) { 61 | return `${value.name}={${value.name}}`; 62 | } else if (value instanceof SplicedProp) { 63 | return `{...${value.name}}`; 64 | } else if (typeof value === 'number') { 65 | return `${name}={${value}}`; 66 | } else { 67 | return `${name}=${JSON.stringify(value)}`; 68 | } 69 | }); 70 | } 71 | 72 | export function serialize(el: Element, indent: string = ''): string { 73 | const attrStrings = serializeAttributes(el.attrs); 74 | const attrs = attrStrings.length > 0 ? ' ' + attrStrings.join(' ') : ''; 75 | if (el.children.length === 0) { 76 | return `${indent}<${el.name}${attrs} />`; 77 | } else { 78 | return [ 79 | `${indent}<${el.name}${attrs}>`, 80 | ...el.children.map( 81 | child => (typeof child === 'string' ? indent + ' ' + child : serialize(child, indent + ' ')), 82 | ), 83 | `${indent}`, 84 | ].join('\n'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import {trimText} from './plugins'; 3 | import {parse} from './parse'; 4 | import {serialize} from './serialize'; 5 | import {InheritedProp, SplicedProp} from './jsx'; 6 | 7 | declare global { 8 | interface Element { 9 | name: string; 10 | attrs: Attributes; 11 | children: (Element | string)[]; 12 | 13 | attr(name: string, value: string | null): void; 14 | } 15 | 16 | type Attributes = {[name: string]: string | number | InheritedProp | SplicedProp}; 17 | } 18 | 19 | export function cleanup(el: Element): Element { 20 | if (el.children) { 21 | el.children = el.children.filter(child => child !== null).map(cleanup); 22 | } 23 | return el; 24 | } 25 | 26 | export function expect(processor: (el: Element) => Element, svgString: string, output: string) { 27 | chai.expect(serialize(cleanup(processor(cleanup(trimText(parse(svgString))))))).to.equal(output); 28 | } 29 | 30 | export const describe = 31 | ((global as {describe?: (desc: string, f: () => void) => void}).describe as ( 32 | desc: string, 33 | f: () => void, 34 | ) => void) || function() {}; 35 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import {clean, cleanJSX} from '../src/index'; 2 | import {expect} from 'chai'; 3 | 4 | describe('Clean plain SVG', () => { 5 | it('should clean a complex icon', () => { 6 | const cleaned = clean(` 7 | 8 | coupon 9 | Created with Sketch. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | `); 29 | expect(cleaned).to 30 | .equal(` 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | `); 41 | }); 42 | 43 | it('should handle nested transforms', () => { 44 | const cleaned = clean(` 45 | 46 | 47 | red 48 | Created with Sketch. 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | `); 60 | expect(cleaned).to 61 | .equal(` 62 | 63 | 64 | 65 | `); 66 | }); 67 | 68 | xit('should handle rotated rectangles', () => { 69 | const cleaned = clean(` 70 | 71 | 72 | search 73 | Created with Sketch. 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | `); 91 | expect(cleaned).to 92 | .equal(` 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | `); 105 | }); 106 | }); 107 | 108 | describe('Clean into React/JSX', () => { 109 | it('should clean a simple icon into JSX', () => { 110 | const cleaned = cleanJSX( 111 | ` 112 | 113 | Created with Sketch. 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | `, 129 | 'View', 130 | {component: 'svg'}, 131 | ['stroke'], 132 | ['props'], 133 | ); 134 | expect(cleaned).to 135 | .equal(` 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | `); 146 | }); 147 | }); 148 | 149 | describe('Cleans up Sketch centered stroke hack', () => { 150 | xit('should fix stroke on a rounded rectangle', () => { 151 | const cleaned = clean( 152 | ` 153 | 154 | 155 | icons/checklist 156 | Created with Sketch. 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | `, 184 | ); 185 | expect(cleaned).to 186 | .equal(` 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | `); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "noUnusedLocals": true, 5 | "noUnusedParameters": true, 6 | "lib": ["es2017"], 7 | "outDir": "dist" 8 | }, 9 | "include": ["src/**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/chai@^4.0.4": 6 | version "4.0.4" 7 | resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.0.4.tgz#fe86315d9a66827feeb16f73bc954688ec950e18" 8 | 9 | "@types/ltx@^2.6.2": 10 | version "2.6.2" 11 | resolved "https://registry.yarnpkg.com/@types/ltx/-/ltx-2.6.2.tgz#f585a84356e28dfc371c0b69172630b2ce685b2e" 12 | dependencies: 13 | "@types/node" "*" 14 | 15 | "@types/mocha@^2.2.44": 16 | version "2.2.44" 17 | resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.44.tgz#1d4a798e53f35212fd5ad4d04050620171cd5b5e" 18 | 19 | "@types/node@*": 20 | version "8.0.47" 21 | resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.47.tgz#968e596f91acd59069054558a00708c445ca30c2" 22 | 23 | "@types/yargs@^8.0.2": 24 | version "8.0.2" 25 | resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-8.0.2.tgz#0f9c7b236e2d78cd8f4b6502de15d0728aa29385" 26 | 27 | ansi-regex@^2.0.0: 28 | version "2.1.1" 29 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 30 | 31 | ansi-regex@^3.0.0: 32 | version "3.0.0" 33 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" 34 | 35 | ansi-styles@^3.1.0, ansi-styles@^3.2.0: 36 | version "3.2.0" 37 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" 38 | dependencies: 39 | color-convert "^1.9.0" 40 | 41 | arr-diff@^2.0.0: 42 | version "2.0.0" 43 | resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" 44 | dependencies: 45 | arr-flatten "^1.0.1" 46 | 47 | arr-flatten@^1.0.1: 48 | version "1.1.0" 49 | resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" 50 | 51 | array-unique@^0.2.1: 52 | version "0.2.1" 53 | resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" 54 | 55 | arrify@^1.0.0: 56 | version "1.0.1" 57 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" 58 | 59 | assertion-error@^1.0.1: 60 | version "1.0.2" 61 | resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" 62 | 63 | balanced-match@^1.0.0: 64 | version "1.0.0" 65 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 66 | 67 | brace-expansion@^1.1.7: 68 | version "1.1.8" 69 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" 70 | dependencies: 71 | balanced-match "^1.0.0" 72 | concat-map "0.0.1" 73 | 74 | braces@^1.8.2: 75 | version "1.8.5" 76 | resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" 77 | dependencies: 78 | expand-range "^1.8.1" 79 | preserve "^0.2.0" 80 | repeat-element "^1.1.2" 81 | 82 | browser-stdout@1.3.0: 83 | version "1.3.0" 84 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" 85 | 86 | camelcase@^4.1.0: 87 | version "4.1.0" 88 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" 89 | 90 | chai@^4.1.2: 91 | version "4.1.2" 92 | resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c" 93 | dependencies: 94 | assertion-error "^1.0.1" 95 | check-error "^1.0.1" 96 | deep-eql "^3.0.0" 97 | get-func-name "^2.0.0" 98 | pathval "^1.0.0" 99 | type-detect "^4.0.0" 100 | 101 | chalk@^2.0.0, chalk@^2.0.1: 102 | version "2.3.0" 103 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" 104 | dependencies: 105 | ansi-styles "^3.1.0" 106 | escape-string-regexp "^1.0.5" 107 | supports-color "^4.0.0" 108 | 109 | check-error@^1.0.1: 110 | version "1.0.2" 111 | resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" 112 | 113 | cliui@^3.2.0: 114 | version "3.2.0" 115 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" 116 | dependencies: 117 | string-width "^1.0.1" 118 | strip-ansi "^3.0.1" 119 | wrap-ansi "^2.0.0" 120 | 121 | code-point-at@^1.0.0: 122 | version "1.1.0" 123 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 124 | 125 | color-convert@^1.9.0: 126 | version "1.9.0" 127 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" 128 | dependencies: 129 | color-name "^1.1.1" 130 | 131 | color-name@^1.1.1: 132 | version "1.1.3" 133 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 134 | 135 | commander@2.11.0: 136 | version "2.11.0" 137 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" 138 | 139 | concat-map@0.0.1: 140 | version "0.0.1" 141 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 142 | 143 | cross-spawn@^5.0.1: 144 | version "5.1.0" 145 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" 146 | dependencies: 147 | lru-cache "^4.0.1" 148 | shebang-command "^1.2.0" 149 | which "^1.2.9" 150 | 151 | d-path-parser@^1.0.0: 152 | version "1.0.0" 153 | resolved "https://registry.yarnpkg.com/d-path-parser/-/d-path-parser-1.0.0.tgz#87bfb41b44c55962b4efbe5da31502b5ee8b0ab7" 154 | 155 | debug@3.1.0: 156 | version "3.1.0" 157 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 158 | dependencies: 159 | ms "2.0.0" 160 | 161 | decamelize@^1.1.1: 162 | version "1.2.0" 163 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 164 | 165 | deep-eql@^3.0.0: 166 | version "3.0.1" 167 | resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" 168 | dependencies: 169 | type-detect "^4.0.0" 170 | 171 | diff@3.3.1: 172 | version "3.3.1" 173 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" 174 | 175 | diff@^3.1.0: 176 | version "3.2.0" 177 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" 178 | 179 | diff@^3.2.0: 180 | version "3.4.0" 181 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" 182 | 183 | escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: 184 | version "1.0.5" 185 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 186 | 187 | execa@^0.7.0: 188 | version "0.7.0" 189 | resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" 190 | dependencies: 191 | cross-spawn "^5.0.1" 192 | get-stream "^3.0.0" 193 | is-stream "^1.1.0" 194 | npm-run-path "^2.0.0" 195 | p-finally "^1.0.0" 196 | signal-exit "^3.0.0" 197 | strip-eof "^1.0.0" 198 | 199 | expand-brackets@^0.1.4: 200 | version "0.1.5" 201 | resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" 202 | dependencies: 203 | is-posix-bracket "^0.1.0" 204 | 205 | expand-range@^1.8.1: 206 | version "1.8.2" 207 | resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" 208 | dependencies: 209 | fill-range "^2.1.0" 210 | 211 | expect@^21.2.1: 212 | version "21.2.1" 213 | resolved "https://registry.yarnpkg.com/expect/-/expect-21.2.1.tgz#003ac2ac7005c3c29e73b38a272d4afadd6d1d7b" 214 | dependencies: 215 | ansi-styles "^3.2.0" 216 | jest-diff "^21.2.1" 217 | jest-get-type "^21.2.0" 218 | jest-matcher-utils "^21.2.1" 219 | jest-message-util "^21.2.1" 220 | jest-regex-util "^21.2.0" 221 | 222 | extglob@^0.3.1: 223 | version "0.3.2" 224 | resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" 225 | dependencies: 226 | is-extglob "^1.0.0" 227 | 228 | filename-regex@^2.0.0: 229 | version "2.0.1" 230 | resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" 231 | 232 | fill-range@^2.1.0: 233 | version "2.2.3" 234 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" 235 | dependencies: 236 | is-number "^2.1.0" 237 | isobject "^2.0.0" 238 | randomatic "^1.1.3" 239 | repeat-element "^1.1.2" 240 | repeat-string "^1.5.2" 241 | 242 | find-up@^2.1.0: 243 | version "2.1.0" 244 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" 245 | dependencies: 246 | locate-path "^2.0.0" 247 | 248 | for-in@^1.0.1: 249 | version "1.0.2" 250 | resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" 251 | 252 | for-own@^0.1.4: 253 | version "0.1.5" 254 | resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" 255 | dependencies: 256 | for-in "^1.0.1" 257 | 258 | fs.realpath@^1.0.0: 259 | version "1.0.0" 260 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 261 | 262 | get-caller-file@^1.0.1: 263 | version "1.0.2" 264 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" 265 | 266 | get-func-name@^2.0.0: 267 | version "2.0.0" 268 | resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" 269 | 270 | get-stream@^3.0.0: 271 | version "3.0.0" 272 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" 273 | 274 | glob-base@^0.3.0: 275 | version "0.3.0" 276 | resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" 277 | dependencies: 278 | glob-parent "^2.0.0" 279 | is-glob "^2.0.0" 280 | 281 | glob-parent@^2.0.0: 282 | version "2.0.0" 283 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" 284 | dependencies: 285 | is-glob "^2.0.0" 286 | 287 | glob@7.1.2: 288 | version "7.1.2" 289 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 290 | dependencies: 291 | fs.realpath "^1.0.0" 292 | inflight "^1.0.4" 293 | inherits "2" 294 | minimatch "^3.0.4" 295 | once "^1.3.0" 296 | path-is-absolute "^1.0.0" 297 | 298 | growl@1.10.3: 299 | version "1.10.3" 300 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" 301 | 302 | has-flag@^2.0.0: 303 | version "2.0.0" 304 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" 305 | 306 | he@1.1.1: 307 | version "1.1.1" 308 | resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" 309 | 310 | homedir-polyfill@^1.0.1: 311 | version "1.0.1" 312 | resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" 313 | dependencies: 314 | parse-passwd "^1.0.0" 315 | 316 | inflight@^1.0.4: 317 | version "1.0.6" 318 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 319 | dependencies: 320 | once "^1.3.0" 321 | wrappy "1" 322 | 323 | inherits@2, inherits@^2.0.1: 324 | version "2.0.3" 325 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 326 | 327 | invert-kv@^1.0.0: 328 | version "1.0.0" 329 | resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" 330 | 331 | is-buffer@^1.1.5: 332 | version "1.1.6" 333 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 334 | 335 | is-dotfile@^1.0.0: 336 | version "1.0.3" 337 | resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" 338 | 339 | is-equal-shallow@^0.1.3: 340 | version "0.1.3" 341 | resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" 342 | dependencies: 343 | is-primitive "^2.0.0" 344 | 345 | is-extendable@^0.1.1: 346 | version "0.1.1" 347 | resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" 348 | 349 | is-extglob@^1.0.0: 350 | version "1.0.0" 351 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" 352 | 353 | is-fullwidth-code-point@^1.0.0: 354 | version "1.0.0" 355 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" 356 | dependencies: 357 | number-is-nan "^1.0.0" 358 | 359 | is-fullwidth-code-point@^2.0.0: 360 | version "2.0.0" 361 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" 362 | 363 | is-glob@^2.0.0, is-glob@^2.0.1: 364 | version "2.0.1" 365 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" 366 | dependencies: 367 | is-extglob "^1.0.0" 368 | 369 | is-number@^2.1.0: 370 | version "2.1.0" 371 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" 372 | dependencies: 373 | kind-of "^3.0.2" 374 | 375 | is-number@^3.0.0: 376 | version "3.0.0" 377 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" 378 | dependencies: 379 | kind-of "^3.0.2" 380 | 381 | is-posix-bracket@^0.1.0: 382 | version "0.1.1" 383 | resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" 384 | 385 | is-primitive@^2.0.0: 386 | version "2.0.0" 387 | resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" 388 | 389 | is-stream@^1.1.0: 390 | version "1.1.0" 391 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 392 | 393 | isarray@1.0.0: 394 | version "1.0.0" 395 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 396 | 397 | isexe@^2.0.0: 398 | version "2.0.0" 399 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 400 | 401 | isobject@^2.0.0: 402 | version "2.1.0" 403 | resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" 404 | dependencies: 405 | isarray "1.0.0" 406 | 407 | jest-diff@^21.2.1: 408 | version "21.2.1" 409 | resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-21.2.1.tgz#46cccb6cab2d02ce98bc314011764bb95b065b4f" 410 | dependencies: 411 | chalk "^2.0.1" 412 | diff "^3.2.0" 413 | jest-get-type "^21.2.0" 414 | pretty-format "^21.2.1" 415 | 416 | jest-get-type@^21.2.0: 417 | version "21.2.0" 418 | resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-21.2.0.tgz#f6376ab9db4b60d81e39f30749c6c466f40d4a23" 419 | 420 | jest-matcher-utils@^21.2.1: 421 | version "21.2.1" 422 | resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-21.2.1.tgz#72c826eaba41a093ac2b4565f865eb8475de0f64" 423 | dependencies: 424 | chalk "^2.0.1" 425 | jest-get-type "^21.2.0" 426 | pretty-format "^21.2.1" 427 | 428 | jest-message-util@^21.2.1: 429 | version "21.2.1" 430 | resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-21.2.1.tgz#bfe5d4692c84c827d1dcf41823795558f0a1acbe" 431 | dependencies: 432 | chalk "^2.0.1" 433 | micromatch "^2.3.11" 434 | slash "^1.0.0" 435 | 436 | jest-regex-util@^21.2.0: 437 | version "21.2.0" 438 | resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-21.2.0.tgz#1b1e33e63143babc3e0f2e6c9b5ba1eb34b2d530" 439 | 440 | kind-of@^3.0.2: 441 | version "3.2.2" 442 | resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" 443 | dependencies: 444 | is-buffer "^1.1.5" 445 | 446 | kind-of@^4.0.0: 447 | version "4.0.0" 448 | resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" 449 | dependencies: 450 | is-buffer "^1.1.5" 451 | 452 | lcid@^1.0.0: 453 | version "1.0.0" 454 | resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" 455 | dependencies: 456 | invert-kv "^1.0.0" 457 | 458 | locate-path@^2.0.0: 459 | version "2.0.0" 460 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" 461 | dependencies: 462 | p-locate "^2.0.0" 463 | path-exists "^3.0.0" 464 | 465 | lru-cache@^4.0.1: 466 | version "4.1.1" 467 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" 468 | dependencies: 469 | pseudomap "^1.0.2" 470 | yallist "^2.1.2" 471 | 472 | ltx@^2.7.1: 473 | version "2.7.1" 474 | resolved "https://registry.yarnpkg.com/ltx/-/ltx-2.7.1.tgz#0e5cbdcb5bf178cfa7831ea41dc323d97422315a" 475 | dependencies: 476 | inherits "^2.0.1" 477 | 478 | make-error@^1.1.1: 479 | version "1.2.1" 480 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.2.1.tgz#9a6dfb4844423b9f145806728d05c6e935670e75" 481 | 482 | mem@^1.1.0: 483 | version "1.1.0" 484 | resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" 485 | dependencies: 486 | mimic-fn "^1.0.0" 487 | 488 | micromatch@^2.3.11: 489 | version "2.3.11" 490 | resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" 491 | dependencies: 492 | arr-diff "^2.0.0" 493 | array-unique "^0.2.1" 494 | braces "^1.8.2" 495 | expand-brackets "^0.1.4" 496 | extglob "^0.3.1" 497 | filename-regex "^2.0.0" 498 | is-extglob "^1.0.0" 499 | is-glob "^2.0.1" 500 | kind-of "^3.0.2" 501 | normalize-path "^2.0.1" 502 | object.omit "^2.0.0" 503 | parse-glob "^3.0.4" 504 | regex-cache "^0.4.2" 505 | 506 | mimic-fn@^1.0.0: 507 | version "1.1.0" 508 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" 509 | 510 | minimatch@^3.0.4: 511 | version "3.0.4" 512 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 513 | dependencies: 514 | brace-expansion "^1.1.7" 515 | 516 | minimist@0.0.8: 517 | version "0.0.8" 518 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 519 | 520 | minimist@^1.2.0: 521 | version "1.2.0" 522 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 523 | 524 | mkdirp@0.5.1, mkdirp@^0.5.1: 525 | version "0.5.1" 526 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 527 | dependencies: 528 | minimist "0.0.8" 529 | 530 | mocha@^4.0.1: 531 | version "4.0.1" 532 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.0.1.tgz#0aee5a95cf69a4618820f5e51fa31717117daf1b" 533 | dependencies: 534 | browser-stdout "1.3.0" 535 | commander "2.11.0" 536 | debug "3.1.0" 537 | diff "3.3.1" 538 | escape-string-regexp "1.0.5" 539 | glob "7.1.2" 540 | growl "1.10.3" 541 | he "1.1.1" 542 | mkdirp "0.5.1" 543 | supports-color "4.4.0" 544 | 545 | ms@2.0.0: 546 | version "2.0.0" 547 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 548 | 549 | normalize-path@^2.0.1: 550 | version "2.1.1" 551 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" 552 | dependencies: 553 | remove-trailing-separator "^1.0.1" 554 | 555 | npm-run-path@^2.0.0: 556 | version "2.0.2" 557 | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" 558 | dependencies: 559 | path-key "^2.0.0" 560 | 561 | number-is-nan@^1.0.0: 562 | version "1.0.1" 563 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 564 | 565 | object.omit@^2.0.0: 566 | version "2.0.1" 567 | resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" 568 | dependencies: 569 | for-own "^0.1.4" 570 | is-extendable "^0.1.1" 571 | 572 | once@^1.3.0: 573 | version "1.4.0" 574 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 575 | dependencies: 576 | wrappy "1" 577 | 578 | os-locale@^2.0.0: 579 | version "2.1.0" 580 | resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" 581 | dependencies: 582 | execa "^0.7.0" 583 | lcid "^1.0.0" 584 | mem "^1.1.0" 585 | 586 | p-finally@^1.0.0: 587 | version "1.0.0" 588 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" 589 | 590 | p-limit@^1.1.0: 591 | version "1.1.0" 592 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" 593 | 594 | p-locate@^2.0.0: 595 | version "2.0.0" 596 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" 597 | dependencies: 598 | p-limit "^1.1.0" 599 | 600 | parse-glob@^3.0.4: 601 | version "3.0.4" 602 | resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" 603 | dependencies: 604 | glob-base "^0.3.0" 605 | is-dotfile "^1.0.0" 606 | is-extglob "^1.0.0" 607 | is-glob "^2.0.0" 608 | 609 | parse-passwd@^1.0.0: 610 | version "1.0.0" 611 | resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" 612 | 613 | path-exists@^3.0.0: 614 | version "3.0.0" 615 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" 616 | 617 | path-is-absolute@^1.0.0: 618 | version "1.0.1" 619 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 620 | 621 | path-key@^2.0.0: 622 | version "2.0.1" 623 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" 624 | 625 | pathval@^1.0.0: 626 | version "1.1.0" 627 | resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" 628 | 629 | preserve@^0.2.0: 630 | version "0.2.0" 631 | resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" 632 | 633 | pretty-format@^21.2.1: 634 | version "21.2.1" 635 | resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-21.2.1.tgz#ae5407f3cf21066cd011aa1ba5fce7b6a2eddb36" 636 | dependencies: 637 | ansi-regex "^3.0.0" 638 | ansi-styles "^3.2.0" 639 | 640 | pseudomap@^1.0.2: 641 | version "1.0.2" 642 | resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" 643 | 644 | randomatic@^1.1.3: 645 | version "1.1.7" 646 | resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" 647 | dependencies: 648 | is-number "^3.0.0" 649 | kind-of "^4.0.0" 650 | 651 | regex-cache@^0.4.2: 652 | version "0.4.4" 653 | resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" 654 | dependencies: 655 | is-equal-shallow "^0.1.3" 656 | 657 | remove-trailing-separator@^1.0.1: 658 | version "1.1.0" 659 | resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" 660 | 661 | repeat-element@^1.1.2: 662 | version "1.1.2" 663 | resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" 664 | 665 | repeat-string@^1.5.2: 666 | version "1.6.1" 667 | resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" 668 | 669 | require-directory@^2.1.1: 670 | version "2.1.1" 671 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 672 | 673 | require-main-filename@^1.0.1: 674 | version "1.0.1" 675 | resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" 676 | 677 | set-blocking@^2.0.0: 678 | version "2.0.0" 679 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 680 | 681 | shebang-command@^1.2.0: 682 | version "1.2.0" 683 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" 684 | dependencies: 685 | shebang-regex "^1.0.0" 686 | 687 | shebang-regex@^1.0.0: 688 | version "1.0.0" 689 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" 690 | 691 | signal-exit@^3.0.0: 692 | version "3.0.2" 693 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 694 | 695 | slash@^1.0.0: 696 | version "1.0.0" 697 | resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" 698 | 699 | source-map-support@^0.4.0: 700 | version "0.4.8" 701 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.8.tgz#4871918d8a3af07289182e974e32844327b2e98b" 702 | dependencies: 703 | source-map "^0.5.3" 704 | 705 | source-map@^0.5.3: 706 | version "0.5.6" 707 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" 708 | 709 | string-width@^1.0.1: 710 | version "1.0.2" 711 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" 712 | dependencies: 713 | code-point-at "^1.0.0" 714 | is-fullwidth-code-point "^1.0.0" 715 | strip-ansi "^3.0.0" 716 | 717 | string-width@^2.0.0: 718 | version "2.1.1" 719 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" 720 | dependencies: 721 | is-fullwidth-code-point "^2.0.0" 722 | strip-ansi "^4.0.0" 723 | 724 | strip-ansi@^3.0.0, strip-ansi@^3.0.1: 725 | version "3.0.1" 726 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 727 | dependencies: 728 | ansi-regex "^2.0.0" 729 | 730 | strip-ansi@^4.0.0: 731 | version "4.0.0" 732 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" 733 | dependencies: 734 | ansi-regex "^3.0.0" 735 | 736 | strip-bom@^3.0.0: 737 | version "3.0.0" 738 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" 739 | 740 | strip-eof@^1.0.0: 741 | version "1.0.0" 742 | resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" 743 | 744 | strip-json-comments@^2.0.0: 745 | version "2.0.1" 746 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 747 | 748 | supports-color@4.4.0: 749 | version "4.4.0" 750 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" 751 | dependencies: 752 | has-flag "^2.0.0" 753 | 754 | supports-color@^4.0.0: 755 | version "4.5.0" 756 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" 757 | dependencies: 758 | has-flag "^2.0.0" 759 | 760 | ts-node@^3.3.0: 761 | version "3.3.0" 762 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-3.3.0.tgz#c13c6a3024e30be1180dd53038fc209289d4bf69" 763 | dependencies: 764 | arrify "^1.0.0" 765 | chalk "^2.0.0" 766 | diff "^3.1.0" 767 | make-error "^1.1.1" 768 | minimist "^1.2.0" 769 | mkdirp "^0.5.1" 770 | source-map-support "^0.4.0" 771 | tsconfig "^6.0.0" 772 | v8flags "^3.0.0" 773 | yn "^2.0.0" 774 | 775 | tsconfig@^6.0.0: 776 | version "6.0.0" 777 | resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-6.0.0.tgz#6b0e8376003d7af1864f8df8f89dd0059ffcd032" 778 | dependencies: 779 | strip-bom "^3.0.0" 780 | strip-json-comments "^2.0.0" 781 | 782 | type-detect@^4.0.0: 783 | version "4.0.3" 784 | resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea" 785 | 786 | typescript@^2.5.3: 787 | version "2.5.3" 788 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d" 789 | 790 | v8flags@^3.0.0: 791 | version "3.0.1" 792 | resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.0.1.tgz#dce8fc379c17d9f2c9e9ed78d89ce00052b1b76b" 793 | dependencies: 794 | homedir-polyfill "^1.0.1" 795 | 796 | which-module@^2.0.0: 797 | version "2.0.0" 798 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" 799 | 800 | which@^1.2.9: 801 | version "1.3.0" 802 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" 803 | dependencies: 804 | isexe "^2.0.0" 805 | 806 | wrap-ansi@^2.0.0: 807 | version "2.1.0" 808 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" 809 | dependencies: 810 | string-width "^1.0.1" 811 | strip-ansi "^3.0.1" 812 | 813 | wrappy@1: 814 | version "1.0.2" 815 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 816 | 817 | y18n@^3.2.1: 818 | version "3.2.1" 819 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" 820 | 821 | yallist@^2.1.2: 822 | version "2.1.2" 823 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" 824 | 825 | yargs-parser@^8.0.0: 826 | version "8.0.0" 827 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.0.0.tgz#21d476330e5a82279a4b881345bf066102e219c6" 828 | dependencies: 829 | camelcase "^4.1.0" 830 | 831 | yargs@^10.0.3: 832 | version "10.0.3" 833 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.0.3.tgz#6542debd9080ad517ec5048fb454efe9e4d4aaae" 834 | dependencies: 835 | cliui "^3.2.0" 836 | decamelize "^1.1.1" 837 | find-up "^2.1.0" 838 | get-caller-file "^1.0.1" 839 | os-locale "^2.0.0" 840 | require-directory "^2.1.1" 841 | require-main-filename "^1.0.1" 842 | set-blocking "^2.0.0" 843 | string-width "^2.0.0" 844 | which-module "^2.0.0" 845 | y18n "^3.2.1" 846 | yargs-parser "^8.0.0" 847 | 848 | yn@^2.0.0: 849 | version "2.0.0" 850 | resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" 851 | --------------------------------------------------------------------------------