Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support image-set without url #879

Merged
merged 1 commit into from Dec 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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();
});
});