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}