/* * YUI Compressor * Author: Julien Lecomte - http://www.julienlecomte.net/ * Author: Isaac Schlueter - http://foohack.com/ * Author: Stoyan Stefanov - http://phpied.com/ * Copyright (c) 2009 Yahoo! Inc. All rights reserved. * The copyrights embodied in the content of this file are licensed * by Yahoo! Inc. under the BSD (revised) open source license. */ package org.exoplatform.portal.resource.compressor.css; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; public class YUICSSCompressor { private StringBuffer srcsb = new StringBuffer(); public YUICSSCompressor(Reader in) throws IOException { // Read the stream... int c; while ((c = in.read()) != -1) { srcsb.append((char) c); } } public void compress(Writer out, int linebreakpos) throws IOException { Pattern p; Matcher m; String css = srcsb.toString(); StringBuffer sb = new StringBuffer(css); int startIndex = 0; int endIndex = 0; int i = 0; int max = 0; ArrayList preservedTokens = new ArrayList(0); ArrayList comments = new ArrayList(0); String token; int totallen = css.length(); String placeholder; // collect all comment blocks... while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) { endIndex = sb.indexOf("*/", startIndex + 2); if (endIndex < 0) { endIndex = totallen; } token = sb.substring(startIndex + 2, endIndex); comments.add(token); sb.replace(startIndex + 2, endIndex, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1) + "___"); startIndex += 2; } css = sb.toString(); // preserve strings so their content doesn't get accidentally minified sb = new StringBuffer(); p = Pattern.compile("(\"([^\\\\\"]|\\\\.|\\\\)*\")|(\'([^\\\\\']|\\\\.|\\\\)*\')"); m = p.matcher(css); while (m.find()) { token = m.group(); char quote = token.charAt(0); token = token.substring(1, token.length() - 1); // maybe the string contains a comment-like substring? // one, maybe more? put'em back then if (token.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) { for (i = 0, max = comments.size(); i < max; i += 1) { token = token.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments.get(i).toString()); } } // minify alpha opacity in filter strings token = token.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity="); preservedTokens.add(token); String preserver = quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___" + quote; m.appendReplacement(sb, preserver); } m.appendTail(sb); css = sb.toString(); // strings are safe, now wrestle the comments for (i = 0, max = comments.size(); i < max; i += 1) { token = comments.get(i).toString(); placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___"; // ! in the first position of the comment means preserve // so push to the preserved tokens while stripping the ! if (token.startsWith("!")) { preservedTokens.add(token); css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); continue; } // \ in the last position looks like hack for Mac/IE5 // shorten that to /*\*/ and the next one to /**/ if (token.endsWith("\\")) { preservedTokens.add("\\"); css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); i = i + 1; // attn: advancing the loop preservedTokens.add(""); css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); continue; } // keep empty comments after child selectors (IE7 hack) // e.g. html >/**/ body if (token.length() == 0) { startIndex = css.indexOf(placeholder); if (startIndex > 2) { if (css.charAt(startIndex - 3) == '>') { preservedTokens.add(""); css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); } } } // in all other cases kill the comment css = css.replace("/*" + placeholder + "*/", ""); } // Normalize all whitespace strings to single spaces. Easier to work with that way. css = css.replaceAll("\\s+", " "); // Remove the spaces before the things that should not have spaces before them. // But, be careful not to turn "p :link {...}" into "p:link{...}" // Swap out any pseudo-class colons with the token, and then swap back. sb = new StringBuffer(); p = Pattern.compile("(^|\\})(([^\\{:])+:)+([^\\{]*\\{)"); m = p.matcher(css); while (m.find()) { String s = m.group(); s = s.replaceAll(":", "___YUICSSMIN_PSEUDOCLASSCOLON___"); s = s.replaceAll("\\\\", "\\\\\\\\").replaceAll("\\$", "\\\\\\$"); m.appendReplacement(sb, s); } m.appendTail(sb); css = sb.toString(); // Remove spaces before the things that should not have spaces before them. css = css.replaceAll("\\s+([!{};:>+\\(\\)\\],])", "$1"); // bring back the colon css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":"); // retain space for special IE6 cases css = css.replaceAll(":first\\-(line|letter)(\\{|,)", ":first-$1 $2"); // no space after the end of a preserved comment css = css.replaceAll("\\*/ ", "*/"); // If there is a @charset, then only allow one, and push to the top of the file. css = css.replaceAll("^(.*)(@charset \"[^\"]*\";)", "$2$1"); css = css.replaceAll("^(\\s*@charset [^;]+;\\s*)+", "$1"); // Put the space back in some cases, to support stuff like // @media screen and (-webkit-min-device-pixel-ratio:0){ css = css.replaceAll("\\band\\(", "and ("); // Remove the spaces after the things that should not have spaces after them. css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1"); // remove unnecessary semicolons css = css.replaceAll(";+}", "}"); // Replace 0(px,em,%) with 0. css = css.replaceAll("([\\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", "$1$2"); // Replace 0 0 0 0; with 0. css = css.replaceAll(":0 0 0 0(;|})", ":0$1"); css = css.replaceAll(":0 0 0(;|})", ":0$1"); css = css.replaceAll(":0 0(;|})", ":0$1"); // Replace background-position:0; with background-position:0 0; css = css.replaceAll("(?i)background-position:0(;|})", "background-position:0 0$1"); // Replace 0.6 to .6, but only when preceded by : or a white-space css = css.replaceAll("(:|\\s)0+\\.(\\d+)", "$1.$2"); // Shorten colors from rgb(51,102,153) to #336699 // This makes it more likely that it'll get further compressed in the next step. p = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)"); m = p.matcher(css); sb = new StringBuffer(); while (m.find()) { String[] rgbcolors = m.group(1).split(","); StringBuffer hexcolor = new StringBuffer("#"); for (i = 0; i < rgbcolors.length; i++) { int val = Integer.parseInt(rgbcolors[i]); if (val < 16) { hexcolor.append("0"); } hexcolor.append(Integer.toHexString(val)); } m.appendReplacement(sb, hexcolor.toString()); } m.appendTail(sb); css = sb.toString(); // Shorten colors from #AABBCC to #ABC. Note that we want to make sure // the color is not preceded by either ", " or =. Indeed, the property // filter: chroma(color="#FFFFFF"); // would become // filter: chroma(color="#FFF"); // which makes the filter break in IE. 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])"); m = p.matcher(css); sb = new StringBuffer(); while (m.find()) { // Test for AABBCC pattern if (m.group(3).equalsIgnoreCase(m.group(4)) && m.group(5).equalsIgnoreCase(m.group(6)) && m.group(7).equalsIgnoreCase(m.group(8))) { m.appendReplacement(sb, (m.group(1) + m.group(2) + "#" + m.group(3) + m.group(5) + m.group(7)).toLowerCase()); } else { m.appendReplacement(sb, m.group().toLowerCase()); } } m.appendTail(sb); css = sb.toString(); // shorter opacity IE filter css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity="); // Remove empty rules. css = css.replaceAll("[^\\}\\{/;]+\\{\\}", ""); if (linebreakpos >= 0) { // Some source control tools don't like it when files containing lines longer // than, say 8000 characters, are checked in. The linebreak option is used in // that case to split long lines after a specific column. i = 0; int linestartpos = 0; sb = new StringBuffer(css); while (i < sb.length()) { char c = sb.charAt(i++); if (c == '}' && i - linestartpos > linebreakpos) { sb.insert(i, '\n'); linestartpos = i; } } css = sb.toString(); } // Replace multiple semi-colons in a row by a single one // See SF bug #1980989 css = css.replaceAll(";;+", ";"); // restore preserved comments and strings for (i = 0, max = preservedTokens.size(); i < max; i++) { css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens.get(i).toString()); } // Trim the final string (for any leading or trailing white spaces) css = css.trim(); // Write the output... out.write(css); } }