Viewing file: processExtends.php (15.42 KB) -rw-rw-rw- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?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 seperation 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); }
}
|