Skip to content

Commit

Permalink
feat: support image-set without url (#879)
Browse files Browse the repository at this point in the history
  • Loading branch information
evilebottnawi committed Dec 25, 2018
1 parent 9436371 commit 21884e2
Show file tree
Hide file tree
Showing 9 changed files with 898 additions and 83 deletions.
4 changes: 2 additions & 2 deletions src/index.js
Expand Up @@ -280,7 +280,7 @@ export default function loader(content, map, meta) {
}

const { item } = message;
const { url, placeholder } = item;
const { url, placeholder, needQuotes } = item;
// Remove `#hash` and `?#hash` from `require`
const [normalizedUrl, singleQuery, hashValue] = url.split(/(\?)?#/);
const hash =
Expand All @@ -292,7 +292,7 @@ export default function loader(content, map, meta) {
`var ${placeholder} = urlEscape(require(${stringifyRequest(
this,
urlToRequest(normalizedUrl)
)})${hash ? ` + ${hash}` : ''});`
)})${hash ? ` + ${hash}` : ''}${needQuotes ? ', true' : ''});`
);

cssAsString = cssAsString.replace(
Expand Down
87 changes: 53 additions & 34 deletions src/plugins/postcss-url-parser.js
@@ -1,45 +1,67 @@
import postcss from 'postcss';
import valueParser from 'postcss-value-parser';
import _ from 'lodash';

const pluginName = 'postcss-url-parser';

function getArg(nodes) {
return nodes.length !== 0 && nodes[0].type === 'string'
? nodes[0].value
: valueParser.stringify(nodes);
const isUrlFunc = /url/i;
const isImageSetFunc = /^(?:-webkit-)?image-set$/i;
const needParseDecl = /(?:url|(?:-webkit-)?image-set)\(/i;

function getNodeFromUrlFunc(node) {
return node.nodes && node.nodes[0];
}

function getUrlFromUrlFunc(node) {
return node.nodes.length !== 0 && node.nodes[0].type === 'string'
? node.nodes[0].value
: valueParser.stringify(node.nodes);
}

function walkUrls(parsed, callback) {
parsed.walk((node) => {
if (node.type !== 'function' || node.value.toLowerCase() !== 'url') {
if (node.type !== 'function') {
return;
}

/* eslint-disable */
node.before = '';
node.after = '';
/* eslint-enable */
if (isUrlFunc.test(node.value)) {
callback(getNodeFromUrlFunc(node), getUrlFromUrlFunc(node), false);

callback(node, getArg(node.nodes));
// Do not traverse inside `url`
// eslint-disable-next-line consistent-return
return false;
}

if (isImageSetFunc.test(node.value)) {
node.nodes.forEach((nNode) => {
if (nNode.type === 'function' && isUrlFunc.test(nNode.value)) {
callback(getNodeFromUrlFunc(nNode), getUrlFromUrlFunc(nNode), false);
}

if (nNode.type === 'string') {
callback(nNode, nNode.value, true);
}
});

// Do not traverse inside url
// eslint-disable-next-line consistent-return
return false;
// Do not traverse inside `image-set`
// eslint-disable-next-line consistent-return
return false;
}
});
}

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

css.walkDecls((decl) => {
if (!/url\(/i.test(decl.value)) {
if (!needParseDecl.test(decl.value)) {
return;
}

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

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

urls.push(url);
urls.push({ url, needQuotes });
});

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

function flatten(array) {
return array.reduce((acc, d) => [...acc, ...d], []);
}

function uniq(array) {
return array.reduce(
(acc, d) => (acc.indexOf(d) === -1 ? [...acc, d] : acc),
[]
);
}

export default postcss.plugin(
pluginName,
(options = {}) =>
function process(css, result) {
const traversed = walkDeclsWithUrl(css, result, options.filter);
const paths = uniq(flatten(traversed.map((item) => item.urls)));
const paths = _.uniqWith(
_.flatten(traversed.map((item) => item.urls)),
_.isEqual
);

if (paths.length === 0) {
return;
}

const urls = {};
const placeholders = [];

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

urls[path] = placeholder;
placeholders.push({ placeholder, path });

result.messages.push({
pluginName,
type: 'url',
item: { url: path, placeholder },
item: { url, placeholder, needQuotes },
});
});

traversed.forEach((item) => {
walkUrls(item.parsed, (node, url) => {
const value = urls[url];
walkUrls(item.parsed, (node, url, needQuotes) => {
const value = _.find(placeholders, { path: { url, needQuotes } });

if (!value) {
return;
}

const { placeholder } = value;

// eslint-disable-next-line no-param-reassign
node.type = 'word';
// eslint-disable-next-line no-param-reassign
node.nodes = [{ type: 'word', value }];
node.value = placeholder;
});

// eslint-disable-next-line no-param-reassign
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/url-escape.js
@@ -1,4 +1,4 @@
module.exports = function escape(url) {
module.exports = function escape(url, needQuotes) {
if (typeof url !== 'string') {
return url;
}
Expand All @@ -7,9 +7,10 @@ module.exports = function escape(url) {
if (/^['"].*['"]$/.test(url)) {
url = url.slice(1, -1);
}

// Should url be wrapped?
// See https://drafts.csswg.org/css-values-3/#urls
if (/["'() \t\n]/.test(url)) {
if (/["'() \t\n]/.test(url) || needQuotes) {
return '"' + url.replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
}

Expand Down
20 changes: 12 additions & 8 deletions test/__snapshots__/loader.test.js.snap
Expand Up @@ -89,7 +89,7 @@ function toComment(sourceMap) {
exports[`loader should compile with \`css\` entry point (with \`modules\` and scope \`global\`): errors 1`] = `Array []`;

exports[`loader should compile with \`css\` entry point (with \`modules\` and scope \`global\`): escape 1`] = `
"module.exports = function escape(url) {
"module.exports = function escape(url, needQuotes) {
if (typeof url !== 'string') {
return url;
}
Expand All @@ -98,9 +98,10 @@ exports[`loader should compile with \`css\` entry point (with \`modules\` and sc
if (/^['\\"].*['\\"]$/.test(url)) {
url = url.slice(1, -1);
}
// Should url be wrapped?
// See https://drafts.csswg.org/css-values-3/#urls
if (/[\\"'() \\\\t\\\\n]/.test(url)) {
if (/[\\"'() \\\\t\\\\n]/.test(url) || needQuotes) {
return '\\"' + url.replace(/\\"/g, '\\\\\\\\\\"').replace(/\\\\n/g, '\\\\\\\\n') + '\\"';
}
Expand Down Expand Up @@ -339,7 +340,7 @@ function toComment(sourceMap) {
exports[`loader should compile with \`css\` entry point (with \`modules\` and scope \`local\`): errors 1`] = `Array []`;

exports[`loader should compile with \`css\` entry point (with \`modules\` and scope \`local\`): escape 1`] = `
"module.exports = function escape(url) {
"module.exports = function escape(url, needQuotes) {
if (typeof url !== 'string') {
return url;
}
Expand All @@ -348,9 +349,10 @@ exports[`loader should compile with \`css\` entry point (with \`modules\` and sc
if (/^['\\"].*['\\"]$/.test(url)) {
url = url.slice(1, -1);
}
// Should url be wrapped?
// See https://drafts.csswg.org/css-values-3/#urls
if (/[\\"'() \\\\t\\\\n]/.test(url)) {
if (/[\\"'() \\\\t\\\\n]/.test(url) || needQuotes) {
return '\\"' + url.replace(/\\"/g, '\\\\\\\\\\"').replace(/\\\\n/g, '\\\\\\\\n') + '\\"';
}
Expand Down Expand Up @@ -609,7 +611,7 @@ function toComment(sourceMap) {
exports[`loader should compile with \`css\` entry point: errors 1`] = `Array []`;

exports[`loader should compile with \`css\` entry point: escape 1`] = `
"module.exports = function escape(url) {
"module.exports = function escape(url, needQuotes) {
if (typeof url !== 'string') {
return url;
}
Expand All @@ -618,9 +620,10 @@ exports[`loader should compile with \`css\` entry point: escape 1`] = `
if (/^['\\"].*['\\"]$/.test(url)) {
url = url.slice(1, -1);
}
// Should url be wrapped?
// See https://drafts.csswg.org/css-values-3/#urls
if (/[\\"'() \\\\t\\\\n]/.test(url)) {
if (/[\\"'() \\\\t\\\\n]/.test(url) || needQuotes) {
return '\\"' + url.replace(/\\"/g, '\\\\\\\\\\"').replace(/\\\\n/g, '\\\\\\\\n') + '\\"';
}
Expand Down Expand Up @@ -859,7 +862,7 @@ function toComment(sourceMap) {
exports[`loader should compile with \`js\` entry point: errors 1`] = `Array []`;

exports[`loader should compile with \`js\` entry point: escape 1`] = `
"module.exports = function escape(url) {
"module.exports = function escape(url, needQuotes) {
if (typeof url !== 'string') {
return url;
}
Expand All @@ -868,9 +871,10 @@ exports[`loader should compile with \`js\` entry point: escape 1`] = `
if (/^['\\"].*['\\"]$/.test(url)) {
url = url.slice(1, -1);
}
// Should url be wrapped?
// See https://drafts.csswg.org/css-values-3/#urls
if (/[\\"'() \\\\t\\\\n]/.test(url)) {
if (/[\\"'() \\\\t\\\\n]/.test(url) || needQuotes) {
return '\\"' + url.replace(/\\"/g, '\\\\\\\\\\"').replace(/\\\\n/g, '\\\\\\\\n') + '\\"';
}
Expand Down
796 changes: 759 additions & 37 deletions test/__snapshots__/url-option.test.js.snap

Large diffs are not rendered by default.

Binary file added test/fixtures/url/img3x.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 59 additions & 0 deletions test/fixtures/url/url.css
Expand Up @@ -209,3 +209,62 @@ a {
@font-face {
src: url("//at.alicdn.com/t/font_515771_emcns5054x3whfr.eot");
}

.class {
/* Broken */
background-image: -webkit-image-set();
background-image: -webkit-image-set('');
background-image: image-set();
background-image: image-set('');
background-image: image-set("");
background-image: image-set("" 1x);
background-image: image-set(url());
background-image: image-set(
url()
);
background-image: image-set(URL());
background-image: image-set(url(''));
background-image: image-set(url(""));
background-image: image-set(url('') 1x);
background-image: image-set(1x);
background-image: image-set(
1x
);
background: image-set(calc(1rem + 1px) 1x);

/* Strings */
background-image: -webkit-image-set("./img1x.png" 1x, "./img2x.png" 2x);
background-image: image-set("./img1x.png" 1x);
background-image: image-set("./img1x.png" 1x, "./img2x.png" 2x);
background-image: image-set("./img img.png" 1x, "./img img.png" 2x);
background-image: image-set("./img1x.png" 1x, "./img2x.png" 2x),
image-set("./img1x.png" 1x, "./img2x.png" 2x);
background-image: image-set(
"./img1x.png" 1x,
"./img2x.png" 2x,
"./img3x.png" 600dpi
);
background-image: image-set("./img1x.png?foo=bar" 1x);
background-image: image-set("./img1x.png#hash" 1x);
background-image: image-set("./img1x.png?#iefix" 1x);

/* With `url` function */
background-image: -webkit-image-set(url("./img1x.png") 1x, url("./img2x.png") 2x);
background-image: -webkit-image-set(url("./img1x.png") 1x);
background-image: -webkit-image-set(
url("./img1x.png") 1x
);
background-image: image-set(url(./img1x.png) 1x);
background-image: image-set(
url(./img1x.png) 1x
);
background-image: image-set(url("./img1x.png") 1x, url("./img2x.png") 2x);
background-image: image-set(
url(./img1x.png) 1x,
url(./img2x.png) 2x,
url(./img3x.png) 600dpi
);
background-image: image-set(url("./img img.png") 1x, url("./img img.png") 2x);

background-image: image-set(url("./img1x.png") 1x, "./img2x.png" 2x);
}
6 changes: 6 additions & 0 deletions test/runtime/__snapshots__/url-escape.test.js.snap
Expand Up @@ -17,3 +17,9 @@ exports[`escape should escape url 7`] = `"\\"image other.png\\""`;
exports[`escape should escape url 8`] = `"\\"image\\\\\\"other.png\\""`;

exports[`escape should escape url 9`] = `"\\"image\\\\nother.png\\""`;

exports[`escape should escape url 10`] = `"\\"image.png\\""`;

exports[`escape should escape url 11`] = `"\\"image other.png\\""`;

exports[`escape should escape url 12`] = `"\\"image other.png\\""`;
4 changes: 4 additions & 0 deletions test/runtime/url-escape.test.js
Expand Up @@ -11,5 +11,9 @@ describe('escape', () => {
expect(urlEscape("'image other.png'")).toMatchSnapshot();
expect(urlEscape('image"other.png')).toMatchSnapshot();
expect(urlEscape('image\nother.png')).toMatchSnapshot();

expect(urlEscape('image.png', true)).toMatchSnapshot();
expect(urlEscape("'image other.png'", true)).toMatchSnapshot();
expect(urlEscape('"image other.png"', true)).toMatchSnapshot();
});
});

0 comments on commit 21884e2

Please sign in to comment.