Skip to content

Commit 4c7a1c9

Browse files
committed
Few improvements
+ Add support for namespaced attrs - convert to camelcase as react expects + Fix some issues where svgo options can throw some error by not optimizing and striping unwanted elements in svg
1 parent 583f1dd commit 4c7a1c9

File tree

17 files changed

+360
-17
lines changed

17 files changed

+360
-17
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
npm-debug.log
33
lib
4+
*.svg.react.js

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
"scripts": {
77
"build": "babel src --out-dir lib --copy-files",
88
"clean": "rm -rf lib",
9-
"lint": "eslint src/ test/ example/*.js --ignore-pattern example/*.react.js",
10-
"test": "tape -r babel-register test/loader.js | faucet",
9+
"lint": "eslint src/ test/ example/*.js --ignore-pattern '*.react.js'",
10+
"test": "tape -r babel-register test/index.js | faucet",
1111
"watch": "babel src --out-dir lib --copy-files --watch",
12-
"preversion": "npm run clean && npm run lint && npm run test",
13-
"version": "npm run build",
12+
"preversion": "npm run clean && npm run lint && npm run build && npm run test",
1413
"postversion": "git push && git push --tags"
1514
},
1615
"repository": {
@@ -40,6 +39,7 @@
4039
"babel-preset-react": "~6.5.0",
4140
"js-yaml": "~3.5.5",
4241
"loader-utils": "~0.2.13",
42+
"lodash.isplainobject": "^4.0.4",
4343
"svgo": "~0.6.3",
4444
"yargs": "~3.18.0"
4545
},

src/camelize.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function hyphenToCamel(name) {
2+
return name.replace(/-([a-z])/g, g => g[1].toUpperCase());
3+
}
4+
5+
export function namespaceToCamel(namespace, name) {
6+
return namespace + name.charAt(0).toUpperCase() + name.slice(1);
7+
}

