Skip to content

Commit 9598ab6

Browse files
committed
Initial commit
0 parents  commit 9598ab6

File tree

7 files changed

+256
-0
lines changed

7 files changed

+256
-0
lines changed

.babelrc.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"use strict";
2+
3+
module.exports = {
4+
presets: [
5+
["@babel/preset-env", {
6+
"targets": {
7+
"node": "8",
8+
},
9+
"modules": "commonjs",
10+
}],
11+
],
12+
plugins: [],
13+
};

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
/lib/
3+
/yarn.lock

.npmignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
/test/
3+
/.babelrc.js
4+
/.gitignore
5+
/yarn.lock

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018-present Alex Tsvetkov
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.

package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@deflock/postcss-optimize-inline-svg",
3+
"version": "0.1.0",
4+
"license": "MIT",
5+
"homepage": "https://github.com/deflock/postcss-optimize-inline-svg",
6+
"repository": "deflock/postcss-optimize-inline-svg",
7+
"main": "lib/index.js",
8+
"module": "src/index.mjs",
9+
"publishConfig": {
10+
"access": "public"
11+
},
12+
"engines": {
13+
"node": ">=8.0"
14+
},
15+
"scripts": {
16+
"clean": "rimraf lib",
17+
"compile": "babel src --out-dir lib",
18+
"recompile": "yarn clean && yarn compile",
19+
"pretest": "yarn recompile",
20+
"test": "jest",
21+
"prepublishOnly": "yarn test"
22+
},
23+
"dependencies": {
24+
"is-svg": "^3.0.0",
25+
"postcss": "^7.0.11",
26+
"postcss-value-parser": "^3.3.1",
27+
"svgo": "^1.1.1"
28+
},
29+
"devDependencies": {
30+
"@babel/cli": "^7.2.3",
31+
"@babel/core": "^7.2.2",
32+
"@babel/preset-env": "^7.2.3",
33+
"cross-env": "^5.2.0",
34+
"jest": "^23.6.0",
35+
"rimraf": "^2.6.3"
36+
}
37+
}

