CssParser.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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. // Compress colors
  121. if( self::$config['compress_colors'] && preg_match('/color$/', $rule) )
  122. $value = self::compress_color($value);
  123. // Compress measurements
  124. if( self::$config['compress_measurements']
  125. && ($rule == 'margin' || $rule == 'padding') ) {
  126. if( preg_match('/^0\w+$/', $value, $m) ) {
  127. // Replace zero with unit by just zero
  128. $value = 0;
  129. } elseif( preg_match('/^(\w+) (\w+) (\w+)(?: (\w+))?$/', $value, $m) ) {
  130. // Replace redundant margins and paddings
  131. $value = $m[1].' '.$m[2];
  132. $left_needed = isset($m[4]) && $m[4] != $m[2];
  133. $bottom_needed = $left_needed || $m[3] != $m[1];
  134. if( $bottom_needed ) {
  135. $value .= ' '.$m[3];
  136. $left_needed && $value .= ' '.$m[4];
  137. }
  138. }
  139. }
  140. return $rule.(self::$config['minify'] ? ':' : ': ').trim($value);
  141. }
  142. function parse_rules($rules) {
  143. foreach( preg_split('/\s*;\s*/', trim($rules)) as $rule ) {
  144. $split = preg_split('/\s*:\s*/', $rule, 2);
  145. if( count($split) == 2 && strlen($split[0]) && strlen($split[1]) )
  146. $this->rules[$split[0]] = $split[1];
  147. }
  148. }
  149. function parse() {
  150. $minify = self::$config['minify'];
  151. $current_node = $this;
  152. // Remove comments and redundant whitespaces
  153. $css = preg_replace(array('/\s+/', '%/\*.*?\*/%'), array(' ', ''), $this->css);
  154. foreach( array_map('trim', preg_split('/;|\}/', $css)) as $line ) {
  155. if( preg_match('/^ ?([^{]+) ?\{ ?(.*)$/', $line, $m) ) {
  156. // Start tag
  157. $self = preg_match('/^self(.*)/', $m[1], $selector_match);
  158. $child_selectors = $self ? $selector_match[1] : $m[1];
  159. if( strpos($child_selectors, ',') !== false ) {
  160. $selectors = array();
  161. foreach( preg_split('/ ?, ?/', trim($child_selectors)) as $child_selector )
  162. $selectors[] = $current_node->selector.' '.$child_selector;
  163. $selector = implode(',', $selectors);
  164. } else {
  165. $selector = $current_node->selector.($self ? '' : ' ').$child_selectors;
  166. }
  167. $current_node = $current_node->children[] = new CssNode($selector, $m[2], $current_node);
  168. $line = $m[2];
  169. }
  170. if( strlen($line) ) {
  171. // Normal rule
  172. $current_node->parse_rules($line);
  173. } else {
  174. // End tag
  175. $current_node = $current_node->parent_node;
  176. }
  177. }
  178. // Build new CSS string according to config
  179. $css = '';
  180. if( strlen($this->selector) ) {
  181. $rules = self::$config['replace_shorthands'] ? $this->replace_shorthands() : $this->rules;
  182. self::$config['sort_rules'] && ksort($rules);
  183. $rules = array_map('self::compress', $rules, array_keys($rules));
  184. if( $minify )
  185. $css .= $this->selector.'{'.implode(';', $rules).'}';
  186. else
  187. $css .= preg_replace('/, ?/', ",\n", $this->selector)." {\n\t".implode(";\n\t", $rules).";\n}";
  188. }
  189. // Parse children recursively
  190. foreach( $this->children as $child ) {
  191. $minify || $css .= "\n\n";
  192. $css .= $child->parse();
  193. }
  194. return trim($css);
  195. }
  196. }
  197. class CssParser {
  198. static $default_config = array(
  199. 'replace_shorthands' => true,
  200. 'sort_rules' => true,
  201. 'minify' => true,
  202. 'compress_measurements' => true,
  203. 'compress_colors' => true
  204. );
  205. static function minify($css, $config=array()) {
  206. CssNode::$config = array_merge(self::$default_config, $config);
  207. $node = new CssNode('', $css);
  208. return $node->parse();
  209. }
  210. }
  211. ?>