processExtends.php 16 KB

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