src/index.mjs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import postcss from 'postcss';
2+
import valueParser from 'postcss-value-parser';
3+
import isSvg from 'is-svg';
4+
import SVGO from 'svgo';
5+
6+
const PLUGIN_NAME = 'deflock-optimize-inline-svg';
7+
8+
const DATAURI_DEFAULT_PATTERNS = [/data:image\/svg\+xml(;(charset=)?utf-8)?,/];
9+
10+
/**
11+
*
12+
*/
13+
export default postcss.plugin(PLUGIN_NAME, (opts = {}) => {
14+
const options = Object.assign({
15+
patterns: DATAURI_DEFAULT_PATTERNS,
16+
}, opts);
17+
18+
const svgo = new SVGO(options.svgo);
19+
20+
return (css, result) => {
21+
return new Promise((resolve, reject) => {
22+
const promises = [];
23+
24+
const patterns = Array.isArray(options.patterns)
25+
? options.patterns
26+
: [options.patterns];
27+
28+
css.walkDecls(decl => {
29+
for (const pattern of patterns) {
30+
if (pattern.test(decl.value)) {
31+
promises.push(transformPromise(decl, 'value', options, result, svgo));
32+
break;
33+
}
34+
}
35+
});
36+
37+
return Promise.all(promises).then(resolve, reject);
38+
});
39+
};
40+
});
41+
42+
/**
43+
* @param {Object} decl
44+
* @param {string} property
45+
* @param {Object} options
46+
* @param {Object} result
47+
* @param {Object} svgo
48+
*/
49+
function transformPromise(decl, property, options, result, svgo) {
50+
const promises = [];
51+
52+
const parser = valueParser(decl[property]).walk(node => {
53+
if (node.type !== 'function' || node.value !== 'url' || !node.nodes.length) {
54+
return;
55+
}
56+
let {value} = node.nodes[0];
57+
let decodedUri, isUriEncoded;
58+
59+
try {
60+
decodedUri = decode(value);
61+
isUriEncoded = decodedUri !== value;
62+
}
63+
catch (e) {
64+
// Swallow exception if we cannot decode the value
65+
isUriEncoded = false;
66+
}
67+
68+
if (isUriEncoded) {
69+
value = decodedUri;
70+
}
71+
72+
let svg = value;
73+
74+
const patterns = Array.isArray(options.patterns)
75+
? options.patterns
76+
: [options.patterns];
77+
78+
for (const pattern of patterns) {
79+
if (pattern.test(svg)) {
80+
svg = svg.replace(pattern, '')
81+
}
82+
}
83+
84+
if (!isSvg(svg)) {
85+
return;
86+
}
87+
88+
promises.push(new Promise(async (resolve, reject) => {
89+
const res = await svgo.optimize(svg);
90+
91+
if (res.error) {
92+
return reject(`${PLUGIN_NAME}: ${res.error}`);
93+
}
94+
95+
let data = isUriEncoded ? encode(res.data) : res.data;
96+
97+
// Should always encode # otherwise we yield a broken SVG
98+
// in Firefox (works in Chrome however). See this issue:
99+
// https://github.com/ben-eb/cssnano/issues/245
100+
data = data.replace(/#/g, '%23');
101+
102+
node.nodes[0] = {
103+
...node.nodes[0],
104+
value: 'data:image/svg+xml;charset=utf-8,' + data,
105+
quote: isUriEncoded ? '"' : '\'',
106+
type: 'string',
107+
before: '',
108+
after: '',
109+
};
110+
111+
resolve();
112+
}));
113+
114+
// By returning false we prevent traversal of descendent nodes within functions
115+
return false;
116+
});
117+
118+
return Promise.all(promises).then(() => (decl[property] = parser.toString()));
119+
}
120+
121+
/**
122+
* @param {string} data
123+
* @returns {string}
124+
*/
125+
function encode(data) {
126+
return data
127+
.replace(/"/g, '\'')
128+
.replace(/%/g, '%25')
129+
.replace(/</g, '%3C')
130+
.replace(/>/g, '%3E')
131+
.replace(/&/g, '%26')
132+
.replace(/#/g, '%23')
133+
.replace(/\s+/g, ' ');
134+
}
135+
136+
/**
137+
* @param {string} data
138+
* @returns {string}
139+
*/
140+
function decode(data) {
141+
return decodeURIComponent(data);
142+
}

test/index.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
const postcss = require('postcss');
4+
const plugin = require('..').default;
5+
6+
process.chdir(__dirname);
7+
8+
/**
9+
* @param {string} input
10+
* @param {string} expected
11+
* @param {Object} pluginOptions
12+
* @param {Object} postcssOptions
13+
* @param {Array} warnings
14+
* @returns {Promise}
15+
*/
16+
function run(input, expected, pluginOptions = {}, postcssOptions = {}, warnings = []) {
17+
return postcss([plugin(pluginOptions)])
18+
.process(input, Object.assign({from: 'input.css'}, postcssOptions))
19+
.then((result) => {
20+
const resultWarnings = result.warnings();
21+
resultWarnings.forEach((warning, index) => {
22+
expect(warnings[index]).toEqual(warning.text);
23+
});
24+
expect(resultWarnings.length).toEqual(warnings.length);
25+
expect(result.css).toEqual(expected);
26+
return result;
27+
});
28+
}
29+
30+
it('should work', () => {
31+
run(
32+
'a { background: url(\'data:image/svg+xml;charset=utf-8,<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" id="Layer_13" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 20 20" enable-background="new 0 0 20 20" xml:space="preserve"><polygon id="star_filled_1_" fill-rule="evenodd" clip-rule="evenodd" points="10,0 13.1,6.6 20,7.6 15,12.7 16.2,20 10,16.6 3.8,20 5,12.7 0,7.6 6.9,6.6 "/></svg>\') }',
33+
'a { background: url(\'data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" clip-rule="evenodd" d="M10 0l3.1 6.6 6.9 1-5 5.1 1.2 7.3-6.2-3.4L3.8 20 5 12.7 0 7.6l6.9-1z"/></svg>\') }'
34+
);
35+
});

0 commit comments

Comments
 (0)