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