3 * Class Minify_CSS_Compressor
10 * This is a heavy regex-based removal of whitespace, unnecessary
11 * comments and tokens, and some CSS value minimization, where practical.
12 * Many steps have been taken to avoid breaking comment-based hacks,
13 * including the ie5/mac filter (and its inversion), but expect tricky
14 * hacks involving comment tokens in 'content' value strings to break
15 * minimization badly. A test suite is available.
18 * @author Stephen Clay <steve@mrclay.org>
19 * @author http://code.google.com/u/1stvamp/ (Issue 64 patch)
21 class Minify_CSS_Compressor {
28 * @param array $options (currently ignored)
32 public static function process($css, $options = array())
34 $obj = new Minify_CSS_Compressor($options);
35 return $obj->_process($css);
41 protected $_options = null;
44 * Are we "in" a hack? I.e. are some browsers targetted until the next comment?
48 protected $_inHack = false;
54 * @param array $options (currently ignored)
56 private function __construct($options) {
57 $this->_options = $options;
67 protected function _process($css)
69 $css = str_replace("\r\n", "\n", $css);
71 // preserve empty comment after '>'
72 // http://www.webdevout.net/css-hacks#in_css-selectors
73 $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);
75 // preserve empty comment between property and value
76 // http://css-discuss.incutio.com/?page=BoxModelHack
77 $css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);
78 $css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);
80 // apply callback to all valid comments (and strip out surrounding ws
81 $css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@'
82 ,array($this, '_commentCB'), $css);
84 // remove ws around { } and last semicolon in declaration block
85 $css = preg_replace('/\\s*{\\s*/', '{', $css);
86 $css = preg_replace('/;?\\s*}\\s*/', '}', $css);
88 // remove ws surrounding semicolons
89 $css = preg_replace('/\\s*;\\s*/', ';', $css);
91 // remove ws around urls
92 $css = preg_replace('/
95 ([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis)
98 /x', 'url($1)', $css);
100 // remove ws between rules and colons
101 $css = preg_replace('/
103 ([{;]) # 1 = beginning of block or rule separator
105 ([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter)
109 (\\b|[#\'"-]) # 3 = first character of a value
110 /x', '$1$2:$3', $css);
112 // remove ws in selectors
113 $css = preg_replace_callback('/
116 [^~>+,\\s]+ # selector part
121 [^~>+,\\s]+ # selector part
122 { # open declaration block
124 ,array($this, '_selectorsCB'), $css);
126 // minimize hex colors
127 $css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i'
128 , '$1#$2$3$4$5', $css);
130 // remove spaces between font families
131 $css = preg_replace_callback('/font-family:([^;}]+)([;}])/'
132 ,array($this, '_fontFamilyCB'), $css);
134 $css = preg_replace('/@import\\s+url/', '@import url', $css);
136 // replace any ws involving newlines with a single newline
137 $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);
139 // separate common descendent selectors w/ newlines (to limit line lengths)
140 $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);
142 // Use newline after 1st numeric value (to limit line lengths).
143 $css = preg_replace('/
144 ((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value
149 // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/
150 $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css);
156 * Replace what looks like a set of selectors
158 * @param array $m regex matches
162 protected function _selectorsCB($m)
164 // remove ws around the combinators
165 return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
169 * Process a comment and return a replacement
171 * @param array $m regex matches
175 protected function _commentCB($m)
177 $hasSurroundingWs = (trim($m[0]) !== $m[1]);
179 // $m is the comment content w/o the surrounding tokens,
180 // but the return value will replace the entire comment.
185 // component of http://tantek.com/CSS/Examples/midpass.html
188 if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) {
189 // component of http://tantek.com/CSS/Examples/midpass.html
190 return '/*";}}/* */';
192 if ($this->_inHack) {
193 // inversion: feeding only to one browser
195 ^/ # comment started like /*/
197 (\\S[\\s\\S]+?) # has at least some non-ws content
199 /\\* # ends like /*/ or /**/
201 // end hack mode after this comment, but preserve the hack and comment content
202 $this->_inHack = false;
203 return "/*/{$n[1]}/**/";
206 if (substr($m, -1) === '\\') { // comment ends like \*/
207 // begin hack mode and preserve hack
208 $this->_inHack = true;
211 if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */
212 // begin hack mode and preserve hack
213 $this->_inHack = true;
216 if ($this->_inHack) {
217 // a regular comment ends hack mode but should be preserved
218 $this->_inHack = false;
221 // Issue 107: if there's any surrounding whitespace, it may be important, so
222 // replace the comment with a single space
223 return $hasSurroundingWs // remove all other comments
229 * Process a font-family listing and return a replacement
231 * @param array $m regex matches
235 protected function _fontFamilyCB($m)
237 // Issue 210: must not eliminate WS between words in unquoted families
238 $pieces = preg_split('/(\'[^\']+\'|"[^"]+")/', $m[1], null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
239 $out = 'font-family:';
240 while (null !== ($piece = array_shift($pieces))) {
241 if ($piece[0] !== '"' && $piece[0] !== "'") {
242 $piece = preg_replace('/\\s+/', ' ', $piece);
243 $piece = preg_replace('/\\s?,\\s?/', ',', $piece);