Skip to content

Commit 65621c3

Browse files
authored
fix(color-contrast): support CSS 4 color spaces (#4020)
* fix(color-contrast): support css 4 color spaces * 🤖 Automated formatting fixes * test title --------- Co-authored-by: straker <straker@users.noreply.github.com>
1 parent 949f4f8 commit 65621c3

5 files changed

Lines changed: 300 additions & 261 deletions

File tree

lib/commons/color/color.js

Lines changed: 41 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import standards from '../../standards';
1+
import { Colorjs } from '../../core/imports';
22

33
const hexRegex = /^#[0-9a-f]{3,8}$/i;
4-
const colorFnRegex = /^((?:rgb|hsl)a?)\s*\(([^\)]*)\)/i;
4+
const hslRegex = /hsl\(\s*([\d.]+)(rad|turn)/;
55

66
/**
77
* @class Color
@@ -57,26 +57,35 @@ export default class Color {
5757
* @instance
5858
*/
5959
parseString(colorString) {
60-
// IE occasionally returns named colors instead of RGB(A) values
61-
if (standards.cssColors[colorString] || colorString === 'transparent') {
62-
const [red, green, blue] = standards.cssColors[colorString] || [0, 0, 0];
63-
this.red = red;
64-
this.green = green;
65-
this.blue = blue;
66-
this.alpha = colorString === 'transparent' ? 0 : 1;
67-
return this;
68-
}
60+
// Colorjs currently does not support rad or turn angle values
61+
// @see https://github.com/LeaVerou/color.js/issues/311
62+
colorString = colorString.replace(hslRegex, (match, angle, unit) => {
63+
const value = angle + unit;
64+
65+
switch (unit) {
66+
case 'rad':
67+
return match.replace(value, radToDeg(angle));
68+
case 'turn':
69+
return match.replace(value, turnToDeg(angle));
70+
}
71+
});
6972

70-
if (colorString.match(colorFnRegex)) {
71-
this.parseColorFnString(colorString);
72-
return this;
73+
try {
74+
// srgb values are between 0 and 1
75+
const color = new Colorjs(colorString).to('srgb');
76+
// when converting from one color space to srgb
77+
// the values of rgb may be above 1 so we need to clamp them
78+
// we also need to round the final value as rgb values don't have decimals
79+
this.red = Math.round(clamp(color.r, 0, 1) * 255);
80+
this.green = Math.round(clamp(color.g, 0, 1) * 255);
81+
this.blue = Math.round(clamp(color.b, 0, 1) * 255);
82+
// color.alpha is a Number object so convert it to a number
83+
this.alpha = +color.alpha;
84+
} catch (err) {
85+
throw new Error(`Unable to parse color "${colorString}"`);
7386
}
7487

75-
if (colorString.match(hexRegex)) {
76-
this.parseHexString(colorString);
77-
return this;
78-
}
79-
throw new Error(`Unable to parse color "${colorString}"`);
88+
return this;
8089
}
8190

8291
/**
@@ -88,15 +97,7 @@ export default class Color {
8897
* @param {string} rgb The string value
8998
*/
9099
parseRgbString(colorString) {
91-
// IE can pass transparent as value instead of rgba
92-
if (colorString === 'transparent') {
93-
this.red = 0;
94-
this.green = 0;
95-
this.blue = 0;
96-
this.alpha = 0;
97-
return;
98-
}
99-
this.parseColorFnString(colorString);
100+
this.parseString(colorString);
100101
}
101102

102103
/**
@@ -111,24 +112,8 @@ export default class Color {
111112
if (!colorString.match(hexRegex) || [6, 8].includes(colorString.length)) {
112113
return;
113114
}
114-
colorString = colorString.replace('#', '');
115-
if (colorString.length < 6) {
116-
const [r, g, b, a] = colorString;
117-
colorString = r + r + g + g + b + b;
118-
if (a) {
119-
colorString += a + a;
120-
}
121-
}
122115

123-
var aRgbHex = colorString.match(/.{1,2}/g);
124-
this.red = parseInt(aRgbHex[0], 16);
125-
this.green = parseInt(aRgbHex[1], 16);
126-
this.blue = parseInt(aRgbHex[2], 16);
127-
if (aRgbHex[3]) {
128-
this.alpha = parseInt(aRgbHex[3], 16) / 255;
129-
} else {
130-
this.alpha = 1;
131-
}
116+
this.parseString(colorString);
132117
}
133118

134119
/**
@@ -140,30 +125,7 @@ export default class Color {
140125
* @param {string} rgb The string value
141126
*/
142127
parseColorFnString(colorString) {
143-
const [, colorFunc, colorValStr] = colorString.match(colorFnRegex) || [];
144-
if (!colorFunc || !colorValStr) {
145-
return;
146-
}
147-
148-
// Get array of color number strings from the string:
149-
const colorVals = colorValStr
150-
.split(/\s*[,\/\s]\s*/)
151-
.map(str => str.replace(',', '').trim())
152-
.filter(str => str !== '');
153-
154-
// Convert to numbers
155-
let colorNums = colorVals.map((val, index) => {
156-
return convertColorVal(colorFunc, val, index);
157-
});
158-
159-
if (colorFunc.substr(0, 3) === 'hsl') {
160-
colorNums = hslToRgb(colorNums);
161-
}
162-
163-
this.red = colorNums[0];
164-
this.green = colorNums[1];
165-
this.blue = colorNums[2];
166-
this.alpha = typeof colorNums[3] === 'number' ? colorNums[3] : 1;
128+
this.parseString(colorString);
167129
}
168130

169131
/**
@@ -190,66 +152,17 @@ export default class Color {
190152
}
191153
}
192154

193-
/**
194-
* Convert a CSS color value into a number
195-
*/
196-
function convertColorVal(colorFunc, value, index) {
197-
if (/%$/.test(value)) {
198-
//<percentage>
199-
if (index === 3) {
200-
// alpha
201-
return parseFloat(value) / 100;
202-
}
203-
return (parseFloat(value) * 255) / 100;
204-
}
205-
if (colorFunc[index] === 'h') {
206-
// hue
207-
if (/turn$/.test(value)) {
208-
return parseFloat(value) * 360;
209-
}
210-
if (/rad$/.test(value)) {
211-
return parseFloat(value) * 57.3;
212-
}
213-
}
214-
return parseFloat(value);
155+
// clamp a value between two numbers (inclusive)
156+
function clamp(value, min, max) {
157+
return Math.min(Math.max(min, value), max);
215158
}
216159

217-
/**
218-
* Convert HSL to RGB
219-
*/
220-
function hslToRgb([hue, saturation, lightness, alpha]) {
221-
// Must be fractions of 1
222-
saturation /= 255;
223-
lightness /= 255;
224-
225-
const high = (1 - Math.abs(2 * lightness - 1)) * saturation;
226-
const low = high * (1 - Math.abs(((hue / 60) % 2) - 1));
227-
const base = lightness - high / 2;
228-
229-
let colors;
230-
if (hue < 60) {
231-
// red - yellow
232-
colors = [high, low, 0];
233-
} else if (hue < 120) {
234-
// yellow - green
235-
colors = [low, high, 0];
236-
} else if (hue < 180) {
237-
// green - cyan
238-
colors = [0, high, low];
239-
} else if (hue < 240) {
240-
// cyan - blue
241-
colors = [0, low, high];
242-
} else if (hue < 300) {
243-
// blue - purple
244-
colors = [low, 0, high];
245-
} else {
246-
// purple - red
247-
colors = [high, 0, low];
248-
}
160+
// convert radians to degrees
161+
function radToDeg(rad) {
162+
return (rad * 180) / Math.PI;
163+
}
249164

250-
return colors
251-
.map(color => {
252-
return Math.round((color + base) * 255);
253-
})
254-
.concat(alpha);
165+
// convert turn to degrees
166+
function turnToDeg(turn) {
167+
return turn * 360;
255168
}

lib/core/imports/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CssSelectorParser } from 'css-selector-parser';
22
import doT from '@deque/dot';
33
import emojiRegexText from 'emoji-regex';
44
import memoize from 'memoizee';
5+
import Color from 'colorjs.io';
56

67
import es6promise from 'es6-promise';
78
import { Uint32Array } from 'typedarray';
@@ -40,4 +41,4 @@ if (window.Uint32Array) {
4041
* @namespace imports
4142
* @memberof axe
4243
*/
43-
export { CssSelectorParser, doT, emojiRegexText, memoize };
44+
export { CssSelectorParser, doT, emojiRegexText, memoize, Color as Colorjs };

package-lock.json

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
"chalk": "^4.x",
123123
"chromedriver": "latest",
124124
"clone": "^2.1.2",
125+
"colorjs.io": "^0.4.3",
125126
"conventional-commits-parser": "^3.2.4",
126127
"core-js": "^3.27.1",
127128
"css-selector-parser": "^1.4.1",

0 commit comments

Comments
 (0)