├── .babelrc ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── package-lock.json ├── package.json └── src ├── __tests__ ├── converter.test.js ├── fixtures.test.js └── parser.test.js ├── converter.js └── parser.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # dont check in compiled babel code 61 | lib/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | before_script: 5 | - npm install && npm run install-peers 6 | script: npm run ci 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Target Brands, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-svg-parser 2 | 3 | ``` 4 | This project is a proof of concept only. It is not actively maintained. 5 | ``` 6 | 7 | [![npm version](https://badge.fury.io/js/%40target-corp%2Freact-native-svg-parser.svg)](https://badge.fury.io/js/%40target-corp%2Freact-native-svg-parser) [![Build Status](https://travis-ci.org/target/react-native-svg-parser.svg?branch=master)](https://travis-ci.org/target/react-native-svg-parser) 8 | 9 | An SVG/XML parser that converts to react-native-svg format. This project was 10 | created in order to make it easy to use existing SVG files with the [react-native-svg](https://github.com/react-native-community/react-native-svg) project, 11 | which only supports a subset of SVG and does not provide a method for directly rendering 12 | SVG from an SVG/XML format file. 13 | 14 | ## Installation 15 | 16 | ``` 17 | npm i @target-corp/react-native-svg-parser 18 | ``` 19 | 20 | ## Usage 21 | 22 | ``` 23 | import ReactNativeSvgParser from 'react-native-svg-parser' 24 | 25 | const svgNode = ReactNativeSvgParser(`YOUR SVG XML STRING`, `YOUR CSS STYLESHEET STRING`) 26 | 27 | .... 28 | 29 | render() { 30 | return 31 | { svgNode } 32 | 33 | } 34 | 35 | ``` 36 | 37 | ## Options 38 | 39 | The parser takes a third parameter, and object with config options. You can specify the following values: 40 | 41 | | Prop name | Type | Description | 42 | |-----------|--------| ------------| 43 | | width | number | overrides the width provided by viewbox, becomes "width" prop on ```Svg``` element | 44 | | height | number | overrides the height provided by viewbox, becomes "height" prop on ```Svg``` element | 45 | | viewBox | string | overrides the viewbox element on the SVG and is added as a prop on ```Svg``` element | 46 | | DOMParser | object | this is passed directly to xmldom.DOMParser, see xmldom docs for options available | 47 | | omitById | array | an optional array of ids to omit from the SVG output object | 48 | 49 | Example usage: 50 | 51 | ``` 52 | import ReactNativeSvgParser from 'react-native-svg-parser' 53 | 54 | const svgString = ` 55 | 56 | 57 | ` 58 | const cssString = ` 59 | .red-circle { 60 | fill: red; 61 | stroke: black; 62 | stroke-width: 3; 63 | } 64 | ` 65 | 66 | const svgNode = ReactNativeSvgParser(svgString, cssString, {width: 111, height: 222}) 67 | 68 | .... // (will render a red circle with a black stroke) 69 | 70 | render() { 71 | return 72 | { svgNode } 73 | 74 | } 75 | 76 | ``` 77 | 78 | 79 | ## Developing: Lint test and build 80 | 81 | In order to test and develop locally you will need to install the peer dependencies (React and React Native). However, we have you covered. Just run this command: 82 | 83 | ``` 84 | npm run install-peers 85 | ``` 86 | 87 | Then you can run test lint and build using this command: 88 | 89 | ``` 90 | npm run ci 91 | ``` 92 | 93 | 94 | 95 | ## Console warning, on transform prop 96 | 97 | On v5.5.1 react-native-svg enforced prop type of "object" on transform attribute. However, 98 | as of v6.0.0 this is changed to: 99 | ``` 100 | transform: PropTypes.oneOfType([PropTypes.object, PropTypes.string]) 101 | ``` 102 | https://github.com/react-native-community/react-native-svg/blob/master/lib/props.js#L69 103 | 104 | Therefore, the minimum version compatibility for this libaray with ```react-native-svg``` is version 6.0.0. 105 | 106 | 107 | ## Changelog 108 | 109 | ### v1.0.5 110 | 111 | Fixed text node rendering. 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@target-corp/react-native-svg-parser", 3 | "version": "1.0.6", 4 | "description": "parses SVG/XML format and converts to react-native-svg elements", 5 | "main": "lib/parser.js", 6 | "scripts": { 7 | "lint": "standard", 8 | "lint-fix": "standard --fix", 9 | "test": "jest --verbose --coverage", 10 | "build": "rm -rf lib && babel src --out-dir lib --ignore *.test.js --source-maps", 11 | "ci": "npm test && npm run lint && npm run build", 12 | "install-peers": "npm i --no-save react@16.2.0 react-native-svg@6.0.0 react-native@0.50.0" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com/target/react-native-svg-parser.git" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "react-native", 21 | "react-native-svg", 22 | "svg" 23 | ], 24 | "bugs": { 25 | "url": "https://github.com/target/react-native-svg-parser/issues" 26 | }, 27 | "author": "Aaron Decker ", 28 | "contributors": [ 29 | "Bob Schultz " 30 | ], 31 | "license": "MIT", 32 | "dependencies": { 33 | "camelcase": "^5.0.0", 34 | "css-parse-no-fs": "^2.0.0", 35 | "xmldom": "^0.1.27" 36 | }, 37 | "devDependencies": { 38 | "babel-cli": "^6.26.0", 39 | "babel-core": "^6.26.3", 40 | "babel-eslint": "^8.2.6", 41 | "babel-jest": "^23.4.2", 42 | "babel-preset-react-native": "^4.0.0", 43 | "jest": "^23.5.0", 44 | "regenerator-runtime": "^0.12.1", 45 | "standard": "^11.0.1" 46 | }, 47 | "peerDependencies": { 48 | "react": "*", 49 | "react-native-svg": "^6.0.0", 50 | "react-native": ">=0.50.0" 51 | }, 52 | "jest": { 53 | "preset": "react-native", 54 | "automock": false, 55 | "testPathIgnorePatterns": [ 56 | "node_modules", 57 | "fixtures.test.js" 58 | ] 59 | }, 60 | "standard": { 61 | "parser": "babel-eslint", 62 | "env": [ 63 | "jest", 64 | "es6", 65 | "react-native" 66 | ] 67 | }, 68 | "nativePackage": true 69 | } 70 | -------------------------------------------------------------------------------- /src/__tests__/converter.test.js: -------------------------------------------------------------------------------- 1 | import converter, { extractViewbox, getCssRulesForAttr, findApplicableCssProps, addNonCssAttributes } from '../converter' 2 | import { parseSvg, makeCssAst } from '../parser' 3 | import { SIMPLE_CSS, SIMPLE_SVG, SVG_WITH_UNMAPPED_ELEMENTS } from './fixtures.test' 4 | 5 | function nodeEnumerate (node, nodes) { 6 | if (node.props && node.props.children) { 7 | if (Array.isArray(node.props.children) && node.props.children.length > 0) { 8 | node.props.children.forEach((child) => { 9 | nodes.push(node.type.displayName) 10 | return nodeEnumerate(child, nodes) 11 | }) 12 | } else { 13 | nodes.push(node.type.displayName) 14 | } 15 | } 16 | } 17 | 18 | describe('svg-parser components', () => { 19 | describe('extractViewbox', () => { 20 | it('should make a fancy object', () => { 21 | const viewBox = extractViewbox({ 22 | attributes: { 23 | '1': { 24 | name: 'viewBox', 25 | value: '-4.296122345000001 24.174109004999984 94.51469159 86.29088279' 26 | } 27 | } 28 | }) 29 | 30 | expect(viewBox).toBeTruthy() 31 | expect(viewBox.width).toBe('94.51469159') 32 | expect(viewBox.height).toBe('86.29088279') 33 | expect(viewBox.viewBox).toBe('-4.296122345000001 24.174109004999984 94.51469159 86.29088279') 34 | }) 35 | 36 | it('should return empty object if no viewbox', () => { 37 | const viewBox = extractViewbox({ 38 | attributes: { 39 | '1': { 40 | name: 'fill', 41 | value: '#ffffff' 42 | } 43 | } 44 | }) 45 | 46 | expect(viewBox).toEqual({}) 47 | }) 48 | }) 49 | 50 | describe('getCssRulesForAttr', () => { 51 | it('should find CSS rules for a given attribute (by ID)', () => { 52 | const cssAst = makeCssAst(SIMPLE_CSS) 53 | const rules = getCssRulesForAttr({ name: 'id', value: 'elementid1' }, cssAst.stylesheet.rules) 54 | 55 | expect(rules).toBeTruthy() 56 | expect(rules.length).toBe(1) 57 | expect(rules[0].selectors[0]).toBe('#elementid1') 58 | const declarations = rules[0].declarations 59 | expect(declarations[0].property).toBe('fill') 60 | expect(declarations[0].value).toBe('#F7F7F7') 61 | expect(declarations[1].property).toBe('stroke') 62 | expect(declarations[1].value).toBe('none') 63 | }) 64 | 65 | it('should find CSS rules for a given attribute (by class)', () => { 66 | const cssAst = makeCssAst(SIMPLE_CSS) 67 | const rules = getCssRulesForAttr({ name: 'class', value: 'content' }, cssAst.stylesheet.rules) 68 | 69 | expect(rules).toBeTruthy() 70 | expect(rules.length).toBe(1) 71 | expect(rules[0].selectors[0]).toBe('.content') 72 | const declarations = rules[0].declarations 73 | expect(declarations[2].property).toBe('fill') 74 | expect(declarations[2].value).toBe('#DDDDDD') 75 | expect(declarations[1].property).toBe('font-size') 76 | expect(declarations[1].value).toBe('11px') 77 | expect(declarations[0].property).toBe('font-family') 78 | expect(declarations[0].value).toBe("'Helvetica'") 79 | }) 80 | }) 81 | 82 | describe('findApplicableCssProps', () => { 83 | it('should get a list of css properties', () => { 84 | const dom = parseSvg(SIMPLE_SVG) 85 | const cssAst = makeCssAst(SIMPLE_CSS) 86 | 87 | // grabbing a node that has CSS on it: 88 | const svgNode = dom.documentElement 89 | expect(svgNode.childNodes[3].attributes.length).toBe(2) 90 | expect(svgNode.childNodes[3].attributes[0].name).toBe('class') 91 | expect(svgNode.childNodes[3].attributes[1].name).toBe('transform') 92 | 93 | const contentNode = svgNode.childNodes[3] 94 | const cssProps = findApplicableCssProps(contentNode, { cssRules: cssAst.stylesheet.rules }) 95 | 96 | expect(cssProps).toBeTruthy() 97 | expect(cssProps.cssProps).toEqual([ 'fontFamily', 'fontSize', 'fill' ]) 98 | expect(cssProps.attrs).toEqual([ 99 | { name: 'fontFamily', value: '\'Helvetica\'' }, 100 | { name: 'fontSize', value: '11px' }, 101 | { name: 'fill', value: '#DDDDDD' } 102 | ]) 103 | }) 104 | }) 105 | 106 | describe('addNonCssAttributes', () => { 107 | it('should pick out attributes like "role" or "aria-hidden"', () => { 108 | const dom = parseSvg(SIMPLE_SVG) 109 | const cssAst = makeCssAst(SIMPLE_CSS) 110 | 111 | // this is the "background" node, it has a "role" on it. 112 | const svgNode = dom.documentElement 113 | const backgroundNode = svgNode.childNodes[1] 114 | expect(backgroundNode.attributes.length).toBe(3) 115 | expect(backgroundNode.nodeName).toBe('g') 116 | expect(backgroundNode.attributes[0].name).toBe('id') 117 | expect(backgroundNode.attributes[1].name).toBe('role') 118 | 119 | const cssProps = findApplicableCssProps(backgroundNode, { cssRules: cssAst.stylesheet.rules }) 120 | expect(cssProps).toBeTruthy() 121 | expect(cssProps.cssProps).toEqual(['fill']) 122 | expect(cssProps.attrs).toEqual([ 123 | { name: 'fill', value: '#eeeeee' } 124 | ]) 125 | 126 | const nonCssAttributes = addNonCssAttributes(backgroundNode, cssProps) 127 | expect(nonCssAttributes).toEqual([{ name: 'role', value: 'group' }]) 128 | }) 129 | }) 130 | 131 | describe('converter', () => { 132 | it('should be skipping unmapped elements', () => { 133 | const dom = parseSvg(SVG_WITH_UNMAPPED_ELEMENTS) 134 | const cssAst = makeCssAst(SIMPLE_CSS) 135 | const svgElement = converter(dom, cssAst) 136 | 137 | expect(svgElement).toBeTruthy() 138 | let nodeList = [] 139 | nodeEnumerate(svgElement, nodeList) 140 | nodeList = nodeList.map((n) => n.toLowerCase()) 141 | expect(nodeList.indexOf('svg')).toBe(0) 142 | expect(nodeList.indexOf('g')).toBe(1) 143 | expect(nodeList.indexOf('filter')).toBe(-1) 144 | expect(nodeList.indexOf('feGaussianBlur')).toBe(-1) 145 | }) 146 | 147 | it('should skip ids that are flagged for omision', () => { 148 | const dom = parseSvg(SIMPLE_SVG) 149 | const cssAst = makeCssAst(SIMPLE_CSS) 150 | const SvgElement = converter(dom, cssAst) 151 | // has 4 children normally 152 | expect(SvgElement.props.children[1].props.children.length).toEqual(4) 153 | const SvgElement2 = converter(dom, cssAst, { 154 | omitById: ['elementid1', 'elementid2', 'elementid3'] 155 | }) 156 | // omit 3 and you get 1 child 157 | expect(SvgElement2.props.children[1].props.children.length).toEqual(1) 158 | const wallShapesPath = SvgElement2.props.children[1].props.children[0].props.children[0].props.d 159 | expect(wallShapesPath.startsWith('M773.496 3040.5039 L4018.9941')).toEqual(true) 160 | }) 161 | 162 | it('should skip null tag names elements (e.g. newline #text elements)', () => { 163 | const dom = parseSvg(SVG_WITH_UNMAPPED_ELEMENTS) 164 | const cssAst = makeCssAst(SIMPLE_CSS) 165 | const svgElement = converter(dom, cssAst) 166 | 167 | const nodeNames = Object.values(dom.documentElement.childNodes).map((node) => { 168 | return node.nodeName 169 | }) 170 | const tagNames = Object.values(dom.documentElement.childNodes).map((node) => { 171 | return node.tagName 172 | }) 173 | expect(nodeNames).toEqual([ '#text', 'g', '#text', 'filter', '#text', 'g', '#text', undefined ]) 174 | expect(tagNames).toEqual([ undefined, 'g', undefined, 'filter', undefined, 'g', undefined, undefined ]) 175 | 176 | expect(svgElement).toBeTruthy() 177 | let nodeList = [] 178 | nodeEnumerate(svgElement, nodeList) 179 | nodeList = nodeList.map((n) => n.toLowerCase()) 180 | expect(nodeList.indexOf('svg')).toBe(0) 181 | expect(nodeList.indexOf('#text')).toBe(-1) 182 | }) 183 | }) 184 | }) 185 | -------------------------------------------------------------------------------- /src/__tests__/fixtures.test.js: -------------------------------------------------------------------------------- 1 | 2 | const SIMPLE_CSS = ` 3 | .content { 4 | font-family: 'Helvetica'; 5 | font-size: 11px; 6 | fill: #DDDDDD 7 | } 8 | #background { 9 | fill: #eeeeee; 10 | } 11 | #elementid1 { 12 | fill: #F7F7F7; 13 | stroke: none; 14 | } 15 | #elementid2 { 16 | fill: none; 17 | stroke: none; 18 | } 19 | #elementid3 { 20 | display: none; 21 | } 22 | ` 23 | 24 | const SIMPLE_SVG = ` 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | baby 45 | 46 | 47 | 48 | ` 49 | 50 | const SVG_WITH_UNMAPPED_ELEMENTS = ` 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | baby 82 | 83 | 84 | 85 | ` 86 | 87 | export {SIMPLE_CSS, SIMPLE_SVG, SVG_WITH_UNMAPPED_ELEMENTS} 88 | -------------------------------------------------------------------------------- /src/__tests__/parser.test.js: -------------------------------------------------------------------------------- 1 | import parser, { parseSvg, makeCssAst} from '../parser' 2 | import {SIMPLE_CSS, SIMPLE_SVG} from './fixtures.test' 3 | 4 | describe('svg-parser main lib', () => { 5 | it('should parse css and return a set of rules', () => { 6 | const cssAst = makeCssAst(SIMPLE_CSS) 7 | expect(cssAst).toBeTruthy() 8 | expect(cssAst.stylesheet.rules).toBeTruthy() 9 | expect(cssAst.stylesheet.rules.length).toBe(5) 10 | const rules = cssAst.stylesheet.rules 11 | expect(rules[2].selectors[0]).toBe('#elementid1') 12 | const r2 = rules[2] 13 | expect(r2.declarations[0].property).toBe('fill') 14 | expect(r2.declarations[0].value).toBe('#F7F7F7') 15 | }) 16 | 17 | it('should parse SVG and return a DOM', () => { 18 | const dom = parseSvg(SIMPLE_SVG) 19 | expect(dom).toBeTruthy() 20 | expect(dom.childNodes.length).toBe(3) 21 | expect(dom.childNodes[0].tagName).toBe('xml') 22 | expect(dom.childNodes[2].tagName).toBe('svg') 23 | expect(dom.documentElement.tagName).toBe('svg') 24 | expect(dom.documentElement.namespaceURI).toBe('http://www.w3.org/2000/svg') 25 | expect(dom.documentElement.childNodes.length).toBe(5) 26 | }) 27 | 28 | it('should return an svg in react native SVG format', () => { 29 | const svg = parser(SIMPLE_SVG, SIMPLE_CSS) 30 | expect(svg).toBeTruthy() 31 | const { width, height, viewBox, children } = svg.props 32 | expect(width).toBe('94.51469159') 33 | expect(height).toBe('86.29088279') 34 | expect(viewBox).toBe('-4.296122345000001 24.174109004999984 94.51469159 86.29088279') 35 | const content = children[1] 36 | expect(content.type.displayName).toBe('G') 37 | expect(content.props.transform).toBe('matrix(0.0254 0 0 -0.0254 -19.6129971976 105.69902944479999)') 38 | }) 39 | 40 | it('should return an svg in react native SVG format, with no CSS or config element', () => { 41 | const svg = parser(SIMPLE_SVG) 42 | expect(svg).toBeTruthy() 43 | const { width, height, children } = svg.props 44 | expect(width).toBe('94.51469159') 45 | expect(height).toBe('86.29088279') 46 | const content = children[1] 47 | expect(content.type.displayName).toBe('G') 48 | expect(content.props.transform).toBe('matrix(0.0254 0 0 -0.0254 -19.6129971976 105.69902944479999)') 49 | }) 50 | 51 | it('should format an SVG with width and height if passed', () => { 52 | const svg = parser(SIMPLE_SVG, SIMPLE_CSS, {width: 111, height: 222}) 53 | expect(svg).toBeTruthy() 54 | const { width, height, viewBox } = svg.props 55 | expect(width).toBe(111) 56 | expect(height).toBe(222) 57 | expect(viewBox).toBe('-4.296122345000001 24.174109004999984 94.51469159 86.29088279') 58 | }) 59 | 60 | it('should format an SVG with custom viewbox if passed', () => { 61 | const svg = parser(SIMPLE_SVG, SIMPLE_CSS, {viewBox: '0 0 200 100'}) 62 | expect(svg).toBeTruthy() 63 | const { viewBox } = svg.props 64 | expect(viewBox).toBe('0 0 200 100') 65 | }) 66 | 67 | it('handles text elements', () => { 68 | const svg = parser(SIMPLE_SVG, SIMPLE_CSS) 69 | const { children } = svg.props 70 | const content = children[1] 71 | const lastGElement = content.props.children[3] 72 | const textElement = lastGElement.props.children[0] 73 | expect(textElement.props.children).toEqual('baby') 74 | }) 75 | 76 | it('readme example works', () => { 77 | const svgString = ` 78 | 79 | 80 | ` 81 | const cssString = ` 82 | .red-circle { 83 | fill: red; 84 | stroke: black; 85 | stroke-width: 3; 86 | } 87 | ` 88 | const svgNode = parser(svgString, cssString) 89 | const { children } = svgNode.props 90 | const circle = children[0] 91 | 92 | expect(circle.props).toEqual({ 93 | 'children': [], 94 | 'cx': '50', 95 | 'cy': '50', 96 | 'fill': 'red', 97 | 'r': '40', 98 | 'stroke': 'black', 99 | 'strokeWidth': '3' 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /src/converter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import camelCase from 'camelcase' 3 | 4 | import Svg, { 5 | Circle, 6 | Ellipse, 7 | G, 8 | LinearGradient, 9 | RadialGradient, 10 | Line, 11 | Path, 12 | Polygon, 13 | Polyline, 14 | Rect, 15 | Symbol, 16 | Text, 17 | Use, 18 | Defs, 19 | Stop 20 | } from 'react-native-svg' 21 | 22 | const mapping = { 23 | 'svg': Svg, 24 | 'circle': Circle, 25 | 'ellipse': Ellipse, 26 | 'g': G, 27 | 'line': Line, 28 | 'path': Path, 29 | 'rect': Rect, 30 | 'symbol': Symbol, 31 | 'text': Text, 32 | 'polygon': Polygon, 33 | 'polyline': Polyline, 34 | 'linearGradient': LinearGradient, 35 | 'radialGradient': RadialGradient, 36 | 'use': Use, 37 | 'defs': Defs, 38 | 'stop': Stop 39 | } 40 | 41 | function extractViewbox (markup) { 42 | const viewBox = markup.attributes 43 | ? Object.values(markup.attributes) 44 | .filter((attr) => attr.name === 'viewBox')[0] 45 | : false 46 | 47 | const vbSplits = viewBox ? viewBox.value.split(' ') : false 48 | if (!vbSplits) { 49 | return {} 50 | } 51 | 52 | return { 53 | width: `${vbSplits[2]}`, 54 | height: `${vbSplits[3]}`, 55 | viewBox: viewBox.value 56 | } 57 | } 58 | 59 | function getCssRulesForAttr (attr, cssRules) { 60 | let rules = [] 61 | if (attr.name === 'id') { 62 | const idname = '#' + attr.value 63 | 64 | rules = cssRules.filter((rule) => { 65 | if (rule.selectors.indexOf(idname) > -1) { 66 | return true 67 | } else { 68 | return false 69 | } 70 | }) 71 | } else if (attr.name === 'class') { 72 | const className = '.' + attr.value 73 | rules = cssRules.filter((rule) => { 74 | if (rule.selectors.indexOf(className) > -1) { 75 | return true 76 | } else { 77 | return false 78 | } 79 | }) 80 | } 81 | 82 | return rules 83 | } 84 | 85 | function addNonCssAttributes (markup, cssPropsResult) { 86 | // again look at the attributes and pick up anything else that is not related to CSS 87 | const attrs = [] 88 | Object.values(markup.attributes).forEach((attr) => { 89 | if (!attr || !attr.name) { 90 | return 91 | } 92 | 93 | const propertyName = camelCase(attr.name) 94 | if (propertyName === 'class' || propertyName === 'id') { 95 | return 96 | } 97 | 98 | if (cssPropsResult.cssProps.indexOf(propertyName) > -1) { 99 | return 100 | } 101 | 102 | attrs.push({ 103 | name: propertyName, 104 | value: `${attr.value}` 105 | }) 106 | }) 107 | 108 | return attrs 109 | } 110 | 111 | function findApplicableCssProps (markup, config) { 112 | const cssProps = [] 113 | const attrs = [] 114 | Object.values(markup.attributes).forEach((attr) => { 115 | const rules = getCssRulesForAttr(attr, config.cssRules) 116 | if (rules.length === 0) { 117 | return 118 | } 119 | 120 | rules.forEach((rule) => { 121 | rule.declarations.forEach((declaration) => { 122 | const propertyName = camelCase(declaration.property) 123 | attrs.push({ 124 | name: propertyName, 125 | value: `${declaration.value}` 126 | }) 127 | cssProps.push(propertyName) 128 | }) 129 | }) 130 | }) 131 | return { cssProps, attrs } 132 | } 133 | 134 | function findId (markup) { 135 | const id = Object.values(markup.attributes).find((attr) => attr.name === 'id') 136 | return id && id.value 137 | } 138 | 139 | function traverse (markup, config, i = 0) { 140 | if (!markup || !markup.nodeName || !markup.tagName) { 141 | return null 142 | } 143 | const tagName = markup.nodeName 144 | const idName = findId(markup) 145 | if (idName && config.omitById && config.omitById.includes(idName)) { 146 | return null 147 | } 148 | 149 | let attrs = [] 150 | if (tagName === 'svg') { 151 | const viewBox = extractViewbox(markup) 152 | attrs.push({ 153 | name: 'width', 154 | value: config.width || viewBox.width 155 | }) 156 | attrs.push({ 157 | name: 'height', 158 | value: config.height || viewBox.height 159 | }) 160 | attrs.push({ 161 | name: 'viewBox', 162 | value: config.viewBox || viewBox.viewBox || '0 0 50 50' 163 | }) 164 | } else { 165 | // otherwise, if not SVG, check to see if there is CSS to apply. 166 | const cssPropsResult = findApplicableCssProps(markup, config) 167 | const additionalProps = addNonCssAttributes(markup, cssPropsResult) 168 | // add to the known list of total attributes. 169 | attrs = [...attrs, ...cssPropsResult.attrs, ...additionalProps] 170 | } 171 | 172 | // map the tag to an element. 173 | const Elem = mapping[ tagName.toLowerCase() ] 174 | 175 | // Note, if the element is not found it was not in the mapping. 176 | if (!Elem) { 177 | return null 178 | } 179 | 180 | const children = (Elem === Text && markup.childNodes.length === 1) 181 | ? markup.childNodes[0].data 182 | : markup.childNodes.length ? Object.values(markup.childNodes).map((child) => { 183 | return traverse(child, config, ++i) 184 | }).filter((node) => { 185 | return !!node 186 | }) : [] 187 | 188 | const elemAttributes = {} 189 | attrs.forEach((attr) => { 190 | elemAttributes[attr.name] = attr.value 191 | }) 192 | 193 | const k = i + Math.random() 194 | return { children } 195 | } 196 | 197 | export { extractViewbox, getCssRulesForAttr, findApplicableCssProps, addNonCssAttributes } 198 | 199 | export default (dom, cssAst, config) => { 200 | config = Object.assign({}, config, { 201 | cssRules: (cssAst && cssAst.stylesheet && cssAst.stylesheet.rules) || [] 202 | }) 203 | return traverse(dom.documentElement, config) 204 | } 205 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | import { DOMParser } from 'xmldom' 2 | import cssParse from 'css-parse-no-fs' 3 | import converter from './converter' 4 | 5 | /** 6 | * 2nd argument can be a set of config elements passed to domParser e.g.: 7 | * { 8 | * domParser: { 9 | * errorHandler: { 10 | * warning: function(w){console.warn(w)} 11 | * } 12 | * } 13 | * } 14 | * This defaults to empty object. Check this link for options you can pass: 15 | * https://github.com/jindw/xmldom#api-reference 16 | * 17 | * @param String svgString 18 | * @param Object param1 19 | */ 20 | function parseSvg (svgString, opts) { 21 | return (new DOMParser(opts)) 22 | .parseFromString(svgString, 'image/svg+xml') 23 | } 24 | 25 | /** 26 | * Takes in a string and returns CSS AST 27 | * 28 | * This is the parse method of this library: 29 | * https://github.com/reworkcss/css#api 30 | * 31 | * @param String cssString 32 | */ 33 | function makeCssAst (cssString) { 34 | if (!cssString) { 35 | return null 36 | } 37 | return cssParse(cssString) 38 | } 39 | 40 | /** 41 | * 42 | * Returns SVG object from a dom + config params 43 | * 44 | * @param Element svgDom 45 | * @param CssAst cssAst 46 | * @param Object config 47 | */ 48 | function convertSvg (svgDom, cssAst, config) { 49 | return converter(svgDom, cssAst, config) 50 | } 51 | 52 | export { parseSvg, makeCssAst, convertSvg } 53 | 54 | /** 55 | * svgString is an XML SVG string 56 | * 57 | * config should include width, height, and css, as an optional param which is a CSS stylesheet as a string. 58 | * 59 | * @param String svgString 60 | * @param Object config 61 | */ 62 | export default (svgString, cssString, config = {}) => { 63 | const svgNodes = convertSvg( 64 | parseSvg(svgString, config.DOMParser || {}), 65 | makeCssAst(cssString), 66 | config 67 | ) 68 | return svgNodes 69 | } 70 | --------------------------------------------------------------------------------