processExtends.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. <?php
  2. /**
  3. * Process Extends Visitor
  4. *
  5. * @package Less
  6. * @subpackage visitor
  7. */
  8. class Less_Visitor_processExtends extends Less_Visitor{
  9. public $allExtendsStack;
  10. /**
  11. * @param Less_Tree_Ruleset $root
  12. */
  13. public function run( $root ){
  14. $extendFinder = new Less_Visitor_extendFinder();
  15. $extendFinder->run( $root );
  16. if( !$extendFinder->foundExtends){
  17. return $root;
  18. }
  19. $root->allExtends = $this->doExtendChaining( $root->allExtends, $root->allExtends);
  20. $this->allExtendsStack = array();
  21. $this->allExtendsStack[] = &$root->allExtends;
  22. return $this->visitObj( $root );
  23. }
  24. private function doExtendChaining( $extendsList, $extendsListTarget, $iterationCount = 0){
  25. //
  26. // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting
  27. // the selector we would do normally, but we are also adding an extend with the same target selector
  28. // this means this new extend can then go and alter other extends
  29. //
  30. // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors
  31. // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if
  32. // we look at each selector at a time, as is done in visitRuleset
  33. $extendsToAdd = array();
  34. //loop through comparing every extend with every target extend.
  35. // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place
  36. // e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one
  37. // and the second is the target.
  38. // the separation into two lists allows us to process a subset of chains with a bigger set, as is the
  39. // case when processing media queries
  40. for( $extendIndex = 0, $extendsList_len = count($extendsList); $extendIndex < $extendsList_len; $extendIndex++ ){
  41. for( $targetExtendIndex = 0; $targetExtendIndex < count($extendsListTarget); $targetExtendIndex++ ){
  42. $extend = $extendsList[$extendIndex];
  43. $targetExtend = $extendsListTarget[$targetExtendIndex];
  44. // look for circular references
  45. if( in_array($targetExtend->object_id, $extend->parent_ids,true) ){
  46. continue;
  47. }
  48. // find a match in the target extends self selector (the bit before :extend)
  49. $selectorPath = array( $targetExtend->selfSelectors[0] );
  50. $matches = $this->findMatch( $extend, $selectorPath);
  51. if( $matches ){
  52. // we found a match, so for each self selector..
  53. foreach($extend->selfSelectors as $selfSelector ){
  54. // process the extend as usual
  55. $newSelector = $this->extendSelector( $matches, $selectorPath, $selfSelector);
  56. // but now we create a new extend from it
  57. $newExtend = new Less_Tree_Extend( $targetExtend->selector, $targetExtend->option, 0);
  58. $newExtend->selfSelectors = $newSelector;
  59. // add the extend onto the list of extends for that selector
  60. end($newSelector)->extendList = array($newExtend);
  61. //$newSelector[ count($newSelector)-1]->extendList = array($newExtend);
  62. // record that we need to add it.
  63. $extendsToAdd[] = $newExtend;
  64. $newExtend->ruleset = $targetExtend->ruleset;
  65. //remember its parents for circular references
  66. $newExtend->parent_ids = array_merge($newExtend->parent_ids,$targetExtend->parent_ids,$extend->parent_ids);
  67. // only process the selector once.. if we have :extend(.a,.b) then multiple
  68. // extends will look at the same selector path, so when extending
  69. // we know that any others will be duplicates in terms of what is added to the css
  70. if( $targetExtend->firstExtendOnThisSelectorPath ){
  71. $newExtend->firstExtendOnThisSelectorPath = true;
  72. $targetExtend->ruleset->paths[] = $newSelector;
  73. }
  74. }
  75. }
  76. }
  77. }
  78. if( $extendsToAdd ){
  79. // try to detect circular references to stop a stack overflow.
  80. // may no longer be needed. $this->extendChainCount++;
  81. if( $iterationCount > 100) {
  82. try{
  83. $selectorOne = $extendsToAdd[0]->selfSelectors[0]->toCSS();
  84. $selectorTwo = $extendsToAdd[0]->selector->toCSS();
  85. }catch(Exception $e){
  86. $selectorOne = "{unable to calculate}";
  87. $selectorTwo = "{unable to calculate}";
  88. }
  89. throw new Less_Exception_Parser("extend circular reference detected. One of the circular extends is currently:" . $selectorOne . ":extend(" . $selectorTwo . ")");
  90. }
  91. // now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e...
  92. $extendsToAdd = $this->doExtendChaining( $extendsToAdd, $extendsListTarget, $iterationCount+1);
  93. }
  94. return array_merge($extendsList, $extendsToAdd);
  95. }
  96. protected function visitRule( $ruleNode, &$visitDeeper ){
  97. $visitDeeper = false;
  98. }
  99. protected function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ){
  100. $visitDeeper = false;
  101. }
  102. protected function visitSelector( $selectorNode, &$visitDeeper ){
  103. $visitDeeper = false;
  104. }
  105. protected function visitRuleset($rulesetNode){
  106. if( $rulesetNode->root ){
  107. return;
  108. }
  109. $allExtends = end($this->allExtendsStack);
  110. $paths_len = count($rulesetNode->paths);
  111. // look at each selector path in the ruleset, find any extend matches and then copy, find and replace
  112. foreach($allExtends as $allExtend){
  113. for($pathIndex = 0; $pathIndex < $paths_len; $pathIndex++ ){
  114. // extending extends happens initially, before the main pass
  115. if( isset($rulesetNode->extendOnEveryPath) && $rulesetNode->extendOnEveryPath ){
  116. continue;
  117. }
  118. $selectorPath = $rulesetNode->paths[$pathIndex];
  119. if( end($selectorPath)->extendList ){
  120. continue;
  121. }
  122. $this->ExtendMatch( $rulesetNode, $allExtend, $selectorPath);
  123. }
  124. }
  125. }
  126. private function ExtendMatch( $rulesetNode, $extend, $selectorPath ){
  127. $matches = $this->findMatch($extend, $selectorPath);
  128. if( $matches ){
  129. foreach($extend->selfSelectors as $selfSelector ){
  130. $rulesetNode->paths[] = $this->extendSelector($matches, $selectorPath, $selfSelector);
  131. }
  132. }
  133. }
  134. private function findMatch($extend, $haystackSelectorPath ){
  135. if( !$this->HasMatches($extend, $haystackSelectorPath) ){
  136. return false;
  137. }
  138. //
  139. // look through the haystack selector path to try and find the needle - extend.selector
  140. // returns an array of selector matches that can then be replaced
  141. //
  142. $needleElements = $extend->selector->elements;
  143. $potentialMatches = array();
  144. $potentialMatches_len = 0;
  145. $potentialMatch = null;
  146. $matches = array();
  147. // loop through the haystack elements
  148. $haystack_path_len = count($haystackSelectorPath);
  149. for($haystackSelectorIndex = 0; $haystackSelectorIndex < $haystack_path_len; $haystackSelectorIndex++ ){
  150. $hackstackSelector = $haystackSelectorPath[$haystackSelectorIndex];
  151. $haystack_elements_len = count($hackstackSelector->elements);
  152. for($hackstackElementIndex = 0; $hackstackElementIndex < $haystack_elements_len; $hackstackElementIndex++ ){
  153. $haystackElement = $hackstackSelector->elements[$hackstackElementIndex];
  154. // if we allow elements before our match we can add a potential match every time. otherwise only at the first element.
  155. if( $extend->allowBefore || ($haystackSelectorIndex === 0 && $hackstackElementIndex === 0) ){
  156. $potentialMatches[] = array('pathIndex'=> $haystackSelectorIndex, 'index'=> $hackstackElementIndex, 'matched'=> 0, 'initialCombinator'=> $haystackElement->combinator);
  157. $potentialMatches_len++;
  158. }
  159. for($i = 0; $i < $potentialMatches_len; $i++ ){
  160. $potentialMatch = &$potentialMatches[$i];
  161. $potentialMatch = $this->PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex );
  162. // if we are still valid and have finished, test whether we have elements after and whether these are allowed
  163. if( $potentialMatch && $potentialMatch['matched'] === $extend->selector->elements_len ){
  164. $potentialMatch['finished'] = true;
  165. if( !$extend->allowAfter && ($hackstackElementIndex+1 < $haystack_elements_len || $haystackSelectorIndex+1 < $haystack_path_len) ){
  166. $potentialMatch = null;
  167. }
  168. }
  169. // if null we remove, if not, we are still valid, so either push as a valid match or continue
  170. if( $potentialMatch ){
  171. if( $potentialMatch['finished'] ){
  172. $potentialMatch['length'] = $extend->selector->elements_len;
  173. $potentialMatch['endPathIndex'] = $haystackSelectorIndex;
  174. $potentialMatch['endPathElementIndex'] = $hackstackElementIndex + 1; // index after end of match
  175. $potentialMatches = array(); // we don't allow matches to overlap, so start matching again
  176. $potentialMatches_len = 0;
  177. $matches[] = $potentialMatch;
  178. }
  179. continue;
  180. }
  181. array_splice($potentialMatches, $i, 1);
  182. $potentialMatches_len--;
  183. $i--;
  184. }
  185. }
  186. }
  187. return $matches;
  188. }
  189. // Before going through all the nested loops, lets check to see if a match is possible
  190. // Reduces Bootstrap 3.1 compile time from ~6.5s to ~5.6s
  191. private function HasMatches($extend, $haystackSelectorPath){
  192. if( !$extend->selector->cacheable ){
  193. return true;
  194. }
  195. $first_el = $extend->selector->_oelements[0];
  196. foreach($haystackSelectorPath as $hackstackSelector){
  197. if( !$hackstackSelector->cacheable ){
  198. return true;
  199. }
  200. if( in_array($first_el, $hackstackSelector->_oelements) ){
  201. return true;
  202. }
  203. }
  204. return false;
  205. }
  206. /**
  207. * @param integer $hackstackElementIndex
  208. */
  209. private function PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex ){
  210. if( $potentialMatch['matched'] > 0 ){
  211. // selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't
  212. // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to work out
  213. // what the resulting combinator will be
  214. $targetCombinator = $haystackElement->combinator;
  215. if( $targetCombinator === '' && $hackstackElementIndex === 0 ){
  216. $targetCombinator = ' ';
  217. }
  218. if( $needleElements[ $potentialMatch['matched'] ]->combinator !== $targetCombinator ){
  219. return null;
  220. }
  221. }
  222. // if we don't match, null our match to indicate failure
  223. if( !$this->isElementValuesEqual( $needleElements[$potentialMatch['matched'] ]->value, $haystackElement->value) ){
  224. return null;
  225. }
  226. $potentialMatch['finished'] = false;
  227. $potentialMatch['matched']++;
  228. return $potentialMatch;
  229. }
  230. private function isElementValuesEqual( $elementValue1, $elementValue2 ){
  231. if( $elementValue1 === $elementValue2 ){
  232. return true;
  233. }
  234. if( is_string($elementValue1) || is_string($elementValue2) ) {
  235. return false;
  236. }
  237. if( $elementValue1 instanceof Less_Tree_Attribute ){
  238. return $this->isAttributeValuesEqual( $elementValue1, $elementValue2 );
  239. }
  240. $elementValue1 = $elementValue1->value;
  241. if( $elementValue1 instanceof Less_Tree_Selector ){
  242. return $this->isSelectorValuesEqual( $elementValue1, $elementValue2 );
  243. }
  244. return false;
  245. }
  246. /**
  247. * @param Less_Tree_Selector $elementValue1
  248. */
  249. private function isSelectorValuesEqual( $elementValue1, $elementValue2 ){
  250. $elementValue2 = $elementValue2->value;
  251. if( !($elementValue2 instanceof Less_Tree_Selector) || $elementValue1->elements_len !== $elementValue2->elements_len ){
  252. return false;
  253. }
  254. for( $i = 0; $i < $elementValue1->elements_len; $i++ ){
  255. if( $elementValue1->elements[$i]->combinator !== $elementValue2->elements[$i]->combinator ){
  256. if( $i !== 0 || ($elementValue1->elements[$i]->combinator || ' ') !== ($elementValue2->elements[$i]->combinator || ' ') ){
  257. return false;
  258. }
  259. }
  260. if( !$this->isElementValuesEqual($elementValue1->elements[$i]->value, $elementValue2->elements[$i]->value) ){
  261. return false;
  262. }
  263. }
  264. return true;
  265. }
  266. /**
  267. * @param Less_Tree_Attribute $elementValue1
  268. */
  269. private function isAttributeValuesEqual( $elementValue1, $elementValue2 ){
  270. if( $elementValue1->op !== $elementValue2->op || $elementValue1->key !== $elementValue2->key ){
  271. return false;
  272. }
  273. if( !$elementValue1->value || !$elementValue2->value ){
  274. if( $elementValue1->value || $elementValue2->value ) {
  275. return false;
  276. }
  277. return true;
  278. }
  279. $elementValue1 = ($elementValue1->value->value ? $elementValue1->value->value : $elementValue1->value );
  280. $elementValue2 = ($elementValue2->value->value ? $elementValue2->value->value : $elementValue2->value );
  281. return $elementValue1 === $elementValue2;
  282. }
  283. private function extendSelector($matches, $selectorPath, $replacementSelector){
  284. //for a set of matches, replace each match with the replacement selector
  285. $currentSelectorPathIndex = 0;
  286. $currentSelectorPathElementIndex = 0;
  287. $path = array();
  288. $selectorPath_len = count($selectorPath);
  289. for($matchIndex = 0, $matches_len = count($matches); $matchIndex < $matches_len; $matchIndex++ ){
  290. $match = $matches[$matchIndex];
  291. $selector = $selectorPath[ $match['pathIndex'] ];
  292. $firstElement = new Less_Tree_Element(
  293. $match['initialCombinator'],
  294. $replacementSelector->elements[0]->value,
  295. $replacementSelector->elements[0]->index,
  296. $replacementSelector->elements[0]->currentFileInfo
  297. );
  298. if( $match['pathIndex'] > $currentSelectorPathIndex && $currentSelectorPathElementIndex > 0 ){
  299. $last_path = end($path);
  300. $last_path->elements = array_merge( $last_path->elements, array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex));
  301. $currentSelectorPathElementIndex = 0;
  302. $currentSelectorPathIndex++;
  303. }
  304. $newElements = array_merge(
  305. array_slice($selector->elements, $currentSelectorPathElementIndex, ($match['index'] - $currentSelectorPathElementIndex) ) // last parameter of array_slice is different than the last parameter of javascript's slice
  306. , array($firstElement)
  307. , array_slice($replacementSelector->elements,1)
  308. );
  309. if( $currentSelectorPathIndex === $match['pathIndex'] && $matchIndex > 0 ){
  310. $last_key = count($path)-1;
  311. $path[$last_key]->elements = array_merge($path[$last_key]->elements,$newElements);
  312. }else{
  313. $path = array_merge( $path, array_slice( $selectorPath, $currentSelectorPathIndex, $match['pathIndex'] ));
  314. $path[] = new Less_Tree_Selector( $newElements );
  315. }
  316. $currentSelectorPathIndex = $match['endPathIndex'];
  317. $currentSelectorPathElementIndex = $match['endPathElementIndex'];
  318. if( $currentSelectorPathElementIndex >= count($selectorPath[$currentSelectorPathIndex]->elements) ){
  319. $currentSelectorPathElementIndex = 0;
  320. $currentSelectorPathIndex++;
  321. }
  322. }
  323. if( $currentSelectorPathIndex < $selectorPath_len && $currentSelectorPathElementIndex > 0 ){
  324. $last_path = end($path);
  325. $last_path->elements = array_merge( $last_path->elements, array_slice($selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex));
  326. $currentSelectorPathIndex++;
  327. }
  328. $slice_len = $selectorPath_len - $currentSelectorPathIndex;
  329. $path = array_merge($path, array_slice($selectorPath, $currentSelectorPathIndex, $slice_len));
  330. return $path;
  331. }
  332. protected function visitMedia( $mediaNode ){
  333. $newAllExtends = array_merge( $mediaNode->allExtends, end($this->allExtendsStack) );
  334. $this->allExtendsStack[] = $this->doExtendChaining($newAllExtends, $mediaNode->allExtends);
  335. }
  336. protected function visitMediaOut(){
  337. array_pop( $this->allExtendsStack );
  338. }
  339. protected function visitDirective( $directiveNode ){
  340. $newAllExtends = array_merge( $directiveNode->allExtends, end($this->allExtendsStack) );
  341. $this->allExtendsStack[] = $this->doExtendChaining($newAllExtends, $directiveNode->allExtends);
  342. }
  343. protected function visitDirectiveOut(){
  344. array_pop($this->allExtendsStack);
  345. }
  346. }