PageRenderTime 59ms CodeModel.GetById 1ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Rule.php

http://github.com/peteboere/css-crush
PHP | 375 lines | 261 code | 53 blank | 61 comment | 32 complexity | 77c886f16cfa83c099994d762840dcc0 MD5 | raw file
  1. <?php
  2. /**
  3. *
  4. * CSS rule API
  5. *
  6. */
  7. class CssCrush_Rule implements IteratorAggregate {
  8. public $vendorContext = null;
  9. public $properties = array();
  10. public $selectors = null;
  11. public $parens = array();
  12. public $declarations = array();
  13. public $comments = array();
  14. public function __construct ( $selector_string = null, $declarations_string ) {
  15. $regex = CssCrush::$regex;
  16. // Parse the selectors chunk
  17. if ( !empty( $selector_string ) ) {
  18. $selectors_match = CssCrush::splitDelimList( $selector_string, ',' );
  19. $this->parens += $selectors_match->matches;
  20. // Remove and store comments that sit above the first selector
  21. // remove all comments between the other selectors
  22. preg_match_all( $regex->token->comment, $selectors_match->list[0], $m );
  23. $this->comments = $m[0];
  24. foreach ( $selectors_match->list as &$selector ) {
  25. $selector = preg_replace( $regex->token->comment, '', $selector );
  26. $selector = trim( $selector );
  27. }
  28. $this->selectors = $selectors_match->list;
  29. }
  30. // Apply any custom functions
  31. $declarations_string = CssCrush_Function::parseAndExecuteValue( $declarations_string );
  32. // Parse the declarations chunk
  33. // Need to split safely as there are semi-colons in data-uris
  34. $declarations_match = CssCrush::splitDelimList( $declarations_string, ';' );
  35. $this->parens += $declarations_match->matches;
  36. // Parse declarations in to property/value pairs
  37. foreach ( $declarations_match->list as $declaration ) {
  38. // Strip comments around the property
  39. $declaration = preg_replace( $regex->token->comment, '', $declaration );
  40. // Store the property
  41. $colonPos = strpos( $declaration, ':' );
  42. if ( $colonPos === false ) {
  43. // If there is no colon it's malformed
  44. continue;
  45. }
  46. // The property name
  47. $prop = trim( substr( $declaration, 0, $colonPos ) );
  48. // Test for escape tilde
  49. if ( $skip = strpos( $prop, '~' ) === 0 ) {
  50. $prop = substr( $prop, 1 );
  51. }
  52. // Store the property name
  53. $this->addProperty( $prop );
  54. // Store the property family
  55. // Store the vendor id, if one is present
  56. if ( preg_match( $regex->vendorPrefix, $prop, $vendor ) ) {
  57. $family = $vendor[2];
  58. $vendor = $vendor[1];
  59. }
  60. else {
  61. $vendor = null;
  62. $family = $prop;
  63. }
  64. // Extract the value part of the declaration
  65. $value = substr( $declaration, $colonPos + 1 );
  66. $value = $value !== false ? trim( $value ) : $value;
  67. if ( $value === false or $value === '' ) {
  68. // We'll ignore declarations with empty values
  69. continue;
  70. }
  71. // Create an index of all functions in the current declaration
  72. if ( preg_match_all( $regex->function->match, $value, $functions ) > 0 ) {
  73. // CssCrush::log( $functions );
  74. $out = array();
  75. foreach ( $functions[2] as $index => $fn_name ) {
  76. $out[] = $fn_name;
  77. }
  78. $functions = array_unique( $out );
  79. }
  80. else {
  81. $functions = array();
  82. }
  83. // Store the declaration
  84. $_declaration = (object) array(
  85. 'property' => $prop,
  86. 'family' => $family,
  87. 'vendor' => $vendor,
  88. 'functions' => $functions,
  89. 'value' => $value,
  90. 'skip' => $skip,
  91. );
  92. $this->declarations[] = $_declaration;
  93. }
  94. }
  95. public function addPropertyAliases () {
  96. $regex = CssCrush::$regex;
  97. $aliasedProperties =& CssCrush::$aliases[ 'properties' ];
  98. // First test for the existence of any aliased properties
  99. $intersect = array_intersect( array_keys( $aliasedProperties ), array_keys( $this->properties ) );
  100. if ( empty( $intersect ) ) {
  101. return;
  102. }
  103. // Shim in aliased properties
  104. $new_set = array();
  105. foreach ( $this->declarations as $declaration ) {
  106. $prop = $declaration->property;
  107. if (
  108. !$declaration->skip and
  109. isset( $aliasedProperties[ $prop ] )
  110. ) {
  111. // There are aliases for the current property
  112. foreach ( $aliasedProperties[ $prop ] as $prop_alias ) {
  113. if ( $this->propertyCount( $prop_alias ) ) {
  114. continue;
  115. }
  116. // If the aliased property hasn't been set manually, we create it
  117. $copy = clone $declaration;
  118. $copy->family = $copy->property;
  119. $copy->property = $prop_alias;
  120. // Remembering to set the vendor property
  121. $copy->vendor = null;
  122. // Increment the property count
  123. $this->addProperty( $prop_alias );
  124. if ( preg_match( $regex->vendorPrefix, $prop_alias, $vendor ) ) {
  125. $copy->vendor = $vendor[1];
  126. }
  127. $new_set[] = $copy;
  128. }
  129. }
  130. // Un-aliased property or a property alias that has been manually set
  131. $new_set[] = $declaration;
  132. }
  133. // Re-assign
  134. $this->declarations = $new_set;
  135. }
  136. public function addFunctionAliases () {
  137. $function_aliases =& CssCrush::$aliases[ 'functions' ];
  138. $aliased_functions = array_keys( $function_aliases );
  139. if ( empty( $aliased_functions ) ) {
  140. return;
  141. }
  142. $new_set = array();
  143. // Keep track of the function aliases we apply and to which property 'family'
  144. // they belong, so we can avoid un-unecessary duplications
  145. $used_fn_aliases = array();
  146. // Shim in aliased functions
  147. foreach ( $this->declarations as $declaration ) {
  148. // No functions, skip
  149. if (
  150. $declaration->skip or
  151. empty( $declaration->functions )
  152. ) {
  153. $new_set[] = $declaration;
  154. continue;
  155. }
  156. // Get list of functions used in declaration that are alias-able, if none skip
  157. $intersect = array_intersect( $declaration->functions, $aliased_functions );
  158. if ( empty( $intersect ) ) {
  159. $new_set[] = $declaration;
  160. continue;
  161. }
  162. // CssCrush::log($intersect);
  163. // Loop the aliasable functions
  164. foreach ( $intersect as $fn_name ) {
  165. if ( $declaration->vendor ) {
  166. // If the property is vendor prefixed we use the vendor prefixed version
  167. // of the function if it exists.
  168. // Else we just skip and use the unprefixed version
  169. $fn_search = "-{$declaration->vendor}-$fn_name";
  170. if ( in_array( $fn_search, $function_aliases[ $fn_name ] ) ) {
  171. $declaration->value = preg_replace(
  172. '!(^| |,)' . $fn_name . '!',
  173. '${1}' . $fn_search,
  174. $declaration->value
  175. );
  176. $used_fn_aliases[ $declaration->family ][] = $fn_search;
  177. }
  178. }
  179. else {
  180. // Duplicate the rule for each alias
  181. foreach ( $function_aliases[ $fn_name ] as $fn_alias ) {
  182. if (
  183. isset( $used_fn_aliases[ $declaration->family ] ) and
  184. in_array( $fn_alias, $used_fn_aliases[ $declaration->family ] )
  185. ) {
  186. // If the function alias has already been applied in a vendor property
  187. // for the same declaration property assume all is good
  188. continue;
  189. }
  190. $copy = clone $declaration;
  191. $copy->value = preg_replace(
  192. '!(^| |,)' . $fn_name . '!',
  193. '${1}' . $fn_alias,
  194. $copy->value
  195. );
  196. $new_set[] = $copy;
  197. // Increment the property count
  198. $this->addProperty( $copy->property );
  199. }
  200. }
  201. }
  202. $new_set[] = $declaration;
  203. }
  204. // Re-assign
  205. $this->declarations = $new_set;
  206. }
  207. public function addValueAliases () {
  208. $aliasedValues =& CssCrush::$aliases[ 'values' ];
  209. // First test for the existence of any aliased properties
  210. $intersect = array_intersect( array_keys( $aliasedValues ), array_keys( $this->properties ) );
  211. if ( empty( $intersect ) ) {
  212. return;
  213. }
  214. $new_set = array();
  215. foreach ( $this->declarations as $declaration ) {
  216. if ( !$declaration->skip ) {
  217. foreach ( $aliasedValues as $value_prop => $value_aliases ) {
  218. if ( $this->propertyCount( $value_prop ) < 1 ) {
  219. continue;
  220. }
  221. foreach ( $value_aliases as $value => $aliases ) {
  222. if ( $declaration->value === $value ) {
  223. foreach ( $aliases as $alias ) {
  224. $copy = clone $declaration;
  225. $copy->value = $alias;
  226. $new_set[] = $copy;
  227. }
  228. }
  229. }
  230. }
  231. }
  232. $new_set[] = $declaration;
  233. }
  234. // Re-assign
  235. $this->declarations = $new_set;
  236. }
  237. public function expandSelectors () {
  238. $new_set = array();
  239. $reg_comma = '!\s*,\s*!';
  240. foreach ( $this->selectors as $selector ) {
  241. $pos = strpos( $selector, ':any___' );
  242. if ( $pos !== false ) {
  243. // Contains an :any statement so we expand
  244. $chain = array( '' );
  245. do {
  246. if ( $pos === 0 ) {
  247. preg_match( '!:any(___p\d+___)!', $selector, $m );
  248. // Parse the arguments
  249. $expression = trim( $this->parens[ $m[1] ], '()' );
  250. $parts = preg_split( $reg_comma, $expression, null, PREG_SPLIT_NO_EMPTY );
  251. $tmp = array();
  252. foreach ( $chain as $rowCopy ) {
  253. foreach ( $parts as $part ) {
  254. $tmp[] = $rowCopy . $part;
  255. }
  256. }
  257. $chain = $tmp;
  258. $selector = substr( $selector, strlen( $m[0] ) );
  259. }
  260. else {
  261. foreach ( $chain as &$row ) {
  262. $row .= substr( $selector, 0, $pos );
  263. }
  264. $selector = substr( $selector, $pos );
  265. }
  266. } while ( ( $pos = strpos( $selector, ':any___' ) ) !== false );
  267. // Finish off
  268. foreach ( $chain as &$row ) {
  269. $new_set[] = $row . $selector;
  270. }
  271. }
  272. else {
  273. // Nothing special
  274. $new_set[] = $selector;
  275. }
  276. }
  277. $this->selectors = $new_set;
  278. }
  279. ############
  280. # IteratorAggregate
  281. public function getIterator () {
  282. return new ArrayIterator( $this->declarations );
  283. }
  284. ############
  285. # Rule API
  286. public function propertyCount ( $prop ) {
  287. if ( array_key_exists( $prop, $this->properties ) ) {
  288. return $this->properties[ $prop ];
  289. }
  290. return 0;
  291. }
  292. // Add property to the rule index keeping track of the count
  293. public function addProperty ( $prop ) {
  294. if ( isset( $this->properties[ $prop ] ) ) {
  295. $this->properties[ $prop ]++;
  296. }
  297. else {
  298. $this->properties[ $prop ] = 1;
  299. }
  300. }
  301. public function createDeclaration ( $property, $value, $options = array() ) {
  302. // Test for escape tilde
  303. if ( $skip = strpos( $property, '~' ) === 0 ) {
  304. $property = substr( $property, 1 );
  305. }
  306. $_declaration = array(
  307. 'property' => $property,
  308. 'family' => null,
  309. 'vendor' => null,
  310. 'value' => $value,
  311. 'skip' => $skip,
  312. );
  313. $this->addProperty( $property );
  314. return (object) array_merge( $_declaration, $options );
  315. }
  316. // Get a declaration value without paren tokens
  317. public function getDeclarationValue ( $declaration ) {
  318. $paren_keys = array_keys( $this->parens );
  319. $paren_values = array_values( $this->parens );
  320. return str_replace( $paren_keys, $paren_values, $declaration->value );
  321. }
  322. }