Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.33% covered (success)
90.33%
971 / 1075
65.69% covered (warning)
65.69%
67 / 102
CRAP
0.00% covered (danger)
0.00%
0 / 1
Less_Parser
90.33% covered (success)
90.33%
971 / 1075
65.69% covered (warning)
65.69%
67 / 102
757.09
0.00% covered (danger)
0.00%
0 / 1
 __construct
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 Reset
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 SetOptions
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 SetOption
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
8
 registerFunction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 unregisterFunction
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getCss
92.86% covered (success)
92.86%
26 / 28
0.00% covered (danger)
0.00%
0 / 1
6.01
 findValueOf
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
30
 getVariables
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
6.09
 findVarByName
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 getVariableValue
42.86% covered (danger)
42.86%
9 / 21
0.00% covered (danger)
0.00%
0 / 1
94.64
 rgb2html
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 PreVisitors
25.00% covered (danger)
25.00%
1 / 4
0.00% covered (danger)
0.00%
0 / 1
10.75
 PostVisitors
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
15.38
 parse
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
3.00
 parseFile
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
8.09
 ModifyVars
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 SetFileInfo
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
5.00
 SetCacheDir
n/a
0 / 0
n/a
0 / 0
5
 SetImportDirs
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
5.03
 _parse
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 GetRules
73.53% covered (warning)
73.53%
25 / 34
0.00% covered (danger)
0.00%
0 / 1
16.13
 SetInput
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 UnsetInput
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 CacheFile
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 AllParsedFiles
n/a
0 / 0
n/a
0 / 0
1
 getParsedFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 save
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 restore
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 forget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWhitespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 matchChar
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 matchReg
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 matchStr
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 peekReg
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 peekChar
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 skipWhitespace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 expect
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 expectChar
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 parsePrimary
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
7
 parseComment
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 parseComments
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 parseEntitiesQuoted
85.71% covered (warning)
85.71%
24 / 28
0.00% covered (danger)
0.00%
0 / 1
12.42
 parseEntitiesKeyword
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 parseEntitiesCall
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
6
 parseEntitiesArguments
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 parseEntitiesLiteral
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseEntitiesAssignment
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 parseEntitiesUrl
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
6.04
 parseEntitiesVariable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 parseEntitiesVariableCurly
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 parseEntitiesColor
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 parseEntitiesDimension
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
7
 parseUnicodeDescriptor
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 parseEntitiesJavascript
66.67% covered (warning)
66.67%
12 / 18
0.00% covered (danger)
0.00%
0 / 1
7.33
 parseVariable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 parseRulesetCall
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 parseExtend
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
8
 parseMixinCall
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
6
 parseMixinCallElements
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 parseMixinArgs
85.33% covered (warning)
85.33%
64 / 75
0.00% covered (danger)
0.00%
0 / 1
35.23
 parseMixinDefinition
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
9
 parseEntity
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 parseEnd
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 parseAlpha
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 parseElement
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
6
 parseCombinator
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
13
 parseLessSelector
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseSelector
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
17
 parseAttribute
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 parseBlock
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 parseBlockRuleset
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parseDetachedRuleset
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parseRuleset
90.48% covered (success)
90.48%
19 / 21
0.00% covered (danger)
0.00%
0 / 1
9.07
 parseNameValue
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 parseRule
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
20
 parseAnonymousValue
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parseImport
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 parseImportOptions
85.00% covered (warning)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
6.12
 parseImportOption
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parseMediaFeature
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
8.01
 parseMediaFeatures
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
6.44
 parseMedia
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
3.10
 parseDirective
