initial commit
[namibia] / public / min / lib / Minify / CSS / Compressor.php
1 <?php
2 /**
3  * Class Minify_CSS_Compressor 
4  * @package Minify
5  */
6
7 /**
8  * Compress CSS
9  *
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.
16  * 
17  * @package Minify
18  * @author Stephen Clay <steve@mrclay.org>
19  * @author http://code.google.com/u/1stvamp/ (Issue 64 patch)
20  */
21 class Minify_CSS_Compressor {
22
23     /**
24      * Minify a CSS string
25      * 
26      * @param string $css
27      * 
28      * @param array $options (currently ignored)
29      * 
30      * @return string
31      */
32     public static function process($css, $options = array())
33     {
34         $obj = new Minify_CSS_Compressor($options);
35         return $obj->_process($css);
36     }
37     
38     /**
39      * @var array
40      */
41     protected $_options = null;
42     
43     /**
44      * Are we "in" a hack? I.e. are some browsers targetted until the next comment?
45      *
46      * @var bool
47      */
48     protected $_inHack = false;
49     
50     
51     /**
52      * Constructor
53      * 
54      * @param array $options (currently ignored)
55      */
56     private function __construct($options) {
57         $this->_options = $options;
58     }
59     
60     /**
61      * Minify a CSS string
62      * 
63      * @param string $css
64      * 
65      * @return string
66      */
67     protected function _process($css)
68     {
69         $css = str_replace("\r\n", "\n", $css);
70         
71         // preserve empty comment after '>'
72         // http://www.webdevout.net/css-hacks#in_css-selectors
73         $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);
74         
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);
79         
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);
83
84         // remove ws around { } and last semicolon in declaration block
85         $css = preg_replace('/\\s*{\\s*/', '{', $css);
86         $css = preg_replace('/;?\\s*}\\s*/', '}', $css);
87         
88         // remove ws surrounding semicolons
89         $css = preg_replace('/\\s*;\\s*/', ';', $css);
90         
91         // remove ws around urls
92         $css = preg_replace('/
93                 url\\(      # url(
94                 \\s*
95                 ([^\\)]+?)  # 1 = the URL (really just a bunch of non right parenthesis)
96                 \\s*
97                 \\)         # )
98             /x', 'url($1)', $css);
99         
100         // remove ws between rules and colons
101         $css = preg_replace('/
102                 \\s*
103                 ([{;])              # 1 = beginning of block or rule separator 
104                 \\s*
105                 ([\\*_]?[\\w\\-]+)  # 2 = property (and maybe IE filter)
106                 \\s*
107                 :
108                 \\s*
109                 (\\b|[#\'"-])        # 3 = first character of a value
110             /x', '$1$2:$3', $css);
111         
112         // remove ws in selectors
113         $css = preg_replace_callback('/
114                 (?:              # non-capture
115                     \\s*
116                     [^~>+,\\s]+  # selector part
117                     \\s*
118                     [,>+~]       # combinators
119                 )+
120                 \\s*
121                 [^~>+,\\s]+      # selector part
122                 {                # open declaration block
123             /x'
124             ,array($this, '_selectorsCB'), $css);
125         
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);
129         
130         // remove spaces between font families
131         $css = preg_replace_callback('/font-family:([^;}]+)([;}])/'
132             ,array($this, '_fontFamilyCB'), $css);
133         
134         $css = preg_replace('/@import\\s+url/', '@import url', $css);
135         
136         // replace any ws involving newlines with a single newline
137         $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);
138         
139         // separate common descendent selectors w/ newlines (to limit line lengths)
140         $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);
141         
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
145             \\s+
146             /x'
147             ,"$1\n", $css);
148         
149         // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/
150         $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css);
151             
152         return trim($css);
153     }
154     
155     /**
156      * Replace what looks like a set of selectors  
157      *
158      * @param array $m regex matches
159      * 
160      * @return string
161      */
162     protected function _selectorsCB($m)
163     {
164         // remove ws around the combinators
165         return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
166     }
167     
168     /**
169      * Process a comment and return a replacement
170      * 
171      * @param array $m regex matches
172      * 
173      * @return string
174      */
175     protected function _commentCB($m)
176     {
177         $hasSurroundingWs = (trim($m[0]) !== $m[1]);
178         $m = $m[1]; 
179         // $m is the comment content w/o the surrounding tokens, 
180         // but the return value will replace the entire comment.
181         if ($m === 'keep') {
182             return '/**/';
183         }
184         if ($m === '" "') {
185             // component of http://tantek.com/CSS/Examples/midpass.html
186             return '/*" "*/';
187         }
188         if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) {
189             // component of http://tantek.com/CSS/Examples/midpass.html
190             return '/*";}}/* */';
191         }
192         if ($this->_inHack) {
193             // inversion: feeding only to one browser
194             if (preg_match('@
195                     ^/               # comment started like /*/
196                     \\s*
197                     (\\S[\\s\\S]+?)  # has at least some non-ws content
198                     \\s*
199                     /\\*             # ends like /*/ or /**/
200                 @x', $m, $n)) {
201                 // end hack mode after this comment, but preserve the hack and comment content
202                 $this->_inHack = false;
203                 return "/*/{$n[1]}/**/";
204             }
205         }
206         if (substr($m, -1) === '\\') { // comment ends like \*/
207             // begin hack mode and preserve hack
208             $this->_inHack = true;
209             return '/*\\*/';
210         }
211         if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */
212             // begin hack mode and preserve hack
213             $this->_inHack = true;
214             return '/*/*/';
215         }
216         if ($this->_inHack) {
217             // a regular comment ends hack mode but should be preserved
218             $this->_inHack = false;
219             return '/**/';
220         }
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
224             ? ' '
225             : '';
226     }
227     
228     /**
229      * Process a font-family listing and return a replacement
230      * 
231      * @param array $m regex matches
232      * 
233      * @return string   
234      */
235     protected function _fontFamilyCB($m)
236     {
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);
244             }
245             $out .= $piece;
246         }
247         return $out . $m[2];
248     }
249 }