Skip to content

Commit 7b404be

Browse files
authored
Implement latest CSS Color Level 4 parsing (#190)
* Baseline css color parser * Parser improvements * Switch over all parsing to new parser. * Add tests for #187 * Add tests & fixes re #167, #155, #153 * Update the ranges for lab/lch/oklab/oklch to match CSS reference ranges. * Add docs on newly-exported parse fns
1 parent 4c5f0e5 commit 7b404be

32 files changed

+829
-320
lines changed

benchmark/package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

benchmark/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"chroma-js": "^2.4.2",
99
"d3-color": "^3.1.0",
1010
"d3-interpolate": "^3.0.1",
11-
"tinycolor2": "^1.5.2"
11+
"tinycolor2": "^1.5.2",
12+
"colorjs.io": "^0.4.3"
1213
}
1314
}

benchmark/tests/rgb-parse-speed.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import chroma from 'chroma-js';
22
import { color } from 'd3-color';
33
import tinycolor from 'tinycolor2';
4+
import { ColorSpace, sRGB, parse } from 'colorjs.io/fn';
45
import { rgb } from '../../src/index.js';
56
import benchmark from '../util/benchmark.js';
67

@@ -50,3 +51,24 @@ benchmark('culori: culori("rgb(r,g,b)")', () => {
5051
}
5152
}
5253
});
54+
55+
benchmark('culori: culori("rgb(r g b)")', () => {
56+
for (var r = 0; r <= 255; r += increment) {
57+
for (var g = 0; g <= 255; g += increment) {
58+
for (var b = 0; b <= 255; b += increment) {
59+
rgb(`rgb(${r} ${g} ${b})`);
60+
}
61+
}
62+
}
63+
});
64+
65+
ColorSpace.register(sRGB);
66+
benchmark('colorjs.io: parse("rgb(r g b)")', () => {
67+
for (var r = 0; r <= 255; r += increment) {
68+
for (var g = 0; g <= 255; g += increment) {
69+
for (var b = 0; b <= 255; b += increment) {
70+
parse(`rgb(${r} ${g} ${b})`);
71+
}
72+
}
73+
}
74+
});

docs/api.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,7 +1516,7 @@ The opposite of `toMode`. A set of function to convert from various color spaces
15161516

15171517
#### `ranges` (_object_, optional)
15181518

1519-
The ranges for values in specific channels; if left unspecified, defaults to `[0, 1]`.
1519+
The reference ranges for values in specific channels; if left unspecified, defaults to `[0, 1]`.
15201520

15211521
#### `parse` (_array_, optional)
15221522

@@ -1635,7 +1635,11 @@ parseHex('#abcdef12');
16351635

16361636
<a id="parseHsl" href="#parseHsl">#</a> __parseHsl__(_string_) → _color_
16371637

1638-
Parses `hsl(…)` / `hsla(…)` strings and returns `hsl` color objects.
1638+
Parses `hsl(…)` strings in the modern format and returns `hsl` color objects.
1639+
1640+
<a id="parseHslLegacy" href="#parseHslLegacy">#</a> __parseHslLegacy__(_string_) → _color_
1641+
1642+
Parses `hsl(…)` / `hsla(…)` strings in the legacy (comma-separated) format and returns `hsl` color objects.
16391643

16401644
<a id="parseHwb" href="#parseHwb">#</a> __parseHwb__(_string_) → _color_
16411645

@@ -1653,9 +1657,21 @@ Parses `lch(…)` strings and returns `lch` color objects.
16531657

16541658
Parses named CSS colors (eg. `tomato`) and returns `rgb` color objects.
16551659

1660+
<a id="parseOklab" href="#parseOklab">#</a> __parseOklab__(_string_) → _color_
1661+
1662+
Parses `oklab(…)` strings and returns `oklab` color objects.
1663+
1664+
<a id="parseOklch" href="#parseOklch">#</a> __parseOklch__(_string_) → _color_
1665+
1666+
Parses `oklch(…)` strings and returns `oklch` color objects.
1667+
16561668
<a id="parseRgb" href="#parseRgb">#</a> __parseRgb__(_color_) → _color_
16571669

1658-
Parses `rgb(…)` / `rgba(…)` strings and returns `rgb` color objects.
1670+
Parses `rgb(…)` strings in the modern syntax and returns `rgb` color objects.
1671+
1672+
<a id="parseRgbLegacy" href="#parseRgbLegacy">#</a> __parseRgbLegacy__(_color_) → _color_
1673+
1674+
Parses `rgb(…)` / `rgba(…)` strings in the legacy (comma-separated) syntax and returns `rgb` color objects.
16591675

16601676
<a id="parseTransparent" href="#parseTransparent">#</a>__parseTransparent__(_string_) → _color_
16611677