96.83% covered (success)
96.83%
61 / 63
0.00% covered (danger)
0.00%
0 / 1
25
 parseValue
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 parseImportant
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 parseSub
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 parseMultiplication
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
7
 parseAddition
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
7.01
 parseConditions
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
6.03
 parseCondition
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
6
 parseOperand
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 parseExpression
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 parseProperty
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parseRuleProperty
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
10
 rulePropertyMatch
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 rulePropertyCutOutBlockComments
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 serializeVars
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 is_method
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 round
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 Error
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 WinPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 AbsPath
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
5.12
 CacheEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3/**
4 * Parse and compile Less files into CSS
5 */
6class Less_Parser {
7
8    /**
9     * Default parser options
10     * @var array<string,mixed>
11     */
12    public static $default_options = [
13        'compress'                => false, // option - whether to compress
14        'strictUnits'            => false, // whether units need to evaluate correctly
15        'strictMath'            => false, // whether math has to be within parenthesis
16        'relativeUrls'            => true, // option - whether to adjust URL's to be relative
17        'urlArgs'                => '', // whether to add args into url tokens
18        'numPrecision'            => 8,
19
20        'import_dirs'            => [],
21
22        // Override how imported file names are resolved.
23        //
24        // This legacy calllback exposes internal objects and their implementation
25        // details and is therefore deprecated. Use Less_Parser::SetImportDirs instead
26        // to override the resolution of imported file names.
27        //
28        // Example:
29        //
30        //     $parser = new Less_Parser( [
31        //       'import_callback' => function ( $importNode ) {
32        //            $path = $importNode->getPath();
33        //            if ( $path === 'special.less' ) {
34        //                return [ $mySpecialFilePath, null ];
35        //            }
36        //       }
37        //     ] );
38        //
39        // @since 1.5.1
40        // @deprecated since 4.3.0
41        // @see Less_Environment::callImportCallback
42        // @see Less_Parser::SetImportDirs
43        //
44        'import_callback'        => null,
45        'cache_dir'                => null,
46        'cache_method'            => 'serialize', // false, 'serialize', 'callback';
47        'cache_callback_get'    => null,
48        'cache_callback_set'    => null,
49
50        'sourceMap'                => false, // whether to output a source map
51        'sourceMapBasepath'        => null,
52        'sourceMapWriteTo'        => null,
53        'sourceMapURL'            => null,
54
55        'indentation'             => '  ',
56
57        'plugins'                => [],
58        'functions'             => [],
59
60    ];
61
62    /** @var array{compress:bool,strictUnits:bool,strictMath:bool,relativeUrls:bool,urlArgs:string,numPrecision:int,import_dirs:array,import_callback:null|callable,indentation:string} */
63    public static $options = [];
64
65    /** @var Less_Environment */
66    private static $envCompat;
67
68    private $input;                    // Less input string
69    private $input_len;                // input string length
70    private $pos;                    // current index in `input`
71    private $saveStack = [];    // holds state for backtracking
72    private $furthest;
73    private $mb_internal_encoding = ''; // for remember exists value of mbstring.internal_encoding
74
75    /**
76     * @var Less_Environment
77     */
78    private $env;
79
80    protected $rules = [];
81
82    public static $has_extends = false;
83
84    public static $next_id = 0;
85
86    /**
87     * Filename to contents of all parsed the files
88     *
89     * @var array
90     */
91    public static $contentsMap = [];
92
93    /**
94     * @param Less_Environment|array|null $env
95     */
96    public function __construct( $env = null ) {
97        // Top parser on an import tree must be sure there is one "env"
98        // which will then be passed around by reference.
99        if ( $env instanceof Less_Environment ) {
100            $this->env = $env;
101            self::$envCompat = $this->env;
102        } else {
103            $this->Reset( $env );
104        }
105
106        // mbstring.func_overload > 1 bugfix
107        // The encoding value must be set for each source file,
108        // therefore, to conserve resources and improve the speed of this design is taken here
109        if ( ini_get( 'mbstring.func_overload' ) ) {
110            $this->mb_internal_encoding = ini_get( 'mbstring.internal_encoding' );
111            @ini_set( 'mbstring.internal_encoding', 'ascii' );
112        }
113    }
114
115    /**
116     * Reset the parser state completely
117     */
118    public function Reset( $options = null ) {
119        $this->rules = [];
120        self::$has_extends = false;
121        self::$contentsMap = [];
122
123        $this->env = new Less_Environment();
124        self::$envCompat = $this->env;
125
126        // set new options
127        $this->SetOptions( self::$default_options );
128        if ( is_array( $options ) ) {
129            $this->SetOptions( $options );
130        }
131
132        $this->env->Init();
133    }
134
135    /**
136     * Set one or more compiler options
137     *  options: import_dirs, cache_dir, cache_method
138     */
139    public function SetOptions( $options ) {
140        foreach ( $options as $option => $value ) {
141            $this->SetOption( $option, $value );
142        }
143    }
144
145    /**
146     * Set one compiler option
147     */
148    public function SetOption( $option, $value ) {
149        switch ( $option ) {
150            case 'strictMath':
151                $this->env->strictMath = (bool)$value;
152                self::$options[$option] = $value;
153                return;
154
155            case 'import_dirs':
156                $this->SetImportDirs( $value );
157                return;
158
159            case 'import_callback':
160                $this->env->importCallback = $value;
161                return;
162
163            case 'cache_dir':
164                if ( is_string( $value ) ) {
165                    Less_Cache::SetCacheDir( $value );
166                    Less_Cache::CheckCacheDir();
167                }
168                return;
169            case 'functions':
170                foreach ( $value as $key => $function ) {
171                    $this->registerFunction( $key, $function );
172                }
173                return;
174        }
175
176        self::$options[$option] = $value;
177    }
178
179    /**
180     * Registers a new custom function
181     *
182     * @param string $name function name
183     * @param callable $callback callback
184     */
185    public function registerFunction( $name, $callback ) {
186        $this->env->functions[$name] = $callback;
187    }
188
189    /**
190     * Removed an already registered function
191     *
192     * @param string $name function name
193     */
194    public function unregisterFunction( $name ) {
195        if ( isset( $this->env->functions[$name] ) ) {
196            unset( $this->env->functions[$name] );
197        }
198    }
199
200    /**
201     * Get the current css buffer
202     *
203     * @return string
204     */
205    public function getCss() {
206        $precision = ini_get( 'precision' );
207        @ini_set( 'precision', '16' );
208        $locale = setlocale( LC_NUMERIC, 0 );
209        setlocale( LC_NUMERIC, "C" );
210
211        try {
212            $root = new Less_Tree_Ruleset( null, $this->rules );
213            $root->root = true;
214            $root->firstRoot = true;
215
216            $importVisitor = new Less_ImportVisitor( $this->env );
217            $importVisitor->run( $root );
218
219            $this->PreVisitors( $root );
220
221            self::$has_extends = false;
222            $evaldRoot = $root->compile( $this->env );
223
224            $this->PostVisitors( $evaldRoot );
225
226            if ( self::$options['sourceMap'] ) {
227                $generator = new Less_SourceMap_Generator( $evaldRoot, self::$contentsMap, self::$options );
228                // will also save file
229                // FIXME: should happen somewhere else?
230                $css = $generator->generateCSS();
231            } else {
232                $css = $evaldRoot->toCSS();
233            }
234
235            if ( self::$options['compress'] ) {
236                $css = preg_replace( '/(^(\s)+)|((\s)+$)/', '', $css );
237            }
238
239        } catch ( Exception $exc ) {
240            // Intentional fall-through so we can reset environment
241        }
242
243        // reset php settings
244        @ini_set( 'precision', $precision );
245        setlocale( LC_NUMERIC, $locale );
246
247        // If you previously defined $this->mb_internal_encoding
248        // is required to return the encoding as it was before
249        if ( $this->mb_internal_encoding != '' ) {
250            @ini_set( "mbstring.internal_encoding", $this->mb_internal_encoding );
251            $this->mb_internal_encoding = '';
252        }
253
254        // Rethrow exception after we handled resetting the environment
255        if ( !empty( $exc ) ) {
256            throw $exc;
257        }
258
259        return $css;
260    }
261
262    public function findValueOf( $varName ) {
263        foreach ( $this->rules as $rule ) {
264            if ( isset( $rule->variable ) && ( $rule->variable == true ) && ( str_replace( "@", "", $rule->name ) == $varName ) ) {
265                return $this->getVariableValue( $rule );
266            }
267        }
268        return null;
269    }
270
271    /**
272     * Gets the private rules variable and returns an array of the found variables
273     * it uses a helper method getVariableValue() that contains the logic ot fetch the value
274     * from the rule object
275     *
276     * @return array
277     */
278    public function getVariables() {
279        $variables = [];
280
281        $not_variable_type = [
282            Less_Tree_Comment::class, // this include less comments ( // ) and css comments (/* */)
283            Less_Tree_Import::class, // do not search variables in included files @import
284            Less_Tree_Ruleset::class, // selectors (.someclass, #someid, â€¦)
285            Less_Tree_Operation::class,
286        ];
287
288        // @TODO run compilation if not runned yet
289        foreach ( $this->rules as $key => $rule ) {
290            if ( in_array( get_class( $rule ), $not_variable_type ) ) {
291                continue;
292            }
293
294            // Note: it seems $rule is always Less_Tree_Rule when variable = true
295            if ( $rule instanceof Less_Tree_Rule && $rule->variable ) {
296                $variables[$rule->name] = $this->getVariableValue( $rule );
297            } else {
298                if ( $rule instanceof Less_Tree_Comment ) {
299                    $variables[] = $this->getVariableValue( $rule );
300                }
301            }
302        }
303        return $variables;
304    }
305
306    public function findVarByName( $var_name ) {
307        foreach ( $this->rules as $rule ) {
308            if ( isset( $rule->variable ) && ( $rule->variable == true ) ) {
309                if ( $rule->name == $var_name ) {
310                    return $this->getVariableValue( $rule );
311                }
312            }
313        }
314        return null;
315    }
316
317    /**
318     * This method gets the value of the less variable from the rules object.
319     * Since the objects vary here we add the logic for extracting the css/less value.
320     *
321     * @param Less_Tree $var
322     * @return string
323     */
324    private function getVariableValue( Less_Tree $var ) {
325        switch ( get_class( $var ) ) {
326            case Less_Tree_Color::class:
327                return $this->rgb2html( $var->rgb );
328            case Less_Tree_Variable::class:
329                return $this->findVarByName( $var->name );
330            case Less_Tree_Keyword::class:
331                return $var->value;
332            case Less_Tree_Url::class:
333                // Based on Less_Tree_Url::genCSS()
334                // Recurse to serialize the Less_Tree_Quoted value
335                return 'url(' . $this->getVariableValue( $var->value ) . ')';
336            case Less_Tree_Rule::class:
337                return $this->getVariableValue( $var->value );
338            case Less_Tree_Value::class:
339                $value = '';
340                foreach ( $var->value as $sub_value ) {
341                    $value .= $this->getVariableValue( $sub_value ) . ' ';
342                }
343                return $value;
344            case Less_Tree_Quoted::class:
345                return $var->quote . $var->value . $var->quote;
346            case Less_Tree_Dimension::class:
347                $value = $var->value;
348                if ( $var->unit && $var->unit->numerator ) {
349                    $value .= $var->unit->numerator[0];
350                }
351                return $value;
352            case Less_Tree_Expression::class:
353                $value = '';
354                foreach ( $var->value as $item ) {
355                    $value .= $this->getVariableValue( $item ) . " ";
356                }
357                return $value;
358            case Less_Tree_Operation::class:
359                throw new Exception( 'getVariables() require Less to be compiled. please use $parser->getCss() before calling getVariables()' );
360            case Less_Tree_Unit::class:
361            case Less_Tree_Comment::class:
362            case Less_Tree_Import::class:
363            case Less_Tree_Ruleset::class:
364            default:
365                throw new Exception( "type missing in switch/case getVariableValue for " . get_class( $var ) );
366        }
367    }
368
369    private function rgb2html( $r, $g = -1, $b = -1 ) {
370        if ( is_array( $r ) && count( $r ) == 3 ) {
371            [ $r, $g, $b ] = $r;
372        }
373
374        return sprintf( '#%02x%02x%02x', $r, $g, $b );
375    }
376
377    /**
378     * Run pre-compile visitors
379     */
380    private function PreVisitors( $root ) {
381        if ( self::$options['plugins'] ) {
382            foreach ( self::$options['plugins'] as $plugin ) {
383                if ( !empty( $plugin->isPreEvalVisitor ) ) {
384                    $plugin->run( $root );
385                }
386            }
387        }
388    }
389
390    /**
391     * Run post-compile visitors
392     */
393    private function PostVisitors( $evaldRoot ) {
394        $visitors = [];
395        $visitors[] = new Less_Visitor_joinSelector();
396        if ( self::$has_extends ) {
397            $visitors[] = new Less_Visitor_processExtends();
398        }
399        $visitors[] = new Less_Visitor_toCSS();
400
401        if ( self::$options['plugins'] ) {
402            foreach ( self::$options['plugins'] as $plugin ) {
403                if ( property_exists( $plugin, 'isPreEvalVisitor' ) && $plugin->isPreEvalVisitor ) {
404                    continue;
405                }
406
407                if ( property_exists( $plugin, 'isPreVisitor' ) && $plugin->isPreVisitor ) {
408                    array_unshift( $visitors, $plugin );
409                } else {
410                    $visitors[] = $plugin;
411                }
412            }
413        }
414
415        for ( $i = 0; $i < count( $visitors ); $i++ ) {
416            $visitors[$i]->run( $evaldRoot );
417        }
418    }
419
420    /**
421     * Parse a Less string
422     *
423     * @throws Less_Exception_Parser If the compiler encounters invalid syntax
424     * @param string $str The string to convert
425     * @param string|null $file_uri The url of the file
426     * @return $this
427     */
428    public function parse( $str, $file_uri = null ) {
429        if ( !$file_uri ) {
430            $uri_root = '';
431            $filename = 'anonymous-file-' . self::$next_id++ . '.less';
432        } else {
433            $file_uri = self::WinPath( $file_uri );
434            $filename = $file_uri;
435            $uri_root = dirname( $file_uri );
436        }
437
438        $previousFileInfo = $this->env->currentFileInfo;
439        $uri_root = self::WinPath( $uri_root );
440        $this->SetFileInfo( $filename, $uri_root );
441
442        $this->input = $str;
443        $this->_parse();
444
445        if ( $previousFileInfo ) {
446            $this->env->currentFileInfo = $previousFileInfo;
447        }
448
449        return $this;
450    }
451
452    /**
453     * Parse a Less string from a given file
454     *
455     * @throws Less_Exception_Parser If the compiler encounters invalid syntax
456     * @param string $filename The file to parse
457     * @param string $uri_root The url of the file
458     * @param bool $returnRoot Indicates whether the return value should be a css string a root node
459     * @return Less_Tree_Ruleset|$this
460     */
461    public function parseFile( $filename, $uri_root = '', $returnRoot = false ) {
462        if ( !file_exists( $filename ) ) {
463            $this->Error( sprintf( 'File `%s` not found.', $filename ) );
464        }
465
466        // fix uri_root?
467        // Instead of The mixture of file path for the first argument and directory path for the second argument has bee
468        if ( !$returnRoot && !empty( $uri_root ) && basename( $uri_root ) == basename( $filename ) ) {
469            $uri_root = dirname( $uri_root );
470        }
471
472        $previousFileInfo = $this->env->currentFileInfo;
473
474        if ( $filename ) {
475            $filename = self::AbsPath( $filename, true );
476        }
477        $uri_root = self::WinPath( $uri_root );
478
479        $this->SetFileInfo( $filename, $uri_root );
480
481        $this->env->addParsedFile( $filename );
482
483        if ( $returnRoot ) {
484            $rules = $this->GetRules( $filename );
485            $return = new Less_Tree_Ruleset( null, $rules );
486        } else {
487            $this->_parse( $filename );
488            $return = $this;
489        }
490
491        if ( $previousFileInfo ) {
492            $this->env->currentFileInfo = $previousFileInfo;
493        }
494
495        return $return;
496    }
497
498    /**
499     * Allows a user to set variables values
500     * @param array $vars
501     * @return $this
502     */
503    public function ModifyVars( $vars ) {
504        $this->input = self::serializeVars( $vars );
505        $this->_parse();
506
507        return $this;
508    }
509
510    /**
511     * @param string $filename
512     * @param string $uri_root
513     */
514    public function SetFileInfo( $filename, $uri_root = '' ) {
515        $filename = Less_Environment::normalizePath( $filename );
516        $dirname = preg_replace( '/[^\/\\\\]*$/', '', $filename );
517
518        if ( !empty( $uri_root ) ) {
519            $uri_root = rtrim( $uri_root, '/' ) . '/';
520        }
521
522        $currentFileInfo = [];
523
524        // entry info
525        if ( isset( $this->env->currentFileInfo ) ) {
526            $currentFileInfo['entryPath'] = $this->env->currentFileInfo['entryPath'];
527            $currentFileInfo['entryUri'] = $this->env->currentFileInfo['entryUri'];
528            $currentFileInfo['rootpath'] = $this->env->currentFileInfo['rootpath'];
529
530        } else {
531            $currentFileInfo['entryPath'] = $dirname;
532            $currentFileInfo['entryUri'] = $uri_root;
533            $currentFileInfo['rootpath'] = $dirname;
534        }
535
536        $currentFileInfo['currentDirectory'] = $dirname;
537        $currentFileInfo['currentUri'] = $uri_root . basename( $filename );
538        $currentFileInfo['filename'] = $filename;
539        $currentFileInfo['uri_root'] = $uri_root;
540
541        // inherit reference
542        if ( isset( $this->env->currentFileInfo['reference'] ) && $this->env->currentFileInfo['reference'] ) {
543            $currentFileInfo['reference'] = true;
544        }
545
546        $this->env->currentFileInfo = $currentFileInfo;
547    }
548
549    /**
550     * @deprecated 1.5.1.2
551     */
552    public function SetCacheDir( $dir ) {
553        if ( !file_exists( $dir ) ) {
554            if ( mkdir( $dir ) ) {
555                return true;
556            }
557            throw new Less_Exception_Parser( 'Less.php cache directory couldn\'t be created: ' . $dir );
558
559        } elseif ( !is_dir( $dir ) ) {
560            throw new Less_Exception_Parser( 'Less.php cache directory doesn\'t exist: ' . $dir );
561
562        } elseif ( !is_writable( $dir ) ) {
563            throw new Less_Exception_Parser( 'Less.php cache directory isn\'t writable: ' . $dir );
564
565        } else {
566            $dir = self::WinPath( $dir );
567            Less_Cache::$cache_dir = rtrim( $dir, '/' ) . '/';
568            return true;
569        }
570    }
571
572    /**
573     * Set a list of directories or callbacks the parser should use for determining import paths
574     *
575     * Import closures are called with a single `$path` argument containing the unquoted `@import`
576     * string an input LESS file. The string is unchanged, except for a statically appended ".less"
577     * suffix if the basename does not yet contain a dot. If a dot is present in the filename, you
578     * are responsible for choosing whether to expand "foo.bar" to "foo.bar.less". If your callback
579     * can handle this import statement, return an array with an absolute file path and an optional
580     * URI path, or return void/null to indicate that your callback does not handle this import
581     * statement.
582     *
583     * Example:
584     *
585     *     function ( $path ) {
586     *         if ( $path === 'virtual/something.less' ) {
587     *             return [ '/srv/elsewhere/thing.less', null ];
588     *         }
589     *     }
590     *
591     *
592     * @param array<string|callable> $dirs The key should be a server directory from which LESS
593     * files may be imported. The value is an optional public URL or URL base path that corresponds to
594     * the same directory (use empty string otherwise). The value may also be a closure, in
595     * which case the key is ignored.
596     */
597    public function SetImportDirs( $dirs ) {
598        self::$options['import_dirs'] = [];
599
600        foreach ( $dirs as $path => $uri_root ) {
601
602            $path = self::WinPath( $path );
603            if ( !empty( $path ) ) {
604                $path = rtrim( $path, '/' ) . '/';
605            }
606
607            if ( !is_callable( $uri_root ) ) {
608                $uri_root = self::WinPath( $uri_root );
609                if ( !empty( $uri_root ) ) {
610                    $uri_root = rtrim( $uri_root, '/' ) . '/';
611                }
612            }
613
614            self::$options['import_dirs'][$path] = $uri_root;
615        }
616    }
617
618    /**
619     * @param string|null $file_path
620     */
621    private function _parse( $file_path = null ) {
622        $this->rules = array_merge( $this->rules, $this->GetRules( $file_path ) );
623    }
624
625    /**
626     * Return the results of parsePrimary for $file_path
627     * Use cache and save cached results if possible
628     *
629     * @param string|null $file_path
630     */
631    private function GetRules( $file_path ) {
632        $this->SetInput( $file_path );
633
634        $cache_file = $this->CacheFile( $file_path );
635        if ( $cache_file ) {
636            if ( self::$options['cache_method'] == 'callback' ) {
637                $callback = self::$options['cache_callback_get'];
638                if ( is_callable( $callback ) ) {
639                    $cache = $callback( $this, $file_path, $cache_file );
640
641                    if ( $cache ) {
642                        $this->UnsetInput();
643                        return $cache;
644                    }
645                }
646
647            } elseif ( file_exists( $cache_file ) ) {
648                switch ( self::$options['cache_method'] ) {
649
650                    // Using serialize
651                    case 'serialize':
652                        $cache = unserialize( file_get_contents( $cache_file ) );
653                        if ( $cache ) {
654                            touch( $cache_file );
655                            $this->UnsetInput();
656                            return $cache;
657                        }
658                        break;
659                }
660            }
661        }
662
663        $rules = $this->parsePrimary();
664
665        if ( $this->pos < $this->input_len ) {
666            throw new Less_Exception_Chunk( $this->input, null, $this->furthest, $this->env->currentFileInfo );
667        }
668
669        $this->UnsetInput();
670
671        // save the cache
672        if ( $cache_file ) {
673            if ( self::$options['cache_method'] == 'callback' ) {
674                $callback = self::$options['cache_callback_set'];
675                if ( is_callable( $callback ) ) {
676                    $callback( $this, $file_path, $cache_file, $rules );
677                }
678            } else {
679                switch ( self::$options['cache_method'] ) {
680                    case 'serialize':
681                        file_put_contents( $cache_file, serialize( $rules ) );
682                        break;
683                }
684
685                Less_Cache::CleanCache();
686            }
687        }
688
689        return $rules;
690    }
691
692    /**
693     * @internal since 4.3.0 No longer a public API.
694     */
695    public function SetInput( $file_path ) {
696        // Set up the input buffer
697        if ( $file_path ) {
698            $this->input = file_get_contents( $file_path );
699        }
700
701        $this->pos = $this->furthest = 0;
702
703        // Remove potential UTF Byte Order Mark
704        $this->input = preg_replace( '/\\G\xEF\xBB\xBF/', '', $this->input );
705        $this->input_len = strlen( $this->input );
706
707        if ( self::$options['sourceMap'] && $this->env->currentFileInfo ) {
708            $uri = $this->env->currentFileInfo['currentUri'];
709            self::$contentsMap[$uri] = $this->input;
710        }
711    }
712
713    /**
714     * @internal since 4.3.0 No longer a public API.
715     */
716    public function UnsetInput() {
717        // Free up some memory
718        $this->input = $this->pos = $this->input_len = $this->furthest = null;
719        $this->saveStack = [];
720    }
721
722    /**
723     * @internal since 4.3.0 Use Less_Cache instead.
724     */
725    public function CacheFile( $file_path ) {
726        if ( $file_path && $this->CacheEnabled() ) {
727
728            $env = get_object_vars( $this->env );
729            unset( $env['frames'] );
730
731            $parts = [];
732            $parts[] = $file_path;
733            $parts[] = filesize( $file_path );
734            $parts[] = filemtime( $file_path );
735            $parts[] = $env;
736            $parts[] = Less_Version::cache_version;
737            $parts[] = self::$options['cache_method'];
738            return Less_Cache::$cache_dir . Less_Cache::$prefix . base_convert( sha1( json_encode( $parts ) ), 16, 36 ) . '.lesscache';
739        }
740    }
741
742    /**
743     * @deprecated since 4.3.0 Use $parser->getParsedFiles() instead.
744     * @return string[]
745     */
746    public static function AllParsedFiles() {
747        return self::$envCompat->imports;
748    }
749
750    /**
751     * @since 4.3.0
752     * @return string[]
753     */
754    public function getParsedFiles() {
755        return $this->env->imports;
756    }
757
758    /**
759     * @internal since 4.3.0 No longer a public API.
760     */
761    public function save() {
762        $this->saveStack[] = $this->pos;
763    }
764
765    private function restore() {
766        if ( $this->pos > $this->furthest ) {
767            $this->furthest = $this->pos;
768        }
769        $this->pos = array_pop( $this->saveStack );
770    }
771
772    private function forget() {
773        array_pop( $this->saveStack );
774    }
775
776    /**
777     * Determine if the character at the specified offset from the current position is a white space.
778     *
779     * @param int $offset
780     * @return bool
781     */
782    private function isWhitespace( $offset = 0 ) {
783        // @phan-suppress-next-line PhanParamSuspiciousOrder False positive
784        return strpos( " \t\n\r\v\f", $this->input[$this->pos + $offset] ) !== false;
785    }
786
787    /**
788     * Match a single character in the input.
789     *
790     * @param string $tok
791     * @return string|null
792     * @see less-2.5.3.js#parserInput.$char
793     */
794    private function matchChar( $tok ) {
795        if ( ( $this->pos < $this->input_len ) && ( $this->input[$this->pos] === $tok ) ) {
796            $this->skipWhitespace( 1 );
797            return $tok;
798        }
799    }
800
801    /**
802     * Match a regexp from the current start point
803     *
804     * @return string|array|null
805     * @see less-2.5.3.js#parserInput.$re
806     */
807    private function matchReg( $tok ) {
808        if ( preg_match( $tok, $this->input, $match, 0, $this->pos ) ) {
809            $this->skipWhitespace( strlen( $match[0] ) );
810            return count( $match ) === 1 ? $match[0] : $match;
811        }
812    }
813
814    /**
815     * Match an exact string of characters.
816     *
817     * @param string $tok
818     * @return string|null
819     * @see less-2.5.3.js#parserInput.$str
820     */
821    private function matchStr( $tok ) {
822        $tokLength = strlen( $tok );
823        if (
824            ( $this->pos < $this->input_len ) &&
825            substr( $this->input, $this->pos, $tokLength ) === $tok
826        ) {
827            $this->skipWhitespace( $tokLength );
828            return $tok;
829        }
830    }
831
832    /**
833     * Same as match(), but don't change the state of the parser,
834     * just return the match.
835     *
836     * @param string $tok
837     * @return int|false
838     */
839    private function peekReg( $tok ) {
840        return preg_match( $tok, $this->input, $match, 0, $this->pos );
841    }
842
843    /**
844     * @param string $tok
845     */
846    private function peekChar( $tok ) {
847        return ( $this->pos < $this->input_len ) && ( $this->input[$this->pos] === $tok );
848    }
849
850    /**
851     * @param int $length
852     * @see less-2.5.3.js#skipWhitespace
853     */
854    private function skipWhitespace( $length ) {
855        $this->pos += $length;
856        // Optimization: Skip over irrelevant chars without slow loop
857        $this->pos += strspn( $this->input, "\n\r\t ", $this->pos );
858    }
859
860    /**
861     * Parse a token from a regexp or method name string
862     *
863     * @param string $tok
864     * @param string|null $msg
865     * @see less-2.5.3.js#Parser.expect
866     */
867    private function expect( $tok, $msg = null ) {
868        if ( $tok[0] === '/' ) {
869            $result = $this->matchReg( $tok );
870        } else {
871            $result = $this->$tok();
872        }
873        if ( $result !== null ) {
874            return $result;
875        }
876        $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg );
877    }
878
879    /**
880     * @param string $tok
881     * @param string|null $msg
882     */
883    private function expectChar( $tok, $msg = null ) {
884        $result = $this->matchChar( $tok );
885        if ( !$result ) {
886            $msg = $msg ?: "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'";
887            $this->Error( $msg );
888        } else {
889            return $result;
890        }
891    }
892
893    //
894    // Here in, the parsing rules/functions
895    //
896    // The basic structure of the syntax tree generated is as follows:
897    //
898    //   Ruleset ->  Rule -> Value -> Expression -> Entity
899    //
900    // Here's some LESS code:
901    //
902    //    .class {
903    //      color: #fff;
904    //      border: 1px solid #000;
905    //      width: @w + 4px;
906    //      > .child {...}
907    //    }
908    //
909    // And here's what the parse tree might look like:
910    //
911    //     Ruleset (Selector '.class', [
912    //         Rule ("color",  Value ([Expression [Color #fff]]))
913    //         Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
914    //         Rule ("width",  Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]]))
915    //         Ruleset (Selector [Element '>', '.child'], [...])
916    //     ])
917    //
918    //  In general, most rules will try to parse a token with the `$()` function, and if the return
919    //  value is truly, will return a new node, of the relevant type. Sometimes, we need to check
920    //  first, before parsing, that's when we use `peek()`.
921    //
922
923    //
924    // The `primary` rule is the *entry* and *exit* point of the parser.
925    // The rules here can appear at any level of the parse tree.
926    //
927    // The recursive nature of the grammar is an interplay between the `block`
928    // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
929    // as represented by this simplified grammar:
930    //
931    //     primary  â†’  (ruleset | rule)+
932    //     ruleset  â†’  selector+ block
933    //     block    â†’  '{' primary '}'
934    //
935    // Only at one point is the primary rule not called from the
936    // block rule: at the root level.
937    //
938    // @see less-2.5.3.js#parsers.primary
939    private function parsePrimary() {
940        $root = [];
941
942        while ( true ) {
943
944            if ( $this->pos >= $this->input_len ) {
945                break;
946            }
947
948            $node = $this->parseExtend( true );
949            if ( $node ) {
950                $root = array_merge( $root, $node );
951                continue;
952            }
953
954            $node = $this->parseComment()
955                ?? $this->parseMixinDefinition()
956                // Optimisation: NameValue is specific to less.php
957                ?? $this->parseNameValue()
958                ?? $this->parseRule()
959                ?? $this->parseRuleset()
960                ?? $this->parseMixinCall()
961                ?? $this->parseRulesetCall()
962                ?? $this->parseDirective();
963
964            if ( $node ) {
965                $root[] = $node;
966            } elseif ( !$this->matchReg( '/\\G[\s\n;]+/' ) ) {
967                break;
968            }
969
970            if ( $this->peekChar( '}' ) ) {
971                break;
972            }
973        }
974
975        return $root;
976    }
977
978    // We create a Comment node for CSS comments `/* */`,
979    // but keep the LeSS comments `//` silent, by just skipping
980    // over them.
981    private function parseComment() {
982        $char = $this->input[$this->pos] ?? null;
983        if ( $char !== '/' ) {
984            return;
985        }
986
987        $nextChar = $this->input[$this->pos + 1] ?? null;
988        if ( $nextChar === '/' ) {
989            $match = $this->matchReg( '/\\G\/\/.*/' );
990            return new Less_Tree_Comment( $match, true, $this->pos, $this->env->currentFileInfo );
991        }
992
993        // not the same as less.js to prevent fatal errors
994        //         $this->matchReg( '/\\G\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/') ;
995        $comment = $this->matchReg( '/\\G\/\*(?s).*?\*+\/\n?/' );
996        if ( $comment ) {
997            return new Less_Tree_Comment( $comment, false, $this->pos, $this->env->currentFileInfo );
998        }
999    }
1000
1001    private function parseComments() {
1002        $comments = [];
1003
1004        while ( $this->pos < $this->input_len ) {
1005            $comment = $this->parseComment();
1006            if ( !$comment ) {
1007                break;
1008            }
1009
1010            $comments[] = $comment;
1011        }
1012
1013        return $comments;
1014    }
1015
1016    /**
1017     * A string, which supports escaping " and '
1018     *
1019     *     "milky way" 'he\'s the one!'
1020     *
1021     * @return Less_Tree_Quoted|null
1022     * @see less-2.5.3.js#entities.quoted
1023     */
1024    private function parseEntitiesQuoted() {
1025        // Optimization: Determine match potential without save()/restore() overhead
1026        // Optimization: Inline matchChar() here, with its skipWhitespace(1) call below
1027        $startChar = $this->input[$this->pos] ?? null;
1028        $isEscaped = $startChar === '~';
1029        if ( !$isEscaped && $startChar !== "'" && $startChar !== '"' ) {
1030            return;
1031        }
1032
1033        $index = $this->pos;
1034        $this->save();
1035
1036        if ( $isEscaped ) {
1037            $this->skipWhitespace( 1 );
1038            $startChar = $this->input[$this->pos] ?? null;
1039            if ( $startChar !== "'" && $startChar !== '"' ) {
1040                $this->restore();
1041                return;
1042            }
1043        }
1044
1045        // Optimization: Inline matching of quotes for 8% overall speed up
1046        // on large LESS files. https://gerrit.wikimedia.org/r/939727
1047        // @see less-2.5.3.js#parserInput.$quoted
1048        $i = 1;
1049        while ( $this->pos + $i < $this->input_len ) {
1050            // Optimization: Skip over irrelevant chars without slow loop
1051            $i += strcspn( $this->input, "\n\r$startChar\\", $this->pos + $i );
1052            switch ( $this->input[$this->pos + $i++] ) {
1053                case "\\":
1054                    $i++;
1055                    break;
1056                case "\r":
1057                case "\n":
1058                    break 2;
1059                case $startChar:
1060                    $str = substr( $this->input, $this->pos, $i );
1061                    $this->skipWhitespace( $i );
1062                    $this->forget();
1063                    return new Less_Tree_Quoted( $str[0], substr( $str, 1, -1 ), $isEscaped, $index, $this->env->currentFileInfo );
1064            }
1065        }
1066
1067        $this->restore();
1068    }
1069
1070    /**
1071     * A catch-all word, such as:
1072     *
1073     *     black border-collapse
1074     *
1075     * @return Less_Tree_Keyword|Less_Tree_Color|null
1076     */
1077    private function parseEntitiesKeyword() {
1078        // $k = $this->matchReg('/\\G[_A-Za-z-][_A-Za-z0-9-]*/');
1079        $k = $this->matchReg( '/\\G%|\\G[_A-Za-z-][_A-Za-z0-9-]*/' );
1080        if ( $k ) {
1081            $color = Less_Tree_Color::fromKeyword( $k );
1082            if ( $color ) {
1083                return $color;
1084            }
1085            return new Less_Tree_Keyword( $k );
1086        }
1087    }
1088
1089    //
1090    // A function call
1091    //
1092    //     rgb(255, 0, 255)
1093    //
1094    // We also try to catch IE's `alpha()`, but let the `alpha` parser
1095    // deal with the details.
1096    //
1097    // The arguments are parsed with the `entities.arguments` parser.
1098    //
1099    // @see less-2.5.3.js#parsers.entities.call
1100    private function parseEntitiesCall() {
1101        $index = $this->pos;
1102
1103        if ( $this->peekReg( '/\\Gurl\(/i' ) ) {
1104            return;
1105        }
1106
1107        $this->save();
1108
1109        $name = $this->matchReg( '/\\G([\w-]+|%|progid:[\w\.]+)\(/' );
1110        if ( !$name ) {
1111            $this->forget();
1112            return;
1113        }
1114
1115        $name = $name[1];
1116        $nameLC = strtolower( $name );
1117
1118        if ( $nameLC === 'alpha' ) {
1119            $alpha_ret = $this->parseAlpha();
1120            if ( $alpha_ret ) {
1121                return $alpha_ret;
1122            }
1123        }
1124
1125        $args = $this->parseEntitiesArguments();
1126
1127        if ( !$this->matchChar( ')' ) ) {
1128            $this->restore();
1129            return;
1130        }
1131
1132        $this->forget();
1133        return new Less_Tree_Call( $name, $args, $index, $this->env->currentFileInfo );
1134    }
1135
1136    /**
1137     * Parse a list of arguments
1138     *
1139     * @return array<Less_Tree_Assignment|Less_Tree_Expression>
1140     */
1141    private function parseEntitiesArguments() {
1142        $args = [];
1143        while ( true ) {
1144            $arg = $this->parseEntitiesAssignment() ?? $this->parseExpression();
1145            if ( !$arg ) {
1146                break;
1147            }
1148
1149            $args[] = $arg;
1150            if ( !$this->matchChar( ',' ) ) {
1151                break;
1152            }
1153        }
1154        return $args;
1155    }
1156
1157    /** @return Less_Tree_Dimension|Less_Tree_Color|Less_Tree_Quoted|Less_Tree_UnicodeDescriptor|null */
1158    private function parseEntitiesLiteral() {
1159        return $this->parseEntitiesDimension() ?? $this->parseEntitiesColor() ?? $this->parseEntitiesQuoted() ?? $this->parseUnicodeDescriptor();
1160    }
1161
1162    /**
1163     * Assignments are argument entities for calls.
1164     *
1165     * They are present in IE filter properties as shown below.
1166     *
1167     *     filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )
1168     *
1169     * @return Less_Tree_Assignment|null
1170     * @see less-2.5.3.js#parsers.entities.assignment
1171     */
1172    private function parseEntitiesAssignment() {
1173        $key = $this->matchReg( '/\\G\w+(?=\s?=)/' );
1174        if ( !$key ) {
1175            return;
1176        }
1177
1178        if ( !$this->matchChar( '=' ) ) {
1179            return;
1180        }
1181
1182        $value = $this->parseEntity();
1183        if ( $value ) {
1184            return new Less_Tree_Assignment( $key, $value );
1185        }
1186    }
1187
1188    //
1189    // Parse url() tokens
1190    //
1191    // We use a specific rule for urls, because they don't really behave like
1192    // standard function calls. The difference is that the argument doesn't have
1193    // to be enclosed within a string, so it can't be parsed as an Expression.
1194    //
1195    private function parseEntitiesUrl() {
1196        $char = $this->input[$this->pos] ?? null;
1197        // Optimisation: 'u' check is specific to less.php
1198        if ( $char !== 'u' || !$this->matchReg( '/\\Gurl\(/' ) ) {
1199            return;
1200        }
1201
1202        $value = $this->parseEntitiesQuoted() ?? $this->parseEntitiesVariable() ?? $this->matchReg( '/\\Gdata\:.*?[^\)]+/' ) ?? $this->matchReg( '/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/' ) ?? null;
1203        if ( !$value ) {
1204            $value = '';
1205        }
1206
1207        $this->expectChar( ')' );
1208
1209        if ( $value instanceof Less_Tree_Quoted || $value instanceof Less_Tree_Variable ) {
1210            return new Less_Tree_Url( $value, $this->env->currentFileInfo );
1211        }
1212
1213        return new Less_Tree_Url( new Less_Tree_Anonymous( $value ), $this->env->currentFileInfo );
1214    }
1215
1216    /**
1217     * A Variable entity, such as `@fink`, in
1218     *
1219     *     width: @fink + 2px
1220     *
1221     * We use a different parser for variable definitions,
1222     * see `parsers.variable`.
1223     *
1224     * @return Less_Tree_Variable|null
1225     * @see less-2.5.3.js#parsers.entities.variable
1226     */
1227    private function parseEntitiesVariable() {
1228        $index = $this->pos;
1229        if ( $this->peekChar( '@' ) && ( $name = $this->matchReg( '/\\G@@?[\w-]+/' ) ) ) {
1230            return new Less_Tree_Variable( $name, $index, $this->env->currentFileInfo );
1231        }
1232    }
1233
1234    /**
1235     * A variable entity using the protective `{}` e.g. `@{var}`.
1236     *
1237     * @return Less_Tree_Variable|null
1238     */
1239    private function parseEntitiesVariableCurly() {
1240        $index = $this->pos;
1241
1242        if ( $this->input_len > ( $this->pos + 1 ) && $this->input[$this->pos] === '@' && ( $curly = $this->matchReg( '/\\G@\{([\w-]+)\}/' ) ) ) {
1243            return new Less_Tree_Variable( '@' . $curly[1], $index, $this->env->currentFileInfo );
1244        }
1245    }
1246
1247    /**
1248     * A Hexadecimal color
1249     *
1250     *     #4F3C2F
1251     *
1252     * `rgb` and `hsl` colors are parsed through the `entities.call` parser.
1253     *
1254     * @return Less_Tree_Color|null
1255     */
1256    private function parseEntitiesColor() {
1257        if ( $this->peekChar( '#' ) && ( $rgb = $this->matchReg( '/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/' ) ) ) {
1258            return new Less_Tree_Color( $rgb[1], 1, null, $rgb[0] );
1259        }
1260    }
1261
1262    /**
1263     * A Dimension, that is, a number and a unit
1264     *
1265     *     0.5em 95%
1266     *
1267     * @return Less_Tree_Dimension|null
1268     */
1269    private function parseEntitiesDimension() {
1270        $c = @ord( $this->input[$this->pos] );
1271
1272        // Is the first char of the dimension 0-9, '.', '+' or '-'
1273        if ( ( $c > 57 || $c < 43 ) || $c === 47 || $c == 44 ) {
1274            return;
1275        }
1276
1277        $value = $this->matchReg( '/\\G([+-]?\d*\.?\d+)(%|[a-z]+)?/i' );
1278        if ( $value ) {
1279            if ( isset( $value[2] ) ) {
1280                return new Less_Tree_Dimension( $value[1], $value[2] );
1281            }
1282            return new Less_Tree_Dimension( $value[1] );
1283        }
1284    }
1285
1286    /**
1287     * A unicode descriptor, as is used in unicode-range
1288     *
1289     * U+0?? or U+00A1-00A9
1290     *
1291     * @return Less_Tree_UnicodeDescriptor|null
1292     */
1293    private function parseUnicodeDescriptor() {
1294        // Optimization: Hardcode first char, to avoid matchReg() cost for common case
1295        $char = $this->input[$this->pos] ?? null;
1296        if ( $char !== 'U' ) {
1297            return;
1298        }
1299
1300        $ud = $this->matchReg( '/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/' );
1301        if ( $ud ) {
1302            return new Less_Tree_UnicodeDescriptor( $ud[0] );
1303        }
1304    }
1305
1306    /**
1307     * JavaScript code to be evaluated
1308     *
1309     *     `window.location.href`
1310     *
1311     * @return Less_Tree_JavaScript|null
1312     * @see less-2.5.3.js#parsers.entities.javascript
1313     */
1314    private function parseEntitiesJavascript() {
1315        // Optimization: Hardcode first char, to avoid save()/restore() overhead
1316        // Optimization: Inline matchChar(), with skipWhitespace(1) below
1317        $char = $this->input[$this->pos] ?? null;
1318        $isEscaped = $char === '~';
1319        if ( !$isEscaped && $char !== '`' ) {
1320            return;
1321        }
1322
1323        $index = $this->pos;
1324        $this->save();
1325
1326        if ( $isEscaped ) {
1327            $this->skipWhitespace( 1 );
1328            $char = $this->input[$this->pos] ?? null;
1329            if ( $char !== '`' ) {
1330                $this->restore();
1331                return;
1332            }
1333        }
1334
1335        $this->skipWhitespace( 1 );
1336        $js = $this->matchReg( '/\\G[^`]*`/' );
1337        if ( $js ) {
1338            $this->forget();
1339            return new Less_Tree_JavaScript( substr( $js, 0, -1 ), $index, $isEscaped );
1340        }
1341        $this->restore();
1342    }
1343
1344    //
1345    // The variable part of a variable definition. Used in the `rule` parser
1346    //
1347    //     @fink:
1348    //
1349    // @see less-2.5.3.js#parsers.variable
1350    private function parseVariable() {
1351        if ( $this->peekChar( '@' ) && ( $name = $this->matchReg( '/\\G(@[\w-]+)\s*:/' ) ) ) {
1352            return $name[1];
1353        }
1354    }
1355
1356    //
1357    // The variable part of a variable definition. Used in the `rule` parser
1358    //
1359    // @fink();
1360    //
1361    // @see less-2.5.3.js#parsers.rulesetCall
1362    private function parseRulesetCall() {
1363        if ( $this->peekChar( '@' ) && ( $name = $this->matchReg( '/\\G(@[\w-]+)\s*\(\s*\)\s*;/' ) ) ) {
1364            return new Less_Tree_RulesetCall( $name[1] );
1365        }
1366    }
1367
1368    //
1369    // extend syntax - used to extend selectors
1370    //
1371    // @see less-2.5.3.js#parsers.extend
1372    private function parseExtend( $isRule = false ) {
1373        $index = $this->pos;
1374        $extendList = [];
1375
1376        if ( !$this->matchStr( $isRule ? '&:extend(' : ':extend(' ) ) {
1377            return;
1378        }
1379
1380        do {
1381            $option = null;
1382            $elements = [];
1383            while ( true ) {
1384                $option = $this->matchReg( '/\\G(all)(?=\s*(\)|,))/' );
1385                if ( $option ) {
1386                    break;
1387                }
1388                $e = $this->parseElement();
1389                if ( !$e ) {
1390                    break;
1391                }
1392                $elements[] = $e;
1393            }
1394
1395            if ( $option ) {
1396                $option = $option[1];
1397            }
1398
1399            $extendList[] = new Less_Tree_Extend( new Less_Tree_Selector( $elements ), $option, $index );
1400
1401        } while ( $this->matchChar( "," ) );
1402
1403        $this->expect( '/\\G\)/' );
1404
1405        if ( $isRule ) {
1406            $this->expect( '/\\G;/' );
1407        }
1408
1409        return $extendList;
1410    }
1411
1412    //
1413    // A Mixin call, with an optional argument list
1414    //
1415    //     #mixins > .square(#fff);
1416    //     .rounded(4px, black);
1417    //     .button;
1418    //
1419    // The `while` loop is there because mixins can be
1420    // namespaced, but we only support the child and descendant
1421    // selector for now.
1422    //
1423    private function parseMixinCall() {
1424        $char = $this->input[$this->pos] ?? null;
1425        if ( $char !== '.' && $char !== '#' ) {
1426            return;
1427        }
1428
1429        $index = $this->pos;
1430        $this->save(); // stop us absorbing part of an invalid selector
1431
1432        $elements = $this->parseMixinCallElements();
1433
1434        if ( $elements ) {
1435
1436            if ( $this->matchChar( '(' ) ) {
1437                $returned = $this->parseMixinArgs( true );
1438                $args = $returned['args'];
1439                $this->expectChar( ')' );
1440            } else {
1441                $args = [];
1442            }
1443
1444            $important = $this->parseImportant();
1445
1446            if ( $this->parseEnd() ) {
1447                $this->forget();
1448                return new Less_Tree_Mixin_Call( $elements, $args, $index, $this->env->currentFileInfo, $important );
1449            }
1450        }
1451
1452        $this->restore();
1453    }
1454
1455    private function parseMixinCallElements() {
1456        $elements = [];
1457        $c = null;
1458
1459        while ( true ) {
1460            $elemIndex = $this->pos;
1461            $e = $this->matchReg( '/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/' );
1462            if ( !$e ) {
1463                break;
1464            }
1465            $elements[] = new Less_Tree_Element( $c, $e, $elemIndex, $this->env->currentFileInfo );
1466            $c = $this->matchChar( '>' );
1467        }
1468
1469        return $elements;
1470    }
1471
1472    /**
1473     * @param bool $isCall
1474     * @see less-2.5.3.js#parsers.mixin.args
1475     */
1476    private function parseMixinArgs( $isCall ) {
1477        $expressions = [];
1478        $argsSemiColon = [];
1479        $isSemiColonSeperated = null;
1480        $argsComma = [];
1481        $expressionContainsNamed = null;
1482        $name = null;
1483        $returner = [ 'args' => [], 'variadic' => false ];
1484
1485        $this->save();
1486
1487        while ( true ) {
1488            if ( $isCall ) {
1489                $arg = $this->parseDetachedRuleset() ?? $this->parseExpression();
1490            } else {
1491                $this->parseComments();
1492                if ( $this->input[ $this->pos ] === '.' && $this->matchStr( '...' ) ) {
1493                    $returner['variadic'] = true;
1494                    if ( $this->matchChar( ";" ) && !$isSemiColonSeperated ) {
1495                        $isSemiColonSeperated = true;
1496                    }
1497
1498                    if ( $isSemiColonSeperated ) {
1499                        $argsSemiColon[] = [ 'variadic' => true ];
1500                    } else {
1501                        $argsComma[] = [ 'variadic' => true ];
1502                    }
1503                    break;
1504                }
1505                $arg = $this->parseEntitiesVariable() ?? $this->parseEntitiesLiteral() ?? $this->parseEntitiesKeyword();
1506            }
1507
1508            if ( !$arg ) {
1509                break;
1510            }
1511
1512            $nameLoop = null;
1513            if ( $arg instanceof Less_Tree_Expression ) {
1514                $arg->throwAwayComments();
1515            }
1516            $value = $arg;
1517            $val = null;
1518
1519            if ( $isCall ) {
1520                // Variable
1521                if ( $value instanceof Less_Tree_Expression && count( $arg->value ) == 1 ) {
1522                    $val = $arg->value[0];
1523                }
1524            } else {
1525                $val = $arg;
1526            }
1527
1528            if ( $val instanceof Less_Tree_Variable ) {
1529
1530                if ( $this->matchChar( ':' ) ) {
1531                    if ( $expressions ) {
1532                        if ( $isSemiColonSeperated ) {
1533                            $this->Error( 'Cannot mix ; and , as delimiter types' );
1534                        }
1535                        $expressionContainsNamed = true;
1536                    }
1537
1538                    // we do not support setting a ruleset as a default variable - it doesn't make sense
1539                    // However if we do want to add it, there is nothing blocking it, just don't error
1540                    // and remove isCall dependency below
1541                    $value = $this->parseDetachedRuleset() ?? $this->parseExpression();
1542
1543                    if ( !$value ) {
1544                        if ( $isCall ) {
1545                            $this->Error( 'could not understand value for named argument' );
1546                        } else {
1547                            $this->restore();
1548                            $returner['args'] = [];
1549                            return $returner;
1550                        }
1551                    }
1552
1553                    $nameLoop = ( $name = $val->name );
1554                } elseif ( !$isCall && $this->matchStr( '...' ) ) {
1555                    $returner['variadic'] = true;
1556                    if ( $this->matchChar( ";" ) && !$isSemiColonSeperated ) {
1557                        $isSemiColonSeperated = true;
1558                    }
1559                    if ( $isSemiColonSeperated ) {
1560                        $argsSemiColon[] = [ 'name' => $arg->name, 'variadic' => true ];
1561                    } else {
1562                        $argsComma[] = [ 'name' => $arg->name, 'variadic' => true ];
1563                    }
1564                    break;
1565                } elseif ( !$isCall ) {
1566                    $name = $nameLoop = $val->name;
1567                    $value = null;
1568                }
1569            }
1570
1571            if ( $value ) {
1572                $expressions[] = $value;
1573            }
1574
1575            $argsComma[] = [ 'name' => $nameLoop, 'value' => $value ];
1576
1577            if ( $this->matchChar( ',' ) ) {
1578                continue;
1579            }
1580
1581            if ( $this->matchChar( ';' ) || $isSemiColonSeperated ) {
1582
1583                if ( $expressionContainsNamed ) {
1584                    $this->Error( 'Cannot mix ; and , as delimiter types' );
1585                }
1586
1587                $isSemiColonSeperated = true;
1588
1589                if ( count( $expressions ) > 1 ) {
1590                    $value = new Less_Tree_Value( $expressions );
1591                }
1592                $argsSemiColon[] = [ 'name' => $name, 'value' => $value ];
1593
1594                $name = null;
1595                $expressions = [];
1596                $expressionContainsNamed = false;
1597            }
1598        }
1599
1600        $this->forget();
1601        $returner['args'] = ( $isSemiColonSeperated ? $argsSemiColon : $argsComma );
1602        return $returner;
1603    }
1604
1605    //
1606    // A Mixin definition, with a list of parameters
1607    //
1608    //     .rounded (@radius: 2px, @color) {
1609    //        ...
1610    //     }
1611    //
1612    // Until we have a finer grained state-machine, we have to
1613    // do a look-ahead, to make sure we don't have a mixin call.
1614    // See the `rule` function for more information.
1615    //
1616    // We start by matching `.rounded (`, and then proceed on to
1617    // the argument list, which has optional default values.
1618    // We store the parameters in `params`, with a `value` key,
1619    // if there is a value, such as in the case of `@radius`.
1620    //
1621    // Once we've got our params list, and a closing `)`, we parse
1622    // the `{...}` block.
1623    //
1624    // @see less-2.5.3.js#parsers.mixin.definition
1625    private function parseMixinDefinition() {
1626        $cond = null;
1627
1628        $char = $this->input[$this->pos] ?? null;
1629        // TODO: Less.js doesn't limit this to $char == '{'.
1630        if ( ( $char !== '.' && $char !== '#' ) || ( $char === '{' && $this->peekReg( '/\\G[^{]*\}/' ) ) ) {
1631            return;
1632        }
1633
1634        $this->save();
1635
1636        $match = $this->matchReg( '/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/' );
1637        if ( $match ) {
1638            $name = $match[1];
1639
1640            $argInfo = $this->parseMixinArgs( false );
1641            $params = $argInfo['args'];
1642            $variadic = $argInfo['variadic'];
1643
1644            // .mixincall("@{a}");
1645            // looks a bit like a mixin definition..
1646            // also
1647            // .mixincall(@a: {rule: set;});
1648            // so we have to be nice and restore
1649            if ( !$this->matchChar( ')' ) ) {
1650                $this->restore();
1651                return;
1652            }
1653
1654            $this->parseComments();
1655
1656            if ( $this->matchStr( 'when' ) ) { // Guard
1657                $cond = $this->expect( 'parseConditions', 'Expected conditions' );
1658            }
1659
1660            $ruleset = $this->parseBlock();
1661
1662            if ( $ruleset !== null ) {
1663                $this->forget();
1664                return new Less_Tree_Mixin_Definition( $name, $params, $ruleset, $cond, $variadic );
1665            }
1666
1667            $this->restore();
1668        } else {
1669            $this->forget();
1670        }
1671    }
1672
1673    //
1674    // Entities are the smallest recognized token,
1675    // and can be found inside a rule's value.
1676    //
1677    private function parseEntity() {
1678        return $this->parseEntitiesLiteral() ??
1679            $this->parseEntitiesVariable() ??
1680            $this->parseEntitiesUrl() ??
1681            $this->parseEntitiesCall() ??
1682            $this->parseEntitiesKeyword() ??
1683            $this->parseEntitiesJavascript() ??
1684            $this->parseComment();
1685    }
1686
1687    //
1688    // A Rule terminator. Note that we use `peek()` to check for '}',
1689    // because the `block` rule will be expecting it, but we still need to make sure
1690    // it's there, if ';' was omitted.
1691    //
1692    private function parseEnd() {
1693        return $this->matchChar( ';' ) || $this->peekChar( '}' );
1694    }
1695
1696    //
1697    // IE's alpha function
1698    //
1699    //     alpha(opacity=88)
1700    //
1701    // @see less-2.5.3.js#parsers.alpha
1702    private function parseAlpha() {
1703        if ( !$this->matchReg( '/\\Gopacity=/i' ) ) {
1704            return;
1705        }
1706
1707        $value = $this->matchReg( '/\\G[0-9]+/' );
1708        if ( !$value ) {
1709            $value = $this->expect( 'parseEntitiesVariable', 'Could not parse alpha' );
1710        }
1711
1712        $this->expectChar( ')' );
1713        return new Less_Tree_Alpha( $value );
1714    }
1715
1716    /**
1717     * A Selector Element
1718     *
1719     *     div
1720     *     + h1
1721     *     #socks
1722     *     input[type="text"]
1723     *
1724     * Elements are the building blocks for Selectors,
1725     * they are made out of a `Combinator` (see combinator rule),
1726     * and an element name, such as a tag a class, or `*`.
1727     *
1728     * @return Less_Tree_Element|null
1729     * @see less-2.5.3.js#parsers.element
1730     */
1731    private function parseElement() {
1732        $c = $this->parseCombinator();
1733        $index = $this->pos;
1734
1735        $e = $this->matchReg( '/\\G(?:\d+\.\d+|\d+)%/' )
1736            ?? $this->matchReg( '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/' )
1737            ?? $this->matchChar( '*' )
1738            ?? $this->matchChar( '&' )
1739            ?? $this->parseAttribute()
1740            ?? $this->matchReg( '/\\G\([^&()@]+\)/' )
1741            ?? $this->matchReg( '/\\G[\.#:](?=@)/' )
1742            ?? $this->parseEntitiesVariableCurly();
1743
1744        if ( $e === null ) {
1745            $this->save();
1746            if ( $this->matchChar( '(' ) ) {
1747                if ( ( $v = $this->parseSelector() ) && $this->matchChar( ')' ) ) {
1748                    $e = new Less_Tree_Paren( $v );
1749                    $this->forget();
1750                } else {
1751                    $this->restore();
1752                }
1753            } else {
1754                $this->forget();
1755            }
1756        }
1757
1758        if ( $e !== null ) {
1759            return new Less_Tree_Element( $c, $e, $index, $this->env->currentFileInfo );
1760        }
1761    }
1762
1763    //
1764    // Combinators combine elements together, in a Selector.
1765    //
1766    // Because our parser isn't white-space sensitive, special care
1767    // has to be taken, when parsing the descendant combinator, ` `,
1768    // as it's an empty space. We have to check the previous character
1769    // in the input, to see if it's a ` ` character.
1770    //
1771    // @see less-2.5.3.js#parsers.combinator
1772    private function parseCombinator() {
1773        if ( $this->pos < $this->input_len ) {
1774            $c = $this->input[$this->pos];
1775            if ( $c === '/' ) {
1776                $this->save();
1777                $slashedCombinator = $this->matchReg( '/\\G\/[a-z]+\//i' );
1778                if ( $slashedCombinator ) {
1779                    $this->forget();
1780                    return $slashedCombinator;
1781                }
1782                $this->restore();
1783            }
1784
1785            // TODO: Figure out why less.js also handles '/' here, and implement with regression test.
1786            if ( $c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^' ) {
1787
1788                $this->pos++;
1789                if ( $c === '^' && $this->input[$this->pos] === '^' ) {
1790                    $c = '^^';
1791                    $this->pos++;
1792                }
1793
1794                $this->skipWhitespace( 0 );
1795
1796                return $c;
1797            }
1798
1799            if ( $this->pos > 0 && $this->isWhitespace( -1 ) ) {
1800                return ' ';
1801            }
1802        }
1803    }
1804
1805    /**
1806     * A CSS selector (see selector below)
1807     * with less extensions e.g. the ability to extend and guard
1808     *
1809     * @return Less_Tree_Selector|null
1810     * @see less-2.5.3.js#parsers.lessSelector
1811     */
1812    private function parseLessSelector() {
1813        return $this->parseSelector( true );
1814    }
1815
1816    /**
1817     * A CSS Selector
1818     *
1819     *     .class > div + h1
1820     *     li a:hover
1821     *
1822     * Selectors are made out of one or more Elements, see ::parseElement.
1823     *
1824     * @return Less_Tree_Selector|null
1825     * @see less-2.5.3.js#parsers.selector
1826     */
1827    private function parseSelector( $isLess = false ) {
1828        $elements = [];
1829        $extendList = [];
1830        $condition = null;
1831        $when = false;
1832        $extend = false;
1833        $e = null;
1834        $c = null;
1835        $index = $this->pos;
1836
1837        while ( ( $isLess && ( $extend = $this->parseExtend() ) ) || ( $isLess && ( $when = $this->matchStr( 'when' ) ) ) || ( $e = $this->parseElement() ) ) {
1838            if ( $when ) {
1839                $condition = $this->expect( 'parseConditions', 'expected condition' );
1840            } elseif ( $condition ) {
1841                // error("CSS guard can only be used at the end of selector");
1842            } elseif ( $extend ) {
1843                $extendList = array_merge( $extendList, $extend );
1844            } else {
1845                // if( count($extendList) ){
1846                //error("Extend can only be used at the end of selector");
1847                //}
1848                if ( $this->pos < $this->input_len ) {
1849                    $c = $this->input[ $this->pos ];
1850                }
1851                $elements[] = $e;
1852                $e = null;
1853            }
1854
1855            if ( $c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')' ) {
1856                break;
1857            }
1858        }
1859
1860        if ( $elements ) {
1861            return new Less_Tree_Selector( $elements, $extendList, $condition, $index, $this->env->currentFileInfo );
1862        }
1863        if ( $extendList ) {
1864            $this->Error( 'Extend must be used to extend a selector, it cannot be used on its own' );
1865        }
1866    }
1867
1868    /**
1869     * @return Less_Tree_Attribute|null
1870     * @see less-2.5.3.js#parsers.block
1871     */
1872    private function parseAttribute() {
1873        $val = null;
1874
1875        if ( !$this->matchChar( '[' ) ) {
1876            return;
1877        }
1878
1879        $key = $this->parseEntitiesVariableCurly();
1880        if ( !$key ) {
1881            $key = $this->expect( '/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/' );
1882        }
1883
1884        $op = $this->matchReg( '/\\G[|~*$^]?=/' );
1885        if ( $op ) {
1886            $val = $this->parseEntitiesQuoted() ?? $this->matchReg( '/\\G[0-9]+%/' ) ?? $this->matchReg( '/\\G[\w-]+/' ) ?? $this->parseEntitiesVariableCurly();
1887        }
1888
1889        $this->expectChar( ']' );
1890
1891        return new Less_Tree_Attribute( $key, $op, $val );
1892    }
1893
1894    /**
1895     * The `block` rule is used by `ruleset` and `mixin.definition`.
1896     * It's a wrapper around the `primary` rule, with added `{}`.
1897     *
1898     * @return array<Less_Tree>|null
1899     * @see less-2.5.3.js#parsers.block
1900     */
1901    private function parseBlock() {
1902        if ( $this->matchChar( '{' ) ) {
1903            $content = $this->parsePrimary();
1904            if ( $this->matchChar( '}' ) ) {
1905                return $content;
1906            }
1907        }
1908    }
1909
1910    private function parseBlockRuleset() {
1911        $block = $this->parseBlock();
1912        if ( $block !== null ) {
1913            return new Less_Tree_Ruleset( null, $block );
1914        }
1915    }
1916
1917    /** @return Less_Tree_DetachedRuleset|null */
1918    private function parseDetachedRuleset() {
1919        $blockRuleset = $this->parseBlockRuleset();
1920        if ( $blockRuleset ) {
1921            return new Less_Tree_DetachedRuleset( $blockRuleset );
1922        }
1923    }
1924
1925    /**
1926     * Ruleset such as:
1927     *
1928     *     div, .class, body > p {
1929     *     }
1930     *
1931     * @return Less_Tree_Ruleset|null
1932     * @see less-2.5.3.js#parsers.ruleset
1933     */
1934    private function parseRuleset() {
1935        $selectors = [];
1936
1937        $this->save();
1938
1939        while ( true ) {
1940            $s = $this->parseLessSelector();
1941            if ( !$s ) {
1942                break;
1943            }
1944            $selectors[] = $s;
1945            $this->parseComments();
1946
1947            if ( $s->condition && count( $selectors ) > 1 ) {
1948                $this->Error( 'Guards are only currently allowed on a single selector.' );
1949            }
1950
1951            if ( !$this->matchChar( ',' ) ) {
1952                break;
1953            }
1954            if ( $s->condition ) {
1955                $this->Error( 'Guards are only currently allowed on a single selector.' );
1956            }
1957            $this->parseComments();
1958        }
1959
1960        if ( $selectors ) {
1961            $rules = $this->parseBlock();
1962            if ( is_array( $rules ) ) {
1963                $this->forget();
1964                // TODO: Less_Environment::$strictImports is not yet ported
1965                // It is passed here by less.js
1966                return new Less_Tree_Ruleset( $selectors, $rules );
1967            }
1968        }
1969
1970        // Backtrack
1971        $this->restore();
1972    }
1973
1974    /**
1975     * Custom less.php parse function for finding simple name-value css pairs
1976     * ex: width:100px;
1977     */
1978    private function parseNameValue() {
1979        $index = $this->pos;
1980        $this->save();
1981
1982        $match = $this->matchReg( '/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?\s*) *(! *important)?\s*([;}])/' );
1983        if ( $match ) {
1984
1985            if ( $match[4] == '}' ) {
1986                $this->pos = $index + strlen( $match[0] ) - 1;
1987                $match[2] = rtrim( $match[2] );
1988            }
1989
1990            if ( $match[3] ) {
1991                $match[2] .= $match[3];
1992            }
1993            $this->forget();
1994            return new Less_Tree_NameValue( $match[1], $match[2], $index, $this->env->currentFileInfo );
1995        }
1996
1997        $this->restore();
1998    }
1999
2000    // @see less-2.5.3.js#parsers.rule
2001    private function parseRule( $tryAnonymous = null ) {
2002        $value = null;
2003        $startOfRule = $this->pos;
2004        $c = $this->input[$this->pos] ?? null;
2005        $important = null;
2006        $merge = false;
2007
2008        // TODO: Figure out why less.js also handles ':' here, and implement with regression test.
2009        if ( $c === '.' || $c === '#' || $c === '&' ) {
2010            return;
2011        }
2012
2013        $this->save();
2014        $name = $this->parseVariable() ?? $this->parseRuleProperty();
2015
2016        if ( $name ) {
2017            $isVariable = is_string( $name );
2018
2019            if ( $isVariable ) {
2020                $value = $this->parseDetachedRuleset();
2021            }
2022            $this->parseComments();
2023            if ( !$value ) {
2024                // a name returned by this.ruleProperty() is always an array of the form:
2025                // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"]
2026                // where each item is a tree.Keyword or tree.Variable
2027                if ( !$isVariable && count( $name ) > 1 ) {
2028                    $merge = array_pop( $name )->value;
2029                }
2030
2031                // prefer to try to parse first if its a variable or we are compressing
2032                // but always fallback on the other one
2033                $tryValueFirst = ( !$tryAnonymous && ( self::$options['compress'] || $isVariable ) );
2034                if ( $tryValueFirst ) {
2035                    $value = $this->parseValue();
2036                }
2037                if ( !$value ) {
2038                    $value = $this->parseAnonymousValue();
2039                    if ( $value ) {
2040                        $this->forget();
2041                        // anonymous values absorb the end ';' which is required for them to work
2042                        return new Less_Tree_Rule( $name, $value, false, $merge, $startOfRule, $this->env->currentFileInfo );
2043                    }
2044                }
2045                if ( !$tryValueFirst && !$value ) {
2046                    $value = $this->parseValue();
2047                }
2048
2049                $important = $this->parseImportant();
2050            }
2051
2052            if ( $value && $this->parseEnd() ) {
2053                $this->forget();
2054                return new Less_Tree_Rule( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo );
2055            } else {
2056                $this->restore();
2057                if ( $value && !$tryAnonymous ) {
2058                    return $this->parseRule( true );
2059                }
2060            }
2061        } else {
2062            $this->forget();
2063        }
2064    }
2065
2066    private function parseAnonymousValue() {
2067        $match = $this->matchReg( '/\\G([^@+\/\'"*`(;{}-]*);/' );
2068        if ( $match ) {
2069            return new Less_Tree_Anonymous( $match[1] );
2070        }
2071    }
2072
2073    //
2074    // An @import directive
2075    //
2076    //     @import "lib";
2077    //
2078    // Depending on our environment, importing is done differently:
2079    // In the browser, it's an XHR request, in Node, it would be a
2080    // file-system operation. The function used for importing is
2081    // stored in `import`, which we pass to the Import constructor.
2082    //
2083    private function parseImport() {
2084        $this->save();
2085
2086        $dir = $this->matchReg( '/\\G@import?\s+/' );
2087
2088        if ( $dir ) {
2089            $options = $this->parseImportOptions();
2090            $path = $this->parseEntitiesQuoted() ?? $this->parseEntitiesUrl();
2091
2092            if ( $path ) {
2093                $features = $this->parseMediaFeatures();
2094                if ( $this->matchChar( ';' ) ) {
2095                    if ( $features ) {
2096                        $features = new Less_Tree_Value( $features );
2097                    }
2098
2099                    $this->forget();
2100                    return new Less_Tree_Import( $path, $features, $options, $this->pos, $this->env->currentFileInfo );
2101                }
2102            }
2103        }
2104
2105        $this->restore();
2106    }
2107
2108    private function parseImportOptions() {
2109        $options = [];
2110
2111        // list of options, surrounded by parens
2112        if ( !$this->matchChar( '(' ) ) {
2113            return $options;
2114        }
2115        do {
2116            $optionName = $this->parseImportOption();
2117            if ( $optionName ) {
2118                $value = true;
2119                switch ( $optionName ) {
2120                    case "css":
2121                        $optionName = "less";
2122                        $value = false;
2123                        break;
2124                    case "once":
2125                        $optionName = "multiple";
2126                        $value = false;
2127                        break;
2128                }
2129                $options[$optionName] = $value;
2130                if ( !$this->matchChar( ',' ) ) {
2131                    break;
2132                }
2133            }
2134        } while ( $optionName );
2135        $this->expectChar( ')' );
2136        return $options;
2137    }
2138
2139    private function parseImportOption() {
2140        $opt = $this->matchReg( '/\\G(less|css|multiple|once|inline|reference|optional)/' );
2141        if ( $opt ) {
2142            return $opt[1];
2143        }
2144    }
2145
2146    private function parseMediaFeature() {
2147        $nodes = [];
2148
2149        do {
2150            $e = $this->parseEntitiesKeyword() ?? $this->parseEntitiesVariable();
2151            if ( $e ) {
2152                $nodes[] = $e;
2153            } elseif ( $this->matchChar( '(' ) ) {
2154                $p = $this->parseProperty();
2155                $e = $this->parseValue();
2156                if ( $this->matchChar( ')' ) ) {
2157                    if ( $p && $e ) {
2158                        $r = new Less_Tree_Rule( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true );
2159                        $nodes[] = new Less_Tree_Paren( $r );
2160                    } elseif ( $e ) {
2161                        $nodes[] = new Less_Tree_Paren( $e );
2162                    } else {
2163                        return null;
2164                    }
2165                } else {
2166                    return null;
2167                }
2168            }
2169        } while ( $e );
2170
2171        if ( $nodes ) {
2172            return new Less_Tree_Expression( $nodes );
2173        }
2174    }
2175
2176    private function parseMediaFeatures() {
2177        $features = [];
2178
2179        do {
2180            $e = $this->parseMediaFeature();
2181            if ( $e ) {
2182                $features[] = $e;
2183                if ( !$this->matchChar( ',' ) ) {
2184                    break;
2185                }
2186            } else {
2187                $e = $this->parseEntitiesVariable();
2188                if ( $e ) {
2189                    $features[] = $e;
2190                    if ( !$this->matchChar( ',' ) ) {
2191                        break;
2192                    }
2193                }
2194            }
2195        } while ( $e );
2196
2197        return $features ?: null;
2198    }
2199
2200    /**
2201     * @see less-2.5.3.js#parsers.media
2202     */
2203    private function parseMedia() {
2204        if ( $this->matchStr( '@media' ) ) {
2205            $this->save();
2206
2207            $features = $this->parseMediaFeatures();
2208            $rules = $this->parseBlock();
2209
2210            if ( $rules === null ) {
2211                $this->restore();
2212                return;
2213            }
2214
2215            $this->forget();
2216            return new Less_Tree_Media( $rules, $features, $this->pos, $this->env->currentFileInfo );
2217        }
2218    }
2219
2220    //
2221    // A CSS Directive
2222    //
2223    // @charset "utf-8";
2224    //
2225    // @see less-2.5.3.js#parsers.directive
2226    private function parseDirective() {
2227        if ( !$this->peekChar( '@' ) ) {
2228            return;
2229        }
2230
2231        $rules = null;
2232        $index = $this->pos;
2233        $hasBlock = true;
2234        $hasIdentifier = false;
2235        $hasExpression = false;
2236        $hasUnknown = false;
2237        $isRooted = true;
2238
2239        $value = $this->parseImport() ?? $this->parseMedia();
2240        if ( $value ) {
2241            return $value;
2242        }
2243
2244        $this->save();
2245
2246        $name = $this->matchReg( '/\\G@[a-z-]+/' );
2247
2248        if ( !$name ) {
2249            return;
2250        }
2251
2252        $nonVendorSpecificName = $name;
2253        $pos = strpos( $name, '-', 2 );
2254        if ( $name[1] == '-' && $pos > 0 ) {
2255            $nonVendorSpecificName = "@" . substr( $name, $pos + 1 );
2256        }
2257
2258        switch ( $nonVendorSpecificName ) {
2259            /*
2260            case "@font-face":
2261            case "@viewport":
2262            case "@top-left":
2263            case "@top-left-corner":
2264            case "@top-center":
2265            case "@top-right":
2266            case "@top-right-corner":
2267            case "@bottom-left":
2268            case "@bottom-left-corner":
2269            case "@bottom-center":
2270            case "@bottom-right":
2271            case "@bottom-right-corner":
2272            case "@left-top":
2273            case "@left-middle":
2274            case "@left-bottom":
2275            case "@right-top":
2276            case "@right-middle":
2277            case "@right-bottom":
2278            hasBlock = true;
2279            isRooted = true;
2280            break;
2281            */
2282            case "@counter-style":
2283                $hasIdentifier = true;
2284                break;
2285            case "@charset":
2286                $hasIdentifier = true;
2287                $hasBlock = false;
2288                break;
2289            case "@namespace":
2290                $hasExpression = true;
2291                $hasBlock = false;
2292                break;
2293            case "@keyframes":
2294                $hasIdentifier = true;
2295                break;
2296            case "@host":
2297            case "@page":
2298                $hasUnknown = true;
2299                break;
2300            case "@document":
2301            case "@supports":
2302                $hasUnknown = true;
2303                $isRooted = false;
2304                break;
2305        }
2306        // TODO: T353132 - differs from less.js - we don't have the ParserInput yet
2307        $this->parseComments();
2308
2309        if ( $hasIdentifier ) {
2310            $value = $this->parseEntity();
2311            if ( !$value ) {
2312                $this->error( "expected " . $name . " identifier" );
2313            }
2314        } elseif ( $hasExpression ) {
2315            $value = $this->parseExpression();
2316            if ( !$value ) {
2317                $this->error( "expected " . $name . " expression" );
2318            }
2319        } elseif ( $hasUnknown ) {
2320
2321            $value = $this->matchReg( '/\\G[^{;]+/' );
2322            if ( $value ) {
2323                $value = new Less_Tree_Anonymous( trim( $value ) );
2324            }
2325        }
2326
2327        // TODO: T353132 - differs from less.js - we don't have the ParserInput yet
2328        $this->parseComments();
2329
2330        if ( $hasBlock ) {
2331            $rules = $this->parseBlockRuleset();
2332        }
2333
2334        if ( $rules || ( !$hasBlock && $value && $this->matchChar( ';' ) ) ) {
2335            $this->forget();
2336            return new Less_Tree_Directive( $name, $value, $rules, $index, $isRooted, $this->env->currentFileInfo );
2337        }
2338
2339        $this->restore();
2340    }
2341
2342    //
2343    // A Value is a comma-delimited list of Expressions
2344    //
2345    //     font-family: Baskerville, Georgia, serif;
2346    //
2347    // In a Rule, a Value represents everything after the `:`,
2348    // and before the `;`.
2349    //
2350    private function parseValue() {
2351        $expressions = [];
2352
2353        do {
2354            $e = $this->parseExpression();
2355            if ( $e ) {
2356                $expressions[] = $e;
2357                if ( !$this->matchChar( ',' ) ) {
2358                    break;
2359                }
2360            }
2361        } while ( $e );
2362
2363        if ( $expressions ) {
2364            return new Less_Tree_Value( $expressions );
2365        }
2366    }
2367
2368    private function parseImportant() {
2369        if ( $this->peekChar( '!' ) && $this->matchReg( '/\\G! *important/' ) ) {
2370            return ' !important';
2371        }
2372    }
2373
2374    private function parseSub() {
2375        $this->save();
2376        if ( $this->matchChar( '(' ) ) {
2377            $a = $this->parseAddition();
2378            if ( $a && $this->matchChar( ')' ) ) {
2379                $this->forget();
2380                return new Less_Tree_Expression( [ $a ], true );
2381            }
2382        }
2383        $this->restore();
2384    }
2385
2386    /**
2387     * Parses multiplication operation
2388     *
2389     * @return Less_Tree_Operation|null
2390     */
2391    private function parseMultiplication() {
2392        $return = $m = $this->parseOperand();
2393        if ( $return ) {
2394            while ( true ) {
2395
2396                $isSpaced = $this->isWhitespace( -1 );
2397
2398                if ( $this->peekReg( '/\\G\/[*\/]/' ) ) {
2399                    break;
2400                }
2401                $this->save();
2402
2403                $op = $this->matchChar( '/' );
2404                if ( !$op ) {
2405                    $op = $this->matchChar( '*' );
2406                    if ( !$op ) {
2407                        $this->forget();
2408                        break;
2409                    }
2410                }
2411
2412                $a = $this->parseOperand();
2413
2414                if ( !$a ) {
2415                    $this->restore();
2416                    break;
2417                }
2418                $this->forget();
2419
2420                $m->parensInOp = true;
2421                $a->parensInOp = true;
2422                $return = new Less_Tree_Operation( $op, [ $return, $a ], $isSpaced );
2423            }
2424        }
2425        return $return;
2426    }
2427
2428    /**
2429     * Parses an addition operation
2430     *
2431     * @return Less_Tree_Operation|null
2432     */
2433    private function parseAddition() {
2434        $return = $m = $this->parseMultiplication();
2435        if ( $return ) {
2436            while ( true ) {
2437
2438                $isSpaced = $this->isWhitespace( -1 );
2439
2440                $op = $this->matchReg( '/\\G[-+]\s+/' );
2441                if ( !$op ) {
2442                    if ( !$isSpaced ) {
2443                        $op = $this->matchChar( '+' ) ?? $this->matchChar( '-' );
2444                    }
2445                    if ( !$op ) {
2446                        break;
2447                    }
2448                }
2449
2450                $a = $this->parseMultiplication();
2451                if ( !$a ) {
2452                    break;
2453                }
2454
2455                $m->parensInOp = true;
2456                $a->parensInOp = true;
2457                $return = new Less_Tree_Operation( $op, [ $return, $a ], $isSpaced );
2458            }
2459        }
2460
2461        return $return;
2462    }
2463
2464    /**
2465     * Parses the conditions
2466     *
2467     * @return Less_Tree_Condition|null
2468     */
2469    private function parseConditions() {
2470        $index = $this->pos;
2471        $return = $a = $this->parseCondition();
2472        if ( $a ) {
2473            while ( true ) {
2474                if ( !$this->peekReg( '/\\G,\s*(not\s*)?\(/' ) || !$this->matchChar( ',' ) ) {
2475                    break;
2476                }
2477                $b = $this->parseCondition();
2478                if ( !$b ) {
2479                    break;
2480                }
2481
2482                $return = new Less_Tree_Condition( 'or', $return, $b, $index );
2483            }
2484            return $return;
2485        }
2486    }
2487
2488    /**
2489     * @see less-2.5.3.js#parsers.condition
2490     */
2491    private function parseCondition() {
2492        $index = $this->pos;
2493        $negate = false;
2494        $c = null;
2495
2496        if ( $this->matchStr( 'not' ) ) {
2497            $negate = true;
2498        }
2499        $this->expectChar( '(' );
2500        $a = $this->parseAddition() ?? $this->parseEntitiesKeyword() ?? $this->parseEntitiesQuoted();
2501
2502        if ( $a ) {
2503            $op = $this->matchReg( '/\\G(?:>=|<=|=<|[<=>])/' );
2504            if ( $op ) {
2505                $b = $this->parseAddition() ?? $this->parseEntitiesKeyword() ?? $this->parseEntitiesQuoted();
2506                if ( $b ) {
2507                    $c = new Less_Tree_Condition( $op, $a, $b, $index, $negate );
2508                } else {
2509                    $this->Error( 'Unexpected expression' );
2510                }
2511            } else {
2512                $k = new Less_Tree_Keyword( 'true' );
2513                $c = new Less_Tree_Condition( '=', $a, $k, $index, $negate );
2514            }
2515            $this->expectChar( ')' );
2516            // @phan-suppress-next-line PhanPossiblyInfiniteRecursionSameParams
2517            return $this->matchStr( 'and' ) ? new Less_Tree_Condition( 'and', $c, $this->parseCondition() ) : $c;
2518        }
2519    }
2520
2521    /**
2522     * An operand is anything that can be part of an operation,
2523     * such as a Color, or a Variable
2524     */
2525    private function parseOperand() {
2526        $negate = false;
2527        $offset = $this->pos + 1;
2528        if ( $offset >= $this->input_len ) {
2529            return;
2530        }
2531        $char = $this->input[$offset];
2532        if ( $char === '@' || $char === '(' ) {
2533            $negate = $this->matchChar( '-' );
2534        }
2535
2536        $o = $this->parseSub() ?? $this->parseEntitiesDimension() ?? $this->parseEntitiesColor() ?? $this->parseEntitiesVariable() ?? $this->parseEntitiesCall();
2537
2538        if ( $negate ) {
2539            $o->parensInOp = true;
2540            $o = new Less_Tree_Negative( $o );
2541        }
2542
2543        return $o;
2544    }
2545
2546    /**
2547     * Expressions either represent mathematical operations,
2548     * or white-space delimited Entities.
2549     *
2550     * @return Less_Tree_Expression|null
2551     */
2552    private function parseExpression() {
2553        $entities = [];
2554
2555        do {
2556            $e = $this->parseAddition() ?? $this->parseEntity();
2557            if ( $e ) {
2558                $entities[] = $e;
2559                // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here
2560                if ( !$this->peekReg( '/\\G\/[\/*]/' ) ) {
2561                    $delim = $this->matchChar( '/' );
2562                    if ( $delim ) {
2563                        $entities[] = new Less_Tree_Anonymous( $delim );
2564                    }
2565                }
2566            }
2567        } while ( $e );
2568
2569        if ( $entities ) {
2570            return new Less_Tree_Expression( $entities );
2571        }
2572    }
2573
2574    /**
2575     * Parse a property
2576     * eg: 'min-width', 'orientation', etc
2577     *
2578     * @return string
2579     */
2580    private function parseProperty() {
2581        $name = $this->matchReg( '/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/' );
2582        if ( $name ) {
2583            return $name[1];
2584        }
2585    }
2586
2587    /**
2588     * Parse a rule property
2589     * eg: 'color', 'width', 'height', etc
2590     *
2591     * @return array<Less_Tree_Keyword|Less_Tree_Variable>
2592     * @see less-2.5.3.js#parsers.ruleProperty
2593     */
2594    private function parseRuleProperty() {
2595        $name = [];
2596        $index = [];
2597
2598        $this->save();
2599
2600        $simpleProperty = $this->matchReg( '/\\G([_a-zA-Z0-9-]+)\s*:/' );
2601        if ( $simpleProperty ) {
2602            $name[] = new Less_Tree_Keyword( $simpleProperty[1] );
2603            $this->forget();
2604            return $name;
2605        }
2606
2607        $this->rulePropertyMatch( '/\\G(\*?)/', $index, $name );
2608
2609        // Consume!
2610        // @phan-suppress-next-line PhanPluginEmptyStatementWhileLoop
2611        while ( $this->rulePropertyMatch( '/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/', $index, $name ) );
2612        // @phan-suppress-next-line PhanPluginEmptyStatementWhileLoop
2613        while ( $this->rulePropertyCutOutBlockComments() );
2614
2615        if ( ( count( $name ) > 1 ) && $this->rulePropertyMatch( '/\\G((?:\+_|\+)?)\s*:/', $index, $name ) ) {
2616            $this->forget();
2617
2618            // at last, we have the complete match now. move forward,
2619            // convert name particles to tree objects and return:
2620            if ( $name[0] === '' ) {
2621                array_shift( $name );
2622                array_shift( $index );
2623            }
2624            foreach ( $name as $k => $s ) {
2625                if ( !$s || $s[0] !== '@' ) {
2626                    $name[$k] = new Less_Tree_Keyword( $s );
2627                } else {
2628                    $name[$k] = new Less_Tree_Variable( '@' . substr( $s, 2, -1 ), $index[$k], $this->env->currentFileInfo );
2629                }
2630            }
2631            return $name;
2632        } else {
2633            $this->restore();
2634        }
2635    }
2636
2637    private function rulePropertyMatch( $re, &$index, &$name ) {
2638        $i = $this->pos;
2639        $chunk = $this->matchReg( $re );
2640        if ( $chunk ) {
2641            $index[] = $i;
2642            $name[] = $chunk[1];
2643            return true;
2644        }
2645    }
2646
2647    private function rulePropertyCutOutBlockComments() {
2648        // not the same as less.js to prevent fatal errors
2649        // similar to parseComment()
2650        //    '/\\G\s*\/\*(?:[^*]|\*+[^\/*])*\*+\//'
2651        $re = '/\\G\s*\/\*(?s).*?\*+\//';
2652        $chunk = $this->matchReg( $re );
2653        return $chunk != null;
2654    }
2655
2656    public static function serializeVars( $vars ) {
2657        $s = '';
2658
2659        foreach ( $vars as $name => $value ) {
2660            $s .= ( ( $name[0] === '@' ) ? '' : '@' ) . $name . ': ' . $value . ( ( substr( $value, -1 ) === ';' ) ? '' : ';' );
2661        }
2662
2663        return $s;
2664    }
2665
2666    /**
2667     * Some versions of PHP have trouble with method_exists($a,$b) if $a is not an object
2668     *
2669     * @param mixed $a
2670     * @param string $b
2671     */
2672    public static function is_method( $a, $b ) {
2673        return is_object( $a ) && method_exists( $a, $b );
2674    }
2675
2676    /**
2677     * Round numbers similarly to javascript
2678     * eg: 1.499999 to 1 instead of 2
2679     */
2680    public static function round( $input, $precision = 0 ) {
2681        $precision = pow( 10, $precision );
2682        $i = $input * $precision;
2683
2684        $ceil = ceil( $i );
2685        $floor = floor( $i );
2686        if ( ( $ceil - $i ) <= ( $i - $floor ) ) {
2687            return $ceil / $precision;
2688        } else {
2689            return $floor / $precision;
2690        }
2691    }
2692
2693    /** @return never */
2694    public function Error( $msg ) {
2695        throw new Less_Exception_Parser( $msg, null, $this->furthest, $this->env->currentFileInfo );
2696    }
2697
2698    public static function WinPath( $path ) {
2699        return str_replace( '\\', '/', $path );
2700    }
2701
2702    public static function AbsPath( $path, $winPath = false ) {
2703        if ( strpos( $path, '//' ) !== false && preg_match( '/^(https?:)?\/\//i', $path ) ) {
2704            return $winPath ? '' : false;
2705        } else {
2706            $path = realpath( $path );
2707            if ( $winPath ) {
2708                $path = self::WinPath( $path );
2709            }
2710            return $path;
2711        }
2712    }
2713
2714    public function CacheEnabled() {
2715        return ( self::$options['cache_method'] && ( Less_Cache::$cache_dir || ( self::$options['cache_method'] == 'callback' ) ) );
2716    }
2717
2718}