src/cli.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ argv._.map(file => {
9191
svgo: svgoOpts
9292
});
9393
} catch(e) {
94+
/* eslint-disable no-console */
9495
console.error('The options passed are not serializable.');
96+
/* eslint-enable */
9597
process.exit(1);
9698
}
9799
let loaderContext = {
@@ -101,7 +103,7 @@ argv._.map(file => {
101103
async() {
102104
return function(err, result) {
103105
/* eslint-disable no-console */
104-
if (err) return console.error("ERROR ERROR ERROR \n", file, err.stack);
106+
if (err) return console.error("ERROR ERROR ERROR", file, err.stack);
105107
if (argv['0']) console.log(result);
106108
/* eslint-enable */
107109
else fs.writeFileSync(makeFilename(file), result);

src/hyphen-to-camel.js

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/loader.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import Svgo from 'svgo';
22
import {transform as babelTransform} from 'babel-core';
33
import loaderUtils from 'loader-utils';
44

5+
import {validateAndFix} from './svgo';
56
import plugin from './plugin';
67

78
function optimize (opts) {
9+
validateAndFix(opts);
810
const svgo = new Svgo(opts);
911
return function (content) {
10-
return new Promise(r => svgo.optimize(content, ({data}) => r(data)));
12+
return new Promise((resolve, reject) =>
13+
svgo.optimize(content, ({error, data}) => error ? reject(error) : resolve(data))
14+
);
1115
};
1216
}
1317

@@ -40,6 +44,7 @@ export default function (content) {
4044

4145
Promise.resolve(String(content))
4246
.then(optimize(query.svgo))
47+
// .then(r => (console.log(r), r))
4348
.then(transform({
4449
es5: query.es5
4550
}))

src/plugin.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import cssToObj from './css-to-obj';
2-
import hyphenToCamel from './hyphen-to-camel';
2+
import {hyphenToCamel, namespaceToCamel} from './camelize';
33

44
export default function (babel) {
55
const t = babel.types;
66

77
// converts
8-
// <svg stroke-width="5">
8+
// <svg stroke-width="5" xmlns:xlink="asdf">
99
// to
10-
// <svg strokeWidth="5">
11-
const hyphenToCamelVisitor = {
10+
// <svg strokeWidth="5" xmlnsXlink="asdf">
11+
const camelizeVisitor = {
1212
JSXAttribute(path) {
13-
path.node.name.name = hyphenToCamel(path.node.name.name);
13+
if (t.isJSXNamespacedName(path.node.name)) {
14+
path.node.name = t.jSXIdentifier(
15+
namespaceToCamel(path.node.name.namespace.name, path.node.name.name.name)
16+
);
17+
} else if (t.isJSXIdentifier(path.node.name)) {
18+
path.node.name.name = hyphenToCamel(path.node.name.name);
19+
}
1420
}
1521
};
1622

@@ -20,7 +26,7 @@ export default function (babel) {
2026
// <tag style={{textAlign: 'center', width: '50px'}}>
2127
const styleAttrVisitor = {
2228
JSXAttribute(path) {
23-
if (path.node.name.name === 'style') {
29+
if (t.isJSXIdentifier(path.node.name) && path.node.name.name === 'style') {
2430
let csso = cssToObj(path.node.value.value);
2531
let properties = Object.keys(csso).map(prop => t.objectProperty(
2632
t.identifier(hyphenToCamel(prop)),
@@ -73,9 +79,9 @@ export default function (babel) {
7379
const svgVisitor = {
7480
JSXOpeningElement(path) {
7581
if (t.isJSXIdentifier(path.node.name) && path.node.name.name.toLowerCase() === 'svg') {
82+
path.traverse(camelizeVisitor);
7683
path.traverse(attrVisitor);
7784
path.traverse(styleAttrVisitor);
78-
path.traverse(hyphenToCamelVisitor);
7985

8086
// add spread props
8187
path.node.attributes.push(
@@ -88,8 +94,8 @@ export default function (babel) {
8894
);
8995
} else {
9096
// don't ignore style attr transformations for other nodes
97+
path.traverse(camelizeVisitor);
9198
path.traverse(styleAttrVisitor);
92-
path.traverse(hyphenToCamelVisitor);
9399
}
94100
}
95101
};

src/svgo.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// validates svgo opts
2+
// to contain minimal set of plugins that will strip some stuff
3+
// for the babylon JSX parser to work
4+
import isPlainObject from 'lodash.isplainobject';
5+
6+
export const essentialPlugins = ['removeDoctype', 'removeComments']
7+
8+
export function isEssentialPlugin(p) {
9+
return essentialPlugins.indexOf(p) !== -1;
10+
}
11+
12+
export function validateAndFix(opts) {
13+
14+
if (!isPlainObject(opts)) return;
15+
16+
if (opts.full) {
17+
if (typeof opts.plugins === 'undefined' ||
18+
(Array.isArray(opts.plugins) && opts.plugins.length === 0)) {
19+
opts.plugins = [...essentialPlugins];
20+
return;
21+
}
22+
}
23+
24+
// opts.full is false, plugins can be empty
25+
if (typeof opts.plugins === 'undefined') return;
26+
if (Array.isArray(opts.plugins) && opts.plugins.length === 0) return;
27+
28+
// track whether its defined in opts.plugins
29+
let state = essentialPlugins.reduce((p, c) => Object.assign(p, {[c]: false}), {});
30+
31+
opts.plugins.map(p => {
32+
if (typeof p === 'string' && isEssentialPlugin(p)) {
33+
state[p] = true;
34+
} else if (typeof p === 'object') {
35+
Object.keys(p).forEach(k => {
36+
if (isEssentialPlugin(k)) {
37+
// make it essential
38+
if (!p[k]) p[k] = true;
39+
// and update state
40+
state[k] = true;
41+
}
42+
});
43+
}
44+
});
45+
46+
Object.keys(state)
47+
.filter(key => !state[key])
48+
.forEach(key => opts.plugins.push(key));
49+
50+
}

test/cli.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import test from 'tape';
2+
import {execFile} from 'child_process';
3+
import path from 'path';
4+
5+
Error.stackTraceLimit = Infinity;
6+
7+
function exec (...args) {
8+
return new Promise((resolve, reject) => {
9+
execFile('node', [path.join(__dirname, '..', 'lib', 'cli.js'), '-0'].concat(args), {
10+
cwd: path.join(__dirname, 'resources')
11+
}, function(err, stdout, stderr) {
12+
if (err) {
13+
/* eslint-disable no-console */
14+
console.error(stderr);
15+
/* eslint-enable */
16+
return reject(err);
17+
}
18+
resolve(stdout);
19+
});
20+
});
21+
}
22+
23+
function occurence(content) {
24+
let occ = {};
25+
occ.import = content.match(/import\sReact/g);
26+
occ.export = content.match(/export\sdefault/g);
27+
occ.import = occ.import ? occ.import.length : 0;
28+
occ.export = occ.export ? occ.export.length : 0;
29+
return occ;
30+
}
31+
32+
function testOccurence(t, content, n) {
33+
let o = occurence(content);
34+
t.equal(o.import, n);
35+
t.equal(o.export, n);
36+
}
37+
38+
test('accept single argument', function(t) {
39+
exec('dummy.svg')
40+
.then(r => {
41+
testOccurence(t, r, 1);
42+
t.end();
43+
})
44+
.catch(t.end);
45+
});
46+
47+
test('accept multiple arguments', function(t) {
48+
exec('dummy.svg', 'dummy2.svg')
49+
.then(r => {
50+
testOccurence(t, r, 2);
51+
t.end();
52+
})
53+
.catch(t.end);
54+
});
55+
56+
test('es5 output', function (t) {
57+
exec('dummy.svg', '--es5')
58+
.then(r => {
59+
testOccurence(t, r, 0);
60+
t.end();
61+
})
62+
.catch(t.end);
63+
});
64+
65+
test('pass options to svgo', function(t) {
66+
Promise.all([
67+
exec('dummy.svg'),
68+
exec('dummy.svg', '--svgo.js2svg.pretty'),
69+
exec('dummy2.svg', '--svgo.floatPrecision', '1'),
70+
exec('dummy2.svg', '--svgo.floatPrecision', '8')
71+
]).then(r => {
72+
t.notEqual(r[0], r[1]);
73+
t.notEqual(r[2], r[3]);
74+
t.end();
75+
}).catch(t.end);
76+
});
77+
78+
test('plugins options in svgo', function(t) {
79+
Promise.all([
80+
exec('dummy.svg'),
81+
exec('dummy.svg', '--svgo.full')
82+
]).then(r => {
83+
t.notEqual(r[0], r[1]);
84+
t.end();
85+
}).catch(t.end);
86+
});
87+
88+
test('accepts yaml/json/js input', function(t) {
89+
Promise.all([
90+
exec('dummy.svg', '--svgo', 'config.yaml'),
91+
exec('dummy.svg', '--svgo', 'config.json'),
92+
exec('dummy.svg', '--svgo', 'config.js')
93+
]).then(r => {
94+
t.equal(r[0], r[1]);
95+
t.equal(r[1], r[2]);
96+
t.end();
97+
}).catch(t.end);
98+
});

test/css-to-obj.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import test from 'tape';
2+
import cssToObj from '../src/css-to-obj';
3+
4+
test('no entry check', function(t) {
5+
let css = '';
6+
let o = cssToObj(css);
7+
t.equal(Object.keys(o).length, 0);
8+
t.end();
9+
});
10+
11+
test('single entry check', function(t) {
12+
let css = 'text-align: center';
13+
let o = cssToObj(css);
14+
t.equal(o['text-align'], 'center');
15+
t.equal(Object.keys(o).length, 1);
16+
t.end();
17+
});
18+
19+
test('multiple entries check', function(t) {
20+
let url = 'https://example.com/image.svg';
21+
let css = `width: 50px; height: 50px; text-align: center; background:url(${url})`;
22+
let o = cssToObj(css);
23+
t.equal(Object.keys(o).length, 4);
24+
t.equal(o.width, '50px');
25+
t.equal(o.height, '50px');
26+
t.equal(o['text-align'], 'center');
27+
t.equal(o.background, `url(${url})`);
28+
t.end();
29+
});

0 commit comments

Comments
 (0)