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