Skip to content

Commit 644f1d1

Browse files
refactor: no extra work for CSS unescaping
2 parents a2ad76c + 8edbc7c commit 644f1d1

File tree

2 files changed

+157
-150
lines changed

2 files changed

+157
-150
lines changed

lib/css/CssParser.js

Lines changed: 152 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,136 @@ const normalizeUrl = (str, isString) => {
9999
return str;
100100
};
101101

102+
// eslint-disable-next-line no-useless-escape
103+
const regexSingleEscape = /[ -,.\/:-@[\]\^`{-~]/;
104+
const regexExcessiveSpaces =
105+
/(^|\\+)?(\\[A-F0-9]{1,6})\u0020(?![a-fA-F0-9\u0020])/g;
106+
107+
/**
108+
* @param {string} str string
109+
* @returns {string} escaped identifier
110+
*/
111+
const escapeIdentifier = str => {
112+
let output = "";
113+
let counter = 0;
114+
115+
while (counter < str.length) {
116+
const character = str.charAt(counter++);
117+
118+
let value;
119+
120+
if (/[\t\n\f\r\u000B]/.test(character)) {
121+
const codePoint = character.charCodeAt(0);
122+
123+
value = `\\${codePoint.toString(16).toUpperCase()} `;
124+
} else if (character === "\\" || regexSingleEscape.test(character)) {
125+
value = `\\${character}`;
126+
} else {
127+
value = character;
128+
}
129+
130+
output += value;
131+
}
132+
133+
const firstChar = str.charAt(0);
134+
135+
if (/^-[-\d]/.test(output)) {
136+
output = `\\-${output.slice(1)}`;
137+
} else if (/\d/.test(firstChar)) {
138+
output = `\\3${firstChar} ${output.slice(1)}`;
139+
}
140+
141+
// Remove spaces after `\HEX` escapes that are not followed by a hex digit,
142+
// since they’re redundant. Note that this is only possible if the escape
143+
// sequence isn’t preceded by an odd number of backslashes.
144+
output = output.replace(regexExcessiveSpaces, ($0, $1, $2) => {
145+
if ($1 && $1.length % 2) {
146+
// It’s not safe to remove the space, so don’t.
147+
return $0;
148+
}
149+
150+
// Strip the space.
151+
return ($1 || "") + $2;
152+
});
153+
154+
return output;
155+
};
156+
157+
const CONTAINS_ESCAPE = /\\/;
158+
159+
/**
160+
* @param {string} str string
161+
* @returns {[string, number] | undefined} hex
162+
*/
163+
const gobbleHex = str => {
164+
const lower = str.toLowerCase();
165+
let hex = "";
166+
let spaceTerminated = false;
167+
168+
for (let i = 0; i < 6 && lower[i] !== undefined; i++) {
169+
const code = lower.charCodeAt(i);
170+
// check to see if we are dealing with a valid hex char [a-f|0-9]
171+
const valid = (code >= 97 && code <= 102) || (code >= 48 && code <= 57);
172+
// https://drafts.csswg.org/css-syntax/#consume-escaped-code-point
173+
spaceTerminated = code === 32;
174+
if (!valid) break;
175+
hex += lower[i];
176+
}
177+
178+
if (hex.length === 0) return undefined;
179+
180+
const codePoint = Number.parseInt(hex, 16);
181+
const isSurrogate = codePoint >= 0xd800 && codePoint <= 0xdfff;
182+
183+
// Add special case for
184+
// "If this number is zero, or is for a surrogate, or is greater than the maximum allowed code point"
185+
// https://drafts.csswg.org/css-syntax/#maximum-allowed-code-point
186+
if (isSurrogate || codePoint === 0x0000 || codePoint > 0x10ffff) {
187+
return ["\uFFFD", hex.length + (spaceTerminated ? 1 : 0)];
188+
}
189+
190+
return [
191+
String.fromCodePoint(codePoint),
192+
hex.length + (spaceTerminated ? 1 : 0)
193+
];
194+
};
195+
196+
/**
197+
* @param {string} str string
198+
* @returns {string} unescaped string
199+
*/
200+
const unescapeIdentifier = str => {
201+
const needToProcess = CONTAINS_ESCAPE.test(str);
202+
if (!needToProcess) return str;
203+
let ret = "";
204+
for (let i = 0; i < str.length; i++) {
205+
if (str[i] === "\\") {
206+
const gobbled = gobbleHex(str.slice(i + 1, i + 7));
207+
if (gobbled !== undefined) {
208+
ret += gobbled[0];
209+
i += gobbled[1];
210+
continue;
211+
}
212+
// Retain a pair of \\ if double escaped `\\\\`
213+
// https://github.com/postcss/postcss-selector-parser/commit/268c9a7656fb53f543dc620aa5b73a30ec3ff20e
214+
if (str[i + 1] === "\\") {
215+
ret += "\\";
216+
i += 1;
217+
continue;
218+
}
219+
// if \\ is at the end of the string retain it
220+
// https://github.com/postcss/postcss-selector-parser/commit/01a6b346e3612ce1ab20219acc26abdc259ccefb
221+
if (str.length === i + 1) {
222+
ret += str[i];
223+
}
224+
continue;
225+
}
226+
ret += str[i];
227+
}
228+
229+
return ret;
230+
};
231+
102232
class LocConverter {
103233
/**
104234
* @param {string} input input
@@ -482,17 +612,15 @@ class CssParser extends Parser {
482612
// CSS Variable
483613
const { line: sl, column: sc } = locConverter.get(propertyNameStart);
484614
const { line: el, column: ec } = locConverter.get(propertyNameEnd);
485-
const name = propertyName.slice(2);
615+
const name = unescapeIdentifier(propertyName.slice(2));
486616
const dep = new CssLocalIdentifierDependency(
487617
name,
488618
[propertyNameStart, propertyNameEnd],
489619
"--"
490620
);
491621
dep.setLoc(sl, sc, el, ec);
492622
module.addDependency(dep);
493-
declaredCssVariables.add(
494-
CssSelfLocalIdentifierDependency.unescapeIdentifier(name)
495-
);
623+
declaredCssVariables.add(name);
496624
} else if (
497625
OPTIONALLY_VENDOR_PREFIXED_ANIMATION_PROPERTY.test(propertyName)
498626
) {
@@ -507,9 +635,11 @@ class CssParser extends Parser {
507635
if (inAnimationProperty && lastIdentifier) {
508636
const { line: sl, column: sc } = locConverter.get(lastIdentifier[0]);
509637
const { line: el, column: ec } = locConverter.get(lastIdentifier[1]);
510-
const name = lastIdentifier[2]
511-
? input.slice(lastIdentifier[0], lastIdentifier[1])
512-
: input.slice(lastIdentifier[0] + 1, lastIdentifier[1] - 1);
638+
const name = unescapeIdentifier(
639+
lastIdentifier[2]
640+
? input.slice(lastIdentifier[0], lastIdentifier[1])
641+
: input.slice(lastIdentifier[0] + 1, lastIdentifier[1] - 1)
642+
);
513643
const dep = new CssSelfLocalIdentifierDependency(name, [
514644
lastIdentifier[0],
515645
lastIdentifier[1]
@@ -882,10 +1012,11 @@ class CssParser extends Parser {
8821012
end
8831013
);
8841014
if (!ident) return end;
885-
const name =
1015+
const name = unescapeIdentifier(
8861016
ident[2] === true
8871017
? input.slice(ident[0], ident[1])
888-
: input.slice(ident[0] + 1, ident[1] - 1);
1018+
: input.slice(ident[0] + 1, ident[1] - 1)
1019+
);
8891020
const { line: sl, column: sc } = locConverter.get(ident[0]);
8901021
const { line: el, column: ec } = locConverter.get(ident[1]);
8911022
const dep = new CssLocalIdentifierDependency(name, [
@@ -900,10 +1031,8 @@ class CssParser extends Parser {
9001031
if (!ident) return end;
9011032
let name = input.slice(ident[0], ident[1]);
9021033
if (!name.startsWith("--") || name.length < 3) return end;
903-
name = name.slice(2);
904-
declaredCssVariables.add(
905-
CssSelfLocalIdentifierDependency.unescapeIdentifier(name)
906-
);
1034+
name = unescapeIdentifier(name.slice(2));
1035+
declaredCssVariables.add(name);
9071036
const { line: sl, column: sc } = locConverter.get(ident[0]);
9081037
const { line: el, column: ec } = locConverter.get(ident[1]);
9091038
const dep = new CssLocalIdentifierDependency(
@@ -996,7 +1125,7 @@ class CssParser extends Parser {
9961125
end
9971126
);
9981127
if (!ident) return end;
999-
const name = input.slice(ident[0], ident[1]);
1128+
const name = unescapeIdentifier(input.slice(ident[0], ident[1]));
10001129
const dep = new CssLocalIdentifierDependency(name, [
10011130
ident[0],
10021131
ident[1]
@@ -1013,7 +1142,7 @@ class CssParser extends Parser {
10131142
hash: (input, start, end, isID) => {
10141143
if (isNextRulePrelude && isLocalMode() && isID) {
10151144
const valueStart = start + 1;
1016-
const name = input.slice(valueStart, end);
1145+
const name = unescapeIdentifier(input.slice(valueStart, end));
10171146
const dep = new CssLocalIdentifierDependency(name, [valueStart, end]);
10181147
const { line: sl, column: sc } = locConverter.get(start);
10191148
const { line: el, column: ec } = locConverter.get(end);
@@ -1258,12 +1387,15 @@ class CssParser extends Parser {
12581387
if (name === "var") {
12591388
const customIdent = walkCssTokens.eatIdentSequence(input, end);
12601389
if (!customIdent) return end;
1261-
const name = input.slice(customIdent[0], customIdent[1]);
1390+
let name = input.slice(customIdent[0], customIdent[1]);
12621391
// A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS), like --foo.
12631392
// The <custom-property-name> production corresponds to this:
12641393
// it’s defined as any <dashed-ident> (a valid identifier that starts with two dashes),
12651394
// except -- itself, which is reserved for future use by CSS.
12661395
if (!name.startsWith("--") || name.length < 3) return end;
1396+
name = unescapeIdentifier(
1397+
input.slice(customIdent[0] + 2, customIdent[1])
1398+
);
12671399
const afterCustomIdent = walkCssTokens.eatWhitespaceAndComments(
12681400
input,
12691401
customIdent[1]
@@ -1301,7 +1433,7 @@ class CssParser extends Parser {
13011433
} else if (from[2] === false) {
13021434
const dep = new CssIcssImportDependency(
13031435
path.slice(1, -1),
1304-
name.slice(2),
1436+
name,
13051437
[customIdent[0], from[1] - 1]
13061438
);
13071439
const { line: sl, column: sc } = locConverter.get(
@@ -1321,7 +1453,7 @@ class CssParser extends Parser {
13211453
customIdent[1]
13221454
);
13231455
const dep = new CssSelfLocalIdentifierDependency(
1324-
name.slice(2),
1456+
name,
13251457
[customIdent[0], customIdent[1]],
13261458
"--",
13271459
declaredCssVariables
@@ -1466,3 +1598,5 @@ class CssParser extends Parser {
14661598
}
14671599

14681600
module.exports = CssParser;
1601+
module.exports.escapeIdentifier = escapeIdentifier;
1602+
module.exports.unescapeIdentifier = unescapeIdentifier;

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy