Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
95.83% |
368 / 384 |
|
73.33% |
22 / 30 |
CRAP | |
0.00% |
0 / 1 |
| Less_Tree_Ruleset | |
95.83% |
368 / 384 |
|
73.33% |
22 / 30 |
193 | |
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% |
2 / 2 |
|
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 | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
5.01 | |||
| 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 |
24 | |||
| 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 | /** @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 | } |