Skip to content

Commit a9c69a3

Browse files
authored
Color function updates - #RRGGBBAA and CSS Variables (#3291)
* Supports #RRGGBBAA color form * Allow conversion of #RRGGBBAA to rgba() * Preserves unsupported color values - Fixes #2986 * Maintains color-space when possible
1 parent 59e919b commit a9c69a3

11 files changed

+183
-92
lines changed

lib/less/functions/color.js

+84-41
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,17 @@ var Dimension = require('../tree/dimension'),
88
function clamp(val) {
99
return Math.min(1, Math.max(0, val));
1010
}
11-
function hsla(color) {
12-
return colorFunctions.hsla(color.h, color.s, color.l, color.a);
11+
function hsla(origColor, hsl) {
12+
var color = colorFunctions.hsla(hsl.h, hsl.s, hsl.l, hsl.a);
13+
if (color) {
14+
if (origColor.value &&
15+
/^(rgb|hsl)/.test(origColor.value)) {
16+
color.value = origColor.value;
17+
} else {
18+
color.value = 'rgb';
19+
}
20+
return color;
21+
}
1322
}
1423
function number(n) {
1524
if (n instanceof Dimension) {
@@ -32,46 +41,79 @@ function scaled(n, size) {
3241
}
3342
colorFunctions = {
3443
rgb: function (r, g, b) {
35-
return colorFunctions.rgba(r, g, b, 1.0);
44+
var color = colorFunctions.rgba(r, g, b, 1.0);
45+
if (color) {
46+
color.value = 'rgb';
47+
return color;
48+
}
3649
},
3750
rgba: function (r, g, b, a) {
38-
var rgb = [r, g, b].map(function (c) { return scaled(c, 255); });
39-
a = number(a);
40-
return new Color(rgb, a);
51+
try {
52+
if (r instanceof Color) {
53+
if (g) {
54+
a = number(g);
55+
} else {
56+
a = r.alpha;
57+
}
58+
return new Color(r.rgb, a, 'rgba');
59+
}
60+
var rgb = [r, g, b].map(function (c) { return scaled(c, 255); });
61+
a = number(a);
62+
return new Color(rgb, a, 'rgba');
63+
}
64+
catch (e) {}
4165
},
4266
hsl: function (h, s, l) {
43-
return colorFunctions.hsla(h, s, l, 1.0);
67+
var color = colorFunctions.hsla(h, s, l, 1.0);
68+
if (color) {
69+
color.value = 'hsl';
70+
return color;
71+
}
4472
},
4573
hsla: function (h, s, l, a) {
74+
try {
75+
if (h instanceof Color) {
76+
if (s) {
77+
a = number(s);
78+
} else {
79+
a = h.alpha;
80+
}
81+
return new Color(h.rgb, a, 'hsla');
82+
}
4683

47-
var m1, m2;
84+
var m1, m2;
4885

49-
function hue(h) {
50-
h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h);
51-
if (h * 6 < 1) {
52-
return m1 + (m2 - m1) * h * 6;
53-
}
54-
else if (h * 2 < 1) {
55-
return m2;
56-
}
57-
else if (h * 3 < 2) {
58-
return m1 + (m2 - m1) * (2 / 3 - h) * 6;
59-
}
60-
else {
61-
return m1;
86+
function hue(h) {
87+
h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h);
88+
if (h * 6 < 1) {
89+
return m1 + (m2 - m1) * h * 6;
90+
}
91+
else if (h * 2 < 1) {
92+
return m2;
93+
}
94+
else if (h * 3 < 2) {
95+
return m1 + (m2 - m1) * (2 / 3 - h) * 6;
96+
}
97+
else {
98+
return m1;
99+
}
62100
}
63-
}
64101

65-
h = (number(h) % 360) / 360;
66-
s = clamp(number(s)); l = clamp(number(l)); a = clamp(number(a));
102+
h = (number(h) % 360) / 360;
103+
s = clamp(number(s)); l = clamp(number(l)); a = clamp(number(a));
67104

68-
m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
69-
m1 = l * 2 - m2;
105+
m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
106+
m1 = l * 2 - m2;
70107

71-
return colorFunctions.rgba(hue(h + 1 / 3) * 255,
72-
hue(h) * 255,
73-
hue(h - 1 / 3) * 255,
74-
a);
108+
var rgb = [
109+
hue(h + 1 / 3) * 255,
110+
hue(h) * 255,
111+
hue(h - 1 / 3) * 255
112+
];
113+
a = number(a);
114+
return new Color(rgb, a, 'hsla');
115+
}
116+
catch (e) {}
75117
},
76118

77119
hsv: function(h, s, v) {
@@ -159,7 +201,7 @@ colorFunctions = {
159201
hsl.s += amount.value / 100;
160202
}
161203
hsl.s = clamp(hsl.s);
162-
return hsla(hsl);
204+
return hsla(color, hsl);
163205
},
164206
desaturate: function (color, amount, method) {
165207
var hsl = color.toHSL();
@@ -171,7 +213,7 @@ colorFunctions = {
171213
hsl.s -= amount.value / 100;
172214
}
173215
hsl.s = clamp(hsl.s);
174-
return hsla(hsl);
216+
return hsla(color, hsl);
175217
},
176218
lighten: function (color, amount, method) {
177219
var hsl = color.toHSL();
@@ -183,7 +225,7 @@ colorFunctions = {
183225
hsl.l += amount.value / 100;
184226
}
185227
hsl.l = clamp(hsl.l);
186-
return hsla(hsl);
228+
return hsla(color, hsl);
187229
},
188230
darken: function (color, amount, method) {
189231
var hsl = color.toHSL();
@@ -195,7 +237,7 @@ colorFunctions = {
195237
hsl.l -= amount.value / 100;
196238
}
197239
hsl.l = clamp(hsl.l);
198-
return hsla(hsl);
240+
return hsla(color, hsl);
199241
},
200242
fadein: function (color, amount, method) {
201243
var hsl = color.toHSL();
@@ -207,7 +249,7 @@ colorFunctions = {
207249
hsl.a += amount.value / 100;
208250
}
209251
hsl.a = clamp(hsl.a);
210-
return hsla(hsl);
252+
return hsla(color, hsl);
211253
},
212254
fadeout: function (color, amount, method) {
213255
var hsl = color.toHSL();
@@ -219,22 +261,22 @@ colorFunctions = {
219261
hsl.a -= amount.value / 100;
220262
}
221263
hsl.a = clamp(hsl.a);
222-
return hsla(hsl);
264+
return hsla(color, hsl);
223265
},
224266
fade: function (color, amount) {
225267
var hsl = color.toHSL();
226268

227269
hsl.a = amount.value / 100;
228270
hsl.a = clamp(hsl.a);
229-
return hsla(hsl);
271+
return hsla(color, hsl);
230272
},
231273
spin: function (color, amount) {
232274
var hsl = color.toHSL();
233275
var hue = (hsl.h + amount.value) % 360;
234276

235277
hsl.h = hue < 0 ? 360 + hue : hue;
236278

237-
return hsla(hsl);
279+
return hsla(color, hsl);
238280
},
239281
//
240282
// Copyright (c) 2006-2009 Hampton Catlin, Natalie Weizenbaum, and Chris Eppstein
@@ -338,16 +380,17 @@ colorFunctions = {
338380
},
339381
color: function(c) {
340382
if ((c instanceof Quoted) &&
341-
(/^#([a-f0-9]{6}|[a-f0-9]{3})$/i.test(c.value))) {
342-
return new Color(c.value.slice(1));
383+
(/^#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3,4})$/i.test(c.value))) {
384+
var val = c.value.slice(1);
385+
return new Color(val, undefined, '#' + val);
343386
}
344387
if ((c instanceof Color) || (c = Color.fromKeyword(c.value))) {
345388
c.value = undefined;
346389
return c;
347390
}
348391
throw {
349392
type: 'Argument',
350-
message: 'argument must be a color keyword or 3/6 digit hex e.g. #FFF'
393+
message: 'argument must be a color keyword or 3|4|6|8 digit hex e.g. #FFF'
351394
};
352395
},
353396
tint: function(color, amount) {

lib/less/parser/parser.js

+2-9
Original file line numberDiff line numberDiff line change
@@ -636,15 +636,8 @@ var Parser = function Parser(context, imports, fileInfo) {
636636
color: function () {
637637
var rgb;
638638

639-
if (parserInput.currentChar() === '#' && (rgb = parserInput.$re(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))) {
640-
// strip colons, brackets, whitespaces and other characters that should not
641-
// definitely be part of color string
642-
var colorCandidateString = rgb.input.match(/^#([\w]+).*/);
643-
colorCandidateString = colorCandidateString[1];
644-
if (!colorCandidateString.match(/^[A-Fa-f0-9]+$/)) { // verify if candidate consists only of allowed HEX characters
645-
error('Invalid HEX color code');
646-
}
647-
return new(tree.Color)(rgb[1], undefined, '#' + colorCandidateString);
639+
if (parserInput.currentChar() === '#' && (rgb = parserInput.$re(/^#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3,4})/))) {
640+
return new(tree.Color)(rgb[1], undefined, rgb[0]);
648641
}
649642
},
650643

lib/less/tree/color.js

+58-18
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var Node = require('./node'),
55
// RGB Colors - #ff0014, #eee
66
//
77
var Color = function (rgb, a, originalForm) {
8+
var self = this;
89
//
910
// The end goal here, is to parse the arguments
1011
// into an integer triplet, such as `128, 255, 0`
@@ -13,16 +14,26 @@ var Color = function (rgb, a, originalForm) {
1314
//
1415
if (Array.isArray(rgb)) {
1516
this.rgb = rgb;
16-
} else if (rgb.length == 6) {
17-
this.rgb = rgb.match(/.{2}/g).map(function (c) {
18-
return parseInt(c, 16);
17+
} else if (rgb.length >= 6) {
18+
this.rgb = [];
19+
rgb.match(/.{2}/g).map(function (c, i) {
20+
if (i < 3) {
21+
self.rgb.push(parseInt(c, 16));
22+
} else {
23+
self.alpha = (parseInt(c, 16)) / 255;
24+
}
1925
});
2026
} else {
21-
this.rgb = rgb.split('').map(function (c) {
22-
return parseInt(c + c, 16);
27+
this.rgb = [];
28+
rgb.split('').map(function (c, i) {
29+
if (i < 3) {
30+
self.rgb.push(parseInt(c + c, 16));
31+
} else {
32+
self.alpha = (parseInt(c + c, 16)) / 255;
33+
}
2334
});
2435
}
25-
this.alpha = typeof a === 'number' ? a : 1;
36+
this.alpha = this.alpha || (typeof a === 'number' ? a : 1);
2637
if (typeof originalForm !== 'undefined') {
2738
this.value = originalForm;
2839
}
@@ -57,25 +68,54 @@ Color.prototype.genCSS = function (context, output) {
5768
output.add(this.toCSS(context));
5869
};
5970
Color.prototype.toCSS = function (context, doNotCompress) {
60-
var compress = context && context.compress && !doNotCompress, color, alpha;
71+
var compress = context && context.compress && !doNotCompress, color, alpha,
72+
colorFunction, args = [];
6173

6274
// `value` is set if this color was originally
6375
// converted from a named color string so we need
6476
// to respect this and try to output named color too.
77+
alpha = this.fround(context, this.alpha);
78+
6579
if (this.value) {
66-
return this.value;
80+
if (this.value.indexOf('rgb') === 0) {
81+
if (alpha < 1) {
82+
colorFunction = 'rgba';
83+
}
84+
} else if (this.value.indexOf('hsl') === 0) {
85+
if (alpha === 1) {
86+
colorFunction = 'hsl';
87+
} else {
88+
colorFunction = 'hsla';
89+
}
90+
} else {
91+
return this.value;
92+
}
93+
} else {
94+
if (alpha < 1) {
95+
colorFunction = 'rgba';
96+
}
6797
}
6898

69-
// If we have some transparency, the only way to represent it
70-
// is via `rgba`. Otherwise, we use the hex representation,
71-
// which has better compatibility with older browsers.
72-
// Values are capped between `0` and `255`, rounded and zero-padded.
73-
alpha = this.fround(context, this.alpha);
74-
if (alpha < 1) {
75-
return 'rgba(' + this.rgb.map(function (c) {
76-
return clamp(Math.round(c), 255);
77-
}).concat(clamp(alpha, 1))
78-
.join(',' + (compress ? '' : ' ')) + ')';
99+
switch (colorFunction) {
100+
case 'rgba':
101+
args = this.rgb.map(function (c) {
102+
return clamp(Math.round(c), 255);
103+
}).concat(clamp(alpha, 1));
104+
break;
105+
case 'hsla':
106+
args.push(clamp(alpha, 1));
107+
case 'hsl':
108+
color = this.toHSL();
109+
args = [
110+
this.fround(context, color.h),
111+
this.fround(context, color.s * 100) + '%',
112+
this.fround(context, color.l * 100) + '%'
113+
].concat(args);
114+
}
115+
116+
if (colorFunction) {
117+
// Values are capped between `0` and `255`, rounded and zero-padded.
118+
return colorFunction + '(' + args.join(',' + (compress ? '' : ' ')) + ')';
79119
}
80120

81121
color = this.toRGB();

test/css/colors.css

+18-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
color: #1a0000ff;
2424
}
2525
#alpha #hsla {
26-
color: rgba(61, 45, 41, 0.6);
26+
color: hsla(11, 20%, 20%, 0.6);
2727
}
2828
#overflow .a {
2929
color: #000000;
@@ -47,10 +47,10 @@
4747
color: #333333;
4848
}
4949
#808080 {
50-
color: #808080;
50+
color: hsl(0, 0%, 50%);
5151
}
5252
#00ff00 {
53-
color: #00ff00;
53+
color: hsl(120, 100%, 50%);
5454
}
5555
.lightenblue {
5656
color: #3333ff;
@@ -85,3 +85,18 @@
8585
color: 255;
8686
border-color: rgba(255, 0, 0, 0.5);
8787
}
88+
#rrggbbaa {
89+
test-1: #55FF5599;
90+
test-2: #5F59;
91+
test-3: rgba(136, 255, 136, 0.6);
92+
test-4: rgba(85, 255, 85, 0.1);
93+
test-5: rgba(85, 255, 85, 0.6);
94+
test-6: rgba(85, 255, 85, 0.6);
95+
test-7: rgba(85, 255, 85, 0.5);
96+
test-8: rgba(var(--color-accent), 0.2);
97+
test-9: rgb(var(--color-accent));
98+
test-9: hsla(var(--color-accent));
99+
test-10: #55FF5599;
100+
test-11: hsla(120, 100%, 66.66666667%, 0.6);
101+
test-12: hsla(120, 100%, 66.66666667%, 0.5);
102+
}

0 commit comments

Comments
 (0)