Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.43% covered (warning)
71.43%
500 / 700
59.05% covered (warning)
59.05%
62 / 105
CRAP
0.00% covered (danger)
0.00%
0 / 1
Less_Functions
71.43% covered (warning)
71.43%
500 / 700
59.05% covered (warning)
59.05%
62 / 105
3329.57
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 _clamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 _number
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 _scaled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 rgb
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 rgba
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 hsl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hsla
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 hsla_hue
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 hsv
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hsva
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
1
 hue
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
 saturation
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
 lightness
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
 hsvhue
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
 hsvsaturation
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
 hsvvalue
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
 red
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 green
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 blue
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 alpha
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
 luma
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 luminance
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
3.33
 saturate
62.50% covered (warning)
62.50%
10 / 16
0.00% covered (danger)
0.00%
0 / 1
11.38
 desaturate
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
10.86
 lighten
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
10.86
 darken
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
10.86
 fadein
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
10.86
 fadeout
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
10.86
 fade
50.00% covered (danger)
50.00%
6 / 12
0.00% covered (danger)
0.00%
0 / 1
8.12
 spin
50.00% covered (danger)
50.00%
6 / 12
0.00% covered (danger)
0.00%
0 / 1
10.50
 mix
74.19% covered (warning)
74.19%
23 / 31
0.00% covered (danger)
0.00%
0 / 1
14.47
 greyscale
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 contrast
75.00% covered (warning)
75.00%
18 / 24
0.00% covered (danger)
0.00%
0 / 1
12.89
 e
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 escape
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 replace
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 replace_flags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 _percent
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
 unit
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
5.68
 convert
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 round
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 pi
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 pow
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
8.12
 ceil
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 floor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sqrt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 abs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 cos
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 atan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 asin
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 acos
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 _math
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
4.59
 _minMax
80.43% covered (warning)
80.43%
37 / 46
0.00% covered (danger)
0.00%
0 / 1
39.67
 min
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 max
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getunit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 argb
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 percentage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 color
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 isruleset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 iscolor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isnumber
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isstring
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 iskeyword
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isurl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 ispixel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 ispercentage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isem
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isunit
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
5
 tint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shade
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getItemsFromNode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 _self
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 extract
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 length
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 datauri
93.75% covered (success)
93.75%
30 / 32
0.00% covered (danger)
0.00%
0 / 1
12.04
 svggradient
68.25% covered (warning)
68.25%
43 / 63
0.00% covered (danger)
0.00%
0 / 1
30.55
 getImageSize
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getSvgSize
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 imagesize
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 imagewidth
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 imageheight
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 colorBlend
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 multiply
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
12.41
 colorBlendMultiply
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 screen
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
12.41
 colorBlendScreen
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 overlay
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
12.41
 colorBlendOverlay
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 softlight
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
12.41
 colorBlendSoftlight
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 hardlight
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
12.41
 colorBlendHardlight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 difference
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
12.41
 colorBlendDifference
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 exclusion
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
12.41
 colorBlendExclusion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 average
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
12.41
 colorBlendAverage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 negation
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
12.41
 colorBlendNegation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Builtin functions
