Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.80% covered (success)
95.80%
365 / 381
73.33% covered (warning)
73.33%
22 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
Less_Tree_Ruleset
95.80% covered (success)
95.80%
365 / 381
73.33% covered (warning)
73.33%
22 / 30
192
0.00% covered (danger)
0.00%
0 / 1
 SetRulesetIndex
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 accept
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 compile
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
17
 EvalMixinCalls
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
11
 PrepareRuleset
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
9
 evalImports
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 makeImportant
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 matchArgs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 matchCondition
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 resetCache
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 variables
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 properties
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
10
 variable
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 property
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 transformDeclaration
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 lastDeclaration
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 parseValue
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 find
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
13
 isRulesetLikeNode
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 genCSS
98.18% covered (success)
98.18%
54 / 55
0.00% covered (danger)
0.00%
0 / 1
23
 markReferenced
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 getIsReferenced
50.00% covered (danger)
50.00%
5 / 10
0.00% covered (danger)
0.00%
0 / 1
22.50
 joinSelectors
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 joinSelector
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 replaceParentSelector
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
13
 createSelector
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 findNestedSelector
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 addReplacementIntoPath
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
7
 mergeElementsOnToSelectors
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
5.20
1<?php
2/**
3 * @private
4 */
5class Less_Tree_Ruleset extends Less_Tree {
6
7    protected $lookups;
8    public $_variables;
9    public $_properties;
10    public $_rulesets;
11
12    public $strictImports;
13
14    public $selectors;
15    public $rules;
16    public $root;
17    public $allowImports;
18    public $paths;
19    public $firstRoot;
20    public $multiMedia;
21    public $allExtends;
22
23    /** @var int */
24    public $ruleset_id;
25    /** @var int */
26    public $originalRuleset;
27
28    public $first_oelements;
29
30    public function SetRulesetIndex() {
31        $this->ruleset_id = Less_Parser::$next_id++;
32        $this->originalRuleset = $this->ruleset_id;
33
34        if ( $this->selectors ) {
35            foreach ( $this->selectors as $sel ) {
36                if ( $sel->_oelements ) {
37                    $this->first_oelements[$sel->_oelements[0]] = true;
38                }
39            }
40        }
41    }
42
43    /**
44     * @param null|Less_Tree_Selector[] $selectors
45     * @param Less_Tree[] $rules
46     * @param null|bool $strictImports
47     */
48    public function __construct( $selectors, $rules, $strictImports = null ) {
49        $this->selectors = $selectors;
50        $this->rules = $rules;
51        $this->lookups = [];
52        $this->strictImports = $strictImports;
53        $this->SetRulesetIndex();
54    }
55
56    public function accept( $visitor ) {
57        if ( $this->paths !== null ) {
58            $paths_len = count( $this->paths );
59            for ( $i = 0; $i < $paths_len; $i++ ) {
60                $this->paths[$i] = $visitor->visitArray( $this->paths[$i] );
61            }
62        } elseif ( $this->selectors ) {
63            $this->selectors = $visitor->visitArray( $this->selectors );
64        }
65
66        if ( $this->rules ) {
67            $this->rules = $visitor->visitArray( $this->rules );
68        }
69    }
70
71    /**
72     * @param Less_Environment $env
73     * @return self
74     * @see less-2.5.3.js#Ruleset.prototype.eval
75     */
76    public function compile( $env ) {
77        $ruleset = $this->PrepareRuleset( $env );
78
79        // Store the frames around mixin definitions,
80        // so they can be evaluated like closures when the time comes.
81        $rsRuleCnt = count( $ruleset->rules );
82        for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
83            // These checks are the equivalent of the rule.evalFirst property in less.js
84            if ( $ruleset->rules[$i] instanceof Less_Tree_Mixin_Definition || $ruleset->rules[$i] instanceof Less_Tree_DetachedRuleset ) {
85                $ruleset->rules[$i] = $ruleset->rules[$i]->compile( $env );
86            }
87        }
88
89        $mediaBlockCount = count( $env->mediaBlocks );
90
91        // Evaluate mixin calls.
92        $this->EvalMixinCalls( $ruleset, $env, $rsRuleCnt );
93
94        // Evaluate everything else
95        for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
96            if ( !( $ruleset->rules[$i] instanceof Less_Tree_Mixin_Definition || $ruleset->rules[$i] instanceof Less_Tree_DetachedRuleset ) ) {
97                $ruleset->rules[$i] = $ruleset->rules[$i]->compile( $env );
98            }
99        }
100
101        // Evaluate everything else
102        for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
103            $rule = $ruleset->rules[$i];
104
105            // for rulesets, check if it is a css guard and can be removed
106            if ( $rule instanceof self && $rule->selectors && count( $rule->selectors ) === 1 ) {
107
108                // check if it can be folded in (e.g. & where)
109                if ( $rule->selectors[0]->isJustParentSelector() ) {
110                    array_splice( $ruleset->rules, $i--, 1 );
111                    $rsRuleCnt--;
112
113                    for ( $j = 0; $j < count( $rule->rules ); $j++ ) {
114                        $subRule = $rule->rules[$j];
115                        if ( !( $subRule instanceof Less_Tree_Declaration ) || !$subRule->variable ) {
116                            array_splice( $ruleset->rules, ++$i, 0, [ $subRule ] );
117                            $rsRuleCnt++;
118                        }
119                    }
120
121                }
122            }
123        }
124
125        // Pop the stack
126        $env->shiftFrame();
127
128        if ( $mediaBlockCount ) {
129            $len = count( $env->mediaBlocks );
130            for ( $i = $mediaBlockCount; $i < $len; $i++ ) {
131                $env->mediaBlocks[$i]->bubbleSelectors( $ruleset->selectors );
132            }
133        }
134
135        return $ruleset;
136    }
137
138    /**
139     * Compile Less_Tree_Mixin_Call objects
140     *
141     * @param self $ruleset
142     * @param Less_Environment $env
143     * @param int &$rsRuleCnt
144     */
145    private function EvalMixinCalls( $ruleset, $env, &$rsRuleCnt ) {
146        for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
147            $rule = $ruleset->rules[$i];
148
149            if ( $rule instanceof Less_Tree_Mixin_Call ) {
150                $rule = $rule->compile( $env );
151
152                $temp = [];
153                foreach ( $rule as $r ) {
154                    if ( ( $r instanceof Less_Tree_Declaration ) && $r->variable ) {
155                        // do not pollute the scope if the variable is
156                        // already there. consider returning false here
157                        // but we need a way to "return" variable from mixins
158                        if ( !$ruleset->variable( $r->name ) ) {
159                            $temp[] = $r;
160                        }
161                    } else {
162                        $temp[] = $r;
163                    }
164                }
165                $temp_count = count( $temp ) - 1;
166                array_splice( $ruleset->rules, $i, 1, $temp );
167                $rsRuleCnt += $temp_count;
168                $i += $temp_count;
169                $ruleset->resetCache();
170
171            } elseif ( $rule instanceof Less_Tree_VariableCall ) {
172
173                $rule = $rule->compile( $env );
174                $rules = [];
175                foreach ( $rule->rules as $r ) {
176                    if ( ( $r instanceof Less_Tree_Declaration ) && $r->variable ) {
177                        continue;
178                    }
179                    $rules[] = $r;
180                }
181
182                array_splice( $ruleset->rules, $i, 1, $rules );
183                $temp_count = count( $rules );
184                $rsRuleCnt += $temp_count - 1;
185                $i += $temp_count - 1;
186                $ruleset->resetCache();
187            }
188
189        }
190    }
191
192    /**
193     * Compile the selectors and create a new ruleset object for the compile() method
194     *
195     * @param Less_Environment $env
196     * @return self
197     */
198    private function PrepareRuleset( $env ) {
199        // NOTE: Preserve distinction between null and empty array when compiling
200        // $this->selectors to $selectors
201        $thisSelectors = $this->selectors;
202        $selectors = null;
203        $hasOnePassingSelector = false;
204
205        if ( $thisSelectors ) {
206            Less_Tree_DefaultFunc::error( "it is currently only allowed in parametric mixin guards," );
207
208            $selectors = [];
209            foreach ( $thisSelectors as $s ) {
210                $selector = $s->compile( $env );
211                $selectors[] = $selector;
212                if ( $selector->evaldCondition ) {
213                    $hasOnePassingSelector = true;
214                }
215            }
216
217            Less_Tree_DefaultFunc::reset();
218        } else {
219            $hasOnePassingSelector = true;
220        }
221
222        if ( $this->rules && $hasOnePassingSelector ) {
223            // Copy the array (no need for slice in PHP)
224            $rules = $this->rules;
225        } else {
226            $rules = [];
227        }
228
229        $ruleset = new self( $selectors, $rules, $this->strictImports );
230
231        $ruleset->originalRuleset = $this->ruleset_id;
232        $ruleset->root = $this->root;
233        $ruleset->firstRoot = $this->firstRoot;
234        $ruleset->allowImports = $this->allowImports;
235
236        // push the current ruleset to the frames stack
237        $env->unshiftFrame( $ruleset );
238
239        // Evaluate imports
240        if ( $ruleset->root || $ruleset->allowImports || !$ruleset->strictImports ) {
241            $ruleset->evalImports( $env );
242        }
243
244        return $ruleset;
245    }
246
247    public function evalImports( $env ) {
248        $rules_len = count( $this->rules );
249        for ( $i = 0; $i < $rules_len; $i++ ) {
250            $rule = $this->rules[$i];
251
252            if ( $rule instanceof Less_Tree_Import ) {
253                $rules = $rule->compile( $env );
254                if ( is_array( $rules ) ) {
255                    array_splice( $this->rules, $i, 1, $rules );
256                    $temp_count = count( $rules ) - 1;
257                    $i += $temp_count;
258                    $rules_len += $temp_count;
259                } else {
260                    array_splice( $this->rules, $i, 1, [ $rules ] );
261                }
262
263                $this->resetCache();
264            }
265        }
266    }
267
268    public function makeImportant() {
269        $important_rules = [];
270        foreach ( $this->rules as $rule ) {
271            if ( $rule instanceof Less_Tree_Declaration || $rule instanceof self || $rule instanceof Less_Tree_NameValue ) {
272                $important_rules[] = $rule->makeImportant();
273            } else {
274                $important_rules[] = $rule;
275            }
276        }
277
278        return new self( $this->selectors, $important_rules, $this->strictImports );
279    }
280
281    public function matchArgs( $args, $env = null ) {
282        return !$args;
283    }
284
285    // lets you call a css selector with a guard
286    public function matchCondition( $args, $env ) {
287        $lastSelector = end( $this->selectors );
288
289        if ( !$lastSelector->evaldCondition ) {
290            return false;
291        }
292        if ( $lastSelector->condition && !$lastSelector->condition->compile( $env->copyEvalEnv( $env->frames ) ) ) {
293            return false;
294        }
295        return true;
296    }
297
298    public function resetCache() {
299        $this->_rulesets = null;
300        $this->_variables = null;
301        $this->lookups = [];
302    }
303
304    /**
305     * @see less-3.13.1.js#Ruleset.prototype.variables
306     */
307    public function variables() {
308        $this->_variables = [];
309        foreach ( $this->rules as $r ) {
310            if ( $r instanceof Less_Tree_Declaration && $r->variable === true ) {
311                $this->_variables[$r->name] = $r;
312            }
313            // when evaluating variables in an import statement, imports have not been eval'd
314            // so we need to go inside import statements.
315            // guard against root being a string (in the case of inlined less)
316            if ( $r instanceof Less_Tree_Import && $r->root instanceof Less_Tree_Ruleset ) {
317                $vars = $r->root->variables();
318                foreach ( $vars as $key => $name ) {
319                    $this->_variables[$key] = $name;
320                }
321            }
322        }
323        return $this->_variables;
324    }
325
326    /**
327     * @see less-3.13.1#Ruleset.prototype.properties
328     */
329    public function properties() {
330        $this->_properties = [];
331        foreach ( $this->rules as $r ) {
332
333            if ( $r instanceof Less_Tree_Declaration && $r->variable !== true ) {
334                $name = is_array( $r->name ) && count( $r->name ) === 1 && $r->name[0] instanceof Less_Tree_Keyword
335                    ? $r->name[0]->value
336                    : $r->name;
337                // Properties don't overwrite as they can merge
338
339                // TODO: differs from upstream. Upstream expects $r->name to be only a
340                // Less_Tree_Keyword but somehow our parser also returns Less_Tree_Property.
341                // Let's handle it for now, but we should debug why this happens
342                // caused by test/Fixtures/lessjs-3.13.1/less/_main/property-accessors.less:59
343                if ( is_array( $name ) && $name[0] instanceof Less_Tree_Property ) {
344                    $name = $name[0]->name;
345                }
346
347                $idx = '$' . $name;
348                if ( !array_key_exists( $idx, $this->_properties ) ) {
349                    $this->_properties[ $idx ] = [];
350                }
351                $this->_properties[ $idx ][] = $r;
352            }
353        }
354        return $this->_properties;
355    }
356
357    /**
358     * @param string $name
359     * @return Less_Tree_Declaration|null
360     * @see less-3.13.1#Ruleset.prototype.variable
361     */
362    public function variable( $name ) {
363        if ( $this->_variables === null ) {
364            $this->variables();
365        }
366        return array_key_exists( $name, $this->_variables )
367            ? $this->parseValue( $this->_variables[ $name ] )
368            : null;
369    }
370
371    /**
372     * @param string $name
373     * @see less-3.13.1#Ruleset.prototype.property
374     */
375    public function property( $name ) {
376        if ( $this->_properties === null ) {
377            $this->properties();
378        }
379        return array_key_exists( $name, $this->_properties )
380            ? $this->parseValue( $this->_properties[ $name ] )
381            : null;
382    }
383
384    /**
385     * @param Less_Tree_Declaration $decl
386     * @return mixed
387     * @throws Less_Exception_Parser
388     */
389    private function transformDeclaration( $decl ) {
390        if ( $decl->value instanceof Less_Tree_Anonymous && !$decl->parsed ) {
391            [ $err, $result ] = self::$parse->parseNode( (string)$decl->value->value, [ 'value', 'important' ],
392                $decl->value->index, $decl->value->currentFileInfo ?? [] );
393            if ( $err ) {
394                $decl->parsed = true;
395            }
396            if ( $result ) {
397                $decl->value = $result[0];
398                $decl->important = $result[1] ?? '';
399                $decl->parsed = true;
400            }
401            return $decl;
402        } else {
403            return $decl;
404        }
405    }
406
407    public function lastDeclaration() {
408        for ( $i = count( $this->rules ); $i > 0; $i-- ) {
409            $decl = $this->rules[ $i - 1 ];
410            if ( $decl instanceof Less_Tree_Declaration ) {
411                return $this->parseValue( $decl );
412            }
413        }
414    }
415
416    private function parseValue( $toParse ) {
417        if ( !is_array( $toParse ) ) {
418            return $this->transformDeclaration( $toParse );
419        } else {
420            $nodes = [];
421            foreach ( $toParse as $n ) {
422                $nodes[] = $this->transformDeclaration( $n );
423            }
424            return $nodes;
425        }
426    }
427
428    public function find( $selector, $self = null, $filter = null ) {
429        $key = implode( ' ', $selector->_oelements );
430
431        if ( !isset( $this->lookups[$key] ) ) {
432
433            if ( !$self ) {
434                $self = $this->ruleset_id;
435            }
436
437            $this->lookups[$key] = [];
438
439            $first_oelement = $selector->_oelements[0];
440
441            foreach ( $this->rules as $rule ) {
442                if ( $rule instanceof self && $rule->ruleset_id != $self ) {
443
444                    if ( isset( $rule->first_oelements[$first_oelement] ) ) {
445
446                        foreach ( $rule->selectors as $ruleSelector ) {
447                            $match = $selector->match( $ruleSelector );
448                            if ( $match ) {
449                                if ( $selector->elements_len > $match ) {
450                                    if ( !$filter || $filter( $rule ) ) {
451                                        $foundMixins = $rule->find( new Less_Tree_Selector( array_slice( $selector->elements, $match ) ), $self, $filter );
452                                        for ( $i = 0; $i < count( $foundMixins ); ++$i ) {
453                                            $foundMixins[$i]["path"][] = $rule;
454                                        }
455                                        $this->lookups[$key] = array_merge( $this->lookups[$key], $foundMixins );
456                                    }
457                                } else {
458                                    $this->lookups[$key][] = [ "rule" => $rule, "path" => [] ];
459                                }
460                                break;
461                            }
462                        }
463                    }
464                }
465            }
466
467        }
468
469        return $this->lookups[$key];
470    }
471
472    private function isRulesetLikeNode( $rule ) {
473        // if it has nested rules, then it should be treated like a ruleset
474        // medias and comments do not have nested rules, but should be treated like rulesets anyway
475        // some directives and anonymous nodes are ruleset like, others are not
476        if ( $rule instanceof Less_Tree_Media || $rule instanceof Less_Tree_Ruleset ) {
477            return true;
478        } elseif ( $rule instanceof Less_Tree_Anonymous || $rule instanceof Less_Tree_AtRule ) {
479            return $rule->isRulesetLike();
480        }
481
482        // anything else is assumed to be a rule
483        return false;
484    }
485
486    /**
487     * @param Less_Output $output
488     * @see less-2.5.3.js#Ruleset.prototype.genCSS
489     */
490    public function genCSS( $output ) {
491        if ( !$this->root ) {
492            Less_Environment::$tabLevel++;
493        }
494
495        $tabRuleStr = $tabSetStr = '';
496        if ( !Less_Parser::$options['compress'] ) {
497            if ( Less_Environment::$tabLevel ) {
498                $tabRuleStr = "\n" . str_repeat( Less_Parser::$options['indentation'], Less_Environment::$tabLevel );
499                $tabSetStr = "\n" . str_repeat( Less_Parser::$options['indentation'], Less_Environment::$tabLevel - 1 );
500            } else {
501                $tabSetStr = $tabRuleStr = "\n";
502            }
503        }
504
505        $ruleNodes = [];
506        $charsetNodeIndex = 0;
507        $importNodeIndex = 0;
508        foreach ( $this->rules as $i => $rule ) {
509            if ( $rule instanceof Less_Tree_Comment ) {
510                if ( $importNodeIndex === $i ) {
511                    $importNodeIndex++;
512                }
513                $ruleNodes[] = $rule;
514            } elseif ( $rule instanceof Less_Tree_AtRule && $rule->isCharset() ) {
515                array_splice( $ruleNodes, $charsetNodeIndex, 0, [ $rule ] );
516                $charsetNodeIndex++;
517                $importNodeIndex++;
518            } elseif ( $rule instanceof Less_Tree_Import ) {
519                array_splice( $ruleNodes, $importNodeIndex, 0, [ $rule ] );
520                $importNodeIndex++;
521            } else {
522                $ruleNodes[] = $rule;
523            }
524        }
525
526        // If this is the root node, we don't render
527        // a selector, or {}.
528        if ( !$this->root ) {
529
530            $sep = ',' . $tabSetStr;
531            // TODO: Move to Env object
532            // TODO: Inject Env object to toCSS() and genCSS()
533            $firstSelector = false;
534
535            foreach ( $this->paths as $i => $path ) {
536                $pathSubCnt = count( $path );
537                if ( !$pathSubCnt ) {
538                    continue;
539                }
540                if ( $i > 0 ) {
541                    $output->add( $sep );
542                }
543                $firstSelector = true;
544                $path[0]->genCSS( $output, $firstSelector );
545                $firstSelector = false;
546                for ( $j = 1; $j < $pathSubCnt; $j++ ) {
547                    $path[$j]->genCSS( $output, $firstSelector );
548                }
549            }
550
551            $output->add( ( Less_Parser::$options['compress'] ? '{' : " {" ) . $tabRuleStr );
552        }
553
554        // Compile rules and rulesets
555        foreach ( $ruleNodes as $i => $rule ) {
556
557            if ( $i + 1 === count( $ruleNodes ) ) {
558                Less_Environment::$lastRule = true;
559            }
560            $currentLastRule = Less_Environment::$lastRule;
561
562            if ( $this->isRulesetLikeNode( $rule ) ) {
563                Less_Environment::$lastRule = false;
564            }
565
566            $rule->genCSS( $output );
567
568            Less_Environment::$lastRule = $currentLastRule;
569
570            if ( !Less_Environment::$lastRule ) {
571                $output->add( $tabRuleStr );
572            } else {
573                Less_Environment::$lastRule = false;
574            }
575        }
576
577        if ( !$this->root ) {
578            $output->add( $tabSetStr . '}' );
579            Less_Environment::$tabLevel--;
580        }
581
582        if ( !Less_Parser::$options['compress'] && $this->firstRoot ) {
583            $output->add( "\n" );
584        }
585    }
586
587    public function markReferenced() {
588        if ( $this->selectors !== null ) {
589            foreach ( $this->selectors as $selector ) {
590                $selector->markReferenced();
591            }
592        }
593
594        if ( $this->rules ) {
595            foreach ( $this->rules as $rule ) {
596                if ( method_exists( $rule, 'markReferenced' ) ) {
597                    $rule->markReferenced();
598                }
599            }
600        }
601    }
602
603    public function getIsReferenced() {
604        if ( $this->paths ) {
605            foreach ( $this->paths as $path ) {
606                foreach ( $path as $p ) {
607                    if ( method_exists( $p, 'getIsReferenced' ) && $p->getIsReferenced() ) {
608                        return true;
609                    }
610                }
611            }
612        }
613
614        if ( $this->selectors ) {
615            foreach ( $this->selectors as $selector ) {
616                if ( method_exists( $selector, 'getIsReferenced' ) && $selector->getIsReferenced() ) {
617                    return true;
618                }
619            }
620        }
621
622        return false;
623    }
624
625    /**
626     * @param Less_Tree_Selector[][] $context
627     * @param Less_Tree_Selector[]|null $selectors
628     * @return Less_Tree_Selector[][]
629     */
630    public function joinSelectors( $context, $selectors ) {
631        $paths = [];
632        if ( $selectors !== null ) {
633            foreach ( $selectors as $selector ) {
634                $this->joinSelector( $paths, $context, $selector );
635            }
636        }
637        return $paths;
638    }
639
640    public function joinSelector( array &$paths, array $context, Less_Tree_Selector $selector ) {
641        $newPaths = [];
642        $hadParentSelector = $this->replaceParentSelector( $newPaths, $context, $selector );
643
644        if ( !$hadParentSelector ) {
645            if ( $context ) {
646                $newPaths = [];
647                foreach ( $context as $path ) {
648                    $newPaths[] = array_merge( $path, [ $selector ] );
649                }
650            } else {
651                $newPaths = [ [ $selector ] ];
652            }
653        }
654
655        foreach ( $newPaths as $newPath ) {
656            $paths[] = $newPath;
657        }
658    }
659
660    /**
661     * Replace all parent selectors inside $inSelector with $context.
662     *
663     * @param array &$paths Resulting selectors are appended to $paths.
664     * @param mixed $context
665     * @param Less_Tree_Selector $inSelector Inner selector from Less_Tree_Paren
666     * @return bool True if $inSelector contained at least one parent selector
667     */
668    private function replaceParentSelector( array &$paths, $context, Less_Tree_Selector $inSelector ) {
669        $hadParentSelector = false;
670
671        // The paths are [[Selector]]
672        // The first list is a list of comma separated selectors
673        // The inner list is a list of inheritance separated selectors
674        // e.g.
675        // .a, .b {
676        //   .c {
677        //   }
678        // }
679        // == [[.a] [.c]] [[.b] [.c]]
680        //
681
682        // the elements from the current selector so far
683        $currentElements = [];
684        // the current list of new selectors to add to the path.
685        // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors
686        // by the parents
687        $newSelectors = [
688            []
689        ];
690
691        foreach ( $inSelector->elements as $el ) {
692            // non-parent reference elements just get added
693            if ( $el->value !== '&' ) {
694                $nestedSelector = $this->findNestedSelector( $el );
695                if ( $nestedSelector !== null ) {
696                    $this->mergeElementsOnToSelectors( $currentElements, $newSelectors );
697
698                    $nestedPaths = [];
699                    $replacedNewSelectors = [];
700                    $replaced = $this->replaceParentSelector( $nestedPaths, $context, $nestedSelector );
701                    $hadParentSelector = $hadParentSelector || $replaced;
702                    // $nestedPaths is populated by replaceParentSelector()
703                    // $nestedPaths should have exactly one TODO, replaceParentSelector does not multiply selectors
704                    foreach ( $nestedPaths as $nestedPath ) {
705                        $replacementSelector = $this->createSelector( $nestedPath, $el );
706
707                        // join selector path from $newSelectors with every selector path in $addPaths array.
708                        // $el contains the element that is being replaced by $addPaths
709                        //
710                        // @see less-2.5.3.js#Ruleset-addAllReplacementsIntoPath
711                        $addPaths = [ $replacementSelector ];
712                        foreach ( $newSelectors as $newSelector ) {
713                            $replacedNewSelectors[] = $this->addReplacementIntoPath( $newSelector, $addPaths, $el, $inSelector );
714                        }
715                    }
716                    $newSelectors = $replacedNewSelectors;
717                    $currentElements = [];
718                } else {
719                    $currentElements[] = $el;
720                }
721            } else {
722                $hadParentSelector = true;
723
724                // the new list of selectors to add
725                $selectorsMultiplied = [];
726
727                // merge the current list of non parent selector elements
728                // on to the current list of selectors to add
729                $this->mergeElementsOnToSelectors( $currentElements, $newSelectors );
730
731                foreach ( $newSelectors as $sel ) {
732                    // if we don't have any parent paths, the & might be in a mixin so that it can be used
733                    // whether there are parents or not
734                    if ( !$context ) {
735                        // the combinator used on el should now be applied to the next element instead so that
736                        // it is not lost
737                        if ( $sel ) {
738                            $sel[0]->elements[] = new Less_Tree_Element( $el->combinator, '', $el->index, $el->currentFileInfo );
739                        }
740                        $selectorsMultiplied[] = $sel;
741                    } else {
742                        // and the parent selectors
743                        foreach ( $context as $parentSel ) {
744                            // We need to put the current selectors
745                            // then join the last selector's elements on to the parents selectors
746                            $newSelectorPath = $this->addReplacementIntoPath( $sel, $parentSel, $el, $inSelector );
747                            // add that to our new set of selectors
748                            $selectorsMultiplied[] = $newSelectorPath;
749                        }
750                    }
751                }
752
753                // our new selectors has been multiplied, so reset the state
754                $newSelectors = $selectorsMultiplied;
755                $currentElements = [];
756            }
757        }
758
759        // if we have any elements left over (e.g. .a& .b == .b)
760        // add them on to all the current selectors
761        $this->mergeElementsOnToSelectors( $currentElements, $newSelectors );
762
763        foreach ( $newSelectors as &$sel ) {
764            $length = count( $sel );
765            if ( $length ) {
766                $lastSelector = $sel[$length - 1];
767                $sel[$length - 1] = $lastSelector->createDerived( $lastSelector->elements, $inSelector->extendList );
768                $paths[] = $sel;
769            }
770        }
771
772        return $hadParentSelector;
773    }
774
775    /**
776     * @param array $elementsToPak
777     * @param Less_Tree_Element $originalElement
778     * @return Less_Tree_Selector
779     */
780    private function createSelector( array $elementsToPak, $originalElement ) {
781        if ( !$elementsToPak ) {
782            // This is an invalid call. Kept to match less.js. Appears unreachable.
783            // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal
784            $containedElement = new Less_Tree_Paren( null );
785        } else {
786            $insideParent = [];
787            foreach ( $elementsToPak as $elToPak ) {
788                $insideParent[] = new Less_Tree_Element( null, $elToPak, $originalElement->index, $originalElement->currentFileInfo );
789            }
790            $containedElement = new Less_Tree_Paren( new Less_Tree_Selector( $insideParent ) );
791        }
792
793        $element = new Less_Tree_Element( null, $containedElement, $originalElement->index, $originalElement->currentFileInfo );
794        return new Less_Tree_Selector( [ $element ] );
795    }
796
797    /**
798     * @param Less_Tree_Element $element
799     * @return Less_Tree_Selector|null
800     */
801    private function findNestedSelector( $element ) {
802        $maybeParen = $element->value;
803        if ( !( $maybeParen instanceof Less_Tree_Paren ) ) {
804            return null;
805        }
806        $maybeSelector = $maybeParen->value;
807        if ( !( $maybeSelector instanceof Less_Tree_Selector ) ) {
808            return null;
809        }
810        return $maybeSelector;
811    }
812
813    /**
814     * joins selector path from $beginningPath with selector path in $addPath.
815     *
816     * $replacedElement contains the element that is being replaced by $addPath
817     *
818     * @param Less_Tree_Selector[] $beginningPath
819     * @param Less_Tree_Selector[] $addPath
820     * @param Less_Tree_Element $replacedElement
821     * @param Less_Tree_Selector $originalSelector
822     * @return Less_Tree_Selector[] Concatenated path
823     * @see less-2.5.3.js#Ruleset-addReplacementIntoPath
824     */
825    private function addReplacementIntoPath( array $beginningPath, array $addPath, $replacedElement, $originalSelector ) {
826        // our new selector path
827        $newSelectorPath = [];
828
829        // construct the joined selector - if `&` is the first thing this will be empty,
830        // if not newJoinedSelector will be the last set of elements in the selector
831        if ( $beginningPath ) {
832            // NOTE: less.js uses Array slice() to copy. In PHP, arrays are naturally copied by value.
833            $newSelectorPath = $beginningPath;
834            $lastSelector = array_pop( $newSelectorPath );
835            $newJoinedSelector = $originalSelector->createDerived( $lastSelector->elements );
836        } else {
837            $newJoinedSelector = $originalSelector->createDerived( [] );
838        }
839
840        if ( $addPath ) {
841            // if the & does not have a combinator that is "" or " " then
842            // and there is a combinator on the parent, then grab that.
843            // this also allows `+ a { & .b { .a & { ...`
844            $combinator = $replacedElement->combinator;
845            $parentEl = $addPath[0]->elements[0];
846            if ( $replacedElement->combinatorIsEmptyOrWhitespace && !$parentEl->combinatorIsEmptyOrWhitespace ) {
847                $combinator = $parentEl->combinator;
848            }
849            // join the elements so far with the first part of the parent
850            $newJoinedSelector->elements[] = new Less_Tree_Element( $combinator, $parentEl->value, $replacedElement->index, $replacedElement->currentFileInfo );
851            $newJoinedSelector->elements = array_merge(
852                $newJoinedSelector->elements,
853                array_slice( $addPath[0]->elements, 1 )
854            );
855        }
856
857        // now add the joined selector - but only if it is not empty
858        if ( $newJoinedSelector->elements ) {
859            $newSelectorPath[] = $newJoinedSelector;
860        }
861
862        // put together the parent selectors after the join (e.g. the rest of the parent)
863        if ( count( $addPath ) > 1 ) {
864            $newSelectorPath = array_merge( $newSelectorPath, array_slice( $addPath, 1 ) );
865        }
866        return $newSelectorPath;
867    }
868
869    public function mergeElementsOnToSelectors( $elements, &$selectors ) {
870        if ( !$elements ) {
871            return;
872        }
873        if ( !$selectors ) {
874            $selectors[] = [ new Less_Tree_Selector( $elements ) ];
875            return;
876        }
877
878        foreach ( $selectors as &$sel ) {
879            // if the previous thing in sel is a parent this needs to join on to it
880            if ( $sel ) {
881                $last = count( $sel ) - 1;
882                $sel[$last] = $sel[$last]->createDerived( array_merge( $sel[$last]->elements, $elements ) );
883            } else {
884                $sel[] = new Less_Tree_Selector( $elements );
885            }
886        }
887    }
888}