diff --git a/src/hsl/parseHslLegacy.js b/src/hsl/parseHslLegacy.js index e0b2c13d..b987abe8 100644 --- a/src/hsl/parseHslLegacy.js +++ b/src/hsl/parseHslLegacy.js @@ -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 => { diff --git a/src/parse.js b/src/parse.js index 140c7a07..30933250 100644 --- a/src/parse.js +++ b/src/parse.js @@ -143,7 +143,7 @@ function identlike(chars) { } export function tokenize(str = '') { - let chars = str.trim(); + let chars = str.trim().toLowerCase(); let tokens = []; let ch; @@ -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) { diff --git a/src/rgb/parseRgbLegacy.js b/src/rgb/parseRgbLegacy.js index 12823d2a..22775285 100644 --- a/src/rgb/parseRgbLegacy.js +++ b/src/rgb/parseRgbLegacy.js @@ -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 => { diff --git a/src/rgb/parseTransparent.js b/src/rgb/parseTransparent.js index 6a2279e7..2579f1a2 100644 --- a/src/rgb/parseTransparent.js +++ b/src/rgb/parseTransparent.js @@ -1,5 +1,5 @@ const parseTransparent = c => - c === 'transparent' + c?.toLowerCase() === 'transparent' ? { mode: 'rgb', r: 0, g: 0, b: 0, alpha: 0 } : undefined; diff --git a/src/util/hue.js b/src/util/hue.js index 54a29362..3ae6280d 100644 --- a/src/util/hue.js +++ b/src/util/hue.js @@ -1,5 +1,5 @@ const hueToDeg = (val, unit) => { - switch (unit) { + switch (unit?.toLowerCase()) { case 'deg': return +val; case 'rad': diff --git a/test/parse.test.js b/test/parse.test.js index 6f1fd072..55897bb7 100644 --- a/test/parse.test.js +++ b/test/parse.test.js @@ -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( @@ -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); + } + } +});