Ruleset.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. <?php
  2. /**
  3. * Ruleset
  4. *
  5. * @package Less
  6. * @subpackage tree
  7. */
  8. class Less_Tree_Ruleset extends Less_Tree {
  9. protected $lookups;
  10. public $_variables;
  11. public $_rulesets;
  12. public $strictImports;
  13. public $selectors;
  14. public $rules;
  15. public $root;
  16. public $allowImports;
  17. public $paths;
  18. public $firstRoot;
  19. public $type = 'Ruleset';
  20. public $multiMedia;
  21. public $allExtends;
  22. public $ruleset_id;
  23. public $originalRuleset;
  24. public $first_oelements;
  25. public function SetRulesetIndex() {
  26. $this->ruleset_id = Less_Parser::$next_id++;
  27. $this->originalRuleset = $this->ruleset_id;
  28. if ( $this->selectors ) {
  29. foreach ( $this->selectors as $sel ) {
  30. if ( $sel->_oelements ) {
  31. $this->first_oelements[$sel->_oelements[0]] = true;
  32. }
  33. }
  34. }
  35. }
  36. public function __construct( $selectors, $rules, $strictImports = null ) {
  37. $this->selectors = $selectors;
  38. $this->rules = $rules;
  39. $this->lookups = array();
  40. $this->strictImports = $strictImports;
  41. $this->SetRulesetIndex();
  42. }
  43. public function accept( $visitor ) {
  44. if ( $this->paths ) {
  45. $paths_len = count( $this->paths );
  46. for ( $i = 0,$paths_len; $i < $paths_len; $i++ ) {
  47. $this->paths[$i] = $visitor->visitArray( $this->paths[$i] );
  48. }
  49. } elseif ( $this->selectors ) {
  50. $this->selectors = $visitor->visitArray( $this->selectors );
  51. }
  52. if ( $this->rules ) {
  53. $this->rules = $visitor->visitArray( $this->rules );
  54. }
  55. }
  56. public function compile( $env ) {
  57. $ruleset = $this->PrepareRuleset( $env );
  58. // Store the frames around mixin definitions,
  59. // so they can be evaluated like closures when the time comes.
  60. $rsRuleCnt = count( $ruleset->rules );
  61. for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
  62. if ( $ruleset->rules[$i] instanceof Less_Tree_Mixin_Definition || $ruleset->rules[$i] instanceof Less_Tree_DetachedRuleset ) {
  63. $ruleset->rules[$i] = $ruleset->rules[$i]->compile( $env );
  64. }
  65. }
  66. $mediaBlockCount = 0;
  67. if ( $env instanceof Less_Environment ) {
  68. $mediaBlockCount = count( $env->mediaBlocks );
  69. }
  70. // Evaluate mixin calls.
  71. $this->EvalMixinCalls( $ruleset, $env, $rsRuleCnt );
  72. // Evaluate everything else
  73. for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
  74. if ( !( $ruleset->rules[$i] instanceof Less_Tree_Mixin_Definition || $ruleset->rules[$i] instanceof Less_Tree_DetachedRuleset ) ) {
  75. $ruleset->rules[$i] = $ruleset->rules[$i]->compile( $env );
  76. }
  77. }
  78. // Evaluate everything else
  79. for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
  80. $rule = $ruleset->rules[$i];
  81. // for rulesets, check if it is a css guard and can be removed
  82. if ( $rule instanceof Less_Tree_Ruleset && $rule->selectors && count( $rule->selectors ) === 1 ) {
  83. // check if it can be folded in (e.g. & where)
  84. if ( $rule->selectors[0]->isJustParentSelector() ) {
  85. array_splice( $ruleset->rules, $i--, 1 );
  86. $rsRuleCnt--;
  87. for ( $j = 0; $j < count( $rule->rules ); $j++ ) {
  88. $subRule = $rule->rules[$j];
  89. if ( !( $subRule instanceof Less_Tree_Rule ) || !$subRule->variable ) {
  90. array_splice( $ruleset->rules, ++$i, 0, array( $subRule ) );
  91. $rsRuleCnt++;
  92. }
  93. }
  94. }
  95. }
  96. }
  97. // Pop the stack
  98. $env->shiftFrame();
  99. if ( $mediaBlockCount ) {
  100. $len = count( $env->mediaBlocks );
  101. for ( $i = $mediaBlockCount; $i < $len; $i++ ) {
  102. $env->mediaBlocks[$i]->bubbleSelectors( $ruleset->selectors );
  103. }
  104. }
  105. return $ruleset;
  106. }
  107. /**
  108. * Compile Less_Tree_Mixin_Call objects
  109. *
  110. * @param Less_Tree_Ruleset $ruleset
  111. * @param integer $rsRuleCnt
  112. */
  113. private function EvalMixinCalls( $ruleset, $env, &$rsRuleCnt ) {
  114. for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
  115. $rule = $ruleset->rules[$i];
  116. if ( $rule instanceof Less_Tree_Mixin_Call ) {
  117. $rule = $rule->compile( $env );
  118. $temp = array();
  119. foreach ( $rule as $r ) {
  120. if ( ( $r instanceof Less_Tree_Rule ) && $r->variable ) {
  121. // do not pollute the scope if the variable is
  122. // already there. consider returning false here
  123. // but we need a way to "return" variable from mixins
  124. if ( !$ruleset->variable( $r->name ) ) {
  125. $temp[] = $r;
  126. }
  127. } else {
  128. $temp[] = $r;
  129. }
  130. }
  131. $temp_count = count( $temp ) - 1;
  132. array_splice( $ruleset->rules, $i, 1, $temp );
  133. $rsRuleCnt += $temp_count;
  134. $i += $temp_count;
  135. $ruleset->resetCache();
  136. } elseif ( $rule instanceof Less_Tree_RulesetCall ) {
  137. $rule = $rule->compile( $env );
  138. $rules = array();
  139. foreach ( $rule->rules as $r ) {
  140. if ( ( $r instanceof Less_Tree_Rule ) && $r->variable ) {
  141. continue;
  142. }
  143. $rules[] = $r;
  144. }
  145. array_splice( $ruleset->rules, $i, 1, $rules );
  146. $temp_count = count( $rules );
  147. $rsRuleCnt += $temp_count - 1;
  148. $i += $temp_count - 1;
  149. $ruleset->resetCache();
  150. }
  151. }
  152. }
  153. /**
  154. * Compile the selectors and create a new ruleset object for the compile() method
  155. *
  156. */
  157. private function PrepareRuleset( $env ) {
  158. $hasOnePassingSelector = false;
  159. $selectors = array();
  160. if ( $this->selectors ) {
  161. Less_Tree_DefaultFunc::error( "it is currently only allowed in parametric mixin guards," );
  162. foreach ( $this->selectors as $s ) {
  163. $selector = $s->compile( $env );
  164. $selectors[] = $selector;
  165. if ( $selector->evaldCondition ) {
  166. $hasOnePassingSelector = true;
  167. }
  168. }
  169. Less_Tree_DefaultFunc::reset();
  170. } else {
  171. $hasOnePassingSelector = true;
  172. }
  173. if ( $this->rules && $hasOnePassingSelector ) {
  174. $rules = $this->rules;
  175. } else {
  176. $rules = array();
  177. }
  178. $ruleset = new Less_Tree_Ruleset( $selectors, $rules, $this->strictImports );
  179. $ruleset->originalRuleset = $this->ruleset_id;
  180. $ruleset->root = $this->root;
  181. $ruleset->firstRoot = $this->firstRoot;
  182. $ruleset->allowImports = $this->allowImports;
  183. // push the current ruleset to the frames stack
  184. $env->unshiftFrame( $ruleset );
  185. // Evaluate imports
  186. if ( $ruleset->root || $ruleset->allowImports || !$ruleset->strictImports ) {
  187. $ruleset->evalImports( $env );
  188. }
  189. return $ruleset;
  190. }
  191. function evalImports( $env ) {
  192. $rules_len = count( $this->rules );
  193. for ( $i = 0; $i < $rules_len; $i++ ) {
  194. $rule = $this->rules[$i];
  195. if ( $rule instanceof Less_Tree_Import ) {
  196. $rules = $rule->compile( $env );
  197. if ( is_array( $rules ) ) {
  198. array_splice( $this->rules, $i, 1, $rules );
  199. $temp_count = count( $rules ) - 1;
  200. $i += $temp_count;
  201. $rules_len += $temp_count;
  202. } else {
  203. array_splice( $this->rules, $i, 1, array( $rules ) );
  204. }
  205. $this->resetCache();
  206. }
  207. }
  208. }
  209. function makeImportant() {
  210. $important_rules = array();
  211. foreach ( $this->rules as $rule ) {
  212. if ( $rule instanceof Less_Tree_Rule || $rule instanceof Less_Tree_Ruleset || $rule instanceof Less_Tree_NameValue ) {
  213. $important_rules[] = $rule->makeImportant();
  214. } else {
  215. $important_rules[] = $rule;
  216. }
  217. }
  218. return new Less_Tree_Ruleset( $this->selectors, $important_rules, $this->strictImports );
  219. }
  220. public function matchArgs( $args ) {
  221. return !$args;
  222. }
  223. // lets you call a css selector with a guard
  224. public function matchCondition( $args, $env ) {
  225. $lastSelector = end( $this->selectors );
  226. if ( !$lastSelector->evaldCondition ) {
  227. return false;
  228. }
  229. if ( $lastSelector->condition && !$lastSelector->condition->compile( $env->copyEvalEnv( $env->frames ) ) ) {
  230. return false;
  231. }
  232. return true;
  233. }
  234. function resetCache() {
  235. $this->_rulesets = null;
  236. $this->_variables = null;
  237. $this->lookups = array();
  238. }
  239. public function variables() {
  240. $this->_variables = array();
  241. foreach ( $this->rules as $r ) {
  242. if ( $r instanceof Less_Tree_Rule && $r->variable === true ) {
  243. $this->_variables[$r->name] = $r;
  244. }
  245. }
  246. }
  247. public function variable( $name ) {
  248. if ( is_null( $this->_variables ) ) {
  249. $this->variables();
  250. }
  251. return isset( $this->_variables[$name] ) ? $this->_variables[$name] : null;
  252. }
  253. public function find( $selector, $self = null ) {
  254. $key = implode( ' ', $selector->_oelements );
  255. if ( !isset( $this->lookups[$key] ) ) {
  256. if ( !$self ) {
  257. $self = $this->ruleset_id;
  258. }
  259. $this->lookups[$key] = array();
  260. $first_oelement = $selector->_oelements[0];
  261. foreach ( $this->rules as $rule ) {
  262. if ( $rule instanceof Less_Tree_Ruleset && $rule->ruleset_id != $self ) {
  263. if ( isset( $rule->first_oelements[$first_oelement] ) ) {
  264. foreach ( $rule->selectors as $ruleSelector ) {
  265. $match = $selector->match( $ruleSelector );
  266. if ( $match ) {
  267. if ( $selector->elements_len > $match ) {
  268. $this->lookups[$key] = array_merge( $this->lookups[$key], $rule->find( new Less_Tree_Selector( array_slice( $selector->elements, $match ) ), $self ) );
  269. } else {
  270. $this->lookups[$key][] = $rule;
  271. }
  272. break;
  273. }
  274. }
  275. }
  276. }
  277. }
  278. }
  279. return $this->lookups[$key];
  280. }
  281. /**
  282. * @see Less_Tree::genCSS
  283. */
  284. public function genCSS( $output ) {
  285. if ( !$this->root ) {
  286. Less_Environment::$tabLevel++;
  287. }
  288. $tabRuleStr = $tabSetStr = '';
  289. if ( !Less_Parser::$options['compress'] ) {
  290. if ( Less_Environment::$tabLevel ) {
  291. $tabRuleStr = "\n".str_repeat( Less_Parser::$options['indentation'], Less_Environment::$tabLevel );
  292. $tabSetStr = "\n".str_repeat( Less_Parser::$options['indentation'], Less_Environment::$tabLevel - 1 );
  293. } else {
  294. $tabSetStr = $tabRuleStr = "\n";
  295. }
  296. }
  297. $ruleNodes = array();
  298. $rulesetNodes = array();
  299. foreach ( $this->rules as $rule ) {
  300. $class = get_class( $rule );
  301. if ( ( $class === 'Less_Tree_Media' ) || ( $class === 'Less_Tree_Directive' ) || ( $this->root && $class === 'Less_Tree_Comment' ) || ( $class === 'Less_Tree_Ruleset' && $rule->rules ) ) {
  302. $rulesetNodes[] = $rule;
  303. } else {
  304. $ruleNodes[] = $rule;
  305. }
  306. }
  307. // If this is the root node, we don't render
  308. // a selector, or {}.
  309. if ( !$this->root ) {
  310. /*
  311. debugInfo = tree.debugInfo(env, this, tabSetStr);
  312. if (debugInfo) {
  313. output.add(debugInfo);
  314. output.add(tabSetStr);
  315. }
  316. */
  317. $paths_len = count( $this->paths );
  318. for ( $i = 0; $i < $paths_len; $i++ ) {
  319. $path = $this->paths[$i];
  320. $firstSelector = true;
  321. foreach ( $path as $p ) {
  322. $p->genCSS( $output, $firstSelector );
  323. $firstSelector = false;
  324. }
  325. if ( $i + 1 < $paths_len ) {
  326. $output->add( ',' . $tabSetStr );
  327. }
  328. }
  329. $output->add( ( Less_Parser::$options['compress'] ? '{' : " {" ) . $tabRuleStr );
  330. }
  331. // Compile rules and rulesets
  332. $ruleNodes_len = count( $ruleNodes );
  333. $rulesetNodes_len = count( $rulesetNodes );
  334. for ( $i = 0; $i < $ruleNodes_len; $i++ ) {
  335. $rule = $ruleNodes[$i];
  336. // @page{ directive ends up with root elements inside it, a mix of rules and rulesets
  337. // In this instance we do not know whether it is the last property
  338. if ( $i + 1 === $ruleNodes_len && ( !$this->root || $rulesetNodes_len === 0 || $this->firstRoot ) ) {
  339. Less_Environment::$lastRule = true;
  340. }
  341. $rule->genCSS( $output );
  342. if ( !Less_Environment::$lastRule ) {
  343. $output->add( $tabRuleStr );
  344. } else {
  345. Less_Environment::$lastRule = false;
  346. }
  347. }
  348. if ( !$this->root ) {
  349. $output->add( $tabSetStr . '}' );
  350. Less_Environment::$tabLevel--;
  351. }
  352. $firstRuleset = true;
  353. $space = ( $this->root ? $tabRuleStr : $tabSetStr );
  354. for ( $i = 0; $i < $rulesetNodes_len; $i++ ) {
  355. if ( $ruleNodes_len && $firstRuleset ) {
  356. $output->add( $space );
  357. } elseif ( !$firstRuleset ) {
  358. $output->add( $space );
  359. }
  360. $firstRuleset = false;
  361. $rulesetNodes[$i]->genCSS( $output );
  362. }
  363. if ( !Less_Parser::$options['compress'] && $this->firstRoot ) {
  364. $output->add( "\n" );
  365. }
  366. }
  367. function markReferenced() {
  368. if ( !$this->selectors ) {
  369. return;
  370. }
  371. foreach ( $this->selectors as $selector ) {
  372. $selector->markReferenced();
  373. }
  374. }
  375. public function joinSelectors( $context, $selectors ) {
  376. $paths = array();
  377. if ( is_array( $selectors ) ) {
  378. foreach ( $selectors as $selector ) {
  379. $this->joinSelector( $paths, $context, $selector );
  380. }
  381. }
  382. return $paths;
  383. }
  384. public function joinSelector( &$paths, $context, $selector ) {
  385. $hasParentSelector = false;
  386. foreach ( $selector->elements as $el ) {
  387. if ( $el->value === '&' ) {
  388. $hasParentSelector = true;
  389. }
  390. }
  391. if ( !$hasParentSelector ) {
  392. if ( $context ) {
  393. foreach ( $context as $context_el ) {
  394. $paths[] = array_merge( $context_el, array( $selector ) );
  395. }
  396. } else {
  397. $paths[] = array( $selector );
  398. }
  399. return;
  400. }
  401. // The paths are [[Selector]]
  402. // The first list is a list of comma separated selectors
  403. // The inner list is a list of inheritance separated selectors
  404. // e.g.
  405. // .a, .b {
  406. // .c {
  407. // }
  408. // }
  409. // == [[.a] [.c]] [[.b] [.c]]
  410. //
  411. // the elements from the current selector so far
  412. $currentElements = array();
  413. // the current list of new selectors to add to the path.
  414. // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors
  415. // by the parents
  416. $newSelectors = array( array() );
  417. foreach ( $selector->elements as $el ) {
  418. // non parent reference elements just get added
  419. if ( $el->value !== '&' ) {
  420. $currentElements[] = $el;
  421. } else {
  422. // the new list of selectors to add
  423. $selectorsMultiplied = array();
  424. // merge the current list of non parent selector elements
  425. // on to the current list of selectors to add
  426. if ( $currentElements ) {
  427. $this->mergeElementsOnToSelectors( $currentElements, $newSelectors );
  428. }
  429. // loop through our current selectors
  430. foreach ( $newSelectors as $sel ) {
  431. // if we don't have any parent paths, the & might be in a mixin so that it can be used
  432. // whether there are parents or not
  433. if ( !$context ) {
  434. // the combinator used on el should now be applied to the next element instead so that
  435. // it is not lost
  436. if ( $sel ) {
  437. $sel[0]->elements = array_slice( $sel[0]->elements, 0 );
  438. $sel[0]->elements[] = new Less_Tree_Element( $el->combinator, '', $el->index, $el->currentFileInfo );
  439. }
  440. $selectorsMultiplied[] = $sel;
  441. } else {
  442. // and the parent selectors
  443. foreach ( $context as $parentSel ) {
  444. // We need to put the current selectors
  445. // then join the last selector's elements on to the parents selectors
  446. // our new selector path
  447. $newSelectorPath = array();
  448. // selectors from the parent after the join
  449. $afterParentJoin = array();
  450. $newJoinedSelectorEmpty = true;
  451. // construct the joined selector - if & is the first thing this will be empty,
  452. // if not newJoinedSelector will be the last set of elements in the selector
  453. if ( $sel ) {
  454. $newSelectorPath = $sel;
  455. $lastSelector = array_pop( $newSelectorPath );
  456. $newJoinedSelector = $selector->createDerived( array_slice( $lastSelector->elements, 0 ) );
  457. $newJoinedSelectorEmpty = false;
  458. } else {
  459. $newJoinedSelector = $selector->createDerived( array() );
  460. }
  461. // put together the parent selectors after the join
  462. if ( count( $parentSel ) > 1 ) {
  463. $afterParentJoin = array_merge( $afterParentJoin, array_slice( $parentSel, 1 ) );
  464. }
  465. if ( $parentSel ) {
  466. $newJoinedSelectorEmpty = false;
  467. // join the elements so far with the first part of the parent
  468. $newJoinedSelector->elements[] = new Less_Tree_Element( $el->combinator, $parentSel[0]->elements[0]->value, $el->index, $el->currentFileInfo );
  469. $newJoinedSelector->elements = array_merge( $newJoinedSelector->elements, array_slice( $parentSel[0]->elements, 1 ) );
  470. }
  471. if ( !$newJoinedSelectorEmpty ) {
  472. // now add the joined selector
  473. $newSelectorPath[] = $newJoinedSelector;
  474. }
  475. // and the rest of the parent
  476. $newSelectorPath = array_merge( $newSelectorPath, $afterParentJoin );
  477. // add that to our new set of selectors
  478. $selectorsMultiplied[] = $newSelectorPath;
  479. }
  480. }
  481. }
  482. // our new selectors has been multiplied, so reset the state
  483. $newSelectors = $selectorsMultiplied;
  484. $currentElements = array();
  485. }
  486. }
  487. // if we have any elements left over (e.g. .a& .b == .b)
  488. // add them on to all the current selectors
  489. if ( $currentElements ) {
  490. $this->mergeElementsOnToSelectors( $currentElements, $newSelectors );
  491. }
  492. foreach ( $newSelectors as $new_sel ) {
  493. if ( $new_sel ) {
  494. $paths[] = $new_sel;
  495. }
  496. }
  497. }
  498. function mergeElementsOnToSelectors( $elements, &$selectors ) {
  499. if ( !$selectors ) {
  500. $selectors[] = array( new Less_Tree_Selector( $elements ) );
  501. return;
  502. }
  503. foreach ( $selectors as &$sel ) {
  504. // if the previous thing in sel is a parent this needs to join on to it
  505. if ( $sel ) {
  506. $last = count( $sel ) - 1;
  507. $sel[$last] = $sel[$last]->createDerived( array_merge( $sel[$last]->elements, $elements ) );
  508. } else {
  509. $sel[] = new Less_Tree_Selector( $elements );
  510. }
  511. }
  512. }
  513. }