5 * @see https://lesscss.org/functions/
6 */
7class Less_Functions {
8
9    /** @var Less_Environment */
10    public $env;
11    /** @var array|null */
12    public $currentFileInfo;
13
14    public function __construct( $env, ?array $currentFileInfo = null ) {
15        $this->env = $env;
16        $this->currentFileInfo = $currentFileInfo;
17    }
18
19    private static function _clamp( $val, $max = 1 ) {
20        return min( max( $val, 0 ), $max );
21    }
22
23    private static function _number( $n ) {
24        if ( $n instanceof Less_Tree_Dimension ) {
25            return floatval( $n->unit->is( '%' ) ? $n->value / 100 : $n->value );
26        } elseif ( is_numeric( $n ) ) {
27            return $n;
28        } else {
29            throw new Less_Exception_Compiler( "color functions take numbers as parameters" );
30        }
31    }
32
33    private static function _scaled( $n, $size = 255 ) {
34        if ( $n instanceof Less_Tree_Dimension && $n->unit->is( '%' ) ) {
35            return (float)$n->value * $size / 100;
36        } else {
37            return self::_number( $n );
38        }
39    }
40
41    public function rgb( $r = null, $g = null, $b = null ) {
42        if ( $r === null || $g === null || $b === null ) {
43            throw new Less_Exception_Compiler( "rgb expects three parameters" );
44        }
45        return $this->rgba( $r, $g, $b, 1.0 );
46    }
47
48    public function rgba( $r = null, $g = null, $b = null, $a = null ) {
49        $rgb = [
50            self::_scaled( $r ),
51            self::_scaled( $g ),
52            self::_scaled( $b )
53        ];
54
55        $a = self::_number( $a );
56        return new Less_Tree_Color( $rgb, $a );
57    }
58
59    public function hsl( $h, $s, $l ) {
60        return $this->hsla( $h, $s, $l, 1.0 );
61    }
62
63    public function hsla( $h, $s, $l, $a ) {
64        $h = fmod( self::_number( $h ), 360 ) / 360; // Classic % operator will change float to int
65        $s = self::_clamp( self::_number( $s ) );
66        $l = self::_clamp( self::_number( $l ) );
67        $a = self::_clamp( self::_number( $a ) );
68
69        $m2 = $l <= 0.5 ? $l * ( $s + 1 ) : $l + $s - $l * $s;
70
71        $m1 = $l * 2 - $m2;
72
73        return $this->rgba(
74            self::hsla_hue( $h + 1 / 3, $m1, $m2 ) * 255,
75            self::hsla_hue( $h, $m1, $m2 ) * 255,
76            self::hsla_hue( $h - 1 / 3, $m1, $m2 ) * 255,
77            $a
78        );
79    }
80
81    /**
82     * @param float $h
83     * @param float $m1
84     * @param float $m2
85     */
86    public function hsla_hue( $h, $m1, $m2 ) {
87        $h = $h < 0 ? $h + 1 : ( $h > 1 ? $h - 1 : $h );
88        if ( $h * 6 < 1 ) {
89            return $m1 + ( $m2 - $m1 ) * $h * 6;
90        } elseif ( $h * 2 < 1 ) {
91            return $m2;
92        } elseif ( $h * 3 < 2 ) {
93            return $m1 + ( $m2 - $m1 ) * ( 2 / 3 - $h ) * 6;
94        } else {
95            return $m1;
96        }
97    }
98
99    public function hsv( $h, $s, $v ) {
100        return $this->hsva( $h, $s, $v, 1.0 );
101    }
102
103    /**
104     * @param Less_Tree|float $h
105     * @param Less_Tree|float $s
106     * @param Less_Tree|float $v
107     * @param float $a
108     */
109    public function hsva( $h, $s, $v, $a ) {
110        $h = ( ( (int)self::_number( $h ) % 360 ) / 360 ) * 360;
111        $s = self::_number( $s );
112        $v = self::_number( $v );
113        $a = self::_number( $a );
114
115        $i = (int)floor( (int)( $h / 60 ) % 6 );
116        $f = ( $h / 60 ) - $i;
117
118        $vs = [
119            $v,
120            $v * ( 1 - $s ),
121            $v * ( 1 - $f * $s ),
122            $v * ( 1 - ( 1 - $f ) * $s )
123        ];
124
125        $perm = [
126            [ 0, 3, 1 ],
127            [ 2, 0, 1 ],
128            [ 1, 0, 3 ],
129            [ 1, 2, 0 ],
130            [ 3, 1, 0 ],
131            [ 0, 1, 2 ]
132        ];
133
134        return $this->rgba(
135            $vs[$perm[$i][0]] * 255,
136            $vs[$perm[$i][1]] * 255,
137            $vs[$perm[$i][2]] * 255,
138            $a
139        );
140    }
141
142    public function hue( $color = null ) {
143        if ( !$color instanceof Less_Tree_Color ) {
144            throw new Less_Exception_Compiler(
145                'The first argument to hue must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
146            );
147        }
148
149        $c = $color->toHSL();
150        return new Less_Tree_Dimension( $c['h'] );
151    }
152
153    public function saturation( $color = null ) {
154        if ( !$color instanceof Less_Tree_Color ) {
155            throw new Less_Exception_Compiler(
156                'The first argument to saturation must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
157            );
158        }
159
160        $c = $color->toHSL();
161        return new Less_Tree_Dimension( $c['s'] * 100, '%' );
162    }
163
164    public function lightness( $color = null ) {
165        if ( !$color instanceof Less_Tree_Color ) {
166            throw new Less_Exception_Compiler(
167                'The first argument to lightness must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
168            );
169        }
170
171        $c = $color->toHSL();
172        return new Less_Tree_Dimension( $c['l'] * 100, '%' );
173    }
174
175    public function hsvhue( $color = null ) {
176        if ( !$color instanceof Less_Tree_Color ) {
177            throw new Less_Exception_Compiler(
178                'The first argument to hsvhue must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
179            );
180        }
181
182        $hsv = $color->toHSV();
183        return new Less_Tree_Dimension( $hsv['h'] );
184    }
185
186    public function hsvsaturation( $color = null ) {
187        if ( !$color instanceof Less_Tree_Color ) {
188            throw new Less_Exception_Compiler(
189                'The first argument to hsvsaturation must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
190            );
191        }
192
193        $hsv = $color->toHSV();
194        return new Less_Tree_Dimension( $hsv['s'] * 100, '%' );
195    }
196
197    public function hsvvalue( $color = null ) {
198        if ( !$color instanceof Less_Tree_Color ) {
199            throw new Less_Exception_Compiler(
200                'The first argument to hsvvalue must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
201            );
202        }
203
204        $hsv = $color->toHSV();
205        return new Less_Tree_Dimension( $hsv['v'] * 100, '%' );
206    }
207
208    public function red( $color = null ) {
209        if ( !$color instanceof Less_Tree_Color ) {
210            throw new Less_Exception_Compiler(
211                'The first argument to red must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
212            );
213        }
214
215        return new Less_Tree_Dimension( $color->rgb[0] );
216    }
217
218    public function green( $color = null ) {
219        if ( !$color instanceof Less_Tree_Color ) {
220            throw new Less_Exception_Compiler(
221                'The first argument to green must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
222            );
223        }
224
225        return new Less_Tree_Dimension( $color->rgb[1] );
226    }
227
228    public function blue( $color = null ) {
229        if ( !$color instanceof Less_Tree_Color ) {
230            throw new Less_Exception_Compiler(
231                'The first argument to blue must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
232            );
233        }
234
235        return new Less_Tree_Dimension( $color->rgb[2] );
236    }
237
238    public function alpha( $color = null ) {
239        if ( !$color instanceof Less_Tree_Color ) {
240            throw new Less_Exception_Compiler(
241                'The first argument to alpha must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
242            );
243        }
244
245        $c = $color->toHSL();
246        return new Less_Tree_Dimension( $c['a'] );
247    }
248
249    public function luma( $color = null ) {
250        if ( !$color instanceof Less_Tree_Color ) {
251            throw new Less_Exception_Compiler(
252                'The first argument to luma must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
253            );
254        }
255
256        return new Less_Tree_Dimension( $color->luma() * $color->alpha * 100, '%' );
257    }
258
259    public function luminance( $color = null ) {
260        if ( !$color instanceof Less_Tree_Color ) {
261            throw new Less_Exception_Compiler(
262                'The first argument to luminance must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
263            );
264        }
265
266        $luminance =
267            ( 0.2126 * $color->rgb[0] / 255 )
268            + ( 0.7152 * $color->rgb[1] / 255 )
269            + ( 0.0722 * $color->rgb[2] / 255 );
270
271        return new Less_Tree_Dimension( $luminance * $color->alpha * 100, '%' );
272    }
273
274    public function saturate( $color = null, $amount = null, $method = null ) {
275        // filter: saturate(3.2);
276        // should be kept as is, so check for color
277        if ( $color instanceof Less_Tree_Dimension ) {
278            return null;
279        }
280
281        if ( !$color instanceof Less_Tree_Color ) {
282            throw new Less_Exception_Compiler(
283                'The first argument to saturate must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
284            );
285        }
286        if ( !$amount instanceof Less_Tree_Dimension ) {
287            throw new Less_Exception_Compiler(
288                'The second argument to saturate must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
289            );
290        }
291
292        $hsl = $color->toHSL();
293
294        if ( isset( $method ) && $method->value === "relative" ) {
295            $hsl['s'] += $hsl['s'] * $amount->value / 100;
296        } else {
297            $hsl['s'] += $amount->value / 100;
298        }        $hsl['s'] = self::_clamp( $hsl['s'] );
299
300        return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
301    }
302
303    /**
304     * @param Less_Tree_Color|null $color
305     * @param Less_Tree_Dimension|null $amount
306     * @param Less_Tree_Quoted|Less_Tree_Color|Less_Tree_Keyword|null $method
307     */
308    public function desaturate( $color = null, $amount = null, $method = null ) {
309        if ( !$color instanceof Less_Tree_Color ) {
310            throw new Less_Exception_Compiler(
311                'The first argument to desaturate must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
312            );
313        }
314        if ( !$amount instanceof Less_Tree_Dimension ) {
315            throw new Less_Exception_Compiler(
316                'The second argument to desaturate must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
317            );
318        }
319
320        $hsl = $color->toHSL();
321
322        if ( isset( $method ) && $method->value === "relative" ) {
323            $hsl['s'] -= $hsl['s'] * $amount->value / 100;
324        } else {
325            $hsl['s'] -= $amount->value / 100;
326        }
327
328        $hsl['s'] = self::_clamp( $hsl['s'] );
329
330        return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
331    }
332
333    public function lighten( $color = null, $amount = null, $method = null ) {
334        if ( !$color instanceof Less_Tree_Color ) {
335            throw new Less_Exception_Compiler(
336                'The first argument to lighten must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
337            );
338        }
339        if ( !$amount instanceof Less_Tree_Dimension ) {
340            throw new Less_Exception_Compiler(
341                'The second argument to lighten must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
342            );
343        }
344
345        $hsl = $color->toHSL();
346
347        if ( isset( $method ) && $method->value === "relative" ) {
348            $hsl['l'] += $hsl['l'] * $amount->value / 100;
349        } else {
350            $hsl['l'] += $amount->value / 100;
351        }
352
353        $hsl['l'] = self::_clamp( $hsl['l'] );
354
355        return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
356    }
357
358    public function darken( $color = null, $amount = null, $method = null ) {
359        if ( !$color instanceof Less_Tree_Color ) {
360            throw new Less_Exception_Compiler(
361                'The first argument to darken must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
362            );
363        }
364        if ( !$amount instanceof Less_Tree_Dimension ) {
365            throw new Less_Exception_Compiler(
366                'The second argument to darken must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
367            );
368        }
369
370        $hsl = $color->toHSL();
371        if ( isset( $method ) && $method->value === "relative" ) {
372            $hsl['l'] -= $hsl['l'] * $amount->value / 100;
373        } else {
374            $hsl['l'] -= $amount->value / 100;
375        }
376        $hsl['l'] = self::_clamp( $hsl['l'] );
377
378        return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
379    }
380
381    public function fadein( $color = null, $amount = null, $method = null ) {
382        if ( !$color instanceof Less_Tree_Color ) {
383            throw new Less_Exception_Compiler(
384                'The first argument to fadein must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
385            );
386        }
387        if ( !$amount instanceof Less_Tree_Dimension ) {
388            throw new Less_Exception_Compiler(
389                'The second argument to fadein must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
390            );
391        }
392
393        $hsl = $color->toHSL();
394
395        if ( isset( $method ) && $method->value === "relative" ) {
396            $hsl['a'] += $hsl['a'] * $amount->value / 100;
397        } else {
398            $hsl['a'] += $amount->value / 100;
399        }
400
401        $hsl['a'] = self::_clamp( $hsl['a'] );
402        return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
403    }
404
405    public function fadeout( $color = null, $amount = null, $method = null ) {
406        if ( !$color instanceof Less_Tree_Color ) {
407            throw new Less_Exception_Compiler(
408                'The first argument to fadeout must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
409            );
410        }
411        if ( !$amount instanceof Less_Tree_Dimension ) {
412            throw new Less_Exception_Compiler(
413                'The second argument to fadeout must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
414            );
415        }
416
417        $hsl = $color->toHSL();
418
419        if ( isset( $method ) && $method->value === "relative" ) {
420            $hsl['a'] -= $hsl['a'] * $amount->value / 100;
421        } else {
422            $hsl['a'] -= $amount->value / 100;
423        }
424
425        $hsl['a'] = self::_clamp( $hsl['a'] );
426        return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
427    }
428
429    public function fade( $color = null, $amount = null ) {
430        if ( !$color instanceof Less_Tree_Color ) {
431            throw new Less_Exception_Compiler(
432                'The first argument to fade must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
433            );
434        }
435        if ( !$amount instanceof Less_Tree_Dimension ) {
436            throw new Less_Exception_Compiler(
437                'The second argument to fade must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
438            );
439        }
440
441        $hsl = $color->toHSL();
442
443        $hsl['a'] = $amount->value / 100;
444        $hsl['a'] = self::_clamp( $hsl['a'] );
445        return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
446    }
447
448    public function spin( $color = null, $amount = null ) {
449        if ( !$color instanceof Less_Tree_Color ) {
450            throw new Less_Exception_Compiler(
451                'The first argument to spin must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
452            );
453        }
454        if ( !$amount instanceof Less_Tree_Dimension ) {
455            throw new Less_Exception_Compiler(
456                'The second argument to spin must be a number' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
457            );
458        }
459
460        $hsl = $color->toHSL();
461        $hue = fmod( $hsl['h'] + $amount->value, 360 );
462
463        $hsl['h'] = $hue < 0 ? 360 + $hue : $hue;
464
465        return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] );
466    }
467
468    //
469    // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein
470    // https://sass-lang.com/
471    //
472
473    /**
474     * @param Less_Tree|null $color1
475     * @param Less_Tree|null $color2
476     * @param Less_Tree|null $weight
477     */
478    public function mix( $color1 = null, $color2 = null, $weight = null ) {
479        if ( !$color1 instanceof Less_Tree_Color ) {
480            $type = is_object( $color1 ) ? get_class( $color1 ) : gettype( $color1 );
481            throw new Less_Exception_Compiler(
482                "The first argument must be a color, $type given" . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
483            );
484        }
485        if ( !$color2 instanceof Less_Tree_Color ) {
486            $type = is_object( $color2 ) ? get_class( $color2 ) : gettype( $color2 );
487            throw new Less_Exception_Compiler(
488                "The second argument must be a color, $type given" . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
489            );
490        }
491        if ( !$weight ) {
492            $weight = new Less_Tree_Dimension( '50', '%' );
493        }
494        if ( !$weight instanceof Less_Tree_Dimension ) {
495            $type = is_object( $weight ) ? get_class( $weight ) : gettype( $weight );
496            throw new Less_Exception_Compiler(
497                "The third argument must be a percentage, $type given" . ( $weight instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
498            );
499        }
500
501        $p = $weight->value / 100.0;
502        $w = $p * 2 - 1;
503        $hsl1 = $color1->toHSL();
504        $hsl2 = $color2->toHSL();
505        $a = $hsl1['a'] - $hsl2['a'];
506
507        $w1 = ( ( ( ( $w * $a ) == -1 ) ? $w : ( $w + $a ) / ( 1 + $w * $a ) ) + 1 ) / 2;
508        $w2 = 1 - $w1;
509
510        $rgb = [
511            $color1->rgb[0] * $w1 + $color2->rgb[0] * $w2,
512            $color1->rgb[1] * $w1 + $color2->rgb[1] * $w2,
513            $color1->rgb[2] * $w1 + $color2->rgb[2] * $w2
514        ];
515
516        $alpha = $color1->alpha * $p + $color2->alpha * ( 1 - $p );
517
518        return new Less_Tree_Color( $rgb, $alpha );
519    }
520
521    public function greyscale( $color ) {
522        return $this->desaturate( $color, new Less_Tree_Dimension( 100, '%' ) );
523    }
524
525    public function contrast( $color, $dark = null, $light = null, $threshold = null ) {
526        // filter: contrast(3.2);
527        // should be kept as is, so check for color
528        if ( !$color instanceof Less_Tree_Color ) {
529            return null;
530        }
531        if ( !$light ) {
532            $light = $this->rgba( 255, 255, 255, 1.0 );
533        }
534        if ( !$dark ) {
535            $dark = $this->rgba( 0, 0, 0, 1.0 );
536        }
537
538        if ( !$dark instanceof Less_Tree_Color ) {
539            throw new Less_Exception_Compiler(
540                'The second argument to contrast must be a color' . ( $dark instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
541            );
542        }
543        if ( !$light instanceof Less_Tree_Color ) {
544            throw new Less_Exception_Compiler(
545                'The third argument to contrast must be a color' . ( $light instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
546            );
547        }
548
549        // Figure out which is actually light and dark!
550        if ( $dark->luma() > $light->luma() ) {
551            $t = $light;
552            $light = $dark;
553            $dark = $t;
554        }
555        if ( !$threshold ) {
556            $threshold = 0.43;
557        } else {
558            $threshold = self::_number( $threshold );
559        }
560
561        if ( $color->luma() < $threshold ) {
562            return $light;
563        } else {
564            return $dark;
565        }
566    }
567
568    public function e( $str ) {
569        if ( is_string( $str ) ) {
570            return new Less_Tree_Anonymous( $str );
571        }
572        return new Less_Tree_Anonymous( $str instanceof Less_Tree_JavaScript ? $str->expression : $str->value );
573    }
574
575    public function escape( $str ) {
576        $revert = [
577            '%21' => '!',
578            '%2A' => '*',
579            '%27' => "'",
580            '%3F' => '?',
581            '%26' => '&',
582            '%2C' => ',',
583            '%2F' => '/',
584            '%40' => '@',
585            '%2B' => '+',
586            '%24' => '$'
587        ];
588
589        return new Less_Tree_Anonymous( strtr( rawurlencode( $str->value ), $revert ) );
590    }
591
592    /**
593     * todo: This function will need some additional work to make it work the same as less.js
594     *
595     */
596    public function replace( $string, $pattern, $replacement, $flags = null ) {
597        $result = $string->value;
598
599        $expr = '/' . str_replace( '/', '\\/', $pattern->value ) . '/';
600        if ( $flags && $flags->value ) {
601            $expr .= self::replace_flags( $flags->value );
602        }
603        $replacement = ( $replacement instanceof Less_Tree_Quoted ) ?
604            $replacement->value : $replacement->toCSS();
605
606        if ( $flags && $flags->value && preg_match( '/g/', $flags->value ) ) {
607            $result = preg_replace( $expr, $replacement, $result );
608        } else {
609            $result = preg_replace( $expr, $replacement, $result, 1 );
610        }
611
612        if ( $string instanceof Less_Tree_Quoted ) {
613            return new Less_Tree_Quoted( $string->quote, $result, $string->escaped );
614        }
615        return new Less_Tree_Quoted( '', $result );
616    }
617
618    private static function replace_flags( $flags ) {
619        return str_replace( [ 'e', 'g' ], '', $flags );
620    }
621
622    public function _percent( $string, ...$args ) {
623        $result = $string->value;
624
625        foreach ( $args as $arg ) {
626            if ( preg_match( '/%[sda]/i', $result, $token ) ) {
627                $token = $token[0];
628                $value = ( ( $arg instanceof Less_Tree_Quoted ) &&
629                    stristr( $token, 's' ) ? $arg->value : $arg->toCSS() );
630
631                $value = preg_match( '/[A-Z]$/', $token ) ? urlencode( $value ) : $value;
632                $result = preg_replace( '/%[sda]/i', $value, $result, 1 );
633            }
634        }
635        $result = str_replace( '%%', '%', $result );
636
637        if ( $string instanceof Less_Tree_Quoted ) {
638            return new Less_Tree_Quoted( $string->quote, $result, $string->escaped );
639        }
640        return new Less_Tree_Quoted( '', $result );
641    }
642
643    public function unit( $val, $unit = null ) {
644        if ( !( $val instanceof Less_Tree_Dimension ) ) {
645            throw new Less_Exception_Compiler(
646                'The first argument to unit must be a number' . ( $val instanceof Less_Tree_Operation ? '. Have you forgotten parenthesis?' : '.' )
647            );
648        }
649
650        if ( $unit ) {
651            if ( $unit instanceof Less_Tree_Keyword ) {
652                $unit = $unit->value;
653            } else {
654                $unit = $unit->toCSS();
655            }
656        } else {
657            $unit = "";
658        }
659        return new Less_Tree_Dimension( $val->value, $unit );
660    }
661
662    public function convert( $val, $unit ) {
663        return $val->convertTo( $unit->value );
664    }
665
666    public function round( $n, $f = false ) {
667        $fraction = 0;
668        if ( $f !== false ) {
669            $fraction = $f->value;
670        }
671
672        return $this->_math( [ Less_Parser::class, 'round' ], null, $n, $fraction );
673    }
674
675    public function pi() {
676        return new Less_Tree_Dimension( M_PI );
677    }
678
679    public function mod( $a, $b ) {
680        return new Less_Tree_Dimension( $a->value % $b->value, $a->unit );
681    }
682
683    public function pow( $x, $y ) {
684        if ( is_numeric( $x ) && is_numeric( $y ) ) {
685            $x = new Less_Tree_Dimension( $x );
686            $y = new Less_Tree_Dimension( $y );
687        } elseif ( !( $x instanceof Less_Tree_Dimension ) || !( $y instanceof Less_Tree_Dimension ) ) {
688            throw new Less_Exception_Compiler( 'Arguments must be numbers' );
689        }
690
691        return new Less_Tree_Dimension( pow( $x->value, $y->value ), $x->unit );
692    }
693
694    // var mathFunctions = [{name:"ce ...
695    public function ceil( $n ) {
696        return $this->_math( 'ceil', null, $n );
697    }
698
699    public function floor( $n ) {
700        return $this->_math( 'floor', null, $n );
701    }
702
703    public function sqrt( $n ) {
704        return $this->_math( 'sqrt', null, $n );
705    }
706
707    public function abs( $n ) {
708        return $this->_math( 'abs', null, $n );
709    }
710
711    public function tan( $n ) {
712        return $this->_math( 'tan', '', $n );
713    }
714
715    public function sin( $n ) {
716        return $this->_math( 'sin', '', $n );
717    }
718
719    public function cos( $n ) {
720        return $this->_math( 'cos', '', $n );
721    }
722
723    public function atan( $n ) {
724        return $this->_math( 'atan', 'rad', $n );
725    }
726
727    public function asin( $n ) {
728        return $this->_math( 'asin', 'rad', $n );
729    }
730
731    public function acos( $n ) {
732        return $this->_math( 'acos', 'rad', $n );
733    }
734
735    private function _math( $fn, $unit, ...$args ) {
736        if ( $args[0] instanceof Less_Tree_Dimension ) {
737            if ( $unit === null ) {
738                $unit = $args[0]->unit;
739            } else {
740                $args[0] = $args[0]->unify();
741            }
742            $args[0] = (float)$args[0]->value;
743            return new Less_Tree_Dimension( $fn( ...$args ), $unit );
744        } elseif ( is_numeric( $args[0] ) ) {
745            return $fn( ...$args );
746        } else {
747            throw new Less_Exception_Compiler( "math functions take numbers as parameters" );
748        }
749    }
750
751    /**
752     * @param bool $isMin
753     * @param array<Less_Tree> $args
754     * @see less-2.5.3.js#minMax
755     */
756    private function _minMax( $isMin, $args ) {
757        $arg_count = count( $args );
758
759        if ( $arg_count < 1 ) {
760            throw new Less_Exception_Compiler( 'one or more arguments required' );
761        }
762
763        $j = null;
764        $unitClone = null;
765        $unitStatic = null;
766
767        // elems only contains original argument values.
768        $order = [];
769        // key is the unit.toString() for unified tree.Dimension values,
770        // value is the index into the order array.
771        $values = [];
772
773        for ( $i = 0; $i < $arg_count; $i++ ) {
774            $current = $args[$i];
775            if ( !( $current instanceof Less_Tree_Dimension ) ) {
776                if ( $args[$i] instanceof Less_Tree_HasValueProperty && is_array( $args[$i]->value ) ) {
777                    $args[] = $args[$i]->value;
778                }
779                continue;
780            }
781            // PhanTypeInvalidDimOffset -- False positive, safe after continue or non-first iterations
782            '@phan-var non-empty-list<Less_Tree_Dimension> $order';
783
784            if ( $current->unit->toString() === '' && !$unitClone ) {
785                $temp = new Less_Tree_Dimension( $current->value, $unitClone );
786                $currentUnified = $temp->unify();
787            } else {
788                $currentUnified = $current->unify();
789            }
790
791            if ( $currentUnified->unit->toString() === "" && !$unitStatic ) {
792                $unit = $unitStatic;
793            } else {
794                $unit = $currentUnified->unit->toString();
795            }
796
797            if ( ( $unit !== '' && !$unitStatic ) || ( $unit !== '' && $order[0]->unify()->unit->toString() === "" ) ) {
798                $unitStatic = $unit;
799            }
800
801            if ( $unit != '' && !$unitClone ) {
802                $unitClone = $current->unit->toString();
803            }
804
805            if ( isset( $values[''] ) && $unit !== '' && $unit === $unitStatic ) {
806                $j = $values[''];
807            } elseif ( isset( $values[$unit] ) ) {
808                $j = $values[$unit];
809            } else {
810
811                if ( $unitStatic && $unit !== $unitStatic ) {
812                    throw new Less_Exception_Compiler( 'incompatible types' );
813                }
814                $values[$unit] = count( $order );
815                $order[] = $current;
816                continue;
817            }
818
819            if ( $order[$j]->unit->toString() === "" && $unitClone ) {
820                $temp = new Less_Tree_Dimension( $order[$j]->value, $unitClone );
821                $referenceUnified = $temp->unify();
822            } else {
823                $referenceUnified = $order[$j]->unify();
824            }
825            if ( ( $isMin && $currentUnified->value < $referenceUnified->value ) || ( !$isMin && $currentUnified->value > $referenceUnified->value ) ) {
826                $order[$j] = $current;
827            }
828        }
829
830        if ( count( $order ) == 1 ) {
831            return $order[0];
832        }
833        $args = [];
834        foreach ( $order as $a ) {
835            $args[] = $a->toCSS();
836        }
837        return new Less_Tree_Anonymous( ( $isMin ? 'min(' : 'max(' ) . implode( ( Less_Parser::$options['compress'] ? ',' : ', ' ), $args ) . ')' );
838    }
839
840    public function min( ...$args ) {
841        return $this->_minMax( true, $args );
842    }
843
844    public function max( ...$args ) {
845        return $this->_minMax( false, $args );
846    }
847
848    public function getunit( $n ) {
849        return new Less_Tree_Anonymous( $n->unit );
850    }
851
852    public function argb( $color ) {
853        if ( !$color instanceof Less_Tree_Color ) {
854            throw new Less_Exception_Compiler(
855                'The first argument to argb must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
856            );
857        }
858
859        return new Less_Tree_Anonymous( $color->toARGB() );
860    }
861
862    public function percentage( $n ) {
863        return new Less_Tree_Dimension( $n->value * 100, '%' );
864    }
865
866    /**
867     * @see less-2.5.3.js#colorFunctions.color
868     * @param Less_Tree_Quoted|Less_Tree_Color|Less_Tree_Keyword $c
869     * @return Less_Tree_Color
870     */
871    public function color( $c ) {
872        if ( ( $c instanceof Less_Tree_Quoted ) &&
873            preg_match( '/^#([a-f0-9]{6}|[a-f0-9]{3})/', $c->value )
874        ) {
875            return new Less_Tree_Color( substr( $c->value, 1 ) );
876        }
877
878        // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition
879        if ( ( $c instanceof Less_Tree_Color ) || ( $c = Less_Tree_Color::fromKeyword( $c->value ) ) ) {
880            $c->value = null;
881            return $c;
882        }
883
884        throw new Less_Exception_Compiler( "argument must be a color keyword or 3/6 digit hex e.g. #FFF" );
885    }
886
887    public function isruleset( $n ) {
888        return new Less_Tree_Keyword( $n instanceof Less_Tree_DetachedRuleset ? 'true' : 'false' );
889    }
890
891    public function iscolor( $n ) {
892        return new Less_Tree_Keyword( $n instanceof Less_Tree_Color ? 'true' : 'false' );
893    }
894
895    public function isnumber( $n ) {
896        return new Less_Tree_Keyword( $n instanceof Less_Tree_Dimension ? 'true' : 'false' );
897    }
898
899    public function isstring( $n ) {
900        return new Less_Tree_Keyword( $n instanceof Less_Tree_Quoted ? 'true' : 'false' );
901    }
902
903    public function iskeyword( $n ) {
904        return new Less_Tree_Keyword( $n instanceof Less_Tree_Keyword ? 'true' : 'false' );
905    }
906
907    public function isurl( $n ) {
908        return new Less_Tree_Keyword( $n instanceof Less_Tree_Url ? 'true' : 'false' );
909    }
910
911    public function ispixel( $n ) {
912        return $this->isunit( $n, 'px' );
913    }
914
915    public function ispercentage( $n ) {
916        return $this->isunit( $n, '%' );
917    }
918
919    public function isem( $n ) {
920        return $this->isunit( $n, 'em' );
921    }
922
923    /**
924     * @param Less_Tree $n
925     * @param Less_Tree|string $unit
926     */
927    public function isunit( $n, $unit ) {
928        if ( $unit instanceof Less_Tree_Keyword || $unit instanceof Less_Tree_Quoted ) {
929            $unit = $unit->value;
930        }
931
932        return new Less_Tree_Keyword( $n instanceof Less_Tree_Dimension && $n->unit->is( $unit ) ? 'true' : 'false' );
933    }
934
935    public function tint( $color, $amount = null ) {
936        return $this->mix( $this->rgb( 255, 255, 255 ), $color, $amount );
937    }
938
939    public function shade( $color, $amount = null ) {
940        return $this->mix( $this->rgb( 0, 0, 0 ), $color, $amount );
941    }
942
943    /**
944     * @see less-3.13.1.js#getItemsFromNode
945     */
946    private function getItemsFromNode( Less_Tree $node ) {
947        // handle non-array values as an array of length 1
948        // return 'undefined' if index is invalid
949        //
950        // NOTE: Less.js uses duck-typing `isArray(node.value)`, which would cause warnings in PHP,
951        // and potentially bugs for Less_Tree classes with a $value that is only sometimes an array.
952        // Instead, check for Less_Tree classes that always implement an array $value.
953        return ( $node instanceof Less_Tree_Expression || $node instanceof Less_Tree_Value )
954            ? $node->value
955            : [ $node ];
956    }
957
958    /**
959     * @see less-3.13.1.js#_SELF
960     */
961    public function _self( $args ) {
962        return $args;
963    }
964
965    /**
966     * @see less-3.13.1.js#extract
967     */
968    public function extract( $values, $index ) {
969        // (1-based index)
970        $index = (int)$index->value - 1;
971        return $this->getItemsFromNode( $values )[ $index ] ?? null;
972    }
973
974    /**
975     * @see less-3.13.1.js#length
976     */
977    public function length( $values ) {
978        return new Less_Tree_Dimension( count( $this->getItemsFromNode( $values ) ) );
979    }
980
981    /**
982     * @see less-2.5.3.js#data-uri
983     */
984    public function datauri( $mimetypeNode, $filePathNode = null ) {
985        if ( !$filePathNode ) {
986            $filePathNode = $mimetypeNode;
987            $mimetypeNode = null;
988        }
989
990        $filePath = $filePathNode->value;
991        $mimetype = ( $mimetypeNode ? $mimetypeNode->value : null );
992
993        $filePath = str_replace( '\\', '/', $filePath );
994        $fragmentStart = strpos( $filePath, '#' );
995        $fragment = '';
996        if ( $fragmentStart !== false ) {
997            $fragment = substr( $filePath, $fragmentStart );
998            $filePath = substr( $filePath, 0, $fragmentStart );
999        }
1000
1001        [ $filePath ] = Less_FileManager::getFilePath( $filePath, $this->currentFileInfo );
1002
1003        // detect the mimetype if not given
1004        if ( !$mimetype ) {
1005
1006            $mimetype = Less_Mime::lookup( $filePath );
1007
1008            if ( $mimetype === "image/svg+xml" ) {
1009                $useBase64 = false;
1010            } else {
1011                $charset = Less_Mime::charsets_lookup( $mimetype );
1012                $useBase64 = !in_array( $charset, [ 'US-ASCII', 'UTF-8' ] );
1013            }
1014            if ( $useBase64 ) {
1015                $mimetype .= ';base64';
1016            }
1017
1018        } else {
1019            $useBase64 = preg_match( '/;base64$/', $mimetype );
1020        }
1021
1022        if ( !file_exists( $filePath ) ) {
1023            $fallback = new Less_Tree_Url( ( $filePathNode ?: $mimetypeNode ), $this->currentFileInfo );
1024            return $fallback->compile( $this->env );
1025        }
1026        $buf = @file_get_contents( $filePath );
1027
1028        $buf = $useBase64 ? base64_encode( $buf ) : rawurlencode( $buf );
1029        $url = "data:" . $mimetype . ',' . $buf . $fragment;
1030
1031        // IE8 cannot handle a data-uri larger than 32KB. If this is exceeded
1032        // and the --ieCompat flag is enabled, return a normal url() instead.
1033        $DATA_URI_MAX_KB = 32768;
1034        if ( strlen( $buf ) >= $DATA_URI_MAX_KB ) {
1035            // NOTE: Less.js checks for ieCompat here (true by default).
1036            // For Less.php, ieCompat is not configurable, and always true.
1037            $fallback = new Less_Tree_Url( ( $filePathNode ?: $mimetypeNode ), $this->currentFileInfo );
1038            return $fallback->compile( $this->env );
1039        }
1040
1041        return new Less_Tree_Url( new Less_Tree_Quoted( '"' . $url . '"', $url, false ) );
1042    }
1043
1044    // svg-gradient
1045    public function svggradient( $direction, ...$stops ) {
1046        $throw_message = 'svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position]';
1047
1048        if ( count( $stops ) < 2 ) {
1049            throw new Less_Exception_Compiler( $throw_message );
1050        }
1051
1052        $gradientType = 'linear';
1053        $rectangleDimension = 'x="0" y="0" width="1" height="1"';
1054        $directionValue = $direction->toCSS();
1055
1056        switch ( $directionValue ) {
1057            case "to bottom":
1058                $gradientDirectionSvg = 'x1="0%" y1="0%" x2="0%" y2="100%"';
1059                break;
1060            case "to right":
1061                $gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="0%"';
1062                break;
1063            case "to bottom right":
1064                $gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="100%"';
1065                break;
1066            case "to top right":
1067                $gradientDirectionSvg = 'x1="0%" y1="100%" x2="100%" y2="0%"';
1068                break;
1069            case "ellipse":
1070            case "ellipse at center":
1071                $gradientType = "radial";
1072                $gradientDirectionSvg = 'cx="50%" cy="50%" r="75%"';
1073                $rectangleDimension = 'x="-50" y="-50" width="101" height="101"';
1074                break;
1075            default:
1076                throw new Less_Exception_Compiler(
1077                    "svg-gradient direction must be 'to bottom', 'to right', 'to bottom right', 'to top right' or 'ellipse at center'"
1078                );
1079        }
1080
1081        $returner = '<?xml version="1.0" ?>' .
1082            '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100%" height="100%" viewBox="0 0 1 1" preserveAspectRatio="none">' .
1083            '<' . $gradientType . 'Gradient id="gradient" gradientUnits="userSpaceOnUse" ' . $gradientDirectionSvg . '>';
1084
1085        for ( $i = 0; $i < count( $stops ); $i++ ) {
1086
1087            if ( $stops[$i] instanceof Less_Tree_Expression ) {
1088                $color = $stops[$i]->value[0];
1089                $position = $stops[$i]->value[1];
1090            } else {
1091                $color = $stops[$i];
1092                $position = null;
1093            }
1094
1095            if ( !( $color instanceof Less_Tree_Color ) ||
1096                ( !( ( $i === 0 || $i + 1 === count( $stops ) ) && $position === null ) && !( $position instanceof Less_Tree_Dimension ) )
1097            ) {
1098                throw new Less_Exception_Compiler( $throw_message );
1099            }
1100            if ( $position ) {
1101                $positionValue = $position->toCSS();
1102            } elseif ( $i === 0 ) {
1103                $positionValue = '0%';
1104            } else {
1105                $positionValue = '100%';
1106            }
1107            $alpha = $color->alpha;
1108            $returner .= '<stop offset="' . $positionValue . '" stop-color="' . $color->toRGB() . '"' .
1109                ( $alpha < 1 ? ' stop-opacity="' . $alpha . '"' : '' ) . '/>';
1110        }
1111
1112        $returner .= '</' . $gradientType . 'Gradient><rect ' . $rectangleDimension . ' fill="url(#gradient)" /></svg>';
1113
1114        $revert = [
1115            '%21' => '!',
1116            '%2A' => '*',
1117            '%27' => "'",
1118            '%26' => '&',
1119            '%2C' => ',',
1120            '%40' => '@',
1121            '%2B' => '+',
1122            '%24' => '$',
1123            '%28' => '(',
1124            '%29' => ')'
1125        ];
1126        $returner = strtr( rawurlencode( $returner ), $revert );
1127
1128        $returner = "data:image/svg+xml," . $returner;
1129
1130        return new Less_Tree_Url( new Less_Tree_Quoted( "'" . $returner . "'", $returner, false ) );
1131    }
1132
1133    /**
1134     * @see https://github.com/less/less.js/blob/v2.5.3/lib/less-node/image-size.js
1135     */
1136    private function getImageSize( $filePathNode ) {
1137        $filePath = $filePathNode->value;
1138
1139        $filePath = str_replace( '\\', '/', $filePath );
1140
1141        [ $filePath ] = Less_FileManager::getFilePath( $filePath, $this->currentFileInfo );
1142
1143        $mimetype = Less_Mime::lookup( $filePath );
1144
1145        if ( $mimetype === "image/svg+xml" ) {
1146            return $this->getSvgSize( $filePath );
1147        }
1148
1149        [ $imagewidth, $imageheight ] = getimagesize( $filePath );
1150
1151        return [ "width" => $imagewidth, "height" => $imageheight ];
1152    }
1153
1154    /**
1155     * @see https://github.com/image-size/image-size/blob/main/lib/types/svg.ts
1156     */
1157    private function getSvgSize( string $filePathNode ) {
1158        $xml     = simplexml_load_string( file_get_contents( $filePathNode ) );
1159        $attributes = $xml->attributes();
1160        $width          = (string)$attributes->width;
1161        $height         = (string)$attributes->height;
1162
1163        return [ "width" => $width, "height" => $height ];
1164    }
1165
1166    public function imagesize( $filePathNode = null ) {
1167        $imagesize = $this->getImageSize( $filePathNode );
1168        return new Less_Tree_Expression( [
1169            new Less_Tree_Dimension( $imagesize["width"], "px" ),
1170            new Less_Tree_Dimension( $imagesize["height"], "px" )
1171        ] );
1172    }
1173
1174    public function imagewidth( $filePathNode = null ) {
1175        $imagesize = $this->getImageSize( $filePathNode );
1176        return new Less_Tree_Dimension( $imagesize["width"], "px" );
1177    }
1178
1179    public function imageheight( $filePathNode = null ) {
1180        $imagesize = $this->getImageSize( $filePathNode );
1181        return new Less_Tree_Dimension( $imagesize["height"], "px" );
1182    }
1183
1184    // Color Blending
1185    // ref: https://www.w3.org/TR/compositing-1/
1186    public function colorBlend( $mode, $color1, $color2 ) {
1187        // backdrop
1188        $ab = $color1->alpha;
1189        // source
1190        $as = $color2->alpha;
1191        $result = [];
1192
1193        $ar = $as + $ab * ( 1 - $as );
1194        for ( $i = 0; $i < 3; $i++ ) {
1195            $cb = $color1->rgb[$i] / 255;
1196            $cs = $color2->rgb[$i] / 255;
1197            $cr = $mode( $cb, $cs );
1198            if ( $ar ) {
1199                $cr = ( $as * $cs + $ab * ( $cb - $as * ( $cb + $cs - $cr ) ) ) / $ar;
1200            }
1201            $result[$i] = $cr * 255;
1202        }
1203
1204        return new Less_Tree_Color( $result, $ar );
1205    }
1206
1207    public function multiply( $color1 = null, $color2 = null ) {
1208        if ( !$color1 instanceof Less_Tree_Color ) {
1209            throw new Less_Exception_Compiler(
1210                'The first argument to multiply must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1211            );
1212        }
1213        if ( !$color2 instanceof Less_Tree_Color ) {
1214            throw new Less_Exception_Compiler(
1215                'The second argument to multiply must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1216            );
1217        }
1218
1219        return $this->colorBlend( [ $this, 'colorBlendMultiply' ], $color1, $color2 );
1220    }
1221
1222    private function colorBlendMultiply( $cb, $cs ) {
1223        return $cb * $cs;
1224    }
1225
1226    public function screen( $color1 = null, $color2 = null ) {
1227        if ( !$color1 instanceof Less_Tree_Color ) {
1228            throw new Less_Exception_Compiler(
1229                'The first argument to screen must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1230            );
1231        }
1232        if ( !$color2 instanceof Less_Tree_Color ) {
1233            throw new Less_Exception_Compiler(
1234                'The second argument to screen must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1235            );
1236        }
1237
1238        return $this->colorBlend( [ $this, 'colorBlendScreen' ], $color1, $color2 );
1239    }
1240
1241    private function colorBlendScreen( $cb, $cs ) {
1242        return $cb + $cs - $cb * $cs;
1243    }
1244
1245    public function overlay( $color1 = null, $color2 = null ) {
1246        if ( !$color1 instanceof Less_Tree_Color ) {
1247            throw new Less_Exception_Compiler(
1248                'The first argument to overlay must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1249            );
1250        }
1251        if ( !$color2 instanceof Less_Tree_Color ) {
1252            throw new Less_Exception_Compiler(
1253                'The second argument to overlay must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1254            );
1255        }
1256
1257        return $this->colorBlend( [ $this, 'colorBlendOverlay' ], $color1, $color2 );
1258    }
1259
1260    private function colorBlendOverlay( $cb, $cs ) {
1261        $cb *= 2;
1262        return ( $cb <= 1 )
1263            ? $this->colorBlendMultiply( $cb, $cs )
1264            : $this->colorBlendScreen( $cb - 1, $cs );
1265    }
1266
1267    public function softlight( $color1 = null, $color2 = null ) {
1268        if ( !$color1 instanceof Less_Tree_Color ) {
1269            throw new Less_Exception_Compiler(
1270                'The first argument to softlight must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1271            );
1272        }
1273        if ( !$color2 instanceof Less_Tree_Color ) {
1274            throw new Less_Exception_Compiler(
1275                'The second argument to softlight must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1276            );
1277        }
1278
1279        return $this->colorBlend( [ $this, 'colorBlendSoftlight' ], $color1, $color2 );
1280    }
1281
1282    private function colorBlendSoftlight( $cb, $cs ) {
1283        $d = 1;
1284        $e = $cb;
1285        if ( $cs > 0.5 ) {
1286            $e = 1;
1287            $d = ( $cb > 0.25 ) ? sqrt( $cb )
1288                : ( ( 16 * $cb - 12 ) * $cb + 4 ) * $cb;
1289        }
1290        return $cb - ( 1 - 2 * $cs ) * $e * ( $d - $cb );
1291    }
1292
1293    public function hardlight( $color1 = null, $color2 = null ) {
1294        if ( !$color1 instanceof Less_Tree_Color ) {
1295            throw new Less_Exception_Compiler(
1296                'The first argument to hardlight must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1297            );
1298        }
1299        if ( !$color2 instanceof Less_Tree_Color ) {
1300            throw new Less_Exception_Compiler(
1301                'The second argument to hardlight must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1302            );
1303        }
1304
1305        return $this->colorBlend( [ $this, 'colorBlendHardlight' ], $color1, $color2 );
1306    }
1307
1308    private function colorBlendHardlight( $cb, $cs ) {
1309        return $this->colorBlendOverlay( $cs, $cb );
1310    }
1311
1312    public function difference( $color1 = null, $color2 = null ) {
1313        if ( !$color1 instanceof Less_Tree_Color ) {
1314            throw new Less_Exception_Compiler(
1315                'The first argument to difference must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1316            );
1317        }
1318        if ( !$color2 instanceof Less_Tree_Color ) {
1319            throw new Less_Exception_Compiler(
1320                'The second argument to difference must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1321            );
1322        }
1323
1324        return $this->colorBlend( [ $this, 'colorBlendDifference' ], $color1, $color2 );
1325    }
1326
1327    private function colorBlendDifference( $cb, $cs ) {
1328        return abs( $cb - $cs );
1329    }
1330
1331    public function exclusion( $color1 = null, $color2 = null ) {
1332        if ( !$color1 instanceof Less_Tree_Color ) {
1333            throw new Less_Exception_Compiler(
1334                'The first argument to exclusion must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1335            );
1336        }
1337        if ( !$color2 instanceof Less_Tree_Color ) {
1338            throw new Less_Exception_Compiler(
1339                'The second argument to exclusion must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1340            );
1341        }
1342
1343        return $this->colorBlend( [ $this, 'colorBlendExclusion' ], $color1, $color2 );
1344    }
1345
1346    private function colorBlendExclusion( $cb, $cs ) {
1347        return $cb + $cs - 2 * $cb * $cs;
1348    }
1349
1350    public function average( $color1 = null, $color2 = null ) {
1351        if ( !$color1 instanceof Less_Tree_Color ) {
1352            throw new Less_Exception_Compiler(
1353                'The first argument to average must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1354            );
1355        }
1356        if ( !$color2 instanceof Less_Tree_Color ) {
1357            throw new Less_Exception_Compiler(
1358                'The second argument to average must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1359            );
1360        }
1361
1362        return $this->colorBlend( [ $this, 'colorBlendAverage' ], $color1, $color2 );
1363    }
1364
1365    // non-w3c functions:
1366    private function colorBlendAverage( $cb, $cs ) {
1367        return ( $cb + $cs ) / 2;
1368    }
1369
1370    public function negation( $color1 = null, $color2 = null ) {
1371        if ( !$color1 instanceof Less_Tree_Color ) {
1372            throw new Less_Exception_Compiler(
1373                'The first argument to negation must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1374            );
1375        }
1376        if ( !$color2 instanceof Less_Tree_Color ) {
1377            throw new Less_Exception_Compiler(
1378                'The second argument to negation must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' )
1379            );
1380        }
1381
1382        return $this->colorBlend( [ $this, 'colorBlendNegation' ], $color1, $color2 );
1383    }
1384
1385    private function colorBlendNegation( $cb, $cs ) {
1386        return 1 - abs( $cb + $cs - 1 );
1387    }
1388
1389    // ~ End of Color Blending
1390
1391}