Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.56% covered (success)
90.56%
1382 / 1526
60.53% covered (warning)
60.53%
69 / 114
CRAP
0.00% covered (danger)
0.00%
0 / 1
Less_Parser
90.56% covered (success)
90.56%
1382 / 1526
60.53% covered (warning)
60.53%
69 / 114
1007.11
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 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
96.15% covered (success)
96.15%
25 / 26
0.00% covered (danger)
0.00%
0 / 1
13
 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
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
6
 findValueOf
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 getVariables
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 findVarByName
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 getVariableValue
93.10% covered (success)
93.10%
27 / 29
0.00% covered (danger)
0.00%
0 / 1
26.22
 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
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
8.01
 ModifyVars
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 SetFileInfo
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 SetCacheDir
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 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
70.00% covered (warning)
70.00%
21 / 30
0.00% covered (danger)
0.00%
0 / 1
17.56
 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%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 cacheFile
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 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
 parseQuoted
90.91% covered (success)
90.91%
20 / 22
0.00% covered (danger)
0.00%
0 / 1
10.08
 parseUntil
90.36% covered (success)
90.36%
75 / 83
0.00% covered (danger)
0.00%
0 / 1
23.47
 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%
31 / 31
100.00% covered (success)
100.00%
1 / 1
11
 expect
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 expectChar
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 parserInputStart
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 parseNode
60.87% covered (warning)
60.87%
14 / 23
0.00% covered (danger)
0.00%
0 / 1
8.16
 parsePrimary
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
9
 parseComment
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 parseEntitiesMixinLookup
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseEntitiesQuoted
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 parseEntitiesKeyword
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 parseEntitiesCall
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
8
 parseEntitiesArguments
81.82% covered (warning)
81.82%
18 / 22
0.00% covered (danger)
0.00%
0 / 1
11.73
 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
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
7
 parseEntitiesVariable
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 parseEntitiesVariableCurly
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 parseEntitiesProperty
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 parseEntitiesPropertyCurly
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 parseEntitiesColor
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 parseEntitiesDimension
100.00% covered (success)
100.00%
11 / 11
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%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 parseVariableCall
90.91% covered (success)
90.91%
20 / 22
0.00% covered (danger)
0.00%
0 / 1
11.09
 parseExtend
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
8
 parseMixinCall
97.37% covered (success)
97.37%
37 / 38
0.00% covered (danger)
0.00%
0 / 1
17
 parseMixinCallElements
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 parseMixinArgs
87.65% covered (warning)
87.65%
71 / 81
0.00% covered (danger)
0.00%
0 / 1
35.05
 parseMixinRuleLookups
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 parseLookupValue
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
5.39
 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%
9 / 9
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
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 parseElement
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
6
 parseCombinator
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
13.03
 parseLessSelector
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseSelector
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
18
 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
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 parseDeclaration
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
1 / 1
23
 parseAnonymousValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 parsePermissiveValue
93.94% covered (success)
93.94%
62 / 66
0.00% covered (danger)
0.00%
0 / 1
16.06
 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
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
8.09
 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
 parseAtRule
91.18% covered (success)
91.18%
62 / 68
0.00% covered (danger)
0.00%
0 / 1
29.58
 parseValue
100.00% covered (success)
100.00%
10 / 10
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%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 parseMultiplication
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 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
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 parseConditionAnd
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 negatedCondition
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 parenthesisCondition
82.14% covered (warning)
82.14%
23 / 28
0.00% covered (danger)
0.00%
0 / 1
7.28
 atomicCondition
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
4.00
 parseOperand
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
6
 parseExpression
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
7.01
 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%
