001/*
002 * YUI Compressor
003 * http://developer.yahoo.com/yui/compressor/
004 * Author: Julien Lecomte -  http://www.julienlecomte.net/
005 * Author: Isaac Schlueter - http://foohack.com/
006 * Author: Stoyan Stefanov - http://phpied.com/
007 * Contributor: Dan Beam - http://danbeam.org/
008 * Copyright (c) 2013 Yahoo! Inc.  All rights reserved.
009 * The copyrights embodied in the content of this file are licensed
010 * by Yahoo! Inc. under the BSD (revised) open source license.
011 */
012package com.yahoo.platform.yui.compressor;
013
014import java.io.IOException;
015import java.io.Reader;
016import java.io.Writer;
017import java.util.ArrayList;
018import java.util.regex.Matcher;
019import java.util.regex.Pattern;
020
021public class CssCompressor {
022
023    private StringBuffer srcsb = new StringBuffer();
024
025    public CssCompressor(Reader in) throws IOException {
026        // Read the stream...
027        int c;
028        while ((c = in.read()) != -1) {
029            srcsb.append((char) c);
030        }
031    }
032
033    // Leave data urls alone to increase parse performance.
034    protected String extractDataUrls(String css, ArrayList preservedTokens) {
035
036        int maxIndex = css.length() - 1;
037        int appendIndex = 0;
038
039        StringBuffer sb = new StringBuffer();
040
041        Pattern p = Pattern.compile("(?i)url\\(\\s*([\"']?)data\\:");
042        Matcher m = p.matcher(css);
043
044        /*
045         * Since we need to account for non-base64 data urls, we need to handle
046         * ' and ) being part of the data string. Hence switching to indexOf,
047         * to determine whether or not we have matching string terminators and
048         * handling sb appends directly, instead of using matcher.append* methods.
049         */
050
051        while (m.find()) {
052
053            int startIndex = m.start() + 4;      // "url(".length()
054            String terminator = m.group(1);     // ', " or empty (not quoted)
055
056            if (terminator.length() == 0) {
057                terminator = ")";
058            }
059
060            boolean foundTerminator = false;
061
062            int endIndex = m.end() - 1;
063            while(foundTerminator == false && endIndex+1 <= maxIndex) {
064                endIndex = css.indexOf(terminator, endIndex+1);
065
066                if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) {
067                    foundTerminator = true;
068                    if (!")".equals(terminator)) {
069                        endIndex = css.indexOf(")", endIndex);
070                    }
071                }
072            }
073
074            // Enough searching, start moving stuff over to the buffer
075            sb.append(css.substring(appendIndex, m.start()));
076
077            if (foundTerminator) {
078                String token = css.substring(startIndex, endIndex);
079                token = token.replaceAll("\\s+", "");
080                preservedTokens.add(token);
081
082                String preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)";
083                sb.append(preserver);
084
085                appendIndex = endIndex + 1;
086            } else {
087                // No end terminator found, re-add the whole match. Should we throw/warn here?
088                sb.append(css.substring(m.start(), m.end()));
089                appendIndex = m.end();
090            }
091        }
092
093        sb.append(css.substring(appendIndex));
094
095        return sb.toString();
096    }
097
098    private String preserveOldIESpecificMatrixDefinition(String css, ArrayList preservedTokens) {
099        StringBuffer sb = new StringBuffer();
100        Pattern p = Pattern.compile("\\s*filter:\\s*progid:DXImageTransform.Microsoft.Matrix\\(([^\\)]+)\\);");
101        Matcher m = p.matcher(css);
102        while (m.find()) {
103            String token = m.group(1);
104            preservedTokens.add(token);
105            String preserver = "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___";
106            m.appendReplacement(sb, "filter:progid:DXImageTransform.Microsoft.Matrix(" + preserver + ");");
107        }
108        m.appendTail(sb);
109        return sb.toString();
110    }
111
112    public void compress(Writer out, int linebreakpos)
113            throws IOException {
114
115        Pattern p;
116        Matcher m;
117        String css = srcsb.toString();
118
119        int startIndex = 0;
120        int endIndex = 0;
121        int i = 0;
122        int max = 0;
123        ArrayList preservedTokens = new ArrayList(0);
124        ArrayList comments = new ArrayList(0);
125        String token;
126        int totallen = css.length();
127        String placeholder;
128
129        css = this.extractDataUrls(css, preservedTokens);
130
131        StringBuffer sb = new StringBuffer(css);
132
133        // collect all comment blocks...
134        while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) {
135            endIndex = sb.indexOf("*/", startIndex + 2);
136            if (endIndex < 0) {
137                endIndex = totallen;
138            }
139
140            token = sb.substring(startIndex + 2, endIndex);
141            comments.add(token);
142            sb.replace(startIndex + 2, endIndex, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1) + "___");
143            startIndex += 2;
144        }
145        css = sb.toString();
146
147        // preserve strings so their content doesn't get accidentally minified
148        sb = new StringBuffer();
149        p = Pattern.compile("(\"([^\\\\\"]|\\\\.|\\\\)*\")|(\'([^\\\\\']|\\\\.|\\\\)*\')");
150        m = p.matcher(css);
151        while (m.find()) {
152            token = m.group();
153            char quote = token.charAt(0);
154            token = token.substring(1, token.length() - 1);
155
156            // maybe the string contains a comment-like substring?
157            // one, maybe more? put'em back then
158            if (token.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) {
159                for (i = 0, max = comments.size(); i < max; i += 1) {
160                    token = token.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments.get(i).toString());
161                }
162            }
163
164            // minify alpha opacity in filter strings
165            token = token.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity=");
166
167            preservedTokens.add(token);
168            String preserver = quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___" + quote;
169            m.appendReplacement(sb, preserver);
170        }
171        m.appendTail(sb);
172        css = sb.toString();
173
174
175        // strings are safe, now wrestle the comments
176        for (i = 0, max = comments.size(); i < max; i += 1) {
177
178            token = comments.get(i).toString();
179            placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___";
180
181            // ! in the first position of the comment means preserve
182            // so push to the preserved tokens while stripping the !
183            if (token.startsWith("!")) {
184                preservedTokens.add(token);
185                css = css.replace(placeholder,  "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
186                continue;
187            }
188
189            // \ in the last position looks like hack for Mac/IE5
190            // shorten that to /*\*/ and the next one to /**/
191            if (token.endsWith("\\")) {
192                preservedTokens.add("\\");
193                css = css.replace(placeholder,  "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
194                i = i + 1; // attn: advancing the loop
195                preservedTokens.add("");
196                css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___",  "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
197                continue;
198            }
199
200            // keep empty comments after child selectors (IE7 hack)
201            // e.g. html >/**/ body
202            if (token.length() == 0) {
203                startIndex = css.indexOf(placeholder);
204                if (startIndex > 2) {
205                    if (css.charAt(startIndex - 3) == '>') {
206                        preservedTokens.add("");
207                        css = css.replace(placeholder,  "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
208                    }
209                }
210            }
211
212            // in all other cases kill the comment
213            css = css.replace("/*" + placeholder + "*/", "");
214        }
215
216
217        // Normalize all whitespace strings to single spaces. Easier to work with that way.
218        css = css.replaceAll("\\s+", " ");
219
220        css = this.preserveOldIESpecificMatrixDefinition(css, preservedTokens);
221
222        // Remove the spaces before the things that should not have spaces before them.
223        // But, be careful not to turn "p :link {...}" into "p:link{...}"
224        // Swap out any pseudo-class colons with the token, and then swap back.
225        sb = new StringBuffer();
226        p = Pattern.compile("(^|\\})(([^\\{:])+:)+([^\\{]*\\{)");
227        m = p.matcher(css);
228        while (m.find()) {
229            String s = m.group();
230            s = s.replaceAll(":", "___YUICSSMIN_PSEUDOCLASSCOLON___");
231            s = s.replaceAll( "\\\\", "\\\\\\\\" ).replaceAll( "\\$", "\\\\\\$" );
232            m.appendReplacement(sb, s);
233        }
234        m.appendTail(sb);
235        css = sb.toString();
236        // Remove spaces before the things that should not have spaces before them.
237        css = css.replaceAll("\\s+([!{};:>+\\(\\)\\],])", "$1");
238        // Restore spaces for !important
239        css = css.replaceAll("!important", " !important");
240        // bring back the colon
241        css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":");
242
243        // retain space for special IE6 cases
244        sb = new StringBuffer();
245        p = Pattern.compile("(?i):first\\-(line|letter)(\\{|,)");
246        m = p.matcher(css);
247        while (m.find()) {
248            m.appendReplacement(sb, ":first-" + m.group(1).toLowerCase() + " " + m.group(2));
249        }
250        m.appendTail(sb);
251        css = sb.toString();
252
253        // no space after the end of a preserved comment
254        css = css.replaceAll("\\*/ ", "*/");
255
256        // If there are multiple @charset directives, push them to the top of the file.
257        sb = new StringBuffer();
258        p = Pattern.compile("(?i)^(.*)(@charset)( \"[^\"]*\";)");
259        m = p.matcher(css);
260        while (m.find()) {
261            m.appendReplacement(sb, m.group(2).toLowerCase() + m.group(3) + m.group(1));
262        }
263        m.appendTail(sb);
264        css = sb.toString();
265
266        // When all @charset are at the top, remove the second and after (as they are completely ignored).
267        sb = new StringBuffer();
268        p = Pattern.compile("(?i)^((\\s*)(@charset)( [^;]+;\\s*))+");
269        m = p.matcher(css);
270        while (m.find()) {
271            m.appendReplacement(sb, m.group(2) + m.group(3).toLowerCase() + m.group(4));
272        }
273        m.appendTail(sb);
274        css = sb.toString();
275
276        // lowercase some popular @directives (@charset is done right above)
277        sb = new StringBuffer();
278        p = Pattern.compile("(?i)@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)");
279        m = p.matcher(css);
280        while (m.find()) {
281            m.appendReplacement(sb, '@' + m.group(1).toLowerCase());
282        }
283        m.appendTail(sb);
284        css = sb.toString();
285
286        // lowercase some more common pseudo-elements
287        sb = new StringBuffer();
288        p = Pattern.compile("(?i):(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)");
289        m = p.matcher(css);
290        while (m.find()) {
291            m.appendReplacement(sb, ':' + m.group(1).toLowerCase());
292        }
293        m.appendTail(sb);
294        css = sb.toString();
295
296        // lowercase some more common functions
297        sb = new StringBuffer();
298        p = Pattern.compile("(?i):(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\\(");
299        m = p.matcher(css);
300        while (m.find()) {
301            m.appendReplacement(sb, ':' + m.group(1).toLowerCase() + '(');
302        }
303        m.appendTail(sb);
304        css = sb.toString();
305
306        // lower case some common function that can be values
307        // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us right after this
308        sb = new StringBuffer();
309        p = Pattern.compile("(?i)([:,\\( ]\\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)");
310        m = p.matcher(css);
311        while (m.find()) {
312            m.appendReplacement(sb, m.group(1) + m.group(2).toLowerCase());
313        }
314        m.appendTail(sb);
315        css = sb.toString();
316
317        // Put the space back in some cases, to support stuff like
318        // @media screen and (-webkit-min-device-pixel-ratio:0){
319        css = css.replaceAll("(?i)\\band\\(", "and (");
320
321        // Remove the spaces after the things that should not have spaces after them.
322        css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1");
323
324        // remove unnecessary semicolons
325        css = css.replaceAll(";+}", "}");
326
327        // Replace 0(px,em,%) with 0.
328        css = css.replaceAll("(?i)(^|[^0-9])(?:0?\\.)?0(?:px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz)", "$10");
329
330        // Replace 0 0 0 0; with 0.
331        css = css.replaceAll(":0 0 0 0(;|})", ":0$1");
332        css = css.replaceAll(":0 0 0(;|})", ":0$1");
333        css = css.replaceAll(":0 0(;|})", ":0$1");
334
335
336        // Replace background-position:0; with background-position:0 0;
337        // same for transform-origin
338        sb = new StringBuffer();
339        p = Pattern.compile("(?i)(background-position|webkit-mask-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|})");
340        m = p.matcher(css);
341        while (m.find()) {
342            m.appendReplacement(sb, m.group(1).toLowerCase() + ":0 0" + m.group(2));
343        }
344        m.appendTail(sb);
345        css = sb.toString();
346
347        // Replace 0.6 to .6, but only when preceded by : or a white-space
348        css = css.replaceAll("(:|\\s)0+\\.(\\d+)", "$1.$2");
349
350        // Shorten colors from rgb(51,102,153) to #336699
351        // This makes it more likely that it'll get further compressed in the next step.
352        p = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)");
353        m = p.matcher(css);
354        sb = new StringBuffer();
355        while (m.find()) {
356            String[] rgbcolors = m.group(1).split(",");
357            StringBuffer hexcolor = new StringBuffer("#");
358            for (i = 0; i < rgbcolors.length; i++) {
359                int val = Integer.parseInt(rgbcolors[i]);
360                if (val < 16) {
361                    hexcolor.append('0');
362                }
363
364                // If someone passes an RGB value that's too big to express in two characters, round down.
365                // Probably should throw out a warning here, but generating valid CSS is a bigger concern.
366                if (val > 255) {
367                    val = 255;
368                }
369                hexcolor.append(Integer.toHexString(val));
370            }
371            m.appendReplacement(sb, hexcolor.toString());
372        }
373        m.appendTail(sb);
374        css = sb.toString();
375
376        // Shorten colors from #AABBCC to #ABC. Note that we want to make sure
377        // the color is not preceded by either ", " or =. Indeed, the property
378        //     filter: chroma(color="#FFFFFF");
379        // would become
380        //     filter: chroma(color="#FFF");
381        // which makes the filter break in IE.
382        // We also want to make sure we're only compressing #AABBCC patterns inside { }, not id selectors ( #FAABAC {} )
383        // We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD)
384        p = Pattern.compile("(\\=\\s*?[\"']?)?" + "#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])" + "(:?\\}|[^0-9a-fA-F{][^{]*?\\})");
385
386        m = p.matcher(css);
387        sb = new StringBuffer();
388        int index = 0;
389
390        while (m.find(index)) {
391
392            sb.append(css.substring(index, m.start()));
393
394            boolean isFilter = (m.group(1) != null && !"".equals(m.group(1)));
395
396            if (isFilter) {
397                // Restore, as is. Compression will break filters
398                sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7));
399            } else {
400                if( m.group(2).equalsIgnoreCase(m.group(3)) &&
401                        m.group(4).equalsIgnoreCase(m.group(5)) &&
402                        m.group(6).equalsIgnoreCase(m.group(7))) {
403
404                    // #AABBCC pattern
405                    sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase());
406
407                } else {
408
409                    // Non-compressible color, restore, but lower case.
410                    sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase());
411                }
412            }
413
414            index = m.end(7);
415        }
416
417        sb.append(css.substring(index));
418        css = sb.toString();
419
420        // Replace #f00 -> red
421        css = css.replaceAll("(:|\\s)(#f00)(;|})", "$1red$3");
422        // Replace other short color keywords
423        css = css.replaceAll("(:|\\s)(#000080)(;|})", "$1navy$3");
424        css = css.replaceAll("(:|\\s)(#808080)(;|})", "$1gray$3");
425        css = css.replaceAll("(:|\\s)(#808000)(;|})", "$1olive$3");
426        css = css.replaceAll("(:|\\s)(#800080)(;|})", "$1purple$3");
427        css = css.replaceAll("(:|\\s)(#c0c0c0)(;|})", "$1silver$3");
428        css = css.replaceAll("(:|\\s)(#008080)(;|})", "$1teal$3");
429        css = css.replaceAll("(:|\\s)(#ffa500)(;|})", "$1orange$3");
430        css = css.replaceAll("(:|\\s)(#800000)(;|})", "$1maroon$3");
431
432        // border: none -> border:0
433        sb = new StringBuffer();
434        p = Pattern.compile("(?i)(border|border-top|border-right|border-bottom|border-left|outline|background):none(;|})");
435        m = p.matcher(css);
436        while (m.find()) {
437            m.appendReplacement(sb, m.group(1).toLowerCase() + ":0" + m.group(2));
438        }
439        m.appendTail(sb);
440        css = sb.toString();
441
442        // shorter opacity IE filter
443        css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity=");
444
445        // Find a fraction that is used for Opera's -o-device-pixel-ratio query
446        // Add token to add the "\" back in later
447        css = css.replaceAll("\\(([\\-A-Za-z]+):([0-9]+)\\/([0-9]+)\\)", "($1:$2___YUI_QUERY_FRACTION___$3)");
448
449        // Remove empty rules.
450        css = css.replaceAll("[^\\}\\{/;]+\\{\\}", "");
451
452        // Add "\" back to fix Opera -o-device-pixel-ratio query
453        css = css.replaceAll("___YUI_QUERY_FRACTION___", "/");
454
455        // TODO: Should this be after we re-insert tokens. These could alter the break points. However then
456        // we'd need to make sure we don't break in the middle of a string etc.
457        if (linebreakpos >= 0) {
458            // Some source control tools don't like it when files containing lines longer
459            // than, say 8000 characters, are checked in. The linebreak option is used in
460            // that case to split long lines after a specific column.
461            i = 0;
462            int linestartpos = 0;
463            sb = new StringBuffer(css);
464            while (i < sb.length()) {
465                char c = sb.charAt(i++);
466                if (c == '}' && i - linestartpos > linebreakpos) {
467                    sb.insert(i, '\n');
468                    linestartpos = i;
469                }
470            }
471
472            css = sb.toString();
473        }
474
475        // Replace multiple semi-colons in a row by a single one
476        // See SF bug #1980989
477        css = css.replaceAll(";;+", ";");
478
479        // restore preserved comments and strings
480        for(i = preservedTokens.size() - 1; i >= 0 ; i--) {
481            css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens.get(i).toString());
482        }
483
484        // Trim the final string (for any leading or trailing white spaces)
485        css = css.trim();
486
487        // Write the output...
488        out.write(css);
489    }
490}