001/*
002 * YUI Compressor
003 * Author: Julien Lecomte -  http://www.julienlecomte.net/
004 * Author: Isaac Schlueter - http://foohack.com/ 
005 * Author: Stoyan Stefanov - http://phpied.com/
006 * Copyright (c) 2009 Yahoo! Inc.  All rights reserved.
007 * The copyrights embodied in the content of this file are licensed
008 * by Yahoo! Inc. under the BSD (revised) open source license.
009 */
010
011package org.nuxeo.theme.html;
012
013import java.io.IOException;
014import java.io.Reader;
015import java.io.Writer;
016import java.util.regex.Pattern;
017import java.util.regex.Matcher;
018import java.util.ArrayList;
019
020public class CssCompressor {
021
022    private StringBuffer srcsb = new StringBuffer();
023
024    public CssCompressor(Reader in) throws IOException {
025        // Read the stream...
026        int c;
027        while ((c = in.read()) != -1) {
028            srcsb.append((char) c);
029        }
030    }
031
032    public void compress(Writer out, int linebreakpos) throws IOException {
033
034        Pattern p;
035        Matcher m;
036        String css = srcsb.toString();
037        StringBuffer sb = new StringBuffer(css);
038
039        int startIndex = 0;
040        int endIndex = 0;
041        int i = 0;
042        int max = 0;
043        ArrayList preservedTokens = new ArrayList(0);
044        ArrayList comments = new ArrayList(0);
045        String token;
046        int totallen = css.length();
047        String placeholder;
048
049        // collect all comment blocks...
050        while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) {
051            endIndex = sb.indexOf("*/", startIndex + 2);
052            if (endIndex < 0) {
053                endIndex = totallen;
054            }
055
056            token = sb.substring(startIndex + 2, endIndex);
057            comments.add(token);
058            sb.replace(startIndex + 2, endIndex, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1)
059                    + "___");
060            startIndex += 2;
061        }
062        css = sb.toString();
063
064        // preserve strings so their content doesn't get accidentally minified
065        sb = new StringBuffer();
066        p = Pattern.compile("(\"([^\\\\\"]|\\\\.|\\\\)*\")|(\'([^\\\\\']|\\\\.|\\\\)*\')");
067        m = p.matcher(css);
068        while (m.find()) {
069            token = m.group();
070            char quote = token.charAt(0);
071            token = token.substring(1, token.length() - 1);
072
073            // maybe the string contains a comment-like substring?
074            // one, maybe more? put'em back then
075            if (token.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) {
076                for (i = 0, max = comments.size(); i < max; i += 1) {
077                    token = token.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___",
078                            comments.get(i).toString());
079                }
080            }
081
082            // minify alpha opacity in filter strings
083            token = token.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity=");
084
085            preservedTokens.add(token);
086            String preserver = quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___" + quote;
087            m.appendReplacement(sb, preserver);
088        }
089        m.appendTail(sb);
090        css = sb.toString();
091
092        // strings are safe, now wrestle the comments
093        for (i = 0, max = comments.size(); i < max; i += 1) {
094
095            token = comments.get(i).toString();
096            placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___";
097
098            // ! in the first position of the comment means preserve
099            // so push to the preserved tokens while stripping the !
100            if (token.startsWith("!")) {
101                preservedTokens.add(token);
102                css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
103                continue;
104            }
105
106            // \ in the last position looks like hack for Mac/IE5
107            // shorten that to /*\*/ and the next one to /**/
108            if (token.endsWith("\\")) {
109                preservedTokens.add("\\");
110                css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
111                i = i + 1; // attn: advancing the loop
112                preservedTokens.add("");
113                css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___",
114                        "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
115                continue;
116            }
117
118            // keep empty comments after child selectors (IE7 hack)
119            // e.g. html >/**/ body
120            if (token.length() == 0) {
121                startIndex = css.indexOf(placeholder);
122                if (startIndex > 2) {
123                    if (css.charAt(startIndex - 3) == '>') {
124                        preservedTokens.add("");
125                        css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1)
126                                + "___");
127                    }
128                }
129            }
130
131            // in all other cases kill the comment
132            css = css.replace("/*" + placeholder + "*/", "");
133        }
134
135        // Normalize all whitespace strings to single spaces. Easier to work with that way.
136        css = css.replaceAll("\\s+", " ");
137
138        // Remove the spaces before the things that should not have spaces before them.
139        // But, be careful not to turn "p :link {...}" into "p:link{...}"
140        // Swap out any pseudo-class colons with the token, and then swap back.
141        sb = new StringBuffer();
142        p = Pattern.compile("(^|\\})(([^\\{:])+:)+([^\\{]*\\{)");
143        m = p.matcher(css);
144        while (m.find()) {
145            String s = m.group();
146            s = s.replaceAll(":", "___YUICSSMIN_PSEUDOCLASSCOLON___");
147            s = s.replaceAll("\\\\", "\\\\\\\\").replaceAll("\\$", "\\\\\\$");
148            m.appendReplacement(sb, s);
149        }
150        m.appendTail(sb);
151        css = sb.toString();
152        // Remove spaces before the things that should not have spaces before them.
153        css = css.replaceAll("\\s+([!{};:>+\\(\\)\\],])", "$1");
154        // bring back the colon
155        css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":");
156
157        // retain space for special IE6 cases
158        css = css.replaceAll(":first\\-(line|letter)(\\{|,)", ":first-$1 $2");
159
160        // no space after the end of a preserved comment
161        css = css.replaceAll("\\*/ ", "*/");
162
163        // If there is a @charset, then only allow one, and push to the top of the file.
164        css = css.replaceAll("^(.*)(@charset \"[^\"]*\";)", "$2$1");
165        css = css.replaceAll("^(\\s*@charset [^;]+;\\s*)+", "$1");
166
167        // Put the space back in some cases, to support stuff like
168        // @media screen and (-webkit-min-device-pixel-ratio:0){
169        css = css.replaceAll("\\band\\(", "and (");
170
171        // Remove the spaces after the things that should not have spaces after them.
172        css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1");
173
174        // remove unnecessary semicolons
175        css = css.replaceAll(";+}", "}");
176
177        // Replace 0(px,em,%) with 0.
178        css = css.replaceAll("([\\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", "$1$2");
179
180        // Replace 0 0 0 0; with 0.
181        css = css.replaceAll(":0 0 0 0(;|})", ":0$1");
182        css = css.replaceAll(":0 0 0(;|})", ":0$1");
183        css = css.replaceAll(":0 0(;|})", ":0$1");
184        // Replace background-position:0; with background-position:0 0;
185        css = css.replaceAll("(?i)background-position:0(;|})", "background-position:0 0$1");
186
187        // Replace 0.6 to .6, but only when preceded by : or a white-space
188        css = css.replaceAll("(:|\\s)0+\\.(\\d+)", "$1.$2");
189
190        // Shorten colors from rgb(51,102,153) to #336699
191        // This makes it more likely that it'll get further compressed in the next step.
192        p = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)");
193        m = p.matcher(css);
194        sb = new StringBuffer();
195        while (m.find()) {
196            String[] rgbcolors = m.group(1).split(",");
197            StringBuffer hexcolor = new StringBuffer("#");
198            for (i = 0; i < rgbcolors.length; i++) {
199                int val = Integer.parseInt(rgbcolors[i]);
200                if (val < 16) {
201                    hexcolor.append("0");
202                }
203                hexcolor.append(Integer.toHexString(val));
204            }
205            m.appendReplacement(sb, hexcolor.toString());
206        }
207        m.appendTail(sb);
208        css = sb.toString();
209
210        // Shorten colors from #AABBCC to #ABC. Note that we want to make sure
211        // the color is not preceded by either ", " or =. Indeed, the property
212        // filter: chroma(color="#FFFFFF");
213        // would become
214        // filter: chroma(color="#FFF");
215        // which makes the filter break in IE.
216        p = Pattern.compile("([^\"'=\\s])(\\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])");
217        m = p.matcher(css);
218        sb = new StringBuffer();
219        while (m.find()) {
220            // Test for AABBCC pattern
221            if (m.group(3).equalsIgnoreCase(m.group(4)) && m.group(5).equalsIgnoreCase(m.group(6))
222                    && m.group(7).equalsIgnoreCase(m.group(8))) {
223                m.appendReplacement(sb,
224                        (m.group(1) + m.group(2) + "#" + m.group(3) + m.group(5) + m.group(7)).toLowerCase());
225            } else {
226                m.appendReplacement(sb, m.group().toLowerCase());
227            }
228        }
229        m.appendTail(sb);
230        css = sb.toString();
231
232        // shorter opacity IE filter
233        css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity=");
234
235        // Remove empty rules.
236        css = css.replaceAll("[^\\}\\{/;]+\\{\\}", "");
237
238        if (linebreakpos >= 0) {
239            // Some source control tools don't like it when files containing lines longer
240            // than, say 8000 characters, are checked in. The linebreak option is used in
241            // that case to split long lines after a specific column.
242            i = 0;
243            int linestartpos = 0;
244            sb = new StringBuffer(css);
245            while (i < sb.length()) {
246                char c = sb.charAt(i++);
247                if (c == '}' && i - linestartpos > linebreakpos) {
248                    sb.insert(i, '\n');
249                    linestartpos = i;
250                }
251            }
252
253            css = sb.toString();
254        }
255
256        // Replace multiple semi-colons in a row by a single one
257        // See SF bug #1980989
258        css = css.replaceAll(";;+", ";");
259
260        // restore preserved comments and strings
261        for (i = 0, max = preservedTokens.size(); i < max; i++) {
262            css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens.get(i).toString());
263        }
264
265        // Trim the final string (for any leading or trailing white spaces)
266        css = css.trim();
267
268        // Write the output...
269        out.write(css);
270    }
271}