Skip to content

Commit 444e32e

Browse files
feat: support image-set without url
1 parent 9436371 commit 444e32e

File tree

9 files changed

+890
-83
lines changed

9 files changed

+890
-83
lines changed

src/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ export default function loader(content, map, meta) {
280280
}
281281

282282
const { item } = message;
283-
const { url, placeholder } = item;
283+
const { url, placeholder, needQuotes } = item;
284284
// Remove `#hash` and `?#hash` from `require`
285285
const [normalizedUrl, singleQuery, hashValue] = url.split(/(\?)?#/);
286286
const hash =
@@ -292,7 +292,7 @@ export default function loader(content, map, meta) {
292292
`var ${placeholder} = urlEscape(require(${stringifyRequest(
293293
this,
294294
urlToRequest(normalizedUrl)
295-
)})${hash ? ` + ${hash}` : ''});`
295+
)})${hash ? ` + ${hash}` : ''}${needQuotes ? ', true' : ''});`
296296
);
297297

298298
cssAsString = cssAsString.replace(

src/plugins/postcss-url-parser.js

+53-34
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,67 @@
11
import postcss from 'postcss';
22
import valueParser from 'postcss-value-parser';
3+
import _ from 'lodash';
34

45
const pluginName = 'postcss-url-parser';
56

6-
function getArg(nodes) {
7-
return nodes.length !== 0 && nodes[0].type === 'string'
8-
? nodes[0].value
9-
: valueParser.stringify(nodes);
7+
const isUrlFunc = /url/i;
8+
const isImageSetFunc = /^(?:-webkit-)?image-set$/i;
9+
const needParseDecl = /(?:url|(?:-webkit-)?image-set)\(/i;
10+
11+
function getNodeFromUrlFunc(node) {
12+
return node.nodes && node.nodes[0];
13+
}
14+
15+
function getUrlFromUrlFunc(node) {
16+
return node.nodes.length !== 0 && node.nodes[0].type === 'string'
17+
? node.nodes[0].value
18+
: valueParser.stringify(node.nodes);
1019
}
1120

1221
function walkUrls(parsed, callback) {
1322
parsed.walk((node) => {
14-
if (node.type !== 'function' || node.value.toLowerCase() !== 'url') {
23+
if (node.type !== 'function') {
1524
return;
1625
}
1726

18-
/* eslint-disable */
19-
node.before = '';
20-
node.after = '';
21-
/* eslint-enable */
27+
if (isUrlFunc.test(node.value)) {
28+
callback(getNodeFromUrlFunc(node), getUrlFromUrlFunc(node), false);
2229

23-
callback(node, getArg(node.nodes));
30+
// Do not traverse inside `url`
31+
// eslint-disable-next-line consistent-return
32+
return false;
33+
}
34+
35+
if (isImageSetFunc.test(node.value)) {
36+
node.nodes.forEach((nNode) => {
37+
if (nNode.type === 'function' && isUrlFunc.test(nNode.value)) {
38+
callback(getNodeFromUrlFunc(nNode), getUrlFromUrlFunc(nNode), false);
39+
}
40+
41+
if (nNode.type === 'string') {
42+
callback(nNode, nNode.value, true);
43+
}
44+
});
2445

25-
// Do not traverse inside url
26-
// eslint-disable-next-line consistent-return
27-
return false;
46+
// Do not traverse inside `image-set`
47+
// eslint-disable-next-line consistent-return
48+
return false;
49+
}
2850
});
2951
}
3052

3153
function walkDeclsWithUrl(css, result, filter) {
3254
const items = [];
3355

3456
css.walkDecls((decl) => {
35-
if (!/url\(/i.test(decl.value)) {
57+
if (!needParseDecl.test(decl.value)) {
3658
return;
3759
}
3860

3961
const parsed = valueParser(decl.value);
4062
const urls = [];
4163

42-
walkUrls(parsed, (node, url) => {
64+
walkUrls(parsed, (node, url, needQuotes) => {
4365
if (url.trim().replace(/\\[\r\n]/g, '').length === 0) {
4466
result.warn(`Unable to find uri in '${decl.toString()}'`, {
4567
node: decl,
@@ -52,7 +74,7 @@ function walkDeclsWithUrl(css, result, filter) {
5274
return;
5375
}
5476

55-
urls.push(url);
77+
urls.push({ url, needQuotes });
5678
});
5779

5880
if (urls.length === 0) {
@@ -65,52 +87,49 @@ function walkDeclsWithUrl(css, result, filter) {
6587
return items;
6688
}
6789

68-
function flatten(array) {
69-
return array.reduce((acc, d) => [...acc, ...d], []);
70-
}
71-
72-
function uniq(array) {
73-
return array.reduce(
74-
(acc, d) => (acc.indexOf(d) === -1 ? [...acc, d] : acc),
75-
[]
76-
);
77-
}
78-
7990
export default postcss.plugin(
8091
pluginName,
8192
(options = {}) =>
8293
function process(css, result) {
8394
const traversed = walkDeclsWithUrl(css, result, options.filter);
84-
const paths = uniq(flatten(traversed.map((item) => item.urls)));
95+
const paths = _.uniqWith(
96+
_.flatten(traversed.map((item) => item.urls)),
97+
_.isEqual
98+
);
8599

86100
if (paths.length === 0) {
87101
return;
88102
}
89103

90-
const urls = {};
104+
const placeholders = [];
91105

92106
paths.forEach((path, index) => {
93107
const placeholder = `___CSS_LOADER_URL___${index}___`;
108+
const { url, needQuotes } = path;
94109

95-
urls[path] = placeholder;
110+
placeholders.push({ placeholder, path });
96111

97112
result.messages.push({
98113
pluginName,
99114
type: 'url',
100-
item: { url: path, placeholder },
115+
item: { url, placeholder, needQuotes },
101116
});
102117
});
103118

104119
traversed.forEach((item) => {
105-
walkUrls(item.parsed, (node, url) => {
106-
const value = urls[url];
120+
walkUrls(item.parsed, (node, url, needQuotes) => {
121+
const value = _.find(placeholders, { path: { url, needQuotes } });
107122

108123
if (!value) {
109124
return;
110125
}
111126

127+
const { placeholder } = value;
128+
129+
// eslint-disable-next-line no-param-reassign
130+
node.type = 'word';
112131
// eslint-disable-next-line no-param-reassign
113-
node.nodes = [{ type: 'word', value }];
132+
node.value = placeholder;
114133
});
115134

116135
// eslint-disable-next-line no-param-reassign

src/runtime/url-escape.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module.exports = function escape(url) {
1+
module.exports = function escape(url, needQuotes) {
22
if (typeof url !== 'string') {
33
return url;
44
}
@@ -7,9 +7,10 @@ module.exports = function escape(url) {
77
if (/^['"].*['"]$/.test(url)) {
88
url = url.slice(1, -1);
99
}
10+
1011
// Should url be wrapped?
1112
// See https://drafts.csswg.org/css-values-3/#urls
12-
if (/["'() \t\n]/.test(url)) {
13+
if (/["'() \t\n]/.test(url) || needQuotes) {
1314
return '"' + url.replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
1415
}
1516

test/__snapshots__/loader.test.js.snap

+12-8
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ function toComment(sourceMap) {
8989
exports[`loader should compile with \`css\` entry point (with \`modules\` and scope \`global\`): errors 1`] = `Array []`;
9090

