├── .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 | [](https://badge.fury.io/js/%40target-corp%2Freact-native-svg-parser) [](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 = `
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 |
48 | `
49 |
50 | const SVG_WITH_UNMAPPED_ELEMENTS = `
51 |
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 = `
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 |
--------------------------------------------------------------------------------