From 7ad9f272a482802da2d43fe83841adbe9bcd8cb4 Mon Sep 17 00:00:00 2001 From: derchris Date: Sat, 2 Feb 2019 15:35:30 +0100 Subject: update less.php to PHP 7.x compatible fork --- lib/less.php/Visitor/processExtends.php | 469 ++++++++++++++++++++++++++++++++ 1 file changed, 469 insertions(+) create mode 100644 lib/less.php/Visitor/processExtends.php (limited to 'lib/less.php/Visitor/processExtends.php') diff --git a/lib/less.php/Visitor/processExtends.php b/lib/less.php/Visitor/processExtends.php new file mode 100644 index 0000000..bb5f082 --- /dev/null +++ b/lib/less.php/Visitor/processExtends.php @@ -0,0 +1,469 @@ +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 -- cgit v1.2.3