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}