26 / 26
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
 serializeVars
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 round
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 Error
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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
2declare( strict_types = 1 );
3
4/**
5 * Parse and compile Less files into CSS
6 */
7class Less_Parser {
8
9    /**
10     * Default parser options
11     * @var array<string,mixed>
12     */
13    public static $default_options = [
14        // whether to compress
15        'compress' => false,
16        // whether units need to evaluate correctly
17        'strictUnits' => false,
18        // How to process math
19        //
20        //   always           - eagerly try to solve all operations
21        //   parens-division  - require parens for division "/"
22        //   parens | strict  - require parens for all operations
23        //
24        // NOTE: We use the default of Less.js 4.0 (parens-division)
25        //       instead of Less.js 3.13 (always).
26        'math' => 'parens-division',
27        // whether to adjust URL's to be relative
28        'relativeUrls' => true,
29        // whether to add args into url tokens
30        'urlArgs' => '',
31        'numPrecision' => 8,
32
33        'import_dirs' => [],
34
35        /**
36         * Set this to a directory to enable the incremental cache.
37         *
38         * It is recommended to use Less_Cache::Get() instead, which is much faster,
39         * as it can skip compilation alltogether. Refer to API.md#incremental-cache
40         * for more information.
41         */
42        'cache_dir' => null,
43        'cache_incremental' => true,
44        // one of false, 'serialize', or 'callback'
45        'cache_method' => 'serialize',
46        'cache_callback_get' => null,
47        'cache_callback_set' => null,
48
49        // whether to output a source map
50        'sourceMap' => false,
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,relativeUrls:bool,urlArgs:string,numPrecision:int,import_dirs:array,cache_dir:?string,cache_incremental:bool,indentation:string} */
63    public static $options = [];
64
65    /** @var string Less input string */
66    private $input;
67    /** @var int input string length */
68    private $input_len;
69    /** @var int current index in `input` */
70    private $pos;
71    /** @var int[] holds state for backtracking */
72    private $saveStack = [];
73    /** @var int */
74    private $furthest;
75
76    /** @var bool */
77    private $autoCommentAbsorb = true;
78    /**
79     * @var array<array{index:int,text:string,isLineComment:bool}>
80     */
81    private $commentStore = [];
82
83    /**
84     * @var Less_Environment
85     */
86    private $env;
87
88    /** @var Less_Tree[] */
89    protected $rules = [];
90
91    /**
92     * Evaluated ruleset created by `getCss()`. Stored for potential use in `getVariables()`
93     * @var Less_Tree[]|null
94     */
95    private $cachedEvaldRules;
96
97    /** @var bool */
98    public static $has_extends = false;
99
100    /** @var int */
101    public static $next_id = 0;
102
103    /**
104     * Filename to contents of all parsed the files
105     *
106     * @var array
107     */
108    public static $contentsMap = [];
109
110    /**
111     * @param Less_Environment|array|null $env
112     */
113    public function __construct( $env = null ) {
114        // Top parser on an import tree must be sure there is one "env"
115        // which will then be passed around by reference.
116        if ( $env instanceof Less_Environment ) {
117            $this->env = $env;
118        } else {
119            $this->Reset( $env );
120        }
121
122        Less_Tree::$parse = $this;
123    }
124
125    /**
126     * Reset the parser state completely
127     */
128    public function Reset( $options = null ) {
129        $this->rules = [];
130        $this->cachedEvaldRules = null;
131        self::$has_extends = false;
132        self::$contentsMap = [];
133
134        $this->env = new Less_Environment();
135
136        // set new options
137        $this->SetOptions( self::$default_options );
138        if ( is_array( $options ) ) {
139            $this->SetOptions( $options );
140        }
141
142        $this->env->Init();
143    }
144
145    /**
146     * Set one or more compiler options
147     */
148    public function SetOptions( $options ) {
149        foreach ( $options as $option => $value ) {
150            $this->SetOption( $option, $value );
151        }
152    }
153
154    /**
155     * Set one compiler option
156     */
157    public function SetOption( $option, $value ) {
158        switch ( $option ) {
159            case 'strictMath':
160                if ( $value ) {
161                    $this->env->math = Less_Environment::MATH_PARENS;
162                } else {
163                    $this->env->math = Less_Environment::MATH_ALWAYS;
164                }
165                break;
166
167            case 'math':
168                $value = strtolower( $value );
169                if ( $value === 'always' ) {
170                    $this->env->math = Less_Environment::MATH_ALWAYS;
171                } elseif ( $value === 'parens-division' ) {
172                    $this->env->math = Less_Environment::MATH_PARENS_DIVISION;
173                } elseif ( $value === 'parens' || $value === 'strict' ) {
174                    $this->env->math = Less_Environment::MATH_PARENS;
175                }
176                return;
177
178            case 'import_dirs':
179                $this->SetImportDirs( $value );
180                return;
181
182            case 'cache_dir':
183                if ( is_string( $value ) ) {
184                    $value = Less_Cache::CheckCacheDir( $value );
185                }
186                break;
187
188            case 'functions':
189                foreach ( $value as $key => $function ) {
190                    $this->registerFunction( $key, $function );
191                }
192                return;
193        }
194
195        self::$options[$option] = $value;
196    }
197
198    /**
199     * Registers a new custom function
200     *
201     * @param string $name function name
202     * @param callable $callback callback
203     */
204    public function registerFunction( $name, $callback ) {
205        $this->env->functions[$name] = $callback;
206    }
207
208    /**
209     * Removed an already registered function
210     *
211     * @param string $name function name
212     */
213    public function unregisterFunction( $name ) {
214        if ( isset( $this->env->functions[$name] ) ) {
215            unset( $this->env->functions[$name] );
216        }
217    }
218
219    /**
220     * Get the current css buffer
221     *
222     * @return string
223     */
224    public function getCss() {
225        $precision = ini_get( 'precision' );
226        @ini_set( 'precision', '16' );
227        $locale = setlocale( LC_NUMERIC, '0' );
228        setlocale( LC_NUMERIC, "C" );
229
230        $css = '';
231        try {
232            $root = new Less_Tree_Ruleset( null, $this->rules );
233            $root->root = true;
234            $root->firstRoot = true;
235
236            $importVisitor = new Less_ImportVisitor( $this->env );
237            $importVisitor->run( $root );
238
239            $this->PreVisitors( $root );
240
241            self::$has_extends = false;
242            $evaldRoot = $root->compile( $this->env );
243
244            $this->cachedEvaldRules = $evaldRoot->rules;
245
246            $this->PostVisitors( $evaldRoot );
247
248            if ( self::$options['sourceMap'] ) {
249                $generator = new Less_SourceMap_Generator( $evaldRoot, self::$contentsMap, self::$options );
250                // will also save file
251                // FIXME: should happen somewhere else?
252                $css = $generator->generateCSS();
253            } else {
254                $css = $evaldRoot->toCSS();
255            }
256
257            if ( self::$options['compress'] ) {
258                $css = preg_replace( '/(^(\s)+)|((\s)+$)/', '', $css );
259            }
260
261        } catch ( Exception $exc ) {
262            // Intentional fall-through so we can reset environment
263        }
264
265        // reset php settings
266        @ini_set( 'precision', $precision );
267        setlocale( LC_NUMERIC, $locale );
268
269        // Rethrow exception after we handled resetting the environment
270        if ( !empty( $exc ) ) {
271            if ( $exc instanceof Less_Exception_Parser ) {
272                $exc->getFinalMessage();
273            }
274            throw $exc;
275        }
276
277        return $css;
278    }
279
280    public function findValueOf( $varName ) {
281        $rules = $this->cachedEvaldRules ?? $this->rules;
282
283        foreach ( $rules as $rule ) {
284            // @phan-suppress-next-line PhanUndeclaredProperty
285            if ( isset( $rule->variable ) && ( $rule->variable == true ) && ( str_replace( "@", "", $rule->name ) == $varName ) ) {
286                return $this->getVariableValue( $rule );
287            }
288        }
289        return null;
290    }
291
292    /**
293     * Get an array of the found variables in the parsed input.
294     *
295     * @return array
296     * @phan-return array<string,string|float|array>
297     */
298    public function getVariables() {
299        $variables = [];
300
301        $rules = $this->cachedEvaldRules ?? $this->rules;
302        foreach ( $rules as $key => $rule ) {
303            if ( $rule instanceof Less_Tree_Declaration && $rule->variable ) {
304                $variables[$rule->name] = $this->getVariableValue( $rule );
305            }
306        }
307        return $variables;
308    }
309
310    public function findVarByName( $var_name ) {
311        $rules = $this->cachedEvaldRules ?? $this->rules;
312
313        foreach ( $rules as $rule ) {
314            // @phan-suppress-next-line PhanUndeclaredProperty
315            if ( isset( $rule->variable ) && ( $rule->variable == true ) ) {
316                // @phan-suppress-next-line PhanUndeclaredProperty
317                if ( $rule->name == $var_name ) {
318                    return $this->getVariableValue( $rule );
319                }
320            }
321        }
322        return null;
323    }
324
325    /**
326     * This method gets the value of the less variable from the rules object.
327     * Since the objects vary here we add the logic for extracting the css/less value.
328     *
329     * @param Less_Tree $var
330     * @return mixed
331     * @phan-return string|float|array<string|float>
332     */
333    private function getVariableValue( Less_Tree $var ) {
334        switch ( get_class( $var ) ) {
335            case Less_Tree_Color::class:
336                return $this->rgb2html( $var->rgb );
337            case Less_Tree_Variable::class:
338                return $this->findVarByName( $var->name );
339            case Less_Tree_Keyword::class:
340                return $var->value;
341            case Less_Tree_Anonymous::class:
342                $return = [];
343                if ( is_array( $var->value ) ) {
344                    // in compilation phase, Less_Tree_Anonymous::$val can be a Less_Tree[]
345                    foreach ( $var->value as $value ) {
346                        /** @var Less_Tree $value */
347                        $return[ $value->name ] = $this->getVariableValue( $value );
348                    }
349                }
350                return count( $return ) === 1 ? $return[0] : $return;
351            case Less_Tree_Url::class:
352                // Based on Less_Tree_Url::genCSS()
353                // Recurse to serialize the Less_Tree_Quoted value
354                return 'url(' . $this->getVariableValue( $var->value ) . ')';
355            case Less_Tree_Declaration::class:
356                if ( $var->value instanceof Less_Tree_Anonymous ) {
357                    $nodes = $this->parseNode( $var->value->value, [ 'value', 'important' ], 0, [] );
358                    return $this->getVariableValue( $nodes[1][0] );
359                }
360                return $this->getVariableValue( $var->value );
361            case Less_Tree_Value::class:
362                $values = [];
363                foreach ( $var->value as $sub_value ) {
364                    $values[] = $this->getVariableValue( $sub_value );
365                }
366                return count( $values ) === 1 ? $values[0] : $values;
367            case Less_Tree_Quoted::class:
368                return $var->quote . $var->value . $var->quote;
369            case Less_Tree_Dimension::class:
370                $value = $var->value;
371                if ( $var->unit && $var->unit->numerator ) {
372                    $value .= $var->unit->numerator[0];
373                }
374                return $value;
375            case Less_Tree_Expression::class:
376                $values = [];
377                foreach ( $var->value as $item ) {
378                    $values[] = $this->getVariableValue( $item );
379                }
380                return implode( ' ', $values );
381            case Less_Tree_Operation::class:
382                throw new Exception( 'getVariables() require Less to be compiled. please use $parser->getCss() before calling getVariables()' );
383            case Less_Tree_Unit::class:
384            case Less_Tree_Comment::class:
385            case Less_Tree_Import::class:
386            case Less_Tree_Ruleset::class:
387            default:
388                throw new Exception( "type missing in switch/case getVariableValue for " . get_class( $var ) );
389        }
390    }
391
392    private function rgb2html( $r, $g = -1, $b = -1 ) {
393        if ( is_array( $r ) && count( $r ) == 3 ) {
394            [ $r, $g, $b ] = $r;
395        }
396
397        return sprintf( '#%02x%02x%02x', $r, $g, $b );
398    }
399
400    /**
401     * Run pre-compile visitors
402     */
403    private function PreVisitors( $root ) {
404        if ( self::$options['plugins'] ) {
405            foreach ( self::$options['plugins'] as $plugin ) {
406                if ( !empty( $plugin->isPreEvalVisitor ) ) {
407                    $plugin->run( $root );
408                }
409            }
410        }
411    }
412
413    /**
414     * Run post-compile visitors
415     */
416    private function PostVisitors( $evaldRoot ) {
417        $visitors = [];
418        $visitors[] = new Less_Visitor_joinSelector();
419        if ( self::$has_extends ) {
420            $visitors[] = new Less_Visitor_processExtends();
421        }
422        $visitors[] = new Less_Visitor_toCSS();
423
424        if ( self::$options['plugins'] ) {
425            foreach ( self::$options['plugins'] as $plugin ) {
426                if ( property_exists( $plugin, 'isPreEvalVisitor' ) && $plugin->isPreEvalVisitor ) {
427                    continue;
428                }
429
430                if ( property_exists( $plugin, 'isPreVisitor' ) && $plugin->isPreVisitor ) {
431                    array_unshift( $visitors, $plugin );
432                } else {
433                    $visitors[] = $plugin;
434                }
435            }
436        }
437
438        for ( $i = 0; $i < count( $visitors ); $i++ ) {
439            $visitors[$i]->run( $evaldRoot );
440        }
441    }
442
443    /**
444     * Parse a Less string
445     *
446     * @throws Less_Exception_Parser If the compiler encounters invalid syntax
447     * @param string $str The string to convert
448     * @param string|null $file_uri The url of the file
449     * @return $this
450     */
451    public function parse( $str, $file_uri = null ) {
452        if ( !$file_uri ) {
453            $uri_root = '';
454            $filename = 'anonymous-file-' . self::$next_id++ . '.less';
455        } else {
456            $file_uri = self::WinPath( $file_uri );
457            $filename = $file_uri;
458            $uri_root = dirname( $file_uri );
459        }
460
461        $previousFileInfo = $this->env->currentFileInfo;
462        $uri_root = self::WinPath( $uri_root );
463        $this->SetFileInfo( $filename, $uri_root );
464
465        $this->input = $str;
466        $this->_parse();
467
468        if ( $previousFileInfo ) {
469            $this->env->currentFileInfo = $previousFileInfo;
470        }
471
472        return $this;
473    }
474
475    /**
476     * Parse a Less string from a given file
477     *
478     * @throws Less_Exception_Parser If the compiler encounters invalid syntax
479     * @param string $filename The file to parse
480     * @param string $uri_root The url of the file
481     * @param bool $returnRoot Indicates whether the return value should be a css string a root node
482     * @return Less_Tree_Ruleset|$this
483     */
484    public function parseFile( $filename, $uri_root = '', $returnRoot = false ) {
485        if ( !file_exists( $filename ) ) {
486            $this->Error( sprintf( 'File `%s` not found.', $filename ) );
487        }
488
489        // fix uri_root?
490        // Instead of The mixture of file path for the first argument and directory path for the second argument has bee
491        if ( !$returnRoot && !empty( $uri_root ) && basename( $uri_root ) == basename( $filename ) ) {
492            $uri_root = dirname( $uri_root );
493        }
494
495        $previousFileInfo = $this->env->currentFileInfo;
496
497        if ( $filename ) {
498            $filename = self::AbsPath( $filename, true );
499        }
500        $uri_root = self::WinPath( $uri_root );
501
502        $this->SetFileInfo( $filename, $uri_root );
503
504        $this->env->addParsedFile( $filename );
505
506        if ( $returnRoot ) {
507            $rules = $this->GetRules( $filename );
508            $return = new Less_Tree_Ruleset( null, $rules );
509        } else {
510            $this->_parse( $filename );
511            $return = $this;
512        }
513
514        if ( $previousFileInfo ) {
515            $this->env->currentFileInfo = $previousFileInfo;
516        }
517
518        return $return;
519    }
520
521    /**
522     * Allows a user to set variables values
523     * @param array $vars
524     * @return $this
525     */
526    public function ModifyVars( $vars ) {
527        $this->input = self::serializeVars( $vars );
528        $this->_parse();
529
530        return $this;
531    }
532
533    /**
534     * @param string $filename
535     * @param string $uri_root
536     */
537    public function SetFileInfo( $filename, $uri_root = '' ) {
538        $filename = Less_Environment::normalizePath( $filename );
539        $dirname = preg_replace( '/[^\/\\\\]*$/', '', $filename );
540
541        if ( !empty( $uri_root ) ) {
542            $uri_root = rtrim( $uri_root, '/' ) . '/';
543        }
544
545        $currentFileInfo = [];
546
547        // entry info
548        if ( isset( $this->env->currentFileInfo ) ) {
549            $currentFileInfo['entryPath'] = $this->env->currentFileInfo['entryPath'];
550            $currentFileInfo['entryUri'] = $this->env->currentFileInfo['entryUri'];
551            $currentFileInfo['rootpath'] = $this->env->currentFileInfo['rootpath'];
552
553        } else {
554            $currentFileInfo['entryPath'] = $dirname;
555            $currentFileInfo['entryUri'] = $uri_root;
556            $currentFileInfo['rootpath'] = $dirname;
557        }
558
559        $currentFileInfo['currentDirectory'] = $dirname;
560        $currentFileInfo['currentUri'] = $uri_root . basename( $filename );
561        $currentFileInfo['filename'] = $filename;
562        $currentFileInfo['uri_root'] = $uri_root;
563
564        // inherit reference
565        if ( isset( $this->env->currentFileInfo['reference'] ) && $this->env->currentFileInfo['reference'] ) {
566            $currentFileInfo['reference'] = true;
567        }
568
569        $this->env->currentFileInfo = $currentFileInfo;
570    }
571
572    /**
573     * @deprecated 1.5.1.2 Use Less_Cache::SetCacheDir instead.
574     */
575    public function SetCacheDir( $dir ) {
576        trigger_error( 'Less_Parser::SetCacheDir is deprecated, use Less_Cache::SetCacheDir instead', E_USER_DEPRECATED );
577        Less_Cache::SetCacheDir( $dir );
578    }
579
580    /**
581     * Set a list of directories or callbacks the parser should use for determining import paths
582     *
583     * Import closures are called with a single `$path` argument containing the unquoted `@import`
584     * string an input LESS file. The string is unchanged, except for a statically appended ".less"
585     * suffix if the basename does not yet contain a dot. If a dot is present in the filename, you
586     * are responsible for choosing whether to expand "foo.bar" to "foo.bar.less". If your callback
587     * can handle this import statement, return an array with an absolute file path and an optional
588     * URI path, or return void/null to indicate that your callback does not handle this import
589     * statement.
590     *
591     * Example:
592     *
593     *     function ( $path ) {
594     *         if ( $path === 'virtual/something.less' ) {
595     *             return [ '/srv/elsewhere/thing.less', null ];
596     *         }
597     *     }
598     *
599     * @param array $dirs The key should be a server directory from which LESS
600     * files may be imported. The value is an optional public URL or URL base path that corresponds to
601     * the same directory (use empty string otherwise). The value may also be a closure, in
602     * which case the key is ignored.
603     * @phan-param array<string,string|callable>|callable[] $dirs
604     */
605    public function SetImportDirs( $dirs ) {
606        self::$options['import_dirs'] = [];
607
608        foreach ( $dirs as $path => $uri_root ) {
609
610            if ( !is_callable( $uri_root ) ) {
611                $path = self::WinPath( $path );
612                if ( !empty( $path ) ) {
613                    $path = rtrim( $path, '/' ) . '/';
614                }
615
616                $uri_root = self::WinPath( $uri_root );
617                if ( !empty( $uri_root ) ) {
618                    $uri_root = rtrim( $uri_root, '/' ) . '/';
619                }
620            }
621
622            self::$options['import_dirs'][$path] = $uri_root;
623        }
624    }
625
626    /**
627     * @param string|null $file_path
628     */
629    private function _parse( $file_path = null ) {
630        $this->rules = array_merge( $this->rules, $this->GetRules( $file_path ) );
631    }
632
633    /**
634     * Return the results of parsePrimary for $file_path
635     * Use cache and save cached results if possible
636     *
637     * @param string|null $file_path
638     */
639    private function GetRules( $file_path ) {
640        $this->setInput( $file_path );
641
642        $cache_file = $this->cacheFile( $file_path );
643        if ( $cache_file ) {
644            if ( self::$options['cache_method'] === 'callback' ) {
645                $callback = self::$options['cache_callback_get'];
646                if ( is_callable( $callback ) ) {
647                    $cache = $callback( $this, $file_path, $cache_file );
648                    if ( $cache ) {
649                        $this->unsetInput();
650                        return $cache;
651                    }
652                }
653
654            } elseif ( self::$options['cache_method'] === 'serialize' && file_exists( $cache_file ) ) {
655                $cache = unserialize( file_get_contents( $cache_file ) );
656                if ( $cache ) {
657                    touch( $cache_file );
658                    $this->unsetInput();
659                    return $cache;
660                }
661            }
662        }
663        $this->skipWhitespace( 0 );
664        $rules = $this->parsePrimary();
665
666        if ( $this->pos < $this->input_len ) {
667            throw new Less_Exception_Chunk( $this->input, null, $this->furthest, $this->env->currentFileInfo );
668        }
669
670        $this->unsetInput();
671
672        // save the cache
673        if ( $cache_file ) {
674            if ( self::$options['cache_method'] === 'callback' ) {
675                $callback = self::$options['cache_callback_set'];
676                if ( is_callable( $callback ) ) {
677                    $callback( $this, $file_path, $cache_file, $rules );
678                }
679            } elseif ( self::$options['cache_method'] === 'serialize' ) {
680                file_put_contents( $cache_file, serialize( $rules ) );
681                Less_Cache::CleanCache( self::$options['cache_dir'] );
682            }
683        }
684
685        return $rules;
686    }
687
688    /**
689     * @internal since 4.3.0 No longer a public API.
690     */
691    private function setInput( $file_path ) {
692        // Set up the input buffer
693        if ( $file_path ) {
694            $this->input = file_get_contents( $file_path );
695        }
696
697        $this->pos = $this->furthest = 0;
698
699        // Remove potential UTF Byte Order Mark
700        $this->input = preg_replace( '/\\G\xEF\xBB\xBF/', '', $this->input );
701        $this->input_len = strlen( $this->input );
702
703        if ( self::$options['sourceMap'] && $this->env->currentFileInfo ) {
704            $uri = $this->env->currentFileInfo['currentUri'];
705            self::$contentsMap[$uri] = $this->input;
706        }
707    }
708
709    /**
710     * @internal since 4.3.0 No longer a public API.
711     */
712    private function unsetInput() {
713        // Free up some memory
714        $this->input = '';
715        $this->pos = $this->input_len = $this->furthest = 0;
716        $this->saveStack = [];
717    }
718
719    private function cacheFile( $file_path ) {
720        if ( $file_path && $this->CacheEnabled() ) {
721            $env = get_object_vars( $this->env );
722            unset( $env['frames'] );
723
724            $parts = [
725                $file_path,
726                filesize( $file_path ),
727                filemtime( $file_path ),
728                $env,
729                Less_Version::cache_version,
730                self::$options['cache_method'],
731            ];
732            return self::$options['cache_dir'] . Less_Cache::$prefix . base_convert( sha1( json_encode( $parts ) ), 16, 36 ) . '.lesscache';
733        }
734    }
735
736    /**
737     * @since 4.3.0
738     * @return string[]
739     */
740    public function getParsedFiles() {
741        return $this->env->imports;
742    }
743
744    /**
745     * @internal since 4.3.0 No longer a public API.
746     */
747    private function save() {
748        $this->saveStack[] = $this->pos;
749    }
750
751    private function restore() {
752        if ( $this->pos > $this->furthest ) {
753            $this->furthest = $this->pos;
754        }
755        $this->pos = array_pop( $this->saveStack );
756    }
757
758    private function forget() {
759        array_pop( $this->saveStack );
760    }
761
762    /**
763     * Determine if the character at the specified offset from the current position is a white space.
764     *
765     * @param int $offset
766     * @return bool
767     */
768    private function isWhitespace( $offset = 0 ) {
769        // @phan-suppress-next-line PhanParamSuspiciousOrder False positive
770        return strpos( " \t\n\r\v\f", $this->input[$this->pos + $offset] ) !== false;
771    }
772
773    /**
774     * Match a single character in the input.
775     *
776     * @param string $tok
777     * @return string|null
778     * @see less-2.5.3.js#parserInput.$char
779     */
780    private function matchChar( $tok ) {
781        if ( ( $this->pos < $this->input_len ) && ( $this->input[$this->pos] === $tok ) ) {
782            $this->skipWhitespace( 1 );
783            return $tok;
784        }
785    }
786
787    /**
788     * Match a regexp from the current start point
789     *
790     * @return string|array|null
791     * @see less-2.5.3.js#parserInput.$re
792     */
793    private function matchReg( $tok ) {
794        if ( preg_match( $tok, $this->input, $match, 0, $this->pos ) ) {
795            $this->skipWhitespace( strlen( $match[0] ) );
796            return count( $match ) === 1 ? $match[0] : $match;
797        }
798    }
799
800    /**
801     * Match an exact string of characters.
802     *
803     * @param string $tok
804     * @return string|null
805     * @see less-2.5.3.js#parserInput.$str
806     */
807    private function matchStr( $tok ) {
808        $tokLength = strlen( $tok );
809        if (
810            ( $this->pos < $this->input_len ) &&
811            substr( $this->input, $this->pos, $tokLength ) === $tok
812        ) {
813            $this->skipWhitespace( $tokLength );
814            return $tok;
815        }
816    }
817
818    /**
819     * @param int|null $loc
820     * @return array|string|void|null
821     * @see less-3.13.1.js#parserInput.$quoted
822     */
823    private function parseQuoted( $loc = null ) {
824        $pos = $loc ?? $this->pos;
825        $startChar = $this->input[ $pos ] ?? '';
826        if ( $startChar !== '\'' && $startChar !== '"' ) {
827            return;
828        }
829        $currentPos = $pos;
830        $i = 1;
831        while ( $currentPos + $i < $this->input_len ) {
832            // Optimization: Skip over irrelevant chars without slow loop
833            $i += strcspn( $this->input, "\n\r$startChar\\", $currentPos + $i );
834            switch ( $this->input[$currentPos + $i++] ) {
835                case "\\":
836                    $i++;
837                    break;
838                case "\r":
839                case "\n":
840                    break;
841                case $startChar:
842                    // NOTE: Our optimization means we look ahead instead of behind,
843                    // so no +1s here.
844                    $str = substr( $this->input, $currentPos, $i );
845                    if ( !$loc && $loc !== 0 ) {
846                        $this->skipWhitespace( $i );
847                        return $str;
848                    }
849                    return [ $startChar, $str ];
850            }
851        }
852        return null;
853    }
854
855    /**
856     * Permissive parsing. Ignores everything except matching {} [] () and quotes
857     * until matching token (outside of blocks)
858     * @see less-3.13.1.js#parserInput.$parseUntil
859     */
860    private function parseUntil( $tok ) {
861        $quote = '';
862        $returnVal = null;
863        $inComment = false;
864        $blockDepth = 0;
865        $blockStack = [];
866        $parseGroups = [];
867        $startPos = $this->pos;
868        $lastPos = $this->pos;
869        $i = $this->pos;
870        $loop = true;
871        if ( is_string( $tok ) ) {
872            $testChar = static function ( $char ) use ( $tok ) {
873                return $tok === $char;
874            };
875        } else {
876            $testChar = static function ( $char ) use ( $tok ) {
877                return in_array( $char, $tok );
878            };
879        }
880        do {
881            $nextChar = $this->input[$i];
882            if ( $blockDepth === 0 && $testChar( $nextChar ) ) {
883                $returnVal = substr( $this->input, $lastPos, $i - $lastPos );
884                if ( $returnVal ) {
885                    $parseGroups[] = $returnVal;
886                } else {
887                    $parseGroups[] = ' ';
888                }
889                $returnVal = $parseGroups;
890                $this->skipWhitespace( $i - $startPos );
891                $loop = false;
892            } else {
893                if ( $inComment ) {
894                    if ( $nextChar === '*' && ( $this->input[$i + 1] ?? '' ) === '/' ) {
895                        $i++;
896                        $blockDepth--;
897                        $inComment = false;
898                    }
899                    $i++;
900                    continue;
901                }
902                switch ( $nextChar ) {
903                    case '\\':
904                        $i++;
905                        $nextChar = $this->input[$i] ?? '';
906                        $parseGroups[] = substr( $this->input, $lastPos, $i - $lastPos + 1 );
907                        $lastPos = $i + 1;
908                        break;
909                    case '/':
910                        if ( ( $this->input[$i + 1] ?? '' ) === '*' ) {
911                            $i++;
912                            $inComment = true;
913                            $blockDepth++;
914                        }
915                        break;
916                    case '\'':
917                    case '"':
918                        $quote = $this->parseQuoted( $i );
919                        if ( $quote ) {
920                            $parseGroups[] = substr( $this->input, $lastPos, $i - $lastPos );
921                            $parseGroups[] = $quote;
922                            $i += strlen( $quote[1] ) - 1;
923                            $lastPos = $i + 1;
924                        } else {
925                            $this->skipWhitespace( $i - $startPos );
926                            $returnVal = $nextChar;
927                            $loop = false;
928                        }
929                        break;
930                    case '{':
931                        $blockStack[] = '}';
932                        $blockDepth++;
933                        break;
934                    case '(':
935                        $blockStack[] = ')';
936                        $blockDepth++;
937                        break;
938                    case '[':
939                        $blockStack[] = ']';
940                        $blockDepth++;
941                        break;
942                    case '}':
943                    case ')':
944                    case ']':
945                        $expected = array_pop( $blockStack );
946                        if ( $nextChar === $expected ) {
947                            $blockDepth--;
948                        } else {
949                            // move the parser to the error and return expected;
950                            $this->skipWhitespace( $i - $startPos );
951                            $returnVal = $expected;
952                            $loop = false;
953                        }
954                }
955                $i++;
956                if ( $i > $this->input_len ) {
957                    $loop = false;
958                }
959            }
960        } while ( $loop );
961
962        return $returnVal ?: null;
963    }
964
965    /**
966     * Same as match(), but don't change the state of the parser,
967     * just return the match.
968     *
969     * @param string $tok
970     * @return int|false
971     */
972    private function peekReg( $tok ) {
973        return preg_match( $tok, $this->input, $match, 0, $this->pos );
974    }
975
976    /**
977     * @param string $tok
978     */
979    private function peekChar( $tok ) {
980        return ( $this->pos < $this->input_len ) && ( $this->input[$this->pos] === $tok );
981    }
982
983    /**
984     * @param int $length
985     * @see less-2.5.3.js#skipWhitespace
986     */
987    private function skipWhitespace( $length ) {
988        $this->pos += $length;
989
990        for ( ; $this->pos < $this->input_len; $this->pos++ ) {
991            $currentChar = $this->input[$this->pos];
992
993            if ( $this->autoCommentAbsorb && $currentChar === '/' ) {
994                $nextChar = $this->input[$this->pos + 1] ?? '';
995                if ( $nextChar === '/' ) {
996                    $comment = [ 'index' => $this->pos, 'isLineComment' => true ];
997                    $nextNewLine = strpos( $this->input, "\n", $this->pos + 2 );
998                    if ( $nextNewLine === false ) {
999                        $nextNewLine = $this->input_len ?? 0;
1000                    }
1001                    $this->pos = $nextNewLine;
1002                    $comment['text'] = substr( $this->input, $this->pos, $nextNewLine - $this->pos );
1003                    $this->commentStore[] = $comment;
1004                    continue;
1005                } elseif ( $nextChar === '*' ) {
1006                    $nextStarSlash = strpos( $this->input, "*/", $this->pos + 2 );
1007                    if ( $nextStarSlash !== false ) {
1008                        $comment = [
1009                            'index' => $this->pos,
1010                            'text' => substr( $this->input, $this->pos, $nextStarSlash + 2 - $this->pos ),
1011                            'isLineComment' => false,
1012                        ];
1013                        $this->pos += strlen( $comment['text'] ) - 1;
1014                        $this->commentStore[] = $comment;
1015                        continue;
1016                    }
1017                }
1018                break;
1019            }
1020
1021            // Optimization: Skip over irrelevant chars without slow loop
1022            $skip = strspn( $this->input, " \n\t\r", $this->pos );
1023            if ( $skip ) {
1024                $this->pos += $skip - 1;
1025            }
1026            if ( !$skip && $this->pos < $this->input_len ) {
1027                break;
1028            }
1029        }
1030    }
1031
1032    /**
1033     * Parse a token from a regexp or method name string
1034     *
1035     * @param string $tok
1036     * @param string|null $msg
1037     * @return string|array|never
1038     * @see less-3.13.1.js#Parser.expect
1039     */
1040    private function expect( $tok, $msg = null ) {
1041        $result = $this->matchReg( $tok );
1042        if ( $result ) {
1043            return $result;
1044        }
1045        $this->Error( $msg ?? "expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" );
1046    }
1047
1048    /**
1049     * @param string $tok
1050     * @param string|null $msg
1051     */
1052    private function expectChar( $tok, $msg = null ) {
1053        $result = $this->matchChar( $tok );
1054        if ( !$result ) {
1055            $msg = $msg ?: "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'";
1056            $this->Error( $msg );
1057        } else {
1058            return $result;
1059        }
1060    }
1061
1062    /**
1063     * @param string $str
1064     * @see less-3.13.1.js#ParserInput.start
1065     */
1066    private function parserInputStart( $str ) {
1067        $this->pos = $this->furthest = 0;
1068        $this->input = $str;
1069        $this->input_len = strlen( $str );
1070        $this->skipWhitespace( 0 );
1071    }
1072
1073    /**
1074     *  Used after initial parsing to create nodes on the fly
1075     *
1076     * @param string $str string to parse
1077     * @param string[] $parseList array of parsers to run input through e.g. ["value", "important"]
1078     * @param int $currentIndex start number to begin indexing
1079     * @param array $fileInfo fileInfo to attach to created nodes
1080     * @return array
1081     * @see less-3.13.1.js#Parser.parseNode
1082     */
1083    public function parseNode( $str, array $parseList, $currentIndex, $fileInfo ) {
1084        $returnNodes = [];
1085        try {
1086            $this->parserInputStart( $str );
1087            foreach ( $parseList as $p ) {
1088                $i = $this->pos;
1089                $method = 'parse' . ucfirst( $p );
1090                if ( !method_exists( $this, $method ) ) {
1091                    throw new CompileError( 'Unknown parser ' . $p );
1092                }
1093                $result = $this->$method();
1094                if ( $result ) {
1095                    $result->index = $i + $currentIndex;
1096                    $result->currentFileInfo = $fileInfo;
1097                    $returnNodes[] = $result;
1098                } else {
1099                    $returnNodes[] = null;
1100                }
1101            }
1102            if ( $this->pos >= $this->input_len ) {
1103                return [ null, $returnNodes ];
1104            } else {
1105                return [ true, null ];
1106            }
1107        } catch ( Less_Exception_Parser $e ) {
1108            throw new Less_Exception_Parser(
1109                $e->getMessage(),
1110                $e,
1111                ( $e->index ?? 0 ) + $currentIndex,
1112                $fileInfo
1113            );
1114        }
1115    }
1116
1117    //
1118    // Here in, the parsing rules/functions
1119    //
1120    // The basic structure of the syntax tree generated is as follows:
1121    //
1122    //   Ruleset ->  Declaration -> Value -> Expression -> Entity
1123    //
1124    // Here's some LESS code:
1125    //
1126    //    .class {
1127    //      color: #fff;
1128    //      border: 1px solid #000;
1129    //      width: @w + 4px;
1130    //      > .child {...}
1131    //    }
1132    //
1133    // And here's what the parse tree might look like:
1134    //
1135    //     Ruleset (Selector '.class', [
1136    //         Declaration ("color",  Value ([Expression [Color #fff]]))
1137    //         Declaration ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
1138    //         Declaration ("width",  Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]]))
1139    //         Ruleset (Selector [Element '>', '.child'], [...])
1140    //     ])
1141    //
1142    //  In general, most rules will try to parse a token with the `$()` function, and if the return
1143    //  value is truly, will return a new node, of the relevant type. Sometimes, we need to check
1144    //  first, before parsing, that's when we use `peek()`.
1145    //
1146
1147    //
1148    // The `primary` rule is the *entry* and *exit* point of the parser.
1149    // The rules here can appear at any level of the parse tree.
1150    //
1151    // The recursive nature of the grammar is an interplay between the `block`
1152    // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
1153    // as represented by this simplified grammar:
1154    //
1155    //     primary  â†’  (ruleset | declaration )+
1156    //     ruleset  â†’  selector+ block
1157    //     block    â†’  '{' primary '}'
1158    //
1159    // Only at one point is the primary rule not called from the
1160    // block rule: at the root level.
1161    //
1162    // @see less-2.5.3.js#parsers.primary
1163    private function parsePrimary() {
1164        $root = [];
1165
1166        while ( true ) {
1167
1168            while ( true ) {
1169                $node = $this->parseComment();
1170                if ( !$node ) {
1171                    break;
1172                }
1173                $root[] = $node;
1174            }
1175
1176            // always process comments before deciding if finished
1177            if ( $this->pos >= $this->input_len ) {
1178                break;
1179            }
1180
1181            if ( $this->peekChar( '}' ) ) {
1182                break;
1183            }
1184
1185            $node = $this->parseExtend( true );
1186            if ( $node ) {
1187                $root = array_merge( $root, $node );
1188                continue;
1189            }
1190