Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/hsl/parseHslLegacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { hue, per, num_per, c } from '../util/regex.js';
Reference: https://drafts.csswg.org/css-color/#the-hsl-notation
*/
const hsl_old = new RegExp(
`^hsla?\\(\\s*${hue}${c}${per}${c}${per}\\s*(?:,\\s*${num_per}\\s*)?\\)$`
`^hsla?\\(\\s*${hue}${c}${per}${c}${per}\\s*(?:,\\s*${num_per}\\s*)?\\)$`,
'i'
);

const parseHslLegacy = color => {
Expand All @@ -17,7 +18,7 @@ const parseHslLegacy = color => {
if (match[3] !== undefined) {
res.h = +match[3];
} else if (match[1] !== undefined && match[2] !== undefined) {
res.h = hueToDeg(match[1], match[2]);
res.h = hueToDeg(match[1], match[2].toLowerCase());
}

if (match[4] !== undefined) {
Expand Down
14 changes: 11 additions & 3 deletions src/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,18 @@ function digits(chars) {

/*
Consume an identifier.

Every identifier in the CSS color grammar — function names (`rgb`, `oklch`,
`color`, …), keywords (`none`), color-space names (`srgb`, `display-p3`, …)
and hue angle units (`deg`, `turn`, …) — is an ASCII case-insensitive
keyword, so we normalize identifiers to lowercase as we consume them.
*/
function ident(chars) {
let v = '';
while (_i < chars.length && IdentCodePoint.test(chars[_i])) {
v += chars[_i++];
}
return v;
return v.toLowerCase();
}

/*
Expand Down Expand Up @@ -264,10 +269,13 @@ export function parseColorSyntax(tokens) {
if (token.type !== Tok.Ident) {
return undefined;
}
const mode = colorProfiles[token.value];
if (!mode) {
// Use an own-property check so that identifiers inherited from
// `Object.prototype` (`constructor`, `__proto__`, …) don't resolve to
// truthy non-profile values and crash further down.
if (!Object.prototype.hasOwnProperty.call(colorProfiles, token.value)) {
return undefined;
}
const mode = colorProfiles[token.value];
const res = { mode };
const coords = consumeCoords(tokens, false);
if (!coords) {
Expand Down
6 changes: 4 additions & 2 deletions src/rgb/parseRgbLegacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { num, per, num_per, c } from '../util/regex.js';
Reference: https://drafts.csswg.org/css-color/#rgb-functions
*/
const rgb_num_old = new RegExp(
`^rgba?\\(\\s*${num}${c}${num}${c}${num}\\s*(?:,\\s*${num_per}\\s*)?\\)$`
`^rgba?\\(\\s*${num}${c}${num}${c}${num}\\s*(?:,\\s*${num_per}\\s*)?\\)$`,
'i'
);

const rgb_per_old = new RegExp(
`^rgba?\\(\\s*${per}${c}${per}${c}${per}\\s*(?:,\\s*${num_per}\\s*)?\\)$`
`^rgba?\\(\\s*${per}${c}${per}${c}${per}\\s*(?:,\\s*${num_per}\\s*)?\\)$`,
'i'
);

const parseRgbLegacy = color => {
Expand Down
2 changes: 1 addition & 1 deletion src/rgb/parseTransparent.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const parseTransparent = c =>
c === 'transparent'
c.toLowerCase() === 'transparent'
? { mode: 'rgb', r: 0, g: 0, b: 0, alpha: 0 }
: undefined;

Expand Down
63 changes: 63 additions & 0 deletions test/parse.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,66 @@ test('undefined', t => {
test('Issue #204', t => {
assert.equal(parse('oklch(70% 0..1 156)'), undefined);
});

test('case-insensitive function names and keywords', t => {
// Function names are ASCII case-insensitive in CSS.
assert.deepEqual(parse('RGB(255 0 0)'), parse('rgb(255 0 0)'), 'RGB()');
assert.deepEqual(
parse('RGBA(255, 0, 0, 0.5)'),
parse('rgba(255, 0, 0, 0.5)'),
'RGBA() legacy'
);
assert.deepEqual(
parse('HSL(120 50% 50%)'),
parse('hsl(120 50% 50%)'),
'HSL()'
);
assert.deepEqual(
parse('HSLA(120, 50%, 50%, 0.5)'),
parse('hsla(120, 50%, 50%, 0.5)'),
'HSLA() legacy'
);
assert.deepEqual(parse('HWB(120 0% 0%)'), parse('hwb(120 0% 0%)'), 'HWB()');
assert.deepEqual(parse('LAB(50 0 0)'), parse('lab(50 0 0)'), 'LAB()');
assert.deepEqual(parse('LCH(50 40 30)'), parse('lch(50 40 30)'), 'LCH()');
assert.deepEqual(
parse('OkLCH(0.5 0.1 30)'),
parse('oklch(0.5 0.1 30)'),
'OkLCH() (mixed case)'
);

// Color-space identifiers inside color() are case-insensitive too.
assert.deepEqual(
parse('COLOR(DISPLAY-P3 1 0 0)'),
parse('color(display-p3 1 0 0)'),
'color() with uppercase profile'
);

// Hue angle units are case-insensitive, in both modern and legacy syntax.
assert.deepEqual(
parse('hsl(120DEG 50% 50%)'),
parse('hsl(120deg 50% 50%)'),
'uppercase hue unit (modern)'
);
assert.deepEqual(
parse('hsl(0.5TURN, 50%, 50%)'),
parse('hsl(0.5turn, 50%, 50%)'),
'uppercase hue unit (legacy)'
);

// The `transparent` keyword is case-insensitive.
assert.deepEqual(
parse('TRANSPARENT'),
{ mode: 'rgb', r: 0, g: 0, b: 0, alpha: 0 },
'TRANSPARENT'
);
});

test('color() with a non-profile identifier returns undefined', t => {
// Identifiers inherited from Object.prototype must not resolve to a
// truthy value in the color-space lookup (and must not throw).
assert.equal(parse('color(__proto__ 1 0 0)'), undefined, '__proto__');
assert.equal(parse('color(constructor 1 0 0)'), undefined, 'constructor');
assert.equal(parse('color(CONSTRUCTOR 1 0 0)'), undefined, 'CONSTRUCTOR');
assert.equal(parse('color(toString 1 0 0)'), undefined, 'toString');
});