3 * Class Minify_CSS_Compressor
\r
10 * This is a heavy regex-based removal of whitespace, unnecessary
\r
11 * comments and tokens, and some CSS value minimization, where practical.
\r
12 * Many steps have been taken to avoid breaking comment-based hacks,
\r
13 * including the ie5/mac filter (and its inversion), but expect tricky
\r
14 * hacks involving comment tokens in 'content' value strings to break
\r
15 * minimization badly. A test suite is available.
\r
18 * @author Stephen Clay <steve@mrclay.org>
\r
19 * @author http://code.google.com/u/1stvamp/ (Issue 64 patch)
\r
21 class Minify_CSS_Compressor {
\r
24 * Minify a CSS string
\r
26 * @param string $css
\r
28 * @param array $options (currently ignored)
\r
32 public static function process($css, $options = array())
\r
34 $obj = new Minify_CSS_Compressor($options);
\r
35 return $obj->_process($css);
\r
39 * @var array options
\r
41 protected $_options = null;
\r
44 * @var bool Are we "in" a hack?
\r
46 * I.e. are some browsers targetted until the next comment?
\r
48 protected $_inHack = false;
\r
54 * @param array $options (currently ignored)
\r
58 private function __construct($options) {
\r
59 $this->_options = $options;
\r
63 * Minify a CSS string
\r
65 * @param string $css
\r
69 protected function _process($css)
\r
71 $css = str_replace("\r\n", "\n", $css);
\r
73 // preserve empty comment after '>'
\r
74 // http://www.webdevout.net/css-hacks#in_css-selectors
\r
75 $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);
\r
77 // preserve empty comment between property and value
\r
78 // http://css-discuss.incutio.com/?page=BoxModelHack
\r
79 $css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);
\r
80 $css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);
\r
82 // apply callback to all valid comments (and strip out surrounding ws
\r
83 $css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@'
\r
84 ,array($this, '_commentCB'), $css);
\r
86 // remove ws around { } and last semicolon in declaration block
\r
87 $css = preg_replace('/\\s*{\\s*/', '{', $css);
\r
88 $css = preg_replace('/;?\\s*}\\s*/', '}', $css);
\r
90 // remove ws surrounding semicolons
\r
91 $css = preg_replace('/\\s*;\\s*/', ';', $css);
\r
93 // remove ws around urls
\r
94 $css = preg_replace('/
\r
97 ([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis)
\r
100 /x', 'url($1)', $css);
\r
102 // remove ws between rules and colons
\r
103 $css = preg_replace('/
\r
105 ([{;]) # 1 = beginning of block or rule separator
\r
107 ([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter)
\r
111 (\\b|[#\'"]) # 3 = first character of a value
\r
112 /x', '$1$2:$3', $css);
\r
114 // remove ws in selectors
\r
115 $css = preg_replace_callback('/
\r
118 [^~>+,\\s]+ # selector part
\r
120 [,>+~] # combinators
\r
123 [^~>+,\\s]+ # selector part
\r
124 { # open declaration block
\r
126 ,array($this, '_selectorsCB'), $css);
\r
128 // minimize hex colors
\r
129 $css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i'
\r
130 , '$1#$2$3$4$5', $css);
\r
132 // remove spaces between font families
\r
133 $css = preg_replace_callback('/font-family:([^;}]+)([;}])/'
\r
134 ,array($this, '_fontFamilyCB'), $css);
\r
136 $css = preg_replace('/@import\\s+url/', '@import url', $css);
\r
138 // replace any ws involving newlines with a single newline
\r
139 $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);
\r
141 // separate common descendent selectors w/ newlines (to limit line lengths)
\r
142 $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);
\r
144 // Use newline after 1st numeric value (to limit line lengths).
\r
145 $css = preg_replace('/
\r
146 ((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value
\r
151 // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/
\r
152 $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css);
\r
158 * Replace what looks like a set of selectors
\r
160 * @param array $m regex matches
\r
164 protected function _selectorsCB($m)
\r
166 // remove ws around the combinators
\r
167 return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
\r
171 * Process a comment and return a replacement
\r
173 * @param array $m regex matches
\r
177 protected function _commentCB($m)
\r
179 $hasSurroundingWs = (trim($m[0]) !== $m[1]);
\r
181 // $m is the comment content w/o the surrounding tokens,
\r
182 // but the return value will replace the entire comment.
\r
183 if ($m === 'keep') {
\r
186 if ($m === '" "') {
\r
187 // component of http://tantek.com/CSS/Examples/midpass.html
\r
190 if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) {
\r
191 // component of http://tantek.com/CSS/Examples/midpass.html
\r
192 return '/*";}}/* */';
\r
194 if ($this->_inHack) {
\r
195 // inversion: feeding only to one browser
\r
197 ^/ # comment started like /*/
\r
199 (\\S[\\s\\S]+?) # has at least some non-ws content
\r
201 /\\* # ends like /*/ or /**/
\r
203 // end hack mode after this comment, but preserve the hack and comment content
\r
204 $this->_inHack = false;
\r
205 return "/*/{$n[1]}/**/";
\r
208 if (substr($m, -1) === '\\') { // comment ends like \*/
\r
209 // begin hack mode and preserve hack
\r
210 $this->_inHack = true;
\r
213 if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */
\r
214 // begin hack mode and preserve hack
\r
215 $this->_inHack = true;
\r
218 if ($this->_inHack) {
\r
219 // a regular comment ends hack mode but should be preserved
\r
220 $this->_inHack = false;
\r
223 // Issue 107: if there's any surrounding whitespace, it may be important, so
\r
224 // replace the comment with a single space
\r
225 return $hasSurroundingWs // remove all other comments
\r
231 * Process a font-family listing and return a replacement
\r
233 * @param array $m regex matches
\r
237 protected function _fontFamilyCB($m)
\r
239 $m[1] = preg_replace('/
\r
242 "[^"]+" # 1 = family in double qutoes
\r
243 |\'[^\']+\' # or 1 = family in single quotes
\r
244 |[\\w\\-]+ # or 1 = unquoted family
\r
248 return 'font-family:' . $m[1] . $m[2];
\r