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}