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