]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blobdiff - vendor/oyejorge/less.php/lib/Less/Visitor/processExtends.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / vendor / oyejorge / less.php / lib / Less / Visitor / processExtends.php
diff --git a/vendor/oyejorge/less.php/lib/Less/Visitor/processExtends.php b/vendor/oyejorge/less.php/lib/Less/Visitor/processExtends.php
new file mode 100644 (file)
index 0000000..bb5f082
--- /dev/null
@@ -0,0 +1,469 @@
+<?php
+
+/**
+ * Process Extends Visitor
+ *
+ * @package Less
+ * @subpackage visitor
+ */
+class Less_Visitor_processExtends extends Less_Visitor{
+
+       public $allExtendsStack;
+
+       /**
+        * @param Less_Tree_Ruleset $root
+        */
+       public function run( $root ){
+               $extendFinder = new Less_Visitor_extendFinder();
+               $extendFinder->run( $root );
+               if( !$extendFinder->foundExtends){
+                       return $root;
+               }
+
+               $root->allExtends = $this->doExtendChaining( $root->allExtends, $root->allExtends);
+
+               $this->allExtendsStack = array();
+               $this->allExtendsStack[] = &$root->allExtends;
+
+               return $this->visitObj( $root );
+       }
+
+       private function doExtendChaining( $extendsList, $extendsListTarget, $iterationCount = 0){
+               //
+               // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting
+               // the selector we would do normally, but we are also adding an extend with the same target selector
+               // this means this new extend can then go and alter other extends
+               //
+               // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors
+               // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if
+               // we look at each selector at a time, as is done in visitRuleset
+
+               $extendsToAdd = array();
+
+
+               //loop through comparing every extend with every target extend.
+               // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place
+               // e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one
+               // and the second is the target.
+               // the separation into two lists allows us to process a subset of chains with a bigger set, as is the
+               // case when processing media queries
+               for( $extendIndex = 0, $extendsList_len = count($extendsList); $extendIndex < $extendsList_len; $extendIndex++ ){
+                       for( $targetExtendIndex = 0; $targetExtendIndex < count($extendsListTarget); $targetExtendIndex++ ){
+
+                               $extend = $extendsList[$extendIndex];
+                               $targetExtend = $extendsListTarget[$targetExtendIndex];
+
+                               // look for circular references
+                               if( in_array($targetExtend->object_id, $extend->parent_ids,true) ){
+                                       continue;
+                               }
+
+                               // find a match in the target extends self selector (the bit before :extend)
+                               $selectorPath = array( $targetExtend->selfSelectors[0] );
+                               $matches = $this->findMatch( $extend, $selectorPath);
+
+
+                               if( $matches ){
+
+                                       // we found a match, so for each self selector..
+                                       foreach($extend->selfSelectors as $selfSelector ){
+
+
+                                               // process the extend as usual
+                                               $newSelector = $this->extendSelector( $matches, $selectorPath, $selfSelector);
+
+                                               // but now we create a new extend from it
+                                               $newExtend = new Less_Tree_Extend( $targetExtend->selector, $targetExtend->option, 0);
+                                               $newExtend->selfSelectors = $newSelector;
+
+                                               // add the extend onto the list of extends for that selector
+                                               end($newSelector)->extendList = array($newExtend);
+                                               //$newSelector[ count($newSelector)-1]->extendList = array($newExtend);
+
+                                               // record that we need to add it.
+                                               $extendsToAdd[] = $newExtend;
+                                               $newExtend->ruleset = $targetExtend->ruleset;
+
+                                               //remember its parents for circular references
+                                               $newExtend->parent_ids = array_merge($newExtend->parent_ids,$targetExtend->parent_ids,$extend->parent_ids);
+
+                                               // only process the selector once.. if we have :extend(.a,.b) then multiple
+                                               // extends will look at the same selector path, so when extending
+                                               // we know that any others will be duplicates in terms of what is added to the css
+                                               if( $targetExtend->firstExtendOnThisSelectorPath ){
+                                                       $newExtend->firstExtendOnThisSelectorPath = true;
+                                                       $targetExtend->ruleset->paths[] = $newSelector;
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               if( $extendsToAdd ){
+                       // try to detect circular references to stop a stack overflow.
+                       // may no longer be needed.                     $this->extendChainCount++;
+                       if( $iterationCount > 100) {
+
+                               try{
+                                       $selectorOne = $extendsToAdd[0]->selfSelectors[0]->toCSS();
+                                       $selectorTwo = $extendsToAdd[0]->selector->toCSS();
+                               }catch(Exception $e){
+                                       $selectorOne = "{unable to calculate}";
+                                       $selectorTwo = "{unable to calculate}";
+                               }
+
+                               throw new Less_Exception_Parser("extend circular reference detected. One of the circular extends is currently:" . $selectorOne . ":extend(" . $selectorTwo . ")");
+                       }
+
+                       // now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e...
+                       $extendsToAdd = $this->doExtendChaining( $extendsToAdd, $extendsListTarget, $iterationCount+1);
+               }
+
+               return array_merge($extendsList, $extendsToAdd);
+       }
+
+
+       protected function visitRule( $ruleNode, &$visitDeeper ){
+               $visitDeeper = false;
+       }
+
+       protected function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ){
+               $visitDeeper = false;
+       }
+
+       protected function visitSelector( $selectorNode, &$visitDeeper ){
+               $visitDeeper = false;
+       }
+
+       protected function visitRuleset($rulesetNode){
+
+
+               if( $rulesetNode->root ){
+                       return;
+               }
+
+               $allExtends     = end($this->allExtendsStack);
+               $paths_len = count($rulesetNode->paths);
+
+               // look at each selector path in the ruleset, find any extend matches and then copy, find and replace
+               foreach($allExtends as $allExtend){
+                       for($pathIndex = 0; $pathIndex < $paths_len; $pathIndex++ ){
+
+                               // extending extends happens initially, before the main pass
+                               if( isset($rulesetNode->extendOnEveryPath) && $rulesetNode->extendOnEveryPath ){
+                                       continue;
+                               }
+
+                               $selectorPath = $rulesetNode->paths[$pathIndex];
+
+                               if( end($selectorPath)->extendList ){
+                                       continue;
+                               }
+
+                               $this->ExtendMatch( $rulesetNode, $allExtend, $selectorPath);
+
+                       }
+               }
+       }
+
+
+       private function ExtendMatch( $rulesetNode, $extend, $selectorPath ){
+               $matches = $this->findMatch($extend, $selectorPath);
+
+               if( $matches ){
+                       foreach($extend->selfSelectors as $selfSelector ){
+                               $rulesetNode->paths[] = $this->extendSelector($matches, $selectorPath, $selfSelector);
+                       }
+               }
+       }
+
+
+
+       private function findMatch($extend, $haystackSelectorPath ){
+
+
+               if( !$this->HasMatches($extend, $haystackSelectorPath) ){
+                       return false;
+               }
+
+
+               //
+               // look through the haystack selector path to try and find the needle - extend.selector
+               // returns an array of selector matches that can then be replaced
+               //
+               $needleElements = $extend->selector->elements;
+               $potentialMatches = array();
+               $potentialMatches_len = 0;
+               $potentialMatch = null;
+               $matches = array();
+
+
+
+               // loop through the haystack elements
+               $haystack_path_len = count($haystackSelectorPath);
+               for($haystackSelectorIndex = 0; $haystackSelectorIndex < $haystack_path_len; $haystackSelectorIndex++ ){
+                       $hackstackSelector = $haystackSelectorPath[$haystackSelectorIndex];
+
+                       $haystack_elements_len = count($hackstackSelector->elements);
+                       for($hackstackElementIndex = 0; $hackstackElementIndex < $haystack_elements_len; $hackstackElementIndex++ ){
+
+                               $haystackElement = $hackstackSelector->elements[$hackstackElementIndex];
+
+                               // if we allow elements before our match we can add a potential match every time. otherwise only at the first element.
+                               if( $extend->allowBefore || ($haystackSelectorIndex === 0 && $hackstackElementIndex === 0) ){
+                                       $potentialMatches[] = array('pathIndex'=> $haystackSelectorIndex, 'index'=> $hackstackElementIndex, 'matched'=> 0, 'initialCombinator'=> $haystackElement->combinator);
+                                       $potentialMatches_len++;
+                               }
+
+                               for($i = 0; $i < $potentialMatches_len; $i++ ){
+
+                                       $potentialMatch = &$potentialMatches[$i];
+                                       $potentialMatch = $this->PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex );
+
+
+                                       // if we are still valid and have finished, test whether we have elements after and whether these are allowed
+                                       if( $potentialMatch && $potentialMatch['matched'] === $extend->selector->elements_len ){
+                                               $potentialMatch['finished'] = true;
+
+                                               if( !$extend->allowAfter && ($hackstackElementIndex+1 < $haystack_elements_len || $haystackSelectorIndex+1 < $haystack_path_len) ){
+                                                       $potentialMatch = null;
+                                               }
+                                       }
+
+                                       // if null we remove, if not, we are still valid, so either push as a valid match or continue
+                                       if( $potentialMatch ){
+                                               if( $potentialMatch['finished'] ){
+                                                       $potentialMatch['length'] = $extend->selector->elements_len;
+                                                       $potentialMatch['endPathIndex'] = $haystackSelectorIndex;
+                                                       $potentialMatch['endPathElementIndex'] = $hackstackElementIndex + 1; // index after end of match
+                                                       $potentialMatches = array(); // we don't allow matches to overlap, so start matching again
+                                                       $potentialMatches_len = 0;
+                                                       $matches[] = $potentialMatch;
+                                               }
+                                               continue;
+                                       }
+
+                                       array_splice($potentialMatches, $i, 1);
+                                       $potentialMatches_len--;
+                                       $i--;
+                               }
+                       }
+               }
+
+               return $matches;
+       }
+
+
+       // Before going through all the nested loops, lets check to see if a match is possible
+       // Reduces Bootstrap 3.1 compile time from ~6.5s to ~5.6s
+       private function HasMatches($extend, $haystackSelectorPath){
+
+               if( !$extend->selector->cacheable ){
+                       return true;
+               }
+
+               $first_el = $extend->selector->_oelements[0];
+
+               foreach($haystackSelectorPath as $hackstackSelector){
+                       if( !$hackstackSelector->cacheable ){
+                               return true;
+                       }
+
+                       if( in_array($first_el, $hackstackSelector->_oelements) ){
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
+
+       /**
+        * @param integer $hackstackElementIndex
+        */
+       private function PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex ){
+
+
+               if( $potentialMatch['matched'] > 0 ){
+
+                       // selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't
+                       // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to work out
+                       // what the resulting combinator will be
+                       $targetCombinator = $haystackElement->combinator;
+                       if( $targetCombinator === '' && $hackstackElementIndex === 0 ){
+                               $targetCombinator = ' ';
+                       }
+
+                       if( $needleElements[ $potentialMatch['matched'] ]->combinator !== $targetCombinator ){
+                               return null;
+                       }
+               }
+
+               // if we don't match, null our match to indicate failure
+               if( !$this->isElementValuesEqual( $needleElements[$potentialMatch['matched'] ]->value, $haystackElement->value) ){
+                       return null;
+               }
+
+               $potentialMatch['finished'] = false;
+               $potentialMatch['matched']++;
+
+               return $potentialMatch;
+       }
+
+
+       private function isElementValuesEqual( $elementValue1, $elementValue2 ){
+
+               if( $elementValue1 === $elementValue2 ){
+                       return true;
+               }
+
+               if( is_string($elementValue1) || is_string($elementValue2) ) {
+                       return false;
+               }
+
+               if( $elementValue1 instanceof Less_Tree_Attribute ){
+                       return $this->isAttributeValuesEqual( $elementValue1, $elementValue2 );
+               }
+
+               $elementValue1 = $elementValue1->value;
+               if( $elementValue1 instanceof Less_Tree_Selector ){
+                       return $this->isSelectorValuesEqual( $elementValue1, $elementValue2 );
+               }
+
+               return false;
+       }
+
+
+       /**
+        * @param Less_Tree_Selector $elementValue1
+        */
+       private function isSelectorValuesEqual( $elementValue1, $elementValue2 ){
+
+               $elementValue2 = $elementValue2->value;
+               if( !($elementValue2 instanceof Less_Tree_Selector) || $elementValue1->elements_len !== $elementValue2->elements_len ){
+                       return false;
+               }
+
+               for( $i = 0; $i < $elementValue1->elements_len; $i++ ){
+
+                       if( $elementValue1->elements[$i]->combinator !== $elementValue2->elements[$i]->combinator ){
+                               if( $i !== 0 || ($elementValue1->elements[$i]->combinator || ' ') !== ($elementValue2->elements[$i]->combinator || ' ') ){
+                                       return false;
+                               }
+                       }
+
+                       if( !$this->isElementValuesEqual($elementValue1->elements[$i]->value, $elementValue2->elements[$i]->value) ){
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+
+
+       /**
+        * @param Less_Tree_Attribute $elementValue1
+        */
+       private function isAttributeValuesEqual( $elementValue1, $elementValue2 ){
+
+               if( $elementValue1->op !== $elementValue2->op || $elementValue1->key !== $elementValue2->key ){
+                       return false;
+               }
+
+               if( !$elementValue1->value || !$elementValue2->value ){
+                       if( $elementValue1->value || $elementValue2->value ) {
+                               return false;
+                       }
+                       return true;
+               }
+
+               $elementValue1 = ($elementValue1->value->value ? $elementValue1->value->value : $elementValue1->value );
+               $elementValue2 = ($elementValue2->value->value ? $elementValue2->value->value : $elementValue2->value );
+
+               return $elementValue1 === $elementValue2;
+       }
+
+
+       private function extendSelector($matches, $selectorPath, $replacementSelector){
+
+               //for a set of matches, replace each match with the replacement selector
+
+               $currentSelectorPathIndex = 0;
+               $currentSelectorPathElementIndex = 0;
+               $path = array();
+               $selectorPath_len = count($selectorPath);
+
+               for($matchIndex = 0, $matches_len = count($matches); $matchIndex < $matches_len; $matchIndex++ ){
+
+
+                       $match = $matches[$matchIndex];
+                       $selector = $selectorPath[ $match['pathIndex'] ];
+
+                       $firstElement = new Less_Tree_Element(
+                               $match['initialCombinator'],
+                               $replacementSelector->elements[0]->value,
+                               $replacementSelector->elements[0]->index,
+                               $replacementSelector->elements[0]->currentFileInfo
+                       );
+
+                       if( $match['pathIndex'] > $currentSelectorPathIndex && $currentSelectorPathElementIndex > 0 ){
+                               $last_path = end($path);
+                               $last_path->elements = array_merge( $last_path->elements, array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex));
+                               $currentSelectorPathElementIndex = 0;
+                               $currentSelectorPathIndex++;
+                       }
+
+                       $newElements = array_merge(
+                               array_slice($selector->elements, $currentSelectorPathElementIndex, ($match['index'] - $currentSelectorPathElementIndex) ) // last parameter of array_slice is different than the last parameter of javascript's slice
+                               , array($firstElement)
+                               , array_slice($replacementSelector->elements,1)
+                               );
+
+                       if( $currentSelectorPathIndex === $match['pathIndex'] && $matchIndex > 0 ){
+                               $last_key = count($path)-1;
+                               $path[$last_key]->elements = array_merge($path[$last_key]->elements,$newElements);
+                       }else{
+                               $path = array_merge( $path, array_slice( $selectorPath, $currentSelectorPathIndex, $match['pathIndex'] ));
+                               $path[] = new Less_Tree_Selector( $newElements );
+                       }
+
+                       $currentSelectorPathIndex = $match['endPathIndex'];
+                       $currentSelectorPathElementIndex = $match['endPathElementIndex'];
+                       if( $currentSelectorPathElementIndex >= count($selectorPath[$currentSelectorPathIndex]->elements) ){
+                               $currentSelectorPathElementIndex = 0;
+                               $currentSelectorPathIndex++;
+                       }
+               }
+
+               if( $currentSelectorPathIndex < $selectorPath_len && $currentSelectorPathElementIndex > 0 ){
+                       $last_path = end($path);
+                       $last_path->elements = array_merge( $last_path->elements, array_slice($selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex));
+                       $currentSelectorPathIndex++;
+               }
+
+               $slice_len = $selectorPath_len - $currentSelectorPathIndex;
+               $path = array_merge($path, array_slice($selectorPath, $currentSelectorPathIndex, $slice_len));
+
+               return $path;
+       }
+
+
+       protected function visitMedia( $mediaNode ){
+               $newAllExtends = array_merge( $mediaNode->allExtends, end($this->allExtendsStack) );
+               $this->allExtendsStack[] = $this->doExtendChaining($newAllExtends, $mediaNode->allExtends);
+       }
+
+       protected function visitMediaOut(){
+               array_pop( $this->allExtendsStack );
+       }
+
+       protected function visitDirective( $directiveNode ){
+               $newAllExtends = array_merge( $directiveNode->allExtends, end($this->allExtendsStack) );
+               $this->allExtendsStack[] = $this->doExtendChaining($newAllExtends, $directiveNode->allExtends);
+       }
+
+       protected function visitDirectiveOut(){
+               array_pop($this->allExtendsStack);
+       }
+
+}
\ No newline at end of file