PageRenderTime 26ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/CssCrush/DeclarationList.php

http://github.com/peteboere/css-crush
PHP | 598 lines | 431 code | 106 blank | 61 comment | 72 complexity | 85a314a5f1423101453c073855e65838 MD5 | raw file
  1. <?php
  2. /**
  3. *
  4. * Declaration lists.
  5. *
  6. */
  7. namespace CssCrush;
  8. class DeclarationList extends Iterator
  9. {
  10. public $flattened = true;
  11. public $processed = false;
  12. protected $rule;
  13. public $properties = [];
  14. public $canonicalProperties = [];
  15. // Declarations hash table for inter-rule this() referencing.
  16. public $data = [];
  17. // Declarations hash table for external query() referencing.
  18. public $queryData = [];
  19. public function __construct($declarationsString, Rule $rule)
  20. {
  21. parent::__construct();
  22. $this->rule = $rule;
  23. $pairs = DeclarationList::parse($declarationsString);
  24. foreach ($pairs as $index => $pair) {
  25. list($prop, $value) = $pair;
  26. // Directives.
  27. if ($prop === 'extends') {
  28. $this->rule->addExtendSelectors($value);
  29. unset($pairs[$index]);
  30. }
  31. elseif ($prop === 'name') {
  32. if (! $this->rule->name) {
  33. $this->rule->name = $value;
  34. }
  35. unset($pairs[$index]);
  36. }
  37. }
  38. // Build declaration list.
  39. foreach ($pairs as $index => &$pair) {
  40. list($prop, $value) = $pair;
  41. if (trim($value) !== '') {
  42. if ($prop === 'mixin') {
  43. $this->flattened = false;
  44. $this->store[] = $pair;
  45. }
  46. else {
  47. // Only store to $this->data if the value does not itself make a
  48. // this() call to avoid circular references.
  49. if (! preg_match(Regex::$patt->thisFunction, $value)) {
  50. $this->data[strtolower($prop)] = $value;
  51. }
  52. $this->add($prop, $value, $index);
  53. }
  54. }
  55. }
  56. }
  57. public function add($property, $value, $contextIndex = 0)
  58. {
  59. $declaration = new Declaration($property, $value, $contextIndex);
  60. if ($declaration->valid) {
  61. $this->index($declaration);
  62. $this->store[] = $declaration;
  63. return $declaration;
  64. }
  65. return false;
  66. }
  67. public function reset(array $declaration_stack)
  68. {
  69. $this->store = $declaration_stack;
  70. $this->updateIndex();
  71. }
  72. public function index($declaration)
  73. {
  74. $property = $declaration->property;
  75. if (isset($this->properties[$property])) {
  76. $this->properties[$property]++;
  77. }
  78. else {
  79. $this->properties[$property] = 1;
  80. }
  81. $this->canonicalProperties[$declaration->canonicalProperty] = true;
  82. }
  83. public function updateIndex()
  84. {
  85. $this->properties = [];
  86. $this->canonicalProperties = [];
  87. foreach ($this->store as $declaration) {
  88. $this->index($declaration);
  89. }
  90. }
  91. public function propertyCount($property)
  92. {
  93. return isset($this->properties[$property]) ? $this->properties[$property] : 0;
  94. }
  95. public function join($glue = ';')
  96. {
  97. return implode($glue, $this->store);
  98. }
  99. /*
  100. Aliasing.
  101. */
  102. public function aliasProperties($vendor_context = null)
  103. {
  104. $aliased_properties =& Crush::$process->aliases['properties'];
  105. // Bail early if nothing doing.
  106. if (! array_intersect_key($aliased_properties, $this->properties)) {
  107. return;
  108. }
  109. $stack = [];
  110. $rule_updated = false;
  111. $regex = Regex::$patt;
  112. foreach ($this->store as $declaration) {
  113. // Check declaration against vendor context.
  114. if ($vendor_context && $declaration->vendor && $declaration->vendor !== $vendor_context) {
  115. continue;
  116. }
  117. if ($declaration->skip) {
  118. $stack[] = $declaration;
  119. continue;
  120. }
  121. // Shim in aliased properties.
  122. if (isset($aliased_properties[$declaration->property])) {
  123. foreach ($aliased_properties[$declaration->property] as $prop_alias) {
  124. // If an aliased version already exists do not create one.
  125. if ($this->propertyCount($prop_alias)) {
  126. continue;
  127. }
  128. // Get property alias vendor.
  129. preg_match($regex->vendorPrefix, $prop_alias, $alias_vendor);
  130. // Check against vendor context.
  131. if ($vendor_context && $alias_vendor && $alias_vendor[1] !== $vendor_context) {
  132. continue;
  133. }
  134. // Create the aliased declaration.
  135. $copy = clone $declaration;
  136. $copy->property = $prop_alias;
  137. // Set the aliased declaration vendor property.
  138. $copy->vendor = null;
  139. if ($alias_vendor) {
  140. $copy->vendor = $alias_vendor[1];
  141. }
  142. $stack[] = $copy;
  143. $rule_updated = true;
  144. }
  145. }
  146. // Un-aliased property or a property alias that has been manually set.
  147. $stack[] = $declaration;
  148. }
  149. // Re-assign if any updates have been made.
  150. if ($rule_updated) {
  151. $this->reset($stack);
  152. }
  153. }
  154. public function aliasFunctions($vendor_context = null)
  155. {
  156. $function_aliases =& Crush::$process->aliases['functions'];
  157. $function_alias_groups =& Crush::$process->aliases['function_groups'];
  158. // The new modified set of declarations.
  159. $new_set = [];
  160. $rule_updated = false;
  161. // Shim in aliased functions.
  162. foreach ($this->store as $declaration) {
  163. // No functions, bail.
  164. if (! $declaration->functions || $declaration->skip) {
  165. $new_set[] = $declaration;
  166. continue;
  167. }
  168. // Get list of functions used in declaration that are alias-able, bail if none.
  169. $intersect = array_intersect_key($declaration->functions, $function_aliases);
  170. if (! $intersect) {
  171. $new_set[] = $declaration;
  172. continue;
  173. }
  174. // Keep record of which groups have been applied.
  175. $processed_groups = [];
  176. foreach (array_keys($intersect) as $fn_name) {
  177. // Store for all the duplicated declarations.
  178. $prefixed_copies = [];
  179. // Grouped function aliases.
  180. if ($function_aliases[$fn_name][0] === '.') {
  181. $group_id = $function_aliases[$fn_name];
  182. // If this group has been applied we can skip over.
  183. if (isset($processed_groups[$group_id])) {
  184. continue;
  185. }
  186. // Mark group as applied.
  187. $processed_groups[$group_id] = true;
  188. $groups =& $function_alias_groups[$group_id];
  189. foreach ($groups as $group_key => $replacements) {
  190. // If the declaration is vendor specific only create aliases for the same vendor.
  191. if (
  192. ($declaration->vendor && $group_key !== $declaration->vendor) ||
  193. ($vendor_context && $group_key !== $vendor_context)
  194. ) {
  195. continue;
  196. }
  197. $copy = clone $declaration;
  198. // Make swaps.
  199. $copy->value = preg_replace(
  200. $replacements['find'],
  201. $replacements['replace'],
  202. $copy->value
  203. );
  204. $prefixed_copies[] = $copy;
  205. $rule_updated = true;
  206. }
  207. // Post fixes.
  208. if (isset(PostAliasFix::$functions[$group_id])) {
  209. call_user_func(PostAliasFix::$functions[$group_id], $prefixed_copies, $group_id);
  210. }
  211. }
  212. // Single function aliases.
  213. else {
  214. foreach ($function_aliases[$fn_name] as $fn_alias) {
  215. // If the declaration is vendor specific only create aliases for the same vendor.
  216. if ($declaration->vendor) {
  217. preg_match(Regex::$patt->vendorPrefix, $fn_alias, $m);
  218. if (
  219. $m[1] !== $declaration->vendor ||
  220. ($vendor_context && $m[1] !== $vendor_context)
  221. ) {
  222. continue;
  223. }
  224. }
  225. $copy = clone $declaration;
  226. // Make swaps.
  227. $copy->value = preg_replace(
  228. Regex::make("~{{ LB }}$fn_name(?=\()~iS"),
  229. $fn_alias,
  230. $copy->value
  231. );
  232. $prefixed_copies[] = $copy;
  233. $rule_updated = true;
  234. }
  235. // Post fixes.
  236. if (isset(PostAliasFix::$functions[$fn_name])) {
  237. call_user_func(PostAliasFix::$functions[$fn_name], $prefixed_copies, $fn_name);
  238. }
  239. }
  240. $new_set = array_merge($new_set, $prefixed_copies);
  241. }
  242. $new_set[] = $declaration;
  243. }
  244. // Re-assign if any updates have been made.
  245. if ($rule_updated) {
  246. $this->reset($new_set);
  247. }
  248. }
  249. public function aliasDeclarations($vendor_context = null)
  250. {
  251. $declaration_aliases =& Crush::$process->aliases['declarations'];
  252. // First test for the existence of any aliased properties.
  253. if (! ($intersect = array_intersect_key($declaration_aliases, $this->properties))) {
  254. return;
  255. }
  256. $intersect = array_flip(array_keys($intersect));
  257. $new_set = [];
  258. $rule_updated = false;
  259. foreach ($this->store as $declaration) {
  260. // Check the current declaration property is actually aliased.
  261. if (isset($intersect[$declaration->property]) && ! $declaration->skip) {
  262. // Iterate on the current declaration property for value matches.
  263. foreach ($declaration_aliases[$declaration->property] as $value_match => $replacements) {
  264. // Create new alias declaration if the property and value match.
  265. if ($declaration->value === $value_match) {
  266. foreach ($replacements as $values) {
  267. // Check the vendor against context.
  268. if ($vendor_context && $vendor_context !== $values[2]) {
  269. continue;
  270. }
  271. // If the replacement property is null use the original declaration property.
  272. $new = new Declaration(
  273. ! empty($values[0]) ? $values[0] : $declaration->property,
  274. $values[1]
  275. );
  276. $new->important = $declaration->important;
  277. $new_set[] = $new;
  278. $rule_updated = true;
  279. }
  280. }
  281. }
  282. }
  283. $new_set[] = $declaration;
  284. }
  285. // Re-assign if any updates have been made.
  286. if ($rule_updated) {
  287. $this->reset($new_set);
  288. }
  289. }
  290. public static function parse($str, $options = [])
  291. {
  292. $str = Util::stripCommentTokens($str);
  293. $lines = preg_split('~\s*;\s*~', $str, null, PREG_SPLIT_NO_EMPTY);
  294. $options += [
  295. 'keyed' => false,
  296. 'ignore_directives' => false,
  297. 'lowercase_keys' => false,
  298. 'context' => null,
  299. 'flatten' => false,
  300. 'apply_hooks' => false,
  301. ];
  302. $pairs = [];
  303. foreach ($lines as $line) {
  304. if (! $options['ignore_directives'] && preg_match(Regex::$patt->ruleDirective, $line, $m)) {
  305. if (! empty($m[1])) {
  306. $property = 'mixin';
  307. }
  308. elseif (! empty($m[2])) {
  309. $property = 'extends';
  310. }
  311. else {
  312. $property = 'name';
  313. }
  314. $value = trim(substr($line, strlen($m[0])));
  315. }
  316. elseif (($colon_pos = strpos($line, ':')) !== false) {
  317. $property = trim(substr($line, 0, $colon_pos));
  318. $value = trim(substr($line, $colon_pos + 1));
  319. if ($options['lowercase_keys']) {
  320. $property = strtolower($property);
  321. }
  322. if ($options['apply_hooks']) {
  323. Crush::$process->emit('declaration_preprocess', [
  324. 'property' => &$property,
  325. 'value' => &$value,
  326. ]);
  327. }
  328. }
  329. else {
  330. continue;
  331. }
  332. if ($property === '' || $value === '') {
  333. continue;
  334. }
  335. if ($property === 'mixin' && $options['flatten']) {
  336. $pairs = Mixin::merge($pairs, $value, [
  337. 'keyed' => $options['keyed'],
  338. 'context' => $options['context'],
  339. ]);
  340. }
  341. elseif ($options['keyed']) {
  342. $pairs[$property] = $value;
  343. }
  344. else {
  345. $pairs[] = [$property, $value];
  346. }
  347. }
  348. return $pairs;
  349. }
  350. public function flatten()
  351. {
  352. if ($this->flattened) {
  353. return;
  354. }
  355. $newSet = [];
  356. foreach ($this->store as $declaration) {
  357. if (is_array($declaration) && $declaration[0] === 'mixin') {
  358. foreach (Mixin::merge([], $declaration[1], ['context' => $this->rule]) as $mixable) {
  359. if ($mixable instanceof Declaration) {
  360. $clone = clone $mixable;
  361. $clone->index = count($newSet);
  362. $newSet[] = $clone;
  363. }
  364. elseif ($mixable[0] === 'extends') {
  365. $this->rule->addExtendSelectors($mixable[1]);
  366. }
  367. else {
  368. $newSet[] = new Declaration($mixable[0], $mixable[1], count($newSet));
  369. }
  370. }
  371. }
  372. else {
  373. $declaration->index = count($newSet);
  374. $newSet[] = $declaration;
  375. }
  376. }
  377. $this->reset($newSet);
  378. $this->flattened = true;
  379. }
  380. public function process()
  381. {
  382. if ($this->processed) {
  383. return;
  384. }
  385. foreach ($this->store as $index => $declaration) {
  386. // Execute functions, store as data etc.
  387. $declaration->process($this->rule);
  388. // Drop declaration if value is now empty.
  389. if (! $declaration->valid) {
  390. unset($this->store[$index]);
  391. }
  392. }
  393. // data is done with, reclaim memory.
  394. unset($this->data);
  395. $this->processed = true;
  396. }
  397. public function expandData($dataset, $property)
  398. {
  399. // Expand shorthand properties to make them available
  400. // as data for this() and query().
  401. static $expandables = [
  402. 'margin-top' => 'margin',
  403. 'margin-right' => 'margin',
  404. 'margin-bottom' => 'margin',
  405. 'margin-left' => 'margin',
  406. 'padding-top' => 'padding',
  407. 'padding-right' => 'padding',
  408. 'padding-bottom' => 'padding',
  409. 'padding-left' => 'padding',
  410. 'border-top-width' => 'border-width',
  411. 'border-right-width' => 'border-width',
  412. 'border-bottom-width' => 'border-width',
  413. 'border-left-width' => 'border-width',
  414. 'border-top-left-radius' => 'border-radius',
  415. 'border-top-right-radius' => 'border-radius',
  416. 'border-bottom-right-radius' => 'border-radius',
  417. 'border-bottom-left-radius' => 'border-radius',
  418. 'border-top-color' => 'border-color',
  419. 'border-right-color' => 'border-color',
  420. 'border-bottom-color' => 'border-color',
  421. 'border-left-color' => 'border-color',
  422. ];
  423. $dataset =& $this->{$dataset};
  424. $property_group = isset($expandables[$property]) ? $expandables[$property] : null;
  425. // Bail if property non-expandable or already set.
  426. if (! $property_group || isset($dataset[$property]) || ! isset($dataset[$property_group])) {
  427. return;
  428. }
  429. // Get the expandable property value.
  430. $value = $dataset[$property_group];
  431. // Top-Right-Bottom-Left "trbl" expandable properties.
  432. $trbl_fmt = null;
  433. switch ($property_group) {
  434. case 'margin':
  435. $trbl_fmt = 'margin-%s';
  436. break;
  437. case 'padding':
  438. $trbl_fmt = 'padding-%s';
  439. break;
  440. case 'border-width':
  441. $trbl_fmt = 'border-%s-width';
  442. break;
  443. case 'border-radius':
  444. $trbl_fmt = 'border-%s-radius';
  445. break;
  446. case 'border-color':
  447. $trbl_fmt = 'border-%s-color';
  448. break;
  449. }
  450. if ($trbl_fmt) {
  451. $parts = explode(' ', $value);
  452. $placeholders = [];
  453. // 4 values.
  454. if (isset($parts[3])) {
  455. $placeholders = $parts;
  456. }
  457. // 3 values.
  458. elseif (isset($parts[2])) {
  459. $placeholders = [$parts[0], $parts[1], $parts[2], $parts[1]];
  460. }
  461. // 2 values.
  462. elseif (isset($parts[1])) {
  463. $placeholders = [$parts[0], $parts[1], $parts[0], $parts[1]];
  464. }
  465. // 1 value.
  466. else {
  467. $placeholders = array_pad($placeholders, 4, $parts[0]);
  468. }
  469. // Set positional variants.
  470. if ($property_group === 'border-radius') {
  471. $positions = [
  472. 'top-left',
  473. 'top-right',
  474. 'bottom-right',
  475. 'bottom-left',
  476. ];
  477. }
  478. else {
  479. $positions = [
  480. 'top',
  481. 'right',
  482. 'bottom',
  483. 'left',
  484. ];
  485. }
  486. foreach ($positions as $index => $position) {
  487. $prop = sprintf($trbl_fmt, $position);
  488. $dataset += [$prop => $placeholders[$index]];
  489. }
  490. }
  491. }
  492. }