Merge branch 'MDL-46588-27' of git://github.com/jleyva/moodle into MOODLE_27_STABLE
[moodle.git] / lib / lessphp / Visitor / processExtends.php
blobc01f9c5d684654016cc8639762b2ef9cf074dea2
1 <?php
3 /**
4 * Process Extends Visitor
6 * @package Less
7 * @subpackage visitor
8 */
9 class Less_Visitor_processExtends extends Less_Visitor{
11 public $allExtendsStack;
13 /**
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){
20 return $root;
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) ){
58 continue;
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);
66 if( $matches ){
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;
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) {
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}";
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 ){
142 return;
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 ){
154 continue;
157 $selectorPath = $rulesetNode->paths[$pathIndex];
159 if( end($selectorPath)->extendList ){
160 continue;
163 $this->ExtendMatch( $rulesetNode, $allExtend, $selectorPath);
170 private function ExtendMatch( $rulesetNode, $extend, $selectorPath ){
171 $matches = $this->findMatch($extend, $selectorPath);
173 if( $matches ){
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) ){
186 return false;
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;
198 $matches = array();
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;
243 continue;
246 array_splice($potentialMatches, $i, 1);
247 $potentialMatches_len--;
248 $i--;
253 return $matches;
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 ){
262 return true;
265 $first_el = $extend->selector->_oelements[0];
267 foreach($haystackSelectorPath as $hackstackSelector){
268 if( !$hackstackSelector->cacheable ){
269 return true;
272 if( in_array($first_el, $hackstackSelector->_oelements) ){
273 return true;
277 return false;
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 ){
298 return null;
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;
307 $potentialMatch['finished'] = false;
308 $potentialMatch['matched']++;
310 return $potentialMatch;
314 private function isElementValuesEqual( $elementValue1, $elementValue2 ){
316 if( $elementValue1 === $elementValue2 ){
317 return true;
320 if( is_string($elementValue1) || is_string($elementValue2) ) {
321 return false;
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 );
333 return false;
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 ){
344 return false;
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 || ' ') ){
351 return false;
355 if( !$this->isElementValuesEqual($elementValue1->elements[$i]->value, $elementValue2->elements[$i]->value) ){
356 return false;
360 return true;
365 * @param Less_Tree_Attribute $elementValue1
367 private function isAttributeValuesEqual( $elementValue1, $elementValue2 ){
369 if( $elementValue1->op !== $elementValue2->op || $elementValue1->key !== $elementValue2->key ){
370 return false;
373 if( !$elementValue1->value || !$elementValue2->value ){
374 if( $elementValue1->value || $elementValue2->value ) {
375 return false;
377 return true;
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;
393 $path = array();
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);
425 }else{
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));
447 return $path;
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);