Skip to content

Commit a7f5d23

Browse files
anny21jenkins-bot
authored andcommitted
Add support for CSS4 #RRGGBBAA colors
This fixes hex colors compiling to invalid syntax when using 4-char or 8-char notations with an alpha channel. A major change in this patch was matching Less.js 3.13 output of hsl and hsla functions in CSS output, instead of converting to hex or rgb format. All browsers we support, including Grade C browsers, support this as they have been part of browsers for a long time. See https://caniuse.com/?search=hsla Bug: T403056 Change-Id: I3022d677f2f99ccb2f78e105928ae178acc41f68
1 parent c544fa9 commit a7f5d23

11 files changed

Lines changed: 132 additions & 57 deletions

File tree

lib/Less/Functions.php

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ private static function _clamp( $val, $max = 1 ) {
2020
return min( max( $val, 0 ), $max );
2121
}
2222

23+
private function _hsla( $origColor, $hsl ) {
24+
$color = $this->hsla( $hsl["h"], $hsl["s"], $hsl["l"], $hsl["a"] );
25+
if ( $color ) {
26+
if ( $origColor->value && preg_match( '/^(rgb|hsl)/', $origColor->value ) ) {
27+
$color->value = $origColor->value;
28+
} else {
29+
$color->value = 'rgb';
30+
}
31+
return $color;
32+
}
33+
}
34+
2335
private static function _number( $n ) {
2436
if ( $n instanceof Less_Tree_Dimension ) {
2537
return floatval( $n->unit->is( '%' ) ? $n->value / 100 : $n->value );
@@ -42,7 +54,11 @@ public function rgb( $r = null, $g = null, $b = null ) {
4254
if ( $r === null || $g === null || $b === null ) {
4355
throw new Less_Exception_Compiler( "rgb expects three parameters" );
4456
}
45-
return $this->rgba( $r, $g, $b, 1.0 );
57+
$color = $this->rgba( $r, $g, $b, 1.0 );
58+
if ( $color ) {
59+
$color->value = 'rgb';
60+
return $color;
61+
}
4662
}
4763

4864
public function rgba( $r = null, $g = null, $b = null, $a = null ) {
@@ -53,11 +69,15 @@ public function rgba( $r = null, $g = null, $b = null, $a = null ) {
5369
];
5470

5571
$a = self::_number( $a );
56-
return new Less_Tree_Color( $rgb, $a );
72+
return new Less_Tree_Color( $rgb, $a, 'rgba' );
5773
}
5874

5975
public function hsl( $h, $s, $l ) {
60-
return $this->hsla( $h, $s, $l, 1.0 );
76+
$color = $this->hsla( $h, $s, $l, 1.0 );
77+
if ( $color ) {
78+
$color->value = "hsl";
79+
return $color;
80+
}
6181
}
6282

6383
public function hsla( $h, $s, $l, $a ) {
@@ -70,12 +90,13 @@ public function hsla( $h, $s, $l, $a ) {
7090

7191
$m1 = $l * 2 - $m2;
7292

73-
return $this->rgba(
93+
$rgb = [
7494
self::hsla_hue( $h + 1 / 3, $m1, $m2 ) * 255,
7595
self::hsla_hue( $h, $m1, $m2 ) * 255,
7696
self::hsla_hue( $h - 1 / 3, $m1, $m2 ) * 255,
77-
$a
78-
);
97+
];
98+
$a = self::_number( $a );
99+
return new Less_Tree_Color( $rgb, $a, 'hsla' );
79100
}
80101

81102
/**
@@ -297,7 +318,7 @@ public function saturate( $color = null, $amount = null, $method = null ) {
297318
$hsl['s'] += $amount->value / 100;
298319
} $hsl['s'] = self::_clamp( $hsl['s'] );
299320

300-
return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
321+
return $this->_hsla( $color, $hsl );
301322
}
302323

303324
/**
@@ -327,7 +348,7 @@ public function desaturate( $color = null, $amount = null, $method = null ) {
327348

328349
$hsl['s'] = self::_clamp( $hsl['s'] );
329350

330-
return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
351+
return $this->_hsla( $color, $hsl );
331352
}
332353

333354
public function lighten( $color = null, $amount = null, $method = null ) {
@@ -351,8 +372,7 @@ public function lighten( $color = null, $amount = null, $method = null ) {
351372
}
352373

353374
$hsl['l'] = self::_clamp( $hsl['l'] );
354-
355-
return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
375+
return $this->_hsla( $color, $hsl );
356376
}
357377

358378
public function darken( $color = null, $amount = null, $method = null ) {
@@ -374,8 +394,7 @@ public function darken( $color = null, $amount = null, $method = null ) {
374394
$hsl['l'] -= $amount->value / 100;
375395
}
376396
$hsl['l'] = self::_clamp( $hsl['l'] );
377-
378-
return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
397+
return $this->_hsla( $color, $hsl );
379398
}
380399

381400
public function fadein( $color = null, $amount = null, $method = null ) {
@@ -399,7 +418,7 @@ public function fadein( $color = null, $amount = null, $method = null ) {
399418
}
400419

401420
$hsl['a'] = self::_clamp( $hsl['a'] );
402-
return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
421+
return $this->_hsla( $color, $hsl );
403422
}
404423

405424
public function fadeout( $color = null, $amount = null, $method = null ) {
@@ -423,7 +442,7 @@ public function fadeout( $color = null, $amount = null, $method = null ) {
423442
}
424443

425444
$hsl['a'] = self::_clamp( $hsl['a'] );
426-
return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
445+
return $this->_hsla( $color, $hsl );
427446
}
428447

429448
public function fade( $color = null, $amount = null ) {
@@ -442,7 +461,7 @@ public function fade( $color = null, $amount = null ) {
442461

443462
$hsl['a'] = $amount->value / 100;
444463
$hsl['a'] = self::_clamp( $hsl['a'] );
445-
return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
464+
return $this->_hsla( $color, $hsl );
446465
}
447466

448467
public function spin( $color = null, $amount = null ) {
@@ -462,7 +481,7 @@ public function spin( $color = null, $amount = null ) {
462481

463482
$hsl['h'] = $hue < 0 ? 360 + $hue : $hue;
464483

465-
return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
484+
return $this->_hsla( $color, $hsl );
466485
}
467486

468487
//
@@ -863,15 +882,16 @@ public function percentage( $n ) {
863882
}
864883

865884
/**
866-
* @see less-2.5.3.js#colorFunctions.color
885+
* @see less-3.13.1.js#colorFunctions.color
867886
* @param Less_Tree_Quoted|Less_Tree_Color|Less_Tree_Keyword $c
868887
* @return Less_Tree_Color
869888
*/
870889
public function color( $c ) {
871890
if ( ( $c instanceof Less_Tree_Quoted ) &&
872-
preg_match( '/^#([a-f0-9]{6}|[a-f0-9]{3})/', $c->value )
891+
preg_match( '/^#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3,4})$/', $c->value )
873892
) {
874-
return new Less_Tree_Color( substr( $c->value, 1 ) );
893+
$value = substr( $c->value, 1 );
894+
return new Less_Tree_Color( $value, null, '#' . $value );
875895
}
876896

877897
// phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition
@@ -880,7 +900,7 @@ public function color( $c ) {
880900
return $c;
881901
}
882902

883-
throw new Less_Exception_Compiler( "argument must be a color keyword or 3/6 digit hex e.g. #FFF" );
903+
throw new Less_Exception_Compiler( "argument must be a color keyword or 3|4|6|8 digit hex e.g. #FFF" );
884904
}
885905

886906
public function isruleset( $n ) {

lib/Less/Parser.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,12 +1556,15 @@ private function parseEntitiesPropertyCurly() {
15561556
* @return Less_Tree_Color|null
15571557
*/
15581558
private function parseEntitiesColor() {
1559+
$this->save();
15591560
if ( $this->peekChar( '#' ) ) {
1560-
$rgb = $this->matchReg( '/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/' );
1561-
if ( $rgb ) {
1562-
return new Less_Tree_Color( $rgb[1], 1, $rgb[0] );
1561+
$rgb = $this->matchReg( '/\\G#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3,4})([\w.#\[])?/' );
1562+
if ( $rgb && !isset( $rgb[2] ) ) {
1563+
$this->forget();
1564+
return new Less_Tree_Color( $rgb[1], null, $rgb[0] );
15631565
}
15641566
}
1567+
$this->restore();
15651568
}
15661569

15671570
/**

lib/Less/Tree/Color.php

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,26 @@ class Less_Tree_Color extends Less_Tree {
1414
public function __construct( $rgb, $a = null, ?string $originalForm = null ) {
1515
if ( is_array( $rgb ) ) {
1616
$this->rgb = $rgb;
17-
} elseif ( strlen( $rgb ) == 6 ) {
18-
// TODO: Less.js 3.13 supports 8-digit rgba as #RRGGBBAA
17+
} elseif ( strlen( $rgb ) >= 6 ) {
1918
$this->rgb = [];
20-
foreach ( str_split( $rgb, 2 ) as $c ) {
21-
$this->rgb[] = hexdec( $c );
19+
foreach ( str_split( $rgb, 2 ) as $i => $c ) {
20+
if ( $i < 3 ) {
21+
$this->rgb[] = hexdec( $c );
22+
} else {
23+
$this->alpha = hexdec( $c ) / 255;
24+
}
2225
}
2326
} else {
2427
$this->rgb = [];
25-
// TODO: Less.js 3.13 supports 4-digit short rgba as #RGBA
26-
foreach ( str_split( $rgb, 1 ) as $c ) {
27-
$this->rgb[] = hexdec( $c . $c );
28+
foreach ( str_split( $rgb, 1 ) as $i => $c ) {
29+
if ( $i < 3 ) {
30+
$this->rgb[] = hexdec( $c . $c );
31+
} else {
32+
$this->alpha = hexdec( $c . $c ) / 255;
33+
}
2834
}
2935
}
30-
31-
$this->alpha = is_numeric( $a ) ? $a : 1;
36+
$this->alpha ??= ( is_numeric( $a ) ? $a : 1 );
3237

3338
if ( $originalForm !== null ) {
3439
$this->value = $originalForm;
@@ -58,28 +63,56 @@ public function toCSS( $doNotCompress = false ) {
5863
$compress = Less_Parser::$options['compress'] && !$doNotCompress;
5964
$alpha = $this->fround( $this->alpha );
6065

61-
// `value` is set if this color was originally
62-
// converted from a named color string so we need
63-
// to respect this and try to output named color too.
64-
if ( $this->value ) {
65-
return $this->value;
66-
}
67-
6866
// If we have alpha transparency other than 1.0, the only way to represent it
6967
// is via rgba(). Otherwise, we use the hex representation,
7068
// which has better compatibility with older browsers.
7169
// Values are capped between `0` and `255`, rounded and zero-padded.
72-
//
73-
// TODO: Less.js 3.13 supports hsla() and hsl() as well
74-
if ( $alpha < 1 ) {
75-
$values = [];
76-
foreach ( $this->rgb as $c ) {
77-
$values[] = $this->clamp( round( $c ), 255 );
70+
$colorFunction = null;
71+
$args = [];
72+
if ( $this->value ) {
73+
if ( strpos( $this->value, 'rgb' ) === 0 ) {
74+
if ( $alpha < 1 ) {
75+
$colorFunction = 'rgba';
76+
77+
}
78+
} elseif ( strpos( $this->value, 'hsl' ) === 0 ) {
79+
if ( $alpha < 1 ) {
80+
$colorFunction = 'hsla';
81+
} else {
82+
$colorFunction = 'hsl';
83+
}
84+
} else {
85+
return $this->value;
7886
}
79-
$values[] = $alpha;
8087

88+
} else {
89+
if ( $alpha < 1 ) {
90+
$colorFunction = 'rgba';
91+
}
92+
}
93+
94+
switch ( $colorFunction ) {
95+
case 'rgba':
96+
foreach ( $this->rgb as $c ) {
97+
$args[] = $this->clamp( round( $c ), 255 );
98+
}
99+
$args[] = $this->clamp( $alpha, 1 );
100+
break;
101+
case 'hsla':
102+
$args[] = $this->clamp( $alpha, 1 );
103+
// fall through
104+
case 'hsl':
105+
$color = $this->toHSL();
106+
$args = [ $this->fround( $color["h"] ),
107+
$this->fround( $color["s"] * 100 ) . "%",
108+
$this->fround( $color["l"] * 100 ) . "%",
109+
...$args
110+
];
111+
112+
}
113+
if ( $colorFunction ) {
81114
$glue = ( $compress ? ',' : ', ' );
82-
return "rgba(" . implode( $glue, $values ) . ")";
115+
return $colorFunction . "(" . implode( $glue, $args ) . ")";
83116
}
84117

85118
$color = $this->toRGB();
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.test {
2+
background-color: #00000014;
3+
}
4+
div {
5+
background: rgba(41, 14, 39, 0.07843137);
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ParseError: Unexpected input in T403056-invalid-color.less on line 2, column 29
2+
1| .invalid-input {
3+
2| background-color: #1234567;
4+
3| }

test/Fixtures/less.php/css/oyejorge-210-complex-colors.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ body .color-definitions .argb {
2323
color: #e6336699;
2424
}
2525
body .color-definitions .hsl {
26-
color: #336699;
26+
color: hsl(210, 50%, 40%);
2727
}
2828
body .color-definitions .hsla {
29-
color: rgba(51, 102, 153, 0.9);
29+
color: hsla(210, 50%, 40%, 0.9);
3030
}
3131
body .color-definitions .hsv {
3232
color: #336699;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.test {
2+
background-color: #00000014;
3+
}
4+
div {
5+
background: darken(#9C349314, 30%);
6+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.invalid-input {
2+
background-color: #1234567;
3+
}

test/Fixtures/lessjs-2.5.3/css/colors.css

Lines changed: 3 additions & 3 deletions
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;

test/Fixtures/lessjs-2.5.3/css/functions.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@
156156
case-insensitive-2: true;
157157
}
158158
#alpha {
159-
alpha: rgba(153, 94, 51, 0.6);
159+
alpha: hsla(25, 50%, 40%, 0.6);
160160
alpha2: 0.5;
161161
alpha3: 0;
162162
}

0 commit comments

Comments
 (0)