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.regex.Pattern;
018import java.util.regex.Matcher;
019import java.util.ArrayList;
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    /**
034     * @param css - full css string
035     * @param preservedToken - token to preserve
036     * @param tokenRegex - regex to find token
037     * @param removeWhiteSpace - remove any white space in the token
038     * @param preservedTokens - array of token values
039     * @return
040     */
041    protected String preserveToken(String css, String preservedToken,
042            String tokenRegex, boolean removeWhiteSpace, ArrayList preservedTokens) {
043
044        int maxIndex = css.length() - 1;
045        int appendIndex = 0;
046
047        StringBuffer sb = new StringBuffer();
048
049        Pattern p = Pattern.compile(tokenRegex);
050        Matcher m = p.matcher(css);
051
052        while (m.find()) {
053            int startIndex = m.start() + (preservedToken.length() + 1);
054            String terminator = m.group(1);
055
056            // skip this, if CSS was already copied to "sb" upto this position
057            if (m.start() < appendIndex) {
058                continue;
059            }
060
061            if (terminator.length() == 0) {
062                terminator = ")";
063            }
064
065            boolean foundTerminator = false;
066
067            int endIndex = m.end() - 1;
068            while(foundTerminator == false && endIndex+1 <= maxIndex) {
069                endIndex = css.indexOf(terminator, endIndex+1);
070
071                if (endIndex <= 0) {
072                    break;
073                } else if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) {
074                    foundTerminator = true;
075                    if (!")".equals(terminator)) {
076                        endIndex = css.indexOf(")", endIndex);
077                    }
078                }
079            }
080
081            // Enough searching, start moving stuff over to the buffer
082            sb.append(css.substring(appendIndex, m.start()));
083
084            if (foundTerminator) {
085                String token = css.substring(startIndex, endIndex);
086                if(removeWhiteSpace)
087                    token = token.replaceAll("\\s+", "");
088                preservedTokens.add(token);
089
090                String preserver = preservedToken + "(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)";
091                sb.append(preserver);
092
093                appendIndex = endIndex + 1;
094            } else {
095                // No end terminator found, re-add the whole match. Should we throw/warn here?
096                sb.append(css.substring(m.start(), m.end()));
097                appendIndex = m.end();
098            }
099        }
100
101        sb.append(css.substring(appendIndex));
102
103        return sb.toString();
104    }
105
106    public void compress(Writer out, int linebreakpos)
107            throws IOException {
108
109        Pattern p;
110        Matcher m;
111        String css = srcsb.toString();
112
113        int startIndex = 0;
114        int endIndex = 0;
115        int i = 0;
116        int max = 0;
117        ArrayList preservedTokens = new ArrayList(0);
118        ArrayList comments = new ArrayList(0);
119        String token;
120        int totallen = css.length();
121        String placeholder;
122
123
124        StringBuffer sb = new StringBuffer(css);
125
126        // collect all comment blocks...
127        while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) {
128            endIndex = sb.indexOf("*/", startIndex + 2);
129            if (endIndex < 0) {
130                endIndex = totallen;
131            }
132
133            token = sb.substring(startIndex + 2, endIndex);
134            comments.add(token);
135            sb.replace(startIndex + 2, endIndex, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1) + "___");
136            startIndex += 2;
137        }
138        css = sb.toString();
139
140
141        css = this.preserveToken(css, "url", "(?i)url\\(\\s*([\"']?)data\\:\\s*image/svg\\+xml", false, preservedTokens);
142        css = this.preserveToken(css, "url", "(?i)url\\(\\s*([\"']?)data\\:\\s*(?!(image/svg\\+xml))", true, preservedTokens);
143        css = this.preserveToken(css, "calc",  "(?i)calc\\(\\s*([\"']?)", false, preservedTokens);
144        css = this.preserveToken(css, "progid:DXImageTransform.Microsoft.Matrix",  "(?i)progid:DXImageTransform.Microsoft.Matrix\\s*([\"']?)", false, preservedTokens);
145
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        // preserve \9 IE hack
217        final String backslash9 = "\\9"; 
218        while (css.indexOf(backslash9) > -1) {
219            preservedTokens.add(backslash9);
220            css = css.replace(backslash9,  "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
221        }
222        
223        // Normalize all whitespace strings to single spaces. Easier to work with that way.
224        css = css.replaceAll("\\s+", " ");
225
226        // Remove the spaces before the things that should not have spaces before them.
227        // But, be careful not to turn "p :link {...}" into "p:link{...}"
228        // Swap out any pseudo-class colons with the token, and then swap back.
229        sb = new StringBuffer();
230        p = Pattern.compile("(^|\\})((^|([^\\{:])+):)+([^\\{]*\\{)");
231        m = p.matcher(css);
232        while (m.find()) {
233            String s = m.group();
234            s = s.replaceAll(":", "___YUICSSMIN_PSEUDOCLASSCOLON___");
235            s = s.replaceAll( "\\\\", "\\\\\\\\" ).replaceAll( "\\$", "\\\\\\$" );
236            m.appendReplacement(sb, s);
237        }
238        m.appendTail(sb);
239        css = sb.toString();
240        // Remove spaces before the things that should not have spaces before them.
241        css = css.replaceAll("\\s+([!{};:>+\\(\\)\\],])", "$1");
242        // Restore spaces for !important
243        css = css.replaceAll("!important", " !important");
244        // bring back the colon
245        css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":");
246
247        // retain space for special IE6 cases
248        sb = new StringBuffer();
249        p = Pattern.compile("(?i):first\\-(line|letter)(\\{|,)");
250        m = p.matcher(css);
251        while (m.find()) {
252            m.appendReplacement(sb, ":first-" + m.group(1).toLowerCase() + " " + m.group(2));
253        }
254        m.appendTail(sb);
255        css = sb.toString();
256
257        // no space after the end of a preserved comment
258        css = css.replaceAll("\\*/ ", "*/");
259
260        // If there are multiple @charset directives, push them to the top of the file.
261        sb = new StringBuffer();
262        p = Pattern.compile("(?i)^(.*)(@charset)( \"[^\"]*\";)");
263        m = p.matcher(css);
264        while (m.find()) {
265            String s = m.group(1).replaceAll("\\\\", "\\\\\\\\").replaceAll("\\$", "\\\\\\$");
266            m.appendReplacement(sb, m.group(2).toLowerCase() + m.group(3) + s);
267        }
268        m.appendTail(sb);
269        css = sb.toString();
270
271        // When all @charset are at the top, remove the second and after (as they are completely ignored).
272        sb = new StringBuffer();
273        p = Pattern.compile("(?i)^((\\s*)(@charset)( [^;]+;\\s*))+");
274        m = p.matcher(css);
275        while (m.find()) {
276            m.appendReplacement(sb, m.group(2) + m.group(3).toLowerCase() + m.group(4));
277        }
278        m.appendTail(sb);
279        css = sb.toString();
280
281        // lowercase some popular @directives (@charset is done right above)
282        sb = new StringBuffer();
283        p = Pattern.compile("(?i)@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)");
284        m = p.matcher(css);
285        while (m.find()) {
286            m.appendReplacement(sb, '@' + m.group(1).toLowerCase());
287        }
288        m.appendTail(sb);
289        css = sb.toString();
290
291        // lowercase some more common pseudo-elements
292        sb = new StringBuffer();
293        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)");
294        m = p.matcher(css);
295        while (m.find()) {
296            m.appendReplacement(sb, ':' + m.group(1).toLowerCase());
297        }
298        m.appendTail(sb);
299        css = sb.toString();
300
301        // lowercase some more common functions
302        sb = new StringBuffer();
303        p = Pattern.compile("(?i):(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\\(");
304        m = p.matcher(css);
305        while (m.find()) {
306            m.appendReplacement(sb, ':' + m.group(1).toLowerCase() + '(');
307        }
308        m.appendTail(sb);
309        css = sb.toString();
310
311        // lower case some common function that can be values
312        // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us right after this
313        sb = new StringBuffer();
314        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)");
315        m = p.matcher(css);
316        while (m.find()) {
317            m.appendReplacement(sb, m.group(1) + m.group(2).toLowerCase());
318        }
319        m.appendTail(sb);
320        css = sb.toString();
321
322        // Put the space back in some cases, to support stuff like
323        // @media screen and (-webkit-min-device-pixel-ratio:0){
324        css = css.replaceAll("(?i)\\band\\(", "and (");
325
326        // Remove the spaces after the things that should not have spaces after them.
327        css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1");
328
329        // remove unnecessary semicolons
330        css = css.replaceAll(";+}", "}");
331
332        // Replace 0(px,em,%) with 0.
333        String oldCss;
334        p = Pattern.compile("(?i)(^|: ?)((?:[0-9a-z-.]+ )*?)?(?:0?\\.)?0(?:px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz)");
335        do {
336          oldCss = css;
337          m = p.matcher(css);
338          css = m.replaceAll("$1$20");
339        } while (!(css.equals(oldCss)));
340
341        // Replace 0(px,em,%) with 0 inside groups (e.g. -MOZ-RADIAL-GRADIENT(CENTER 45DEG, CIRCLE CLOSEST-SIDE, ORANGE 0%, RED 100%))
342        p = Pattern.compile("(?i)\\( ?((?:[0-9a-z-.]+[ ,])*)?(?:0?\\.)?0(?:px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz)");
343        do {
344          oldCss = css;
345          m = p.matcher(css);
346          css = m.replaceAll("($10");
347        } while (!(css.equals(oldCss)));
348
349        // Replace x.0(px,em,%) with x(px,em,%).
350        css = css.replaceAll("([0-9])\\.0(px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz| |;)", "$1$2");
351
352        // Replace 0 0 0 0; with 0.
353        css = css.replaceAll(":0 0 0 0(;|})", ":0$1");
354        css = css.replaceAll(":0 0 0(;|})", ":0$1");
355        css = css.replaceAll("(?<!flex):0 0(;|})", ":0$1");
356
357
358        // Replace background-position:0; with background-position:0 0;
359        // same for transform-origin
360        sb = new StringBuffer();
361        p = Pattern.compile("(?i)(background-position|webkit-mask-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|})");
362        m = p.matcher(css);
363        while (m.find()) {
364            m.appendReplacement(sb, m.group(1).toLowerCase() + ":0 0" + m.group(2));
365        }
366        m.appendTail(sb);
367        css = sb.toString();
368
369        // Replace 0.6 to .6, but only when preceded by : or a white-space
370        css = css.replaceAll("(:|\\s)0+\\.(\\d+)", "$1.$2");
371
372        // Shorten colors from rgb(51,102,153) to #336699
373        // This makes it more likely that it'll get further compressed in the next step.
374        p = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)");
375        m = p.matcher(css);
376        sb = new StringBuffer();
377        while (m.find()) {
378            String[] rgbcolors = m.group(1).split(",");
379            StringBuffer hexcolor = new StringBuffer("#");
380            for (i = 0; i < rgbcolors.length; i++) {
381                int val = Integer.parseInt(rgbcolors[i]);
382                if (val < 16) {
383                    hexcolor.append('0');
384                }
385
386                // If someone passes an RGB value that's too big to express in two characters, round down.
387                // Probably should throw out a warning here, but generating valid CSS is a bigger concern.
388                if (val > 255) {
389                    val = 255;
390                }
391                hexcolor.append(Integer.toHexString(val));
392            }
393            m.appendReplacement(sb, hexcolor.toString());
394        }
395        m.appendTail(sb);
396        css = sb.toString();
397
398        // Shorten colors from #AABBCC to #ABC. Note that we want to make sure
399        // the color is not preceded by either ", " or =. Indeed, the property
400        //     filter: chroma(color="#FFFFFF");
401        // would become
402        //     filter: chroma(color="#FFF");
403        // which makes the filter break in IE.
404        // We also want to make sure we're only compressing #AABBCC patterns inside { }, not id selectors ( #FAABAC {} )
405        // We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD)
406        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{][^{]*?\\})");
407
408        m = p.matcher(css);
409        sb = new StringBuffer();
410        int index = 0;
411
412        while (m.find(index)) {
413
414            sb.append(css.substring(index, m.start()));
415
416            boolean isFilter = (m.group(1) != null && !"".equals(m.group(1)));
417
418            if (isFilter) {
419                // Restore, as is. Compression will break filters
420                sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7));
421            } else {
422                if( m.group(2).equalsIgnoreCase(m.group(3)) &&
423                    m.group(4).equalsIgnoreCase(m.group(5)) &&
424                    m.group(6).equalsIgnoreCase(m.group(7))) {
425
426                    // #AABBCC pattern
427                    sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase());
428
429                } else {
430
431                    // Non-compressible color, restore, but lower case.
432                    sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase());
433                }
434            }
435
436            index = m.end(7);
437        }
438
439        sb.append(css.substring(index));
440        css = sb.toString();
441
442        // Replace #f00 -> red
443        css = css.replaceAll("(:|\\s)(#f00)(;|})", "$1red$3");
444        // Replace other short color keywords
445        css = css.replaceAll("(:|\\s)(#000080)(;|})", "$1navy$3");
446        css = css.replaceAll("(:|\\s)(#808080)(;|})", "$1gray$3");
447        css = css.replaceAll("(:|\\s)(#808000)(;|})", "$1olive$3");
448        css = css.replaceAll("(:|\\s)(#800080)(;|})", "$1purple$3");
449        css = css.replaceAll("(:|\\s)(#c0c0c0)(;|})", "$1silver$3");
450        css = css.replaceAll("(:|\\s)(#008080)(;|})", "$1teal$3");
451        css = css.replaceAll("(:|\\s)(#ffa500)(;|})", "$1orange$3");
452        css = css.replaceAll("(:|\\s)(#800000)(;|})", "$1maroon$3");
453
454        // border: none -> border:0
455        sb = new StringBuffer();
456        p = Pattern.compile("(?i)(border|border-top|border-right|border-bottom|border-left|outline|background):none(;|})");
457        m = p.matcher(css);
458        while (m.find()) {
459            m.appendReplacement(sb, m.group(1).toLowerCase() + ":0" + m.group(2));
460        }
461        m.appendTail(sb);
462        css = sb.toString();
463
464        // shorter opacity IE filter
465        css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity=");
466
467        // Find a fraction that is used for Opera's -o-device-pixel-ratio query
468        // Add token to add the "\" back in later
469        css = css.replaceAll("\\(([\\-A-Za-z]+):([0-9]+)\\/([0-9]+)\\)", "($1:$2___YUI_QUERY_FRACTION___$3)");
470
471        // Remove empty rules.
472        css = css.replaceAll("[^\\}\\{/;]+\\{\\}", "");
473
474        // Add "\" back to fix Opera -o-device-pixel-ratio query
475        css = css.replaceAll("___YUI_QUERY_FRACTION___", "/");
476
477        // TODO: Should this be after we re-insert tokens. These could alter the break points. However then
478        // we'd need to make sure we don't break in the middle of a string etc.
479        if (linebreakpos >= 0) {
480            // Some source control tools don't like it when files containing lines longer
481            // than, say 8000 characters, are checked in. The linebreak option is used in
482            // that case to split long lines after a specific column.
483            i = 0;
484            int linestartpos = 0;
485            sb = new StringBuffer(css);
486            while (i < sb.length()) {
487                char c = sb.charAt(i++);
488                if (c == '}' && i - linestartpos > linebreakpos) {
489                    sb.insert(i, '\n');
490                    linestartpos = i;
491                }
492            }
493
494            css = sb.toString();
495        }
496
497        // Replace multiple semi-colons in a row by a single one
498        // See SF bug #1980989
499        css = css.replaceAll(";;+", ";");
500
501        // restore preserved comments and strings
502        for(i = 0, max = preservedTokens.size(); i < max; i++) {
503            css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens.get(i).toString());
504        }
505        
506        // Add spaces back in between operators for css calc function
507        // https://developer.mozilla.org/en-US/docs/Web/CSS/calc
508        // Added by Eric Arnol-Martin (earnolmartin@gmail.com)
509        sb = new StringBuffer();
510        p = Pattern.compile("calc\\([^\\)]*\\)");
511        m = p.matcher(css);
512        while (m.find()) {
513            String s = m.group();
514            
515            s = s.replaceAll("(?<=[-|%|px|em|rem|vw|\\d]+)\\+", " + ");
516            s = s.replaceAll("(?<=[-|%|px|em|rem|vw|\\d]+)\\-", " - ");
517            s = s.replaceAll("(?<=[-|%|px|em|rem|vw|\\d]+)\\*", " * ");
518            s = s.replaceAll("(?<=[-|%|px|em|rem|vw|\\d]+)\\/", " / ");
519            
520            m.appendReplacement(sb, s);
521        }
522        m.appendTail(sb);
523        css = sb.toString(); 
524
525        // Trim the final string (for any leading or trailing white spaces)
526        css = css.trim();
527
528        // Write the output...
529        out.write(css);
530    }
531}