docs/color-spaces.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -138,22 +138,22 @@ The [CIELAB color space](https://en.wikipedia.org/wiki/CIELAB_color_space), also
138138

139139
The CIELAB color space using the [D50 standard illuminant](https://en.wikipedia.org/wiki/Standard_illuminant) as the reference white, following the [CSS Color Module Level 4 specification](https://drafts.csswg.org/css-color/#lab-colors).
140140

141-
| Channel | Range | Description |
141+
| Channel | CSS Reference Range | Description |
142142
| ------- | --------------------- | --------------------- |
143143
| `l` | `[0, 100]` | Lightness |
144-
| `a` | `[-79.287, 93.55]` | Green–red component |
145-
| `b` | `[-112.029, 93.388]` | Blue–yellow component |
144+
| `a` | `[-100, 100]` | Green–red component |
145+
| `b` | `[-100, 100]` | Blue–yellow component |
146146

147147
Serialized as `lab(l% a b)`, with the `none` keyword for any missing color channel. An explicit `alpha < 1` is included as ` / alpha`.
148148

149149
#### `lch`
150150

151151
The CIELCh color space using the D50 standard illuminant.
152152

153-
| Channel | Range | Description |
153+
| Channel | CSS Reference Range | Description |
154154
| ------- | --------------- | ----------- |
155155
| `l` | `[0, 100]` | Lightness |
156-
| `c` | `[0, 131.207]` | Chroma |
156+
| `c` | `[0, 150]` | Chroma |
157157
| `h` | `[0, 360)` | Hue |
158158

159159
Serialized as `lch(l% c h)`. A missing hue is serialized as `0`, with the `none` keyword for any other missing color channel. An explicit `alpha < 1` is included as ` / alpha`.
@@ -252,25 +252,25 @@ See also: [Okhsl and Okhsv, two new color spaces for color picking](https://bott
252252

253253
The Oklab color space in Cartesian form.
254254

255-
| Channel | Range | Description |
255+
| Channel | CSS Reference Range | Description |
256256
| ------- | ------------------ | --------------------- |
257-
| `l` | `[0, 0.999]` | Lightness |
258-
| `a` | `[-0.233, 0.276]` | Green–red component |
259-
| `b` | `[-0.311, 0.198]` | Blue–yellow component |
257+
| `l` | `[0, 1]` | Lightness |
258+
| `a` | `[-0.4, 0.4]` | Green–red component |
259+
| `b` | `[-0.4, 0.4]` | Blue–yellow component |
260260

261-
Serialized as `color(--oklab l a b)`, with the `none` keyword for any missing color channel. An explicit `alpha < 1` is included as ` / alpha`.
261+
Serialized as `oklab(l a b)`, with the `none` keyword for any missing color channel. An explicit `alpha < 1` is included as ` / alpha`.
262262

263263
#### `oklch`
264264

265265
The Oklab color space in cylindrical form.
266266

267267
| Channel | Range | Description |
268268
| ------- | ------------- | ----------- |
269-
| `l` | `[0, 0.999]` | Lightness |
270-
| `c` | `[0, 0.322]` | Chroma |
269+
| `l` | `[0, 1]` | Lightness |
270+
| `c` | `[0, 0.4]` | Chroma |
271271
| `h` | `[0, 360)` | Hue |
272272

273-
Serialized as `color(--oklch l c h)`, with the `none` keyword for any missing color channel. An explicit `alpha < 1` is included as ` / alpha`.
273+
Serialized as `oklch(l c h)`, with the `none` keyword for any missing color channel. An explicit `alpha < 1` is included as ` / alpha`.
274274

275275
### `okhsl`
276276

@@ -378,13 +378,15 @@ The XYB color model is part of [the JPEG XL Image Coding System](https://ds.jpeg
378378

379379
#### `xyb`
380380

381-
The default XYB color space, defined in relationship to sRGB, with the default Chroma from Luma adjustment applied.
381+
The default XYB color space, defined in relationship to sRGB.
382+
383+
It has the default _Chroma from Luma_ adjustment applied (effectively Y is subtracted from B) so that colors with `{ x: 0, b: 0 }` coordinates are achromatic.
382384

383385
| Channel | Range | Description |
384386
| ------- | ------------- | ----------- |
385-
| `x` | `[-0.0154, 0.0281]`| ? |
386-
| `y` | `[0, 0.8453]`| ? |
387-
| `b` | `[ -0.2778, 0.3880 ]`| ? |
387+
| `x` | `[-0.0154, 0.0281]`| Cyan-red component |
388+
| `y` | `[0, 0.8453]`| Luma |
389+
| `b` | `[ -0.2778, 0.3880 ]`| Blue-yellow component |
388390

389391
### Cubehelix
390392

src/hsl/definition.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import convertHslToRgb from './convertHslToRgb.js';
22
import convertRgbToHsl from './convertRgbToHsl.js';
3+
import parseHslLegacy from './parseHslLegacy.js';
34
import parseHsl from './parseHsl.js';
45
import { fixupHueShorter } from '../fixup/hue.js';
56
import { fixupAlpha } from '../fixup/alpha.js';
@@ -24,7 +25,7 @@ const definition = {
2425
h: [0, 360]
2526
},
2627

27-
parse: [parseHsl],
28+
parse: [parseHsl, parseHslLegacy],
2829
serialize: c =>
2930
`hsl(${c.h || 0} ${c.s !== undefined ? c.s * 100 + '%' : 'none'} ${
3031
c.l !== undefined ? c.l * 100 + '%' : 'none'

src/hsl/parseHsl.js

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,38 @@
1-
import hueToDeg from '../util/hue.js';
2-
import {
3-
hue,
4-
per,
5-
num_per,
6-
hue_none,
7-
per_none,
8-
num_per_none,
9-
c,
10-
s
11-
} from '../util/regex.js';
1+
import { Tok } from '../parse.js';
122

13-
/*
14-
hsl() regular expressions.
15-
Reference: https://drafts.csswg.org/css-color/#the-hsl-notation
16-
*/
17-
const hsl_old = new RegExp(
18-
`^hsla?\\(\\s*${hue}${c}${per}${c}${per}\\s*(?:,\\s*${num_per}\\s*)?\\)$`
19-
);
20-
const hsl_new = new RegExp(
21-
`^hsla?\\(\\s*${hue_none}${s}${per_none}${s}${per_none}\\s*(?:\\/\\s*${num_per_none}\\s*)?\\)$`
22-
);
23-
24-
const parseHsl = color => {
25-
let match = color.match(hsl_old) || color.match(hsl_new);
26-
if (!match) return;
27-
let res = { mode: 'hsl' };
3+
function parseHsl(color, parsed) {
4+
if (!parsed || (parsed[0] !== 'hsl' && parsed[0] !== 'hsla')) {
5+
return undefined;
6+
}
7+
const res = { mode: 'hsl' };
8+
const [, h, s, l, alpha] = parsed;
289

29-
if (match[3] !== undefined) {
30-
res.h = +match[3];
31-
} else if (match[1] !== undefined && match[2] !== undefined) {
32-
res.h = hueToDeg(match[1], match[2]);
10+
if (h.type !== Tok.None) {
11+
if (h.type === Tok.Percentage) {
12+
return undefined;
13+
}
14+
res.h = h.value;
3315
}
3416

35-
if (match[4] !== undefined) {
36-
res.s = Math.min(Math.max(0, match[4] / 100), 1);
17+
if (s.type !== Tok.None) {
18+
if (s.type === Tok.Hue) {
19+
return undefined;
20+
}
21+
res.s = s.type === Tok.Number ? s.value : s.value / 100;
3722
}
3823

39-
if (match[5] !== undefined) {
40-
res.l = Math.min(Math.max(0, match[5] / 100), 1);
24+
if (l.type !== Tok.None) {
25+
if (l.type === Tok.Hue) {
26+
return undefined;
27+
}
28+
res.l = l.type === Tok.Number ? l.value : l.value / 100;
4129
}
4230

43-
if (match[6] !== undefined) {
44-
res.alpha = match[6] / 100;
45-
} else if (match[7] !== undefined) {
46-
res.alpha = +match[7];
31+
if (alpha.type !== Tok.None) {
32+
res.alpha = alpha.type === Tok.Number ? alpha.value : alpha.value / 100;
4733
}
34+
4835
return res;
49-
};
36+
}
5037

5138
export default parseHsl;

src/hsl/parseHslLegacy.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import hueToDeg from '../util/hue.js';
2+
import { hue, per, num_per, c } from '../util/regex.js';
3+
4+
/*
5+
hsl() regular expressions for legacy format
6+
Reference: https://drafts.csswg.org/css-color/#the-hsl-notation
7+
*/
8+
const hsl_old = new RegExp(
9+
`^hsla?\\(\\s*${hue}${c}${per}${c}${per}\\s*(?:,\\s*${num_per}\\s*)?\\)$`
10+
);
11+
12+
const parseHslLegacy = color => {
13+
let match = color.match(hsl_old);
14+
if (!match) return;
15+
let res = { mode: 'hsl' };
16+
17+
if (match[3] !== undefined) {
18+
res.h = +match[3];
19+
} else if (match[1] !== undefined && match[2] !== undefined) {
20+
res.h = hueToDeg(match[1], match[2]);
21+
}
22+
23+
if (match[4] !== undefined) {
24+
res.s = Math.min(Math.max(0, match[4] / 100), 1);
25+
}
26+
27+
if (match[5] !== undefined) {
28+
res.l = Math.min(Math.max(0, match[5] / 100), 1);
29+
}
30+
31+
if (match[6] !== undefined) {
32+
res.alpha = match[6] / 100;
33+
} else if (match[7] !== undefined) {
34+
res.alpha = +match[7];
35+
}
36+
return res;
37+
};
38+
39+
export default parseHslLegacy;

0 commit comments

Comments
 (0)