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