[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/lessphp/Visitor/ -> processExtends.php (source)

   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  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1