9191
exports[`loader should compile with \`css\` entry point (with \`modules\` and scope \`global\`): escape 1`] = `
92-
"module.exports = function escape(url) {
92+
"module.exports = function escape(url, needQuotes) {
9393
if (typeof url !== 'string') {
9494
return url;
9595
}
@@ -98,9 +98,10 @@ exports[`loader should compile with \`css\` entry point (with \`modules\` and sc
9898
if (/^['\\"].*['\\"]$/.test(url)) {
9999
url = url.slice(1, -1);
100100
}
101+
101102
// Should url be wrapped?
102103
// See https://drafts.csswg.org/css-values-3/#urls
103-
if (/[\\"'() \\\\t\\\\n]/.test(url)) {
104+
if (/[\\"'() \\\\t\\\\n]/.test(url) || needQuotes) {
104105
return '\\"' + url.replace(/\\"/g, '\\\\\\\\\\"').replace(/\\\\n/g, '\\\\\\\\n') + '\\"';
105106
}
106107
@@ -339,7 +340,7 @@ function toComment(sourceMap) {
339340
exports[`loader should compile with \`css\` entry point (with \`modules\` and scope \`local\`): errors 1`] = `Array []`;
340341

341342
exports[`loader should compile with \`css\` entry point (with \`modules\` and scope \`local\`): escape 1`] = `
342-
"module.exports = function escape(url) {
343+
"module.exports = function escape(url, needQuotes) {
343344
if (typeof url !== 'string') {
344345
return url;
345346
}
@@ -348,9 +349,10 @@ exports[`loader should compile with \`css\` entry point (with \`modules\` and sc
348349
if (/^['\\"].*['\\"]$/.test(url)) {
349350
url = url.slice(1, -1);
350351
}
352+
351353
// Should url be wrapped?
352354
// See https://drafts.csswg.org/css-values-3/#urls
353-
if (/[\\"'() \\\\t\\\\n]/.test(url)) {
355+
if (/[\\"'() \\\\t\\\\n]/.test(url) || needQuotes) {
354356
return '\\"' + url.replace(/\\"/g, '\\\\\\\\\\"').replace(/\\\\n/g, '\\\\\\\\n') + '\\"';
355357
}
356358
@@ -609,7 +611,7 @@ function toComment(sourceMap) {
609611
exports[`loader should compile with \`css\` entry point: errors 1`] = `Array []`;
610612

611613
exports[`loader should compile with \`css\` entry point: escape 1`] = `
612-
"module.exports = function escape(url) {
614+
"module.exports = function escape(url, needQuotes) {
613615
if (typeof url !== 'string') {
614616
return url;
615617
}
@@ -618,9 +620,10 @@ exports[`loader should compile with \`css\` entry point: escape 1`] = `
618620
if (/^['\\"].*['\\"]$/.test(url)) {
619621
url = url.slice(1, -1);
620622
}
623+
621624
// Should url be wrapped?
622625
// See https://drafts.csswg.org/css-values-3/#urls
623-
if (/[\\"'() \\\\t\\\\n]/.test(url)) {
626+
if (/[\\"'() \\\\t\\\\n]/.test(url) || needQuotes) {
624627
return '\\"' + url.replace(/\\"/g, '\\\\\\\\\\"').replace(/\\\\n/g, '\\\\\\\\n') + '\\"';
625628
}
626629
@@ -859,7 +862,7 @@ function toComment(sourceMap) {
859862
exports[`loader should compile with \`js\` entry point: errors 1`] = `Array []`;
860863

861864
exports[`loader should compile with \`js\` entry point: escape 1`] = `
862-
"module.exports = function escape(url) {
865+
"module.exports = function escape(url, needQuotes) {
863866
if (typeof url !== 'string') {
864867
return url;
865868
}
@@ -868,9 +871,10 @@ exports[`loader should compile with \`js\` entry point: escape 1`] = `
868871
if (/^['\\"].*['\\"]$/.test(url)) {
869872
url = url.slice(1, -1);
870873
}
874+
871875
// Should url be wrapped?
872876
// See https://drafts.csswg.org/css-values-3/#urls
873-
if (/[\\"'() \\\\t\\\\n]/.test(url)) {
877+
if (/[\\"'() \\\\t\\\\n]/.test(url) || needQuotes) {
874878
return '\\"' + url.replace(/\\"/g, '\\\\\\\\\\"').replace(/\\\\n/g, '\\\\\\\\n') + '\\"';
875879
}
876880

test/__snapshots__/url-option.test.js.snap

+752-37
Large diffs are not rendered by default.

test/fixtures/url/img3x.png

76.3 KB
Loading

test/fixtures/url/url.css

+58
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,61 @@ a {
209209
@font-face {
210210
src: url("//at.alicdn.com/t/font_515771_emcns5054x3whfr.eot");
211211
}
212+
213+
.class {
214+
/* Broken */
215+
background-image: -webkit-image-set();
216+
background-image: -webkit-image-set('');
217+
background-image: image-set();
218+
background-image: image-set('');
219+
background-image: image-set("");
220+
background-image: image-set("" 1x);
221+
background-image: image-set(url());
222+
background-image: image-set(
223+
url()
224+
);
225+
background-image: image-set(URL());
226+
background-image: image-set(url(''));
227+
background-image: image-set(url(""));
228+
background-image: image-set(url('') 1x);
229+
background-image: image-set(1x);
230+
background-image: image-set(
231+
1x
232+
);
233+
background: image-set(calc(1rem + 1px) 1x);
234+
235+
/* Strings */
236+
background-image: -webkit-image-set("./img1x.png" 1x, "./img2x.png" 2x);
237+
background-image: image-set("./img1x.png" 1x);
238+
background-image: image-set("./img1x.png" 1x, "./img2x.png" 2x);
239+
background-image: image-set("./img img.png" 1x, "./img img.png" 2x);
240+
background-image: image-set("./img1x.png" 1x, "./img2x.png" 2x),
241+
image-set("./img1x.png" 1x, "./img2x.png" 2x);
242+
background-image: image-set(
243+
"./img1x.png" 1x,
244+
"./img2x.png" 2x,
245+
"./img3x.png" 600dpi
246+
);
247+
background-image: image-set("./img1x.png?foo=bar" 1x);
248+
background-image: image-set("./img1x.png#hash" 1x);
249+
background-image: image-set("./img1x.png?#iefix" 1x);
250+
251+
/* With `url` function */
252+
background-image: -webkit-image-set(url("./img1x.png") 1x, url("./img2x.png") 2x);
253+
background-image: -webkit-image-set(url("./img1x.png") 1x);
254+
background-image: -webkit-image-set(
255+
url("./img1x.png") 1x
256+
);
257+
background-image: image-set(url(./img1x.png) 1x);
258+
background-image: image-set(
259+
url(./img1x.png) 1x
260+
);
261+
background-image: image-set(url("./img1x.png") 1x, url("./img2x.png") 2x);
262+
background-image: image-set(
263+
url(./img1x.png) 1x,
264+
url(./img2x.png) 2x,
265+
url(./img3x.png) 600dpi
266+
);
267+
268+
background-image: image-set(url("./img1x.png") 1x, "./img2x.png" 2x);
269+
}

test/runtime/__snapshots__/url-escape.test.js.snap

+6
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ exports[`escape should escape url 7`] = `"\\"image other.png\\""`;
1717
exports[`escape should escape url 8`] = `"\\"image\\\\\\"other.png\\""`;
1818

1919
exports[`escape should escape url 9`] = `"\\"image\\\\nother.png\\""`;
20+
21+
exports[`escape should escape url 10`] = `"\\"image.png\\""`;
22+
23+
exports[`escape should escape url 11`] = `"\\"image other.png\\""`;
24+
25+
exports[`escape should escape url 12`] = `"\\"image other.png\\""`;

test/runtime/url-escape.test.js

+4
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@ describe('escape', () => {
1111
expect(urlEscape("'image other.png'")).toMatchSnapshot();
1212
expect(urlEscape('image"other.png')).toMatchSnapshot();
1313
expect(urlEscape('image\nother.png')).toMatchSnapshot();
14+
15+
expect(urlEscape('image.png', true)).toMatchSnapshot();
16+
expect(urlEscape("'image other.png'", true)).toMatchSnapshot();
17+
expect(urlEscape('"image other.png"', true)).toMatchSnapshot();
1418
});
1519
});

0 commit comments

Comments
 (0)