CssParser.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. <?php
  2. class CssNode {
  3. static $shorthands = array(
  4. 'margin' => 'top right bottom left',
  5. 'padding' => 'top right bottom left',
  6. 'font' => 'weight [style] size [/line-height] family',
  7. 'border' => 'width style color',
  8. 'background' => '[color] image repeat [attachment] [position]',
  9. 'list-style' => '[type] [position] [image]',
  10. 'outline' => '[color] [style] [width]'
  11. );
  12. static $colors = array(
  13. '#f0f' => 'aqua', 'black' => '#000', 'fuchsia' => '#f0f', '#808080' => 'grey',
  14. '#008000' => 'green', '#800000' => 'maroon', '#000080' => 'navy', '#808000' => 'olive',
  15. '#800080' => 'purple', '#f00' => 'red', '#c0c0c0' => 'silver', '#008080' => 'teal',
  16. 'white' => '#fff', 'yellow' => '#ff0'
  17. );
  18. static $config;
  19. public $selector;
  20. public $css;
  21. public $parent_node;
  22. public $rules = array();
  23. public $children = array();
  24. function __construct($selector, $css='', $parent_node=null) {
  25. $this->selector = trim($selector);
  26. $this->css = trim($css);
  27. $this->parent_node = $parent_node;
  28. }
  29. static function extract_importance($values) {
  30. $important = '';
  31. foreach( $values as $rule => $value ) {
  32. if( preg_match('/^(."+?) !important$/', $value, $m) ) {
  33. $important = ' !important';
  34. $values[$rule] = $m[1];
  35. }
  36. }
  37. return array($values, $important);
  38. }
  39. function replace_shorthands() {
  40. $rules = array();
  41. // Put sub-selectors in arrays
  42. $pattern = '/^('.implode('|', array_keys(self::$shorthands)).')-([\w-]+)/';
  43. foreach( $this->rules as $rule => $value ) {
  44. if( preg_match($pattern, $rule, $m) ) {
  45. $base_rule = $m[1];
  46. if( isset($rules[$base_rule]) ) {
  47. if( !is_array($rules[$base_rule]) )
  48. $rules[$base_rule] = array('__main__' => $rules[$base_rule]);
  49. } else {
  50. $rules[$base_rule] = array();
  51. }
  52. $rules[$base_rule][$m[2]] = $value;
  53. } else {
  54. $rules[$rule] = $value;
  55. }
  56. }
  57. // Filter out base rules with one property value
  58. foreach( $rules as $rule => $values ) {
  59. if( is_array($values) && count($values) == 1 ) {
  60. $rules[$rule.'-'.key($values)] = reset($values);
  61. unset($rules[$rule]);
  62. }
  63. }
  64. foreach( $rules as $rule => $values ) {
  65. if( is_array($values) ) {
  66. list($values, $important) = self::extract_importance($values);
  67. if( $rule == 'font' && isset($rules['line-height']) ) {
  68. $values['line-height'] = $rules['line-height'];
  69. unset($rules['line-height']);
  70. }
  71. if( isset(self::$shorthands[$rule]) ) {
  72. $replace = true;
  73. $replacement = '';
  74. $parts = explode(' ', self::$shorthands[$rule]);
  75. foreach( array_keys($parts) as $i ) {
  76. $part = $parts[$i];
  77. if( $part == '[' ) {
  78. $parts[$i + 1] = '[ '.$parts[$i + 1];
  79. continue;
  80. }
  81. if( preg_match('%^\[(/)?([^]]+)\]$%', $part, $m) ) {
  82. $part = $m[2];
  83. if( isset($values[$part]) ) {
  84. $value = $values[$part];
  85. if( self::$config['compress_colors'] && strpos($part, 'color') !== false )
  86. $value = self::compress_color($value);
  87. $replacement .= (!strlen($m[1]) ? ' ' : $m[1]).$value;
  88. }
  89. } elseif( isset($values[$part]) ) {
  90. $value = $values[$part];
  91. if( self::$config['compress_colors'] && strpos($part, 'color') !== false )
  92. $value = self::compress_color($value);
  93. $i && $replacement .= ' ';
  94. $replacement .= $value;
  95. } else {
  96. $replace = false;
  97. break;
  98. }
  99. }
  100. $replace && $values['__main__'] = $replacement;
  101. }
  102. if( isset($values['__main__']) ) {
  103. $rules[$rule] = $values['__main__'];
  104. } else {
  105. foreach( $values as $sub_rule => $value )
  106. $rules[$rule.'-'.$sub_rule] = $value;
  107. unset($rules[$rule]);
  108. }
  109. }
  110. }
  111. return $rules;
  112. }
  113. function compress_color($color) {
  114. $color = preg_replace('/#(\w)\1(\w)\2(\w)\3/', '#\1\2\3', strtolower($color));
  115. if( isset(self::$colors[$color]) )
  116. $color = self::$colors[$color];
  117. return $color;
  118. }
  119. function compress($value, $rule) {
  120. // Double rule names
  121. if( preg_match('/^([^#]+)#\d+$/', $rule, $m) )
  122. $rule = $m[1];
  123. // Compress colors
  124. if( self::$config['compress_colors'] && preg_match('/color$/', $rule) )
  125. $value = self::compress_color($value);
  126. // Compress measurements
  127. if( self::$config['compress_measurements']
  128. && ($rule == 'margin' || $rule == 'padding') ) {
  129. if( preg_match('/^0\w+$/', $value, $m) ) {
  130. // Replace zero with unit by just zero
  131. $value = 0;
  132. } elseif( preg_match('/^(\w+) (\w+) (\w+)(?: (\w+))?$/', $value, $m) ) {
  133. // Replace redundant margins and paddings
  134. $value = $m[1].' '.$m[2];
  135. $left_needed = isset($m[4]) && $m[4] != $m[2];
  136. $bottom_needed = $left_needed || $m[3] != $m[1];
  137. if( $bottom_needed ) {
  138. $value .= ' '.$m[3];
  139. $left_needed && $value .= ' '.$m[4];
  140. }
  141. }
  142. }
  143. return $rule.(self::$config['minify'] ? ':' : ': ').trim($value);
  144. }
  145. function parse_rules($rules) {
  146. foreach( preg_split('/\s*;\s*/', trim($rules)) as $rule ) {
  147. $split = preg_split('/\s*:\s*/', $rule, 2);
  148. if( count($split) == 2 && strlen($split[0]) && strlen($split[1]) ) {
  149. list($name, $value) = $split;
  150. $i = 1;
  151. // Double rule names
  152. while( isset($this->rules[$name]) )
  153. $name .= '#'.($i++);
  154. $this->rules[$name] = $value;
  155. }
  156. }
  157. }
  158. function parse() {
  159. $minify = self::$config['minify'];
  160. $current_node = $this;
  161. // Remove comments and redundant whitespaces
  162. $css = preg_replace(array('/\s+/', '%/\*.*?\*/%'), array(' ', ''), $this->css);
  163. foreach( array_map('trim', preg_split('/;|\}/', $css)) as $line ) {
  164. if( preg_match('/^ ?([^{]+) ?\{ ?(.*)$/', $line, $m) ) {
  165. // Start tag
  166. $self = preg_match('/^self(.*)/', $m[1], $selector_match);
  167. $child_selectors = $self ? $selector_match[1] : $m[1];
  168. if( strpos($child_selectors, ',') !== false ) {
  169. $selectors = array();
  170. foreach( preg_split('/ ?, ?/', trim($child_selectors)) as $child_selector )
  171. $selectors[] = $current_node->selector.' '.$child_selector;
  172. $selector = implode(',', $selectors);
  173. } else {
  174. $selector = $current_node->selector.($self ? '' : ' ').$child_selectors;
  175. }
  176. $current_node = $current_node->children[] = new CssNode($selector, '', $current_node);
  177. $line = $m[2];
  178. }
  179. if( strlen($line) ) {
  180. // Normal rule
  181. $current_node->parse_rules($line);
  182. } else {
  183. // End tag
  184. $current_node = $current_node->parent_node;
  185. }
  186. }
  187. // Build new CSS string according to config
  188. $css = '';
  189. if( strlen($this->selector) ) {
  190. $rules = self::$config['replace_shorthands'] ? $this->replace_shorthands() : $this->rules;
  191. self::$config['sort_rules'] && ksort($rules);
  192. $rules = array_map('self::compress', $rules, array_keys($rules));
  193. if( $minify )
  194. $css .= $this->selector.'{'.implode(';', $rules).'}';
  195. else
  196. $css .= preg_replace('/, ?/', ",\n", $this->selector)." {\n\t".implode(";\n\t", $rules).";\n}";
  197. }
  198. // Parse children recursively
  199. foreach( $this->children as $child ) {
  200. $minify || $css .= "\n\n";
  201. $css .= $child->parse();
  202. }
  203. return trim($css);
  204. }
  205. }
  206. class CssParser {
  207. static $default_config = array(
  208. 'replace_shorthands' => true,
  209. 'sort_rules' => true,
  210. 'minify' => true,
  211. 'compress_measurements' => true,
  212. 'compress_colors' => true
  213. );
  214. static function minify($css, $config=array()) {
  215. CssNode::$config = array_merge(self::$default_config, $config);
  216. $node = new CssNode('', $css);
  217. return $node->parse();
  218. }
  219. }
  220. ?>