[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Process Extends Visitor 5 * 6 * @package Less 7 * @subpackage visitor 8 */ 9 class Less_Visitor_processExtends extends Less_Visitor{ 10 11 public $allExtendsStack; 12 13 /** 14 * @param Less_Tree_Ruleset $root 15 */ 16 public function run( $root ){ 17 $extendFinder = new Less_Visitor_extendFinder(); 18 $extendFinder->run( $root ); 19 if( !$extendFinder->foundExtends){ 20 return $root; 21 } 22 23 $root->allExtends = $this->doExtendChaining( $root->allExtends, $root->allExtends); 24 25 $this->allExtendsStack = array(); 26 $this->allExtendsStack[] = &$root->allExtends; 27 28 return $this->visitObj( $root ); 29 } 30 31 private function doExtendChaining( $extendsList, $extendsListTarget, $iterationCount = 0){ 32 // 33 // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting 34 // the selector we would do normally, but we are also adding an extend with the same target selector 35 // this means this new extend can then go and alter other extends 36 // 37 // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors 38 // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if 39 // we look at each selector at a time, as is done in visitRuleset 40 41 $extendsToAdd = array(); 42 43 44 //loop through comparing every extend with every target extend. 45 // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place 46 // e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one 47 // and the second is the target. 48 // the seperation into two lists allows us to process a subset of chains with a bigger set, as is the 49 // case when processing media queries 50 for( $extendIndex = 0, $extendsList_len = count($extendsList); $extendIndex < $extendsList_len; $extendIndex++ ){ 51 for( $targetExtendIndex = 0; $targetExtendIndex < count($extendsListTarget); $targetExtendIndex++ ){ 52 53 $extend = $extendsList[$extendIndex]; 54 $targetExtend = $extendsListTarget[$targetExtendIndex]; 55 56 // look for circular references 57 if( in_array($targetExtend->object_id, $extend->parent_ids,true) ){ 58 continue; 59 } 60 61 // find a match in the target extends self selector (the bit before :extend) 62 $selectorPath = array( $targetExtend->selfSelectors[0] ); 63 $matches = $this->findMatch( $extend, $selectorPath); 64 65 66 if( $matches ){ 67 68 // we found a match, so for each self selector.. 69 foreach($extend->selfSelectors as $selfSelector ){ 70 71 72 // process the extend as usual 73 $newSelector = $this->extendSelector( $matches, $selectorPath, $selfSelector); 74 75 // but now we create a new extend from it 76 $newExtend = new Less_Tree_Extend( $targetExtend->selector, $targetExtend->option, 0); 77 $newExtend->selfSelectors = $newSelector; 78 79 // add the extend onto the list of extends for that selector 80 end($newSelector)->extendList = array($newExtend); 81 //$newSelector[ count($newSelector)-1]->extendList = array($newExtend); 82 83 // record that we need to add it. 84 $extendsToAdd[] = $newExtend; 85 $newExtend->ruleset = $targetExtend->ruleset; 86 87 //remember its parents for circular references 88 $newExtend->parent_ids = array_merge($newExtend->parent_ids,$targetExtend->parent_ids,$extend->parent_ids); 89 90 // only process the selector once.. if we have :extend(.a,.b) then multiple 91 // extends will look at the same selector path, so when extending 92 // we know that any others will be duplicates in terms of what is added to the css 93 if( $targetExtend->firstExtendOnThisSelectorPath ){ 94 $newExtend->firstExtendOnThisSelectorPath = true; 95 $targetExtend->ruleset->paths[] = $newSelector; 96 } 97 } 98 } 99 } 100 } 101 102 if( $extendsToAdd ){ 103 // try to detect circular references to stop a stack overflow. 104 // may no longer be needed. $this->extendChainCount++; 105 if( $iterationCount > 100) { 106 107 try{ 108 $selectorOne = $extendsToAdd[0]->selfSelectors[0]->toCSS(); 109 $selectorTwo = $extendsToAdd[0]->selector->toCSS(); 110 }catch(Exception $e){ 111 $selectorOne = "{unable to calculate}"; 112 $selectorTwo = "{unable to calculate}"; 113 } 114 115 throw new Less_Exception_Parser("extend circular reference detected. One of the circular extends is currently:" . $selectorOne . ":extend(" . $selectorTwo . ")"); 116 } 117 118 // now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e... 119 $extendsToAdd = $this->doExtendChaining( $extendsToAdd, $extendsListTarget, $iterationCount+1); 120 } 121 122 return array_merge($extendsList, $extendsToAdd); 123 } 124 125 126 protected function visitRule( $ruleNode, &$visitDeeper ){ 127 $visitDeeper = false; 128 } 129 130 protected function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ){ 131 $visitDeeper = false; 132 } 133 134 protected function visitSelector( $selectorNode, &$visitDeeper ){ 135 $visitDeeper = false; 136 } 137 138 protected function visitRuleset($rulesetNode){ 139 140 141 if( $rulesetNode->root ){ 142 return; 143 } 144 145 $allExtends = end($this->allExtendsStack); 146 $paths_len = count($rulesetNode->paths); 147 148 // look at each selector path in the ruleset, find any extend matches and then copy, find and replace 149 foreach($allExtends as $allExtend){ 150 for($pathIndex = 0; $pathIndex < $paths_len; $pathIndex++ ){ 151 152 // extending extends happens initially, before the main pass 153 if( isset($rulesetNode->extendOnEveryPath) && $rulesetNode->extendOnEveryPath ){ 154 continue; 155 } 156 157 $selectorPath = $rulesetNode->paths[$pathIndex]; 158 159 if( end($selectorPath)->extendList ){ 160 continue; 161 } 162 163 $this->ExtendMatch( $rulesetNode, $allExtend, $selectorPath); 164 165 } 166 } 167 } 168 169 170 private function ExtendMatch( $rulesetNode, $extend, $selectorPath ){ 171 $matches = $this->findMatch($extend, $selectorPath); 172 173 if( $matches ){ 174 foreach($extend->selfSelectors as $selfSelector ){ 175 $rulesetNode->paths[] = $this->extendSelector($matches, $selectorPath, $selfSelector); 176 } 177 } 178 } 179 180 181 182 private function findMatch($extend, $haystackSelectorPath ){ 183 184 185 if( !$this->HasMatches($extend, $haystackSelectorPath) ){ 186 return false; 187 } 188 189 190 // 191 // look through the haystack selector path to try and find the needle - extend.selector 192 // returns an array of selector matches that can then be replaced 193 // 194 $needleElements = $extend->selector->elements; 195 $potentialMatches = array(); 196 $potentialMatches_len = 0; 197 $potentialMatch = null; 198 $matches = array(); 199 200 201 202 // loop through the haystack elements 203 $haystack_path_len = count($haystackSelectorPath); 204 for($haystackSelectorIndex = 0; $haystackSelectorIndex < $haystack_path_len; $haystackSelectorIndex++ ){ 205 $hackstackSelector = $haystackSelectorPath[$haystackSelectorIndex]; 206 207 $haystack_elements_len = count($hackstackSelector->elements); 208 for($hackstackElementIndex = 0; $hackstackElementIndex < $haystack_elements_len; $hackstackElementIndex++ ){ 209 210 $haystackElement = $hackstackSelector->elements[$hackstackElementIndex]; 211 212 // if we allow elements before our match we can add a potential match every time. otherwise only at the first element. 213 if( $extend->allowBefore || ($haystackSelectorIndex === 0 && $hackstackElementIndex === 0) ){ 214 $potentialMatches[] = array('pathIndex'=> $haystackSelectorIndex, 'index'=> $hackstackElementIndex, 'matched'=> 0, 'initialCombinator'=> $haystackElement->combinator); 215 $potentialMatches_len++; 216 } 217 218 for($i = 0; $i < $potentialMatches_len; $i++ ){ 219 220 $potentialMatch = &$potentialMatches[$i]; 221 $potentialMatch = $this->PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex ); 222 223 224 // if we are still valid and have finished, test whether we have elements after and whether these are allowed 225 if( $potentialMatch && $potentialMatch['matched'] === $extend->selector->elements_len ){ 226 $potentialMatch['finished'] = true; 227 228 if( !$extend->allowAfter && ($hackstackElementIndex+1 < $haystack_elements_len || $haystackSelectorIndex+1 < $haystack_path_len) ){ 229 $potentialMatch = null; 230 } 231 } 232 233 // if null we remove, if not, we are still valid, so either push as a valid match or continue 234 if( $potentialMatch ){ 235 if( $potentialMatch['finished'] ){ 236 $potentialMatch['length'] = $extend->selector->elements_len; 237 $potentialMatch['endPathIndex'] = $haystackSelectorIndex; 238 $potentialMatch['endPathElementIndex'] = $hackstackElementIndex + 1; // index after end of match 239 $potentialMatches = array(); // we don't allow matches to overlap, so start matching again 240 $potentialMatches_len = 0; 241 $matches[] = $potentialMatch; 242 } 243 continue; 244 } 245 246 array_splice($potentialMatches, $i, 1); 247 $potentialMatches_len--; 248 $i--; 249 } 250 } 251 } 252 253 return $matches; 254 } 255 256 257 // Before going through all the nested loops, lets check to see if a match is possible 258 // Reduces Bootstrap 3.1 compile time from ~6.5s to ~5.6s 259 private function HasMatches($extend, $haystackSelectorPath){ 260 261 if( !$extend->selector->cacheable ){ 262 return true; 263 } 264 265 $first_el = $extend->selector->_oelements[0]; 266 267 foreach($haystackSelectorPath as $hackstackSelector){ 268 if( !$hackstackSelector->cacheable ){ 269 return true; 270 } 271 272 if( in_array($first_el, $hackstackSelector->_oelements) ){ 273 return true; 274 } 275 } 276 277 return false; 278 } 279 280 281 /** 282 * @param integer $hackstackElementIndex 283 */ 284 private function PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex ){ 285 286 287 if( $potentialMatch['matched'] > 0 ){ 288 289 // selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't 290 // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to work out 291 // what the resulting combinator will be 292 $targetCombinator = $haystackElement->combinator; 293 if( $targetCombinator === '' && $hackstackElementIndex === 0 ){ 294 $targetCombinator = ' '; 295 } 296 297 if( $needleElements[ $potentialMatch['matched'] ]->combinator !== $targetCombinator ){ 298 return null; 299 } 300 } 301 302 // if we don't match, null our match to indicate failure 303 if( !$this->isElementValuesEqual( $needleElements[$potentialMatch['matched'] ]->value, $haystackElement->value) ){ 304 return null; 305 } 306 307 $potentialMatch['finished'] = false; 308 $potentialMatch['matched']++; 309 310 return $potentialMatch; 311 } 312 313 314 private function isElementValuesEqual( $elementValue1, $elementValue2 ){ 315 316 if( $elementValue1 === $elementValue2 ){ 317 return true; 318 } 319 320 if( is_string($elementValue1) || is_string($elementValue2) ) { 321 return false; 322 } 323 324 if( $elementValue1 instanceof Less_Tree_Attribute ){ 325 return $this->isAttributeValuesEqual( $elementValue1, $elementValue2 ); 326 } 327 328 $elementValue1 = $elementValue1->value; 329 if( $elementValue1 instanceof Less_Tree_Selector ){ 330 return $this->isSelectorValuesEqual( $elementValue1, $elementValue2 ); 331 } 332 333 return false; 334 } 335 336 337 /** 338 * @param Less_Tree_Selector $elementValue1 339 */ 340 private function isSelectorValuesEqual( $elementValue1, $elementValue2 ){ 341 342 $elementValue2 = $elementValue2->value; 343 if( !($elementValue2 instanceof Less_Tree_Selector) || $elementValue1->elements_len !== $elementValue2->elements_len ){ 344 return false; 345 } 346 347 for( $i = 0; $i < $elementValue1->elements_len; $i++ ){ 348 349 if( $elementValue1->elements[$i]->combinator !== $elementValue2->elements[$i]->combinator ){ 350 if( $i !== 0 || ($elementValue1->elements[$i]->combinator || ' ') !== ($elementValue2->elements[$i]->combinator || ' ') ){ 351 return false; 352 } 353 } 354 355 if( !$this->isElementValuesEqual($elementValue1->elements[$i]->value, $elementValue2->elements[$i]->value) ){ 356 return false; 357 } 358 } 359 360 return true; 361 } 362 363 364 /** 365 * @param Less_Tree_Attribute $elementValue1 366 */ 367 private function isAttributeValuesEqual( $elementValue1, $elementValue2 ){ 368 369 if( $elementValue1->op !== $elementValue2->op || $elementValue1->key !== $elementValue2->key ){ 370 return false; 371 } 372 373 if( !$elementValue1->value || !$elementValue2->value ){ 374 if( $elementValue1->value || $elementValue2->value ) { 375 return false; 376 } 377 return true; 378 } 379 380 $elementValue1 = ($elementValue1->value->value ? $elementValue1->value->value : $elementValue1->value ); 381 $elementValue2 = ($elementValue2->value->value ? $elementValue2->value->value : $elementValue2->value ); 382 383 return $elementValue1 === $elementValue2; 384 } 385 386 387 private function extendSelector($matches, $selectorPath, $replacementSelector){ 388 389 //for a set of matches, replace each match with the replacement selector 390 391 $currentSelectorPathIndex = 0; 392 $currentSelectorPathElementIndex = 0; 393 $path = array(); 394 $selectorPath_len = count($selectorPath); 395 396 for($matchIndex = 0, $matches_len = count($matches); $matchIndex < $matches_len; $matchIndex++ ){ 397 398 399 $match = $matches[$matchIndex]; 400 $selector = $selectorPath[ $match['pathIndex'] ]; 401 402 $firstElement = new Less_Tree_Element( 403 $match['initialCombinator'], 404 $replacementSelector->elements[0]->value, 405 $replacementSelector->elements[0]->index, 406 $replacementSelector->elements[0]->currentFileInfo 407 ); 408 409 if( $match['pathIndex'] > $currentSelectorPathIndex && $currentSelectorPathElementIndex > 0 ){ 410 $last_path = end($path); 411 $last_path->elements = array_merge( $last_path->elements, array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex)); 412 $currentSelectorPathElementIndex = 0; 413 $currentSelectorPathIndex++; 414 } 415 416 $newElements = array_merge( 417 array_slice($selector->elements, $currentSelectorPathElementIndex, ($match['index'] - $currentSelectorPathElementIndex) ) // last parameter of array_slice is different than the last parameter of javascript's slice 418 , array($firstElement) 419 , array_slice($replacementSelector->elements,1) 420 ); 421 422 if( $currentSelectorPathIndex === $match['pathIndex'] && $matchIndex > 0 ){ 423 $last_key = count($path)-1; 424 $path[$last_key]->elements = array_merge($path[$last_key]->elements,$newElements); 425 }else{ 426 $path = array_merge( $path, array_slice( $selectorPath, $currentSelectorPathIndex, $match['pathIndex'] )); 427 $path[] = new Less_Tree_Selector( $newElements ); 428 } 429 430 $currentSelectorPathIndex = $match['endPathIndex']; 431 $currentSelectorPathElementIndex = $match['endPathElementIndex']; 432 if( $currentSelectorPathElementIndex >= count($selectorPath[$currentSelectorPathIndex]->elements) ){ 433 $currentSelectorPathElementIndex = 0; 434 $currentSelectorPathIndex++; 435 } 436 } 437 438 if( $currentSelectorPathIndex < $selectorPath_len && $currentSelectorPathElementIndex > 0 ){ 439 $last_path = end($path); 440 $last_path->elements = array_merge( $last_path->elements, array_slice($selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex)); 441 $currentSelectorPathIndex++; 442 } 443 444 $slice_len = $selectorPath_len - $currentSelectorPathIndex; 445 $path = array_merge($path, array_slice($selectorPath, $currentSelectorPathIndex, $slice_len)); 446 447 return $path; 448 } 449 450 451 protected function visitMedia( $mediaNode ){ 452 $newAllExtends = array_merge( $mediaNode->allExtends, end($this->allExtendsStack) ); 453 $this->allExtendsStack[] = $this->doExtendChaining($newAllExtends, $mediaNode->allExtends); 454 } 455 456 protected function visitMediaOut(){ 457 array_pop( $this->allExtendsStack ); 458 } 459 460 protected function visitDirective( $directiveNode ){ 461 $newAllExtends = array_merge( $directiveNode->allExtends, end($this->allExtendsStack) ); 462 $this->allExtendsStack[] = $this->doExtendChaining($newAllExtends, $directiveNode->allExtends); 463 } 464 465 protected function visitDirectiveOut(){ 466 array_pop($this->allExtendsStack); 467 } 468 469 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |