Skip to content

Commit ba34962

Browse files
authored
Use ASCII lowercasing/case-testing instead of Unicode
See jsdom/jsdom#3892 (comment).
1 parent da42d97 commit ba34962

File tree

8 files changed

+195
-14
lines changed

8 files changed

+195
-14
lines changed

lib/CSSStyleDeclaration.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const generatedProperties = require("./generated/properties");
1111
const { hasVarFunc, parseKeyword, parseShorthand, prepareValue, splitValue } = require("./parsers");
1212
const { dashedToCamelCase } = require("./utils/camelize");
1313
const { getPropertyDescriptor } = require("./utils/propertyDescriptors");
14+
const { asciiLowercase } = require("./utils/strings");
1415

1516
/**
1617
* @see https://drafts.csswg.org/cssom/#the-cssstyledeclaration-interface
@@ -275,7 +276,7 @@ class CSSStyleDeclaration {
275276
this._setProperty(property, value);
276277
return;
277278
}
278-
property = property.toLowerCase();
279+
property = asciiLowercase(property);
279280
if (!allProperties.has(property) && !allExtraProperties.has(property)) {
280281
return;
281282
}

lib/parsers.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"use strict";
66

77
const { resolve: resolveColor, utils } = require("@asamuzakjp/css-color");
8+
const { asciiLowercase } = require("./utils/strings");
89

910
const { cssCalc, isColor, isGradient, splitValue } = utils;
1011

@@ -184,7 +185,7 @@ exports.parseLength = function parseLength(val, restrictToPositive = false) {
184185
if (restrictToPositive && num < 0) {
185186
return;
186187
}
187-
return `${num}${unit.toLowerCase()}`;
188+
return `${num}${asciiLowercase(unit)}`;
188189
}
189190
default:
190191
if (varContainedRegEx.test(val)) {
@@ -216,7 +217,7 @@ exports.parsePercent = function parsePercent(val, restrictToPositive = false) {
216217
if (restrictToPositive && num < 0) {
217218
return;
218219
}
219-
return `${num}${unit.toLowerCase()}`;
220+
return `${num}${asciiLowercase(unit)}`;
220221
}
221222
default:
222223
if (varContainedRegEx.test(val)) {
@@ -250,7 +251,7 @@ exports.parseMeasurement = function parseMeasurement(val, restrictToPositive = f
250251
if (restrictToPositive && num < 0) {
251252
return;
252253
}
253-
return `${num}${unit.toLowerCase()}`;
254+
return `${num}${asciiLowercase(unit)}`;
254255
}
255256
default:
256257
if (varContainedRegEx.test(val)) {
@@ -279,7 +280,7 @@ exports.parseAngle = function parseAngle(val, normalizeDeg = false) {
279280
case NUM_TYPE.ANGLE: {
280281
let [, numVal, unit] = unitRegEx.exec(val);
281282
numVal = parseFloat(numVal);
282-
unit = unit.toLowerCase();
283+
unit = asciiLowercase(unit);
283284
if (unit === "deg") {
284285
if (normalizeDeg && numVal < 0) {
285286
while (numVal < 0) {
@@ -391,7 +392,7 @@ exports.parseKeyword = function parseKeyword(val, validKeywords = []) {
391392
if (varRegEx.test(val)) {
392393
return val;
393394
}
394-
val = val.toString().toLowerCase();
395+
val = asciiLowercase(val.toString());
395396
if (validKeywords.includes(val) || GLOBAL_VALUE.includes(val)) {
396397
return val;
397398
}
@@ -404,8 +405,11 @@ exports.parseColor = function parseColor(val) {
404405
if (varRegEx.test(val)) {
405406
return val;
406407
}
407-
if (/^[a-z]+$/i.test(val) && SYS_COLOR.includes(val.toLowerCase())) {
408-
return val.toLowerCase();
408+
if (/^[a-z]+$/i.test(val)) {
409+
const v = asciiLowercase(val);
410+
if (SYS_COLOR.includes(v)) {
411+
return v;
412+
}
409413
}
410414
const res = resolveColor(val, {
411415
format: "specifiedValue"
@@ -491,7 +495,7 @@ exports.parseShorthand = function parseShorthand(val, shorthandFor, preserve = f
491495

492496
// Returns `false` for global values, e.g. "inherit".
493497
exports.isValidColor = function isValidColor(val) {
494-
if (SYS_COLOR.includes(val.toLowerCase())) {
498+
if (SYS_COLOR.includes(asciiLowercase(val))) {
495499
return true;
496500
}
497501
return isColor(val);

lib/properties/background.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// * also fix longhands
55

66
const parsers = require("../parsers");
7+
const strings = require("../utils/strings");
78
const backgroundImage = require("./backgroundImage");
89
const backgroundPosition = require("./backgroundPosition");
910
const backgroundRepeat = require("./backgroundRepeat");
@@ -21,7 +22,12 @@ const shorthandFor = new Map([
2122
module.exports.definition = {
2223
set(v) {
2324
v = parsers.prepareValue(v, this._global);
24-
if (v.toLowerCase() === "none" || parsers.hasVarFunc(v)) {
25+
if (/^none$/i.test(v)) {
26+
for (const [key] of shorthandFor) {
27+
this._setProperty(key, "");
28+
}
29+
this._setProperty("background", strings.asciiLowercase(v));
30+
} else if (parsers.hasVarFunc(v)) {
2531
for (const [key] of shorthandFor) {
2632
this._setProperty(key, "");
2733
}

lib/properties/border.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const shorthandFor = new Map([
1414
module.exports.definition = {
1515
set(v) {
1616
v = parsers.prepareValue(v, this._global);
17-
if (v.toLowerCase() === "none") {
17+
if (/^none$/i.test(v)) {
1818
v = "";
1919
}
2020
if (parsers.hasVarFunc(v)) {

lib/properties/borderStyle.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ module.exports.isValid = function isValid(v) {
2828
module.exports.definition = {
2929
set(v) {
3030
v = parsers.prepareValue(v, this._global);
31-
if (v.toLowerCase() === "none") {
31+
if (/^none$/i.test(v)) {
3232
v = "";
3333
}
3434
if (parsers.hasVarFunc(v)) {

lib/properties/clip.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// @see https://drafts.fxtf.org/css-masking/#clip-property
44

55
const parsers = require("../parsers");
6+
const strings = require("../utils/strings");
67

78
module.exports.parse = function parse(v) {
89
if (v === "") {
@@ -13,7 +14,7 @@ module.exports.parse = function parse(v) {
1314
return val;
1415
}
1516
// parse legacy <shape>
16-
v = v.toLowerCase();
17+
v = strings.asciiLowercase(v);
1718
const matches = v.match(/^rect\(\s*(.*)\s*\)$/);
1819
if (!matches) {
1920
return;

lib/utils/camelize.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use strict";
22

3+
const { asciiLowercase } = require("./strings");
4+
35
// Utility to translate from `border-width` to `borderWidth`.
46
// NOTE: For values prefixed with webkit, e.g. `-webkit-foo`, we need to provide
57
// both `webkitFoo` and `WebkitFoo`. Here we only return `webkitFoo`.
@@ -27,7 +29,7 @@ exports.camelCaseToDashed = function (camelCase) {
2729
if (camelCase.startsWith("--")) {
2830
return camelCase;
2931
}
30-
const dashed = camelCase.replace(/(?<=[a-z])[A-Z]/g, "-$&").toLowerCase();
32+
const dashed = asciiLowercase(camelCase.replace(/(?<=[a-z])[A-Z]/g, "-$&"));
3133
if (/^webkit-/.test(dashed)) {
3234
return `-${dashed}`;
3335
}

lib/utils/strings.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Forked from https://github.com/jsdom/jsdom/blob/main/lib/jsdom/living/helpers/strings.js
2+
3+
"use strict";
4+
5+
// https://infra.spec.whatwg.org/#ascii-whitespace
6+
const asciiWhitespaceRe = /^[\t\n\f\r ]$/;
7+
exports.asciiWhitespaceRe = asciiWhitespaceRe;
8+
9+
// https://infra.spec.whatwg.org/#ascii-lowercase
10+
exports.asciiLowercase = (s) => {
11+
const len = s.length;
12+
const out = new Array(len);
13+
for (let i = 0; i < len; i++) {
14+
const code = s.charCodeAt(i);
15+
// If the character is between 'A' (65) and 'Z' (90), convert using bitwise OR with 32
16+
out[i] = code >= 65 && code <= 90 ? String.fromCharCode(code | 32) : s[i];
17+
}
18+
return out.join("");
19+
};
20+
21+
// https://infra.spec.whatwg.org/#ascii-uppercase
22+
exports.asciiUppercase = (s) => {
23+
const len = s.length;
24+
const out = new Array(len);
25+
for (let i = 0; i < len; i++) {
26+
const code = s.charCodeAt(i);
27+
// If the character is between 'a' (97) and 'z' (122), convert using bitwise AND with ~32
28+
out[i] = code >= 97 && code <= 122 ? String.fromCharCode(code & ~32) : s[i];
29+
}
30+
return out.join("");
31+
};
32+
33+
// https://infra.spec.whatwg.org/#strip-newlines
34+
exports.stripNewlines = (s) => {
35+
return s.replace(/[\n\r]+/g, "");
36+
};
37+
38+
// https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace
39+
exports.stripLeadingAndTrailingASCIIWhitespace = (s) => {
40+
return s.replace(/^[ \t\n\f\r]+/, "").replace(/[ \t\n\f\r]+$/, "");
41+
};
42+
43+
// https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace
44+
exports.stripAndCollapseASCIIWhitespace = (s) => {
45+
return s
46+
.replace(/[ \t\n\f\r]+/g, " ")
47+
.replace(/^[ \t\n\f\r]+/, "")
48+
.replace(/[ \t\n\f\r]+$/, "");
49+
};
50+
51+
// https://html.spec.whatwg.org/multipage/infrastructure.html#valid-simple-colour
52+
exports.isValidSimpleColor = (s) => {
53+
return /^#[a-fA-F\d]{6}$/.test(s);
54+
};
55+
56+
// https://infra.spec.whatwg.org/#ascii-case-insensitive
57+
exports.asciiCaseInsensitiveMatch = (a, b) => {
58+
if (a.length !== b.length) {
59+
return false;
60+
}
61+
62+
for (let i = 0; i < a.length; ++i) {
63+
if ((a.charCodeAt(i) | 32) !== (b.charCodeAt(i) | 32)) {
64+
return false;
65+
}
66+
}
67+
68+
return true;
69+
};
70+
71+
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-integers
72+
// Error is represented as null.
73+
const parseInteger = (exports.parseInteger = (input) => {
74+
// The implementation here is slightly different from the spec's. We want to use parseInt(), but parseInt() trims
75+
// Unicode whitespace in addition to just ASCII ones, so we make sure that the trimmed prefix contains only ASCII
76+
// whitespace ourselves.
77+
const numWhitespace = input.length - input.trimStart().length;
78+
if (/[^\t\n\f\r ]/.test(input.slice(0, numWhitespace))) {
79+
return null;
80+
}
81+
// We don't allow hexadecimal numbers here.
82+
// eslint-disable-next-line radix
83+
const value = parseInt(input, 10);
84+
if (Number.isNaN(value)) {
85+
return null;
86+
}
87+
// parseInt() returns -0 for "-0". Normalize that here.
88+
return value === 0 ? 0 : value;
89+
});
90+
91+
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-non-negative-integers
92+
// Error is represented as null.
93+
exports.parseNonNegativeInteger = (input) => {
94+
const value = parseInteger(input);
95+
if (value === null) {
96+
return null;
97+
}
98+
if (value < 0) {
99+
return null;
100+
}
101+
return value;
102+
};
103+
104+
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-floating-point-number
105+
const floatingPointNumRe = /^-?(?:\d+|\d*\.\d+)(?:[eE][-+]?\d+)?$/;
106+
exports.isValidFloatingPointNumber = (str) => floatingPointNumRe.test(str);
107+
108+
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-floating-point-number-values
109+
// Error is represented as null.
110+
exports.parseFloatingPointNumber = (str) => {
111+
// The implementation here is slightly different from the spec's. We need to use parseFloat() in order to retain
112+
// accuracy, but parseFloat() trims Unicode whitespace in addition to just ASCII ones, so we make sure that the
113+
// trimmed prefix contains only ASCII whitespace ourselves.
114+
const numWhitespace = str.length - str.trimStart().length;
115+
if (/[^\t\n\f\r ]/.test(str.slice(0, numWhitespace))) {
116+
return null;
117+
}
118+
const parsed = parseFloat(str);
119+
return isFinite(parsed) ? parsed : null;
120+
};
121+
122+
// https://infra.spec.whatwg.org/#split-on-ascii-whitespace
123+
exports.splitOnASCIIWhitespace = (str) => {
124+
let position = 0;
125+
const tokens = [];
126+
while (position < str.length && asciiWhitespaceRe.test(str[position])) {
127+
position++;
128+
}
129+
if (position === str.length) {
130+
return tokens;
131+
}
132+
while (position < str.length) {
133+
const start = position;
134+
while (position < str.length && !asciiWhitespaceRe.test(str[position])) {
135+
position++;
136+
}
137+
tokens.push(str.slice(start, position));
138+
while (position < str.length && asciiWhitespaceRe.test(str[position])) {
139+
position++;
140+
}
141+
}
142+
return tokens;
143+
};
144+
145+
// https://infra.spec.whatwg.org/#split-on-commas
146+
exports.splitOnCommas = (str) => {
147+
let position = 0;
148+
const tokens = [];
149+
while (position < str.length) {
150+
let start = position;
151+
while (position < str.length && str[position] !== ",") {
152+
position++;
153+
}
154+
let end = position;
155+
while (start < str.length && asciiWhitespaceRe.test(str[start])) {
156+
start++;
157+
}
158+
while (end > start && asciiWhitespaceRe.test(str[end - 1])) {
159+
end--;
160+
}
161+
tokens.push(str.slice(start, end));
162+
if (position < str.length) {
163+
position++;
164+
}
165+
}
166+
return tokens;
167+
};

0 commit comments

Comments
 (0)