/lib/Less/Node/Ruleset.php

https://github.com/Mordred/less.php · PHP · 269 lines · 214 code · 38 blank · 17 comment · 54 complexity · 066bd3f8b9118804847b0f8fb510f3fc MD5 · raw file

  1. <?php
  2. namespace Less\Node;
  3. class Ruleset
  4. {
  5. protected $lookups;
  6. private $_variables;
  7. private $_rulesets;
  8. public $strictImports;
  9. public $selectors;
  10. public $rules;
  11. public $root;
  12. public $allowImports;
  13. public function __construct($selectors, $rules, $strictImports = false)
  14. {
  15. $this->selectors = $selectors;
  16. $this->rules = (array) $rules;
  17. $this->lookups = array();
  18. $this->strictImports = $strictImports;
  19. }
  20. public function compile($env) {
  21. $selectors = $this->selectors ? array_map(function($s) use ($env) {
  22. return $s->compile($env);
  23. }, $this->selectors) : array();
  24. $ruleset = new Ruleset($selectors, $this->rules, $this->strictImports);
  25. $ruleset->root = $this->root;
  26. $ruleset->allowImports = $this->allowImports;
  27. // push the current ruleset to the frames stack
  28. $env->unshiftFrame($ruleset);
  29. // Evaluate imports
  30. if ($ruleset->root || $ruleset->allowImports || !$ruleset->strictImports) {
  31. for ($i = 0; $i < count($ruleset->rules); $i++) {
  32. if (/*isset($ruleset->rules[$i]) && */$ruleset->rules[$i] instanceof \Less\Node\Import && ! $ruleset->rules[$i]->css) {
  33. $newRules = $ruleset->rules[$i]->compile($env);
  34. $ruleset->rules = array_merge(
  35. array_slice($ruleset->rules, 0, $i),
  36. is_array($newRules) ? $newRules : array($newRules),
  37. array_slice($ruleset->rules, $i + 1)
  38. );
  39. }
  40. }
  41. }
  42. // Store the frames around mixin definitions,
  43. // so they can be evaluated like closures when the time comes.
  44. foreach($ruleset->rules as $i => $rule) {
  45. if ($rule instanceof \Less\Node\Mixin\Definition) {
  46. $ruleset->rules[$i]->frames = $env->frames;
  47. }
  48. }
  49. // Evaluate mixin calls.
  50. for($i = 0; $i < count($ruleset->rules); $i++) {
  51. if (isset($ruleset->rules[$i]) && $ruleset->rules[$i] instanceof \Less\Node\Mixin\Call) {
  52. $newRules = $ruleset->rules[$i]->compile($env);
  53. $ruleset->rules = array_merge(
  54. array_slice($ruleset->rules, 0, $i),
  55. $newRules,
  56. array_slice($ruleset->rules, $i + 1)
  57. );
  58. }
  59. }
  60. // Evaluate everything else
  61. foreach($ruleset->rules as $i => $rule) {
  62. if (! ($rule instanceof \Less\Node\Mixin\Definition)) {
  63. $ruleset->rules[$i] = is_string($rule) ? $rule : $rule->compile($env);
  64. }
  65. }
  66. // Pop the stack
  67. $env->shiftFrame();
  68. return $ruleset;
  69. }
  70. public function match($args)
  71. {
  72. return ! is_array($args) || count($args) === 0;
  73. }
  74. public function variables() {
  75. if ( ! $this->_variables) {
  76. $this->_variables = array_reduce($this->rules, function ($hash, $r) {
  77. if ($r instanceof \Less\Node\Rule && $r->variable === true) {
  78. $hash[$r->name] = $r;
  79. }
  80. return $hash;
  81. });
  82. }
  83. return $this->_variables;
  84. }
  85. public function variable($name)
  86. {
  87. $vars = $this->variables();
  88. return isset($vars[$name]) ? $vars[$name] : null;
  89. }
  90. public function rulesets ()
  91. {
  92. if ($this->_rulesets) {
  93. return $this->_rulesets;
  94. } else {
  95. return $this->_rulesets = array_filter($this->rules, function ($r) {
  96. return ($r instanceof \Less\Node\Ruleset) || ($r instanceof \Less\Node\Mixin\Definition);
  97. });
  98. }
  99. }
  100. public function find ($selector, $self = null, $env = null)
  101. {
  102. $self = $self ?: $this;
  103. $rules = array();
  104. $key = $selector->toCSS($env);
  105. if (array_key_exists($key, $this->lookups)) {
  106. return $this->lookups[$key];
  107. }
  108. foreach($this->rulesets() as $rule) {
  109. if ($rule !== $self) {
  110. foreach($rule->selectors as $ruleSelector) {
  111. if ($selector->match($ruleSelector)) {
  112. if (count($selector->elements) > count($ruleSelector->elements)) {
  113. $rules = array_merge($rules, $rule->find( new \Less\Node\Selector(array_slice($selector->elements, 1)), $self, $env));
  114. } else {
  115. $rules[] = $rule;
  116. }
  117. break;
  118. }
  119. }
  120. }
  121. }
  122. $this->lookups[$key] = $rules;
  123. return $this->lookups[$key];
  124. }
  125. //
  126. // Entry point for code generation
  127. //
  128. // `context` holds an array of arrays.
  129. //
  130. public function toCSS($context, $env)
  131. {
  132. $css = array(); // The CSS output
  133. $rules = array(); // node.Rule instances
  134. $_rules = array();
  135. $rulesets = array(); // node.Ruleset instances
  136. $paths = array(); // Current selectors
  137. if (! $this->root) {
  138. if (count($context) === 0) {
  139. $paths = array_map(function ($s) { return array($s); }, $this->selectors);
  140. } else {
  141. $this->joinSelectors($paths, $context, $this->selectors);
  142. }
  143. }
  144. // Compile rules and rulesets
  145. foreach($this->rules as $rule) {
  146. if (isset($rule->rules) || ($rule instanceof \Less\Node\Directive) || ($rule instanceof \Less\Node\Media)) {
  147. $rulesets[] = $rule->toCSS($paths, $env);
  148. } else if ($rule instanceof \Less\Node\Comment) {
  149. if (!$rule->silent) {
  150. if ($this->root) {
  151. $rulesets[] = $rule->toCSS($env);
  152. } else {
  153. $rules[] = $rule->toCSS($env);
  154. }
  155. }
  156. } else {
  157. if (method_exists($rule, 'toCSS') && ( ! isset($rule->variable) || ! $rule->variable)) {
  158. $rules[] = $rule->toCSS($env);
  159. } else if (isset($rule->value) && $rule->value && ! $rule->variable) {
  160. $rules[] = (string) $rule->value;
  161. }
  162. }
  163. }
  164. $rulesets = implode('', $rulesets);
  165. // If this is the root node, we don't render
  166. // a selector, or {}.
  167. // Otherwise, only output if this ruleset has rules.
  168. if ($this->root) {
  169. $css[] = implode($env->compress ? '' : "\n", $rules);
  170. } else {
  171. if (count($rules)) {
  172. $selector = array_map(function ($p) use ($env) {
  173. return trim(implode('', array_map(function ($s) use ($env) {
  174. return $s->toCSS($env);
  175. }, $p)));
  176. }, $paths);
  177. $selector = implode($env->compress ? ',' : ",\n", $selector);
  178. // Remove duplicates
  179. for ($i = count($rules) - 1; $i >= 0; $i--) {
  180. if (array_search($rules[$i], $_rules) === FALSE) {
  181. array_unshift($_rules, $rules[$i]);
  182. }
  183. }
  184. $rules = $_rules;
  185. $css[] = $selector;
  186. $css[] = ($env->compress ? '{' : " {\n ") .
  187. implode($env->compress ? '' : "\n ", $rules) .
  188. ($env->compress ? '}' : "\n}\n");
  189. }
  190. }
  191. $css[] = $rulesets;
  192. return implode('', $css) . ($env->compress ? "\n" : '');
  193. }
  194. public function joinSelectors (&$paths, $context, $selectors)
  195. {
  196. foreach($selectors as $selector) {
  197. $this->joinSelector($paths, $context, $selector);
  198. }
  199. }
  200. public function joinSelector (&$paths, $context, $selector)
  201. {
  202. $before = array();
  203. $after = array();
  204. $beforeElements = array();
  205. $afterElements = array();
  206. $hasParentSelector = false;
  207. foreach($selector->elements as $el) {
  208. if (strlen($el->combinator->value) > 0 && $el->combinator->value[0] === '&') {
  209. $hasParentSelector = true;
  210. }
  211. if ($hasParentSelector) {
  212. $afterElements[] = $el;
  213. } else {
  214. $beforeElements[] = $el;
  215. }
  216. }
  217. if (! $hasParentSelector) {
  218. $afterElements = $beforeElements;
  219. $beforeElements = array();
  220. }
  221. if (count($beforeElements) > 0) {
  222. $before[] = new \Less\Node\Selector($beforeElements);
  223. }
  224. if (count($afterElements) > 0) {
  225. $after[] = new \Less\Node\Selector($afterElements);
  226. }
  227. foreach($context as $c) {
  228. $paths[] = array_merge($before, $c, $after);
  229. }
  230. }
  231. }