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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle url()/src()/image-set()/image() #16978

Merged
merged 26 commits into from Apr 14, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
182559b
fix: handle `url()`/`src()`/`image-set`/`image`
alexander-akait Apr 12, 2023
c3c5c54
fix: spaces
alexander-akait Apr 12, 2023
cb77a36
test: more
alexander-akait Apr 12, 2023
7ee28a7
fix: more bugs
alexander-akait Apr 12, 2023
6bd80ba
fix: more bugs
alexander-akait Apr 12, 2023
bb4d2f0
chore: fix todo
alexander-akait Apr 12, 2023
5765c9a
fix: bugs
alexander-akait Apr 13, 2023
96d1e06
test: update
alexander-akait Apr 13, 2023
74d69a1
fix: bug with case sensitive plugin and data: protocol
alexander-akait Apr 13, 2023
bb1ae43
fix: bug with case sensitive plugin and data: protocol
alexander-akait Apr 13, 2023
230f830
fix: bug with case sensitive plugin and data: protocol
alexander-akait Apr 13, 2023
432ead0
fix: typo
alexander-akait Apr 13, 2023
8e4efec
test: fix freeze
alexander-akait Apr 13, 2023
cb066be
test: update
alexander-akait Apr 13, 2023
ccbc0a1
test: more
alexander-akait Apr 13, 2023
080afa5
test: more
alexander-akait Apr 13, 2023
758ce77
feat: support `image-set`
alexander-akait Apr 13, 2023
19d3b0d
test: fix
alexander-akait Apr 13, 2023
78369d5
test: update
alexander-akait Apr 13, 2023
aeafcf6
fix: logic
alexander-akait Apr 13, 2023
bb1f0ca
fix: logic with espaced
alexander-akait Apr 13, 2023
308e405
refactor: parser
alexander-akait Apr 14, 2023
65b5216
fix: parsing comments and escaped
alexander-akait Apr 14, 2023
bfcce62
fix: parsing comments
alexander-akait Apr 14, 2023
f1cf224
fix: parsing comments
alexander-akait Apr 14, 2023
d1634b8
refactor: improve
alexander-akait Apr 14, 2023
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
1 change: 1 addition & 0 deletions cspell.json
Expand Up @@ -192,6 +192,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 &&
TheLarkInn marked this conversation as resolved.
Show resolved Hide resolved
/** @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
119 changes: 105 additions & 14 deletions lib/css/CssParser.js
Expand Up @@ -17,21 +17,50 @@ 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];
const STRING_MULTILINE = /\\(\n|\r\n|\r|\f)/g;
const TRIM_WHITE_SPACES = /(^( |\t\n|\r\n|\r|\f)*|( |\t\n|\r\n|\r|\f)*$)/g;
alexander-akait marked this conversation as resolved.
Show resolved Hide resolved
const UNESCAPE = /\\([0-9a-fA-F]{1,6}[ \t\n\r\f]?|[\s\S])/g;

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
TheLarkInn marked this conversation as resolved.
Show resolved Hide resolved
}
});
}

return str;
};

class LocConverter {
Expand Down Expand Up @@ -137,8 +166,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 +336,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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we typedef these functions? It's hard to tell from the PR if types come from/or are provided by walkCssTokens

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TheLarkInn We already have, look at CssTokenCallbacks type

let value = normalizeUrl(
input.slice(contentStart, contentEnd),
isString
);
switch (mode) {
case CSS_MODE_AT_IMPORT_EXPECT_URL: {
modeData.url = value;
Expand All @@ -321,6 +356,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 +379,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" ||
/^(-\w+-)?image-set$/i.test(lastFunction[0].replace(/\\/g, "")))
alexander-akait marked this conversation as resolved.
Show resolved Hide resolved
) {
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 +601,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 +617,7 @@ class CssParser extends Parser {
break;
}
}

return end;
},
pseudoClass: (input, start, end) => {
Expand Down Expand Up @@ -564,9 +645,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 +673,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