Skip to content

Commit

Permalink
Merge pull request #16978 from webpack/feat-improve-url-resolving
Browse files Browse the repository at this point in the history
fix: handle `url()`/`src()`/`image-set()`/`image()`
  • Loading branch information
TheLarkInn committed Apr 14, 2023
2 parents 5579b56 + d1634b8 commit 0c5d8d6
Show file tree
Hide file tree
Showing 33 changed files with 1,374 additions and 144 deletions.
1 change: 1 addition & 0 deletions cspell.json
Expand Up @@ -193,6 +193,7 @@
"queryloader",
"querystrings",
"RBDT",
"reconsume",
"recurse",
"redeclaration",
"reexecuted",
Expand Down
12 changes: 12 additions & 0 deletions lib/WarnCaseSensitiveModulesPlugin.js
Expand Up @@ -9,6 +9,7 @@ const CaseSensitiveModulesWarning = require("./CaseSensitiveModulesWarning");

/** @typedef {import("./Compiler")} Compiler */
/** @typedef {import("./Module")} Module */
/** @typedef {import("./NormalModule")} NormalModule */

class WarnCaseSensitiveModulesPlugin {
/**
Expand All @@ -25,6 +26,17 @@ class WarnCaseSensitiveModulesPlugin {
const moduleWithoutCase = new Map();
for (const module of compilation.modules) {
const identifier = module.identifier();

// Ignore `data:` URLs, because it's not a real path
if (
/** @type {NormalModule} */
(module).resourceResolveData !== undefined &&
/** @type {NormalModule} */
(module).resourceResolveData.encodedContent !== undefined
) {
continue;
}

const lowerIdentifier = identifier.toLowerCase();
let map = moduleWithoutCase.get(lowerIdentifier);
if (map === undefined) {
Expand Down
14 changes: 11 additions & 3 deletions lib/asset/AssetGenerator.js
Expand Up @@ -108,9 +108,17 @@ const encodeDataUri = (encoding, source) => {

const decodeDataUriContent = (encoding, content) => {
const isBase64 = encoding === "base64";
return isBase64
? Buffer.from(content, "base64")
: Buffer.from(decodeURIComponent(content), "ascii");

if (isBase64) {
return Buffer.from(content, "base64");
}

// If we can't decode return the original body
try {
return Buffer.from(decodeURIComponent(content), "ascii");
} catch (_) {
return Buffer.from(content, "ascii");
}
};

const JS_TYPES = new Set(["javascript"]);
Expand Down
123 changes: 109 additions & 14 deletions lib/css/CssParser.js
Expand Up @@ -17,21 +17,54 @@ const walkCssTokens = require("./walkCssTokens");

/** @typedef {import("../Parser").ParserState} ParserState */
/** @typedef {import("../Parser").PreparsedAst} PreparsedAst */

const CC_LEFT_CURLY = "{".charCodeAt(0);
const CC_RIGHT_CURLY = "}".charCodeAt(0);
const CC_COLON = ":".charCodeAt(0);
const CC_SLASH = "/".charCodeAt(0);
const CC_SEMICOLON = ";".charCodeAt(0);

const cssUnescape = str => {
return str.replace(/\\([0-9a-fA-F]{1,6}[ \t\n\r\f]?|[\s\S])/g, match => {
if (match.length > 2) {
return String.fromCharCode(parseInt(match.slice(1).trim(), 16));
} else {
return match[1];
// https://www.w3.org/TR/css-syntax-3/#newline
// We don't have `preprocessing` stage, so we need specify all of them
const STRING_MULTILINE = /\\[\n\r\f]/g;
// https://www.w3.org/TR/css-syntax-3/#whitespace
const TRIM_WHITE_SPACES = /(^[ \t\n\r\f]*|[ \t\n\r\f]*$)/g;
const UNESCAPE = /\\([0-9a-fA-F]{1,6}[ \t\n\r\f]?|[\s\S])/g;
const IMAGE_SET_FUNCTION = /^(-\w+-)?image-set$/i;

const normalizeUrl = (str, isString) => {
// Remove extra spaces and newlines:
// `url("im\
// g.png")`
if (isString) {
str = str.replace(STRING_MULTILINE, "");
}

str = str
// Remove unnecessary spaces from `url(" img.png ")`
.replace(TRIM_WHITE_SPACES, "")
// Unescape
.replace(UNESCAPE, match => {
if (match.length > 2) {
return String.fromCharCode(parseInt(match.slice(1).trim(), 16));
} else {
return match[1];
}
});

if (/^data:/i.test(str)) {
return str;
}

if (str.includes("%")) {
// Convert `url('%2E/img.png')` -> `url('./img.png')`
try {
str = decodeURIComponent(str);
} catch (error) {
// Ignore
}
});
}

return str;
};

class LocConverter {
Expand Down Expand Up @@ -137,8 +170,11 @@ class CssParser extends Parser {
let modeData = undefined;
let singleClassSelector = undefined;
let lastIdentifier = undefined;
const modeStack = [];
let awaitRightParenthesis = false;
/** @type [string, number, number][] */
const functionStack = [];
const modeStack = [];

const isTopLevelLocal = () =>
modeData === "local" ||
(this.defaultMode === "local" && modeData === undefined);
Expand Down Expand Up @@ -304,8 +340,11 @@ class CssParser extends Parser {
isSelector: () => {
return mode !== CSS_MODE_IN_RULE && mode !== CSS_MODE_IN_LOCAL_RULE;
},
url: (input, start, end, contentStart, contentEnd) => {
const value = cssUnescape(input.slice(contentStart, contentEnd));
url: (input, start, end, contentStart, contentEnd, isString) => {
let value = normalizeUrl(
input.slice(contentStart, contentEnd),
isString
);
switch (mode) {
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
modeData.url = value;
Expand All @@ -321,6 +360,15 @@ class CssParser extends Parser {
)} at ${start} during ${explainMode(mode)}`
);
default: {
if (
// Ignore `url(#highlight)` URLs
/^#/.test(value) ||
// Ignore `url()`, `url('')` and `url("")`, they are valid by spec
value.length === 0
) {
break;
}

const dep = new CssUrlDependency(value, [start, end], "url");
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
Expand All @@ -335,10 +383,44 @@ class CssParser extends Parser {
string: (input, start, end) => {
switch (mode) {
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
modeData.url = cssUnescape(input.slice(start + 1, end - 1));
modeData.url = normalizeUrl(input.slice(start + 1, end - 1), true);
mode = CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS;
break;
}
default: {
// TODO move escaped parsing to tokenizer
const lastFunction = functionStack[functionStack.length - 1];

if (
lastFunction &&
(lastFunction[0].replace(/\\/g, "").toLowerCase() === "url" ||
IMAGE_SET_FUNCTION.test(lastFunction[0].replace(/\\/g, "")))
) {
let value = normalizeUrl(input.slice(start + 1, end - 1), true);

if (
// Ignore `url(#highlight)` URLs
/^#/.test(value) ||
// Ignore `url()`, `url('')` and `url("")`, they are valid by spec
value.length === 0
) {
break;
}

const isUrl =
lastFunction[0].replace(/\\/g, "").toLowerCase() === "url";
const dep = new CssUrlDependency(
value,
[start, end],
isUrl ? "string" : "url"
);
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
module.addCodeGenerationDependency(dep);
}
}
}
return end;
},
Expand Down Expand Up @@ -523,6 +605,8 @@ class CssParser extends Parser {
return end;
},
rightParenthesis: (input, start, end) => {
functionStack.pop();

switch (mode) {
case CSS_MODE_TOP_LEVEL: {
if (awaitRightParenthesis) {
Expand All @@ -537,6 +621,7 @@ class CssParser extends Parser {
break;
}
}

return end;
},
pseudoClass: (input, start, end) => {
Expand Down Expand Up @@ -564,9 +649,14 @@ class CssParser extends Parser {
return end;
},
pseudoFunction: (input, start, end) => {
let name = input.slice(start, end - 1);

functionStack.push([name, start, end]);

switch (mode) {
case CSS_MODE_TOP_LEVEL: {
const name = input.slice(start, end - 1).toLowerCase();
name = name.toLowerCase();

if (this.allowModeSwitch && name === ":global") {
modeStack.push(modeData);
modeData = "global";
Expand All @@ -587,9 +677,14 @@ class CssParser extends Parser {
return end;
},
function: (input, start, end) => {
let name = input.slice(start, end - 1);

functionStack.push([name, start, end]);

switch (mode) {
case CSS_MODE_IN_LOCAL_RULE: {
const name = input.slice(start, end - 1).toLowerCase();
name = name.toLowerCase();

if (name === "var") {
let pos = walkCssTokens.eatWhitespaceAndComments(input, end);
if (pos === input.length) return pos;
Expand Down

0 comments on commit 0c5d8d6

Please sign in to comment.