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
3 changes: 2 additions & 1 deletion 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 Down
9 changes: 6 additions & 3 deletions src/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ function identlike(chars) {
}

export function tokenize(str = '') {
let chars = str.trim();
let chars = str.trim().toLowerCase();
let tokens = [];
let ch;

Expand Down Expand Up @@ -264,10 +264,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'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It would be useful to guard for undefined colors with c?.toLowerCase().

c?.toLowerCase() === 'transparent'
? { mode: 'rgb', r: 0, g: 0, b: 0, alpha: 0 }
: undefined;

Expand Down
2 changes: 1 addition & 1 deletion src/util/hue.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const hueToDeg = (val, unit) => {
switch (unit) {
switch (unit?.toLowerCase()) {
case 'deg':
return +val;
case 'rad':
Expand Down
96 changes: 95 additions & 1 deletion test/parse.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert';
import { parse } from '../src/index.js';
import { parse, parseTransparent } from '../src/index.js';

test('named colors', t => {
assert.deepEqual(
Expand Down Expand Up @@ -408,3 +408,97 @@ 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');
});

test('parseTransparent accepts absent colors', t => {
assert.equal(parseTransparent(), undefined);
assert.equal(parseTransparent(null), undefined);
assert.equal(parseTransparent(''), undefined);
assert.equal(parseTransparent('red'), undefined);
assert.deepEqual(parseTransparent('TrAnSpArEnT'), {
mode: 'rgb',
r: 0,
g: 0,
b: 0,
alpha: 0
});
});

test('case-insensitive keywords and exponential notation', t => {
assert.deepEqual(parse(' RGB(1E2 NoNe 0 / NoNe) '), {
mode: 'rgb',
r: 100 / 255,
b: 0
});
});

test('case-insensitive hue units in modern and legacy syntax', t => {
for (const unit of ['DEG', 'RAD', 'GRAD', 'TURN']) {
for (const separator of [' ', ', ']) {
const color = `HSL(0.5${unit}${separator}50%${separator}50%)`;
assert.deepEqual(parse(color), parse(color.toLowerCase()), color);
}
}
});