Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.80% |
365 / 381 |
|
73.33% |
22 / 30 |
CRAP | |
0.00% |
0 / 1 |
Less_Tree_Ruleset | |
95.80% |
365 / 381 |
|
73.33% |
22 / 30 |
192 | |
0.00% |
0 / 1 |
SetRulesetIndex | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
accept | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
compile | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
17 | |||
EvalMixinCalls | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
11 | |||
PrepareRuleset | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
9 | |||
evalImports | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
makeImportant | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
matchArgs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
matchCondition | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 | |||
resetCache | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
variables | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
7 | |||
properties | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
10 | |||
variable | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
property | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
transformDeclaration | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
5.02 | |||
lastDeclaration | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
parseValue | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
find | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
13 | |||
isRulesetLikeNode | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
5 | |||
genCSS | |
98.18% |
54 / 55 |
|
0.00% |
0 / 1 |
23 | |||
markReferenced | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
6 | |||
getIsReferenced | |
50.00% |
5 / 10 |
|
0.00% |
0 / 1 |
22.50 | |||
joinSelectors | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
joinSelector | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
replaceParentSelector | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
13 | |||
createSelector | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
findNestedSelector | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
addReplacementIntoPath | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
7 | |||
mergeElementsOnToSelectors | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
5.20 |
1 | <?php |
2 | /** |
3 | * @private |
4 | */ |
5 | class 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 | } |