4 * Process Extends Visitor
9 class Less_Visitor_processExtends
extends Less_Visitor
{
11 public $allExtendsStack;
14 * @param Less_Tree_Ruleset $root
16 public function run( $root ){
17 $extendFinder = new Less_Visitor_extendFinder();
18 $extendFinder->run( $root );
19 if( !$extendFinder->foundExtends
){
23 $root->allExtends
= $this->doExtendChaining( $root->allExtends
, $root->allExtends
);
25 $this->allExtendsStack
= array();
26 $this->allExtendsStack
[] = &$root->allExtends
;
28 return $this->visitObj( $root );
31 private function doExtendChaining( $extendsList, $extendsListTarget, $iterationCount = 0){
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
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
41 $extendsToAdd = array();
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++
){
53 $extend = $extendsList[$extendIndex];
54 $targetExtend = $extendsListTarget[$targetExtendIndex];
56 // look for circular references
57 if( in_array($targetExtend->object_id
, $extend->parent_ids
,true) ){
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);
68 // we found a match, so for each self selector..
69 foreach($extend->selfSelectors
as $selfSelector ){
72 // process the extend as usual
73 $newSelector = $this->extendSelector( $matches, $selectorPath, $selfSelector);
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;
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);
83 // record that we need to add it.
84 $extendsToAdd[] = $newExtend;
85 $newExtend->ruleset
= $targetExtend->ruleset
;
87 //remember its parents for circular references
88 $newExtend->parent_ids
= array_merge($newExtend->parent_ids
,$targetExtend->parent_ids
,$extend->parent_ids
);
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;
103 // try to detect circular references to stop a stack overflow.
104 // may no longer be needed. $this->extendChainCount++;
105 if( $iterationCount > 100) {
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}";
115 throw new Less_Exception_Parser("extend circular reference detected. One of the circular extends is currently:"+
$selectorOne+
":extend(" +
$selectorTwo+
")");
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);
122 return array_merge($extendsList, $extendsToAdd);
126 protected function visitRule( $ruleNode, &$visitDeeper ){
127 $visitDeeper = false;
130 protected function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ){
131 $visitDeeper = false;
134 protected function visitSelector( $selectorNode, &$visitDeeper ){
135 $visitDeeper = false;
138 protected function visitRuleset($rulesetNode){
141 if( $rulesetNode->root
){
145 $allExtends = end($this->allExtendsStack
);
146 $paths_len = count($rulesetNode->paths
);
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++
){
152 // extending extends happens initially, before the main pass
153 if( isset($rulesetNode->extendOnEveryPath
) && $rulesetNode->extendOnEveryPath
){
157 $selectorPath = $rulesetNode->paths
[$pathIndex];
159 if( end($selectorPath)->extendList
){
163 $this->ExtendMatch( $rulesetNode, $allExtend, $selectorPath);
170 private function ExtendMatch( $rulesetNode, $extend, $selectorPath ){
171 $matches = $this->findMatch($extend, $selectorPath);
174 foreach($extend->selfSelectors
as $selfSelector ){
175 $rulesetNode->paths
[] = $this->extendSelector($matches, $selectorPath, $selfSelector);
182 private function findMatch($extend, $haystackSelectorPath ){
185 if( !$this->HasMatches($extend, $haystackSelectorPath) ){
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
194 $needleElements = $extend->selector
->elements
;
195 $potentialMatches = array();
196 $potentialMatches_len = 0;
197 $potentialMatch = null;
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];
207 $haystack_elements_len = count($hackstackSelector->elements
);
208 for($hackstackElementIndex = 0; $hackstackElementIndex < $haystack_elements_len; $hackstackElementIndex++
){
210 $haystackElement = $hackstackSelector->elements
[$hackstackElementIndex];
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++
;
218 for($i = 0; $i < $potentialMatches_len; $i++
){
220 $potentialMatch = &$potentialMatches[$i];
221 $potentialMatch = $this->PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex );
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;
228 if( !$extend->allowAfter
&& ($hackstackElementIndex+
1 < $haystack_elements_len ||
$haystackSelectorIndex+
1 < $haystack_path_len) ){
229 $potentialMatch = null;
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;
246 array_splice($potentialMatches, $i, 1);
247 $potentialMatches_len--;
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){
261 if( !$extend->selector
->cacheable
){
265 $first_el = $extend->selector
->_oelements
[0];
267 foreach($haystackSelectorPath as $hackstackSelector){
268 if( !$hackstackSelector->cacheable
){
272 if( in_array($first_el, $hackstackSelector->_oelements
) ){
282 * @param integer $hackstackElementIndex
284 private function PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex ){
287 if( $potentialMatch['matched'] > 0 ){
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 = ' ';
297 if( $needleElements[ $potentialMatch['matched'] ]->combinator
!== $targetCombinator ){
302 // if we don't match, null our match to indicate failure
303 if( !$this->isElementValuesEqual( $needleElements[$potentialMatch['matched'] ]->value
, $haystackElement->value
) ){
307 $potentialMatch['finished'] = false;
308 $potentialMatch['matched']++
;
310 return $potentialMatch;
314 private function isElementValuesEqual( $elementValue1, $elementValue2 ){
316 if( $elementValue1 === $elementValue2 ){
320 if( is_string($elementValue1) ||
is_string($elementValue2) ) {
324 if( $elementValue1 instanceof Less_Tree_Attribute
){
325 return $this->isAttributeValuesEqual( $elementValue1, $elementValue2 );
328 $elementValue1 = $elementValue1->value
;
329 if( $elementValue1 instanceof Less_Tree_Selector
){
330 return $this->isSelectorValuesEqual( $elementValue1, $elementValue2 );
338 * @param Less_Tree_Selector $elementValue1
340 private function isSelectorValuesEqual( $elementValue1, $elementValue2 ){
342 $elementValue2 = $elementValue2->value
;
343 if( !($elementValue2 instanceof Less_Tree_Selector
) ||
$elementValue1->elements_len
!== $elementValue2->elements_len
){
347 for( $i = 0; $i < $elementValue1->elements_len
; $i++
){
349 if( $elementValue1->elements
[$i]->combinator
!== $elementValue2->elements
[$i]->combinator
){
350 if( $i !== 0 ||
($elementValue1->elements
[$i]->combinator ||
' ') !== ($elementValue2->elements
[$i]->combinator ||
' ') ){
355 if( !$this->isElementValuesEqual($elementValue1->elements
[$i]->value
, $elementValue2->elements
[$i]->value
) ){
365 * @param Less_Tree_Attribute $elementValue1
367 private function isAttributeValuesEqual( $elementValue1, $elementValue2 ){
369 if( $elementValue1->op
!== $elementValue2->op ||
$elementValue1->key
!== $elementValue2->key
){
373 if( !$elementValue1->value ||
!$elementValue2->value
){
374 if( $elementValue1->value ||
$elementValue2->value
) {
380 $elementValue1 = ($elementValue1->value
->value ?
$elementValue1->value
->value
: $elementValue1->value
);
381 $elementValue2 = ($elementValue2->value
->value ?
$elementValue2->value
->value
: $elementValue2->value
);
383 return $elementValue1 === $elementValue2;
387 private function extendSelector($matches, $selectorPath, $replacementSelector){
389 //for a set of matches, replace each match with the replacement selector
391 $currentSelectorPathIndex = 0;
392 $currentSelectorPathElementIndex = 0;
394 $selectorPath_len = count($selectorPath);
396 for($matchIndex = 0, $matches_len = count($matches); $matchIndex < $matches_len; $matchIndex++
){
399 $match = $matches[$matchIndex];
400 $selector = $selectorPath[ $match['pathIndex'] ];
402 $firstElement = new Less_Tree_Element(
403 $match['initialCombinator'],
404 $replacementSelector->elements
[0]->value
,
405 $replacementSelector->elements
[0]->index
,
406 $replacementSelector->elements
[0]->currentFileInfo
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++
;
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)
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);
426 $path = array_merge( $path, array_slice( $selectorPath, $currentSelectorPathIndex, $match['pathIndex'] ));
427 $path[] = new Less_Tree_Selector( $newElements );
430 $currentSelectorPathIndex = $match['endPathIndex'];
431 $currentSelectorPathElementIndex = $match['endPathElementIndex'];
432 if( $currentSelectorPathElementIndex >= count($selectorPath[$currentSelectorPathIndex]->elements
) ){
433 $currentSelectorPathElementIndex = 0;
434 $currentSelectorPathIndex++
;
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++
;
444 $slice_len = $selectorPath_len - $currentSelectorPathIndex;
445 $path = array_merge($path, array_slice($selectorPath, $currentSelectorPathIndex, $slice_len));
451 protected function visitMedia( $mediaNode ){
452 $newAllExtends = array_merge( $mediaNode->allExtends
, end($this->allExtendsStack
) );
453 $this->allExtendsStack
[] = $this->doExtendChaining($newAllExtends, $mediaNode->allExtends
);
456 protected function visitMediaOut(){
457 array_pop( $this->allExtendsStack
);
460 protected function visitDirective( $directiveNode ){
461 $newAllExtends = array_merge( $directiveNode->allExtends
, end($this->allExtendsStack
) );
462 $this->allExtendsStack
[] = $this->doExtendChaining($newAllExtends, $directiveNode->allExtends
);
465 protected function visitDirectiveOut(){
466 array_pop($this->allExtendsStack
);