CssCompressor.java 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. /*
  2. * YUI Compressor
  3. * http://developer.yahoo.com/yui/compressor/
  4. * Author: Julien Lecomte - http://www.julienlecomte.net/
  5. * Author: Isaac Schlueter - http://foohack.com/
  6. * Author: Stoyan Stefanov - http://phpied.com/
  7. * Copyright (c) 2011 Yahoo! Inc. All rights reserved.
  8. * The copyrights embodied in the content of this file are licensed
  9. * by Yahoo! Inc. under the BSD (revised) open source license.
  10. */
  11. package com.yahoo.platform.yui.compressor;
  12. import java.io.IOException;
  13. import java.io.Reader;
  14. import java.io.Writer;
  15. import java.util.regex.Pattern;
  16. import java.util.regex.Matcher;
  17. import java.util.ArrayList;
  18. public class CssCompressor {
  19. private StringBuffer srcsb = new StringBuffer();
  20. public CssCompressor(Reader in) throws IOException {
  21. // Read the stream...
  22. int c;
  23. while ((c = in.read()) != -1) {
  24. srcsb.append((char) c);
  25. }
  26. }
  27. // Leave data urls alone to increase parse performance.
  28. protected String extractDataUrls(String css, ArrayList preservedTokens) {
  29. int maxIndex = css.length() - 1;
  30. int appendIndex = 0;
  31. StringBuffer sb = new StringBuffer();
  32. Pattern p = Pattern.compile("url\\(\\s*([\"']?)data\\:");
  33. Matcher m = p.matcher(css);
  34. /*
  35. * Since we need to account for non-base64 data urls, we need to handle
  36. * ' and ) being part of the data string. Hence switching to indexOf,
  37. * to determine whether or not we have matching string terminators and
  38. * handling sb appends directly, instead of using matcher.append* methods.
  39. */
  40. while (m.find()) {
  41. int startIndex = m.start() + 4; // "url(".length()
  42. String terminator = m.group(1); // ', " or empty (not quoted)
  43. if (terminator.length() == 0) {
  44. terminator = ")";
  45. }
  46. boolean foundTerminator = false;
  47. int endIndex = m.end() - 1;
  48. while(foundTerminator == false && endIndex+1 <= maxIndex) {
  49. endIndex = css.indexOf(terminator, endIndex+1);
  50. if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) {
  51. foundTerminator = true;
  52. if (!")".equals(terminator)) {
  53. endIndex = css.indexOf(")", endIndex);
  54. }
  55. }
  56. }
  57. // Enough searching, start moving stuff over to the buffer
  58. sb.append(css.substring(appendIndex, m.start()));
  59. if (foundTerminator) {
  60. String token = css.substring(startIndex, endIndex);
  61. token = token.replaceAll("\\s+", "");
  62. preservedTokens.add(token);
  63. String preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)";
  64. sb.append(preserver);
  65. appendIndex = endIndex + 1;
  66. } else {
  67. // No end terminator found, re-add the whole match. Should we throw/warn here?
  68. sb.append(css.substring(m.start(), m.end()));
  69. appendIndex = m.end();
  70. }
  71. }
  72. sb.append(css.substring(appendIndex));
  73. return sb.toString();
  74. }
  75. public void compress(Writer out, int linebreakpos)
  76. throws IOException {
  77. Pattern p;
  78. Matcher m;
  79. String css = srcsb.toString();
  80. int startIndex = 0;
  81. int endIndex = 0;
  82. int i = 0;
  83. int max = 0;
  84. ArrayList preservedTokens = new ArrayList(0);
  85. ArrayList comments = new ArrayList(0);
  86. String token;
  87. int totallen = css.length();
  88. String placeholder;
  89. css = this.extractDataUrls(css, preservedTokens);
  90. StringBuffer sb = new StringBuffer(css);
  91. // collect all comment blocks...
  92. while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) {
  93. endIndex = sb.indexOf("*/", startIndex + 2);
  94. if (endIndex < 0) {
  95. endIndex = totallen;
  96. }
  97. token = sb.substring(startIndex + 2, endIndex);
  98. comments.add(token);
  99. sb.replace(startIndex + 2, endIndex, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1) + "___");
  100. startIndex += 2;
  101. }
  102. css = sb.toString();
  103. // preserve strings so their content doesn't get accidentally minified
  104. sb = new StringBuffer();
  105. p = Pattern.compile("(\"([^\\\\\"]|\\\\.|\\\\)*\")|(\'([^\\\\\']|\\\\.|\\\\)*\')");
  106. m = p.matcher(css);
  107. while (m.find()) {
  108. token = m.group();
  109. char quote = token.charAt(0);
  110. token = token.substring(1, token.length() - 1);
  111. // maybe the string contains a comment-like substring?
  112. // one, maybe more? put'em back then
  113. if (token.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) {
  114. for (i = 0, max = comments.size(); i < max; i += 1) {
  115. token = token.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments.get(i).toString());
  116. }
  117. }
  118. // minify alpha opacity in filter strings
  119. token = token.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity=");
  120. preservedTokens.add(token);
  121. String preserver = quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___" + quote;
  122. m.appendReplacement(sb, preserver);
  123. }
  124. m.appendTail(sb);
  125. css = sb.toString();
  126. // strings are safe, now wrestle the comments
  127. for (i = 0, max = comments.size(); i < max; i += 1) {
  128. token = comments.get(i).toString();
  129. placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___";
  130. // ! in the first position of the comment means preserve
  131. // so push to the preserved tokens while stripping the !
  132. if (token.startsWith("!")) {
  133. preservedTokens.add(token);
  134. css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
  135. continue;
  136. }
  137. // \ in the last position looks like hack for Mac/IE5
  138. // shorten that to /*\*/ and the next one to /**/
  139. if (token.endsWith("\\")) {
  140. preservedTokens.add("\\");
  141. css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
  142. i = i + 1; // attn: advancing the loop
  143. preservedTokens.add("");
  144. css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
  145. continue;
  146. }
  147. // keep empty comments after child selectors (IE7 hack)
  148. // e.g. html >/**/ body
  149. if (token.length() == 0) {
  150. startIndex = css.indexOf(placeholder);
  151. if (startIndex > 2) {
  152. if (css.charAt(startIndex - 3) == '>') {
  153. preservedTokens.add("");
  154. css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
  155. }
  156. }
  157. }
  158. // in all other cases kill the comment
  159. css = css.replace("/*" + placeholder + "*/", "");
  160. }
  161. // Normalize all whitespace strings to single spaces. Easier to work with that way.
  162. css = css.replaceAll("\\s+", " ");
  163. // Remove the spaces before the things that should not have spaces before them.
  164. // But, be careful not to turn "p :link {...}" into "p:link{...}"
  165. // Swap out any pseudo-class colons with the token, and then swap back.
  166. sb = new StringBuffer();
  167. p = Pattern.compile("(^|\\})(([^\\{:])+:)+([^\\{]*\\{)");
  168. m = p.matcher(css);
  169. while (m.find()) {
  170. String s = m.group();
  171. s = s.replaceAll(":", "___YUICSSMIN_PSEUDOCLASSCOLON___");
  172. s = s.replaceAll( "\\\\", "\\\\\\\\" ).replaceAll( "\\$", "\\\\\\$" );
  173. m.appendReplacement(sb, s);
  174. }
  175. m.appendTail(sb);
  176. css = sb.toString();
  177. // Remove spaces before the things that should not have spaces before them.
  178. css = css.replaceAll("\\s+([!{};:>+\\(\\)\\],])", "$1");
  179. // bring back the colon
  180. css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":");
  181. // retain space for special IE6 cases
  182. css = css.replaceAll(":first\\-(line|letter)(\\{|,)", ":first-$1 $2");
  183. // no space after the end of a preserved comment
  184. css = css.replaceAll("\\*/ ", "*/");
  185. // If there is a @charset, then only allow one, and push to the top of the file.
  186. css = css.replaceAll("^(.*)(@charset \"[^\"]*\";)", "$2$1");
  187. css = css.replaceAll("^(\\s*@charset [^;]+;\\s*)+", "$1");
  188. // Put the space back in some cases, to support stuff like
  189. // @media screen and (-webkit-min-device-pixel-ratio:0){
  190. css = css.replaceAll("\\band\\(", "and (");
  191. // Remove the spaces after the things that should not have spaces after them.
  192. css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1");
  193. // remove unnecessary semicolons
  194. css = css.replaceAll(";+}", "}");
  195. // Replace 0(px,em,%) with 0.
  196. css = css.replaceAll("([\\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", "$1$2");
  197. // Replace 0 0 0 0; with 0.
  198. css = css.replaceAll(":0 0 0 0(;|})", ":0$1");
  199. css = css.replaceAll(":0 0 0(;|})", ":0$1");
  200. css = css.replaceAll(":0 0(;|})", ":0$1");
  201. // Replace background-position:0; with background-position:0 0;
  202. // same for transform-origin
  203. sb = new StringBuffer();
  204. p = Pattern.compile("(?i)(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|})");
  205. m = p.matcher(css);
  206. while (m.find()) {
  207. m.appendReplacement(sb, m.group(1).toLowerCase() + ":0 0" + m.group(2));
  208. }
  209. m.appendTail(sb);
  210. css = sb.toString();
  211. // Replace 0.6 to .6, but only when preceded by : or a white-space
  212. css = css.replaceAll("(:|\\s)0+\\.(\\d+)", "$1.$2");
  213. // Shorten colors from rgb(51,102,153) to #336699
  214. // This makes it more likely that it'll get further compressed in the next step.
  215. p = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)");
  216. m = p.matcher(css);
  217. sb = new StringBuffer();
  218. while (m.find()) {
  219. String[] rgbcolors = m.group(1).split(",");
  220. StringBuffer hexcolor = new StringBuffer("#");
  221. for (i = 0; i < rgbcolors.length; i++) {
  222. int val = Integer.parseInt(rgbcolors[i]);
  223. if (val < 16) {
  224. hexcolor.append("0");
  225. }
  226. hexcolor.append(Integer.toHexString(val));
  227. }
  228. m.appendReplacement(sb, hexcolor.toString());
  229. }
  230. m.appendTail(sb);
  231. css = sb.toString();
  232. // Shorten colors from #AABBCC to #ABC. Note that we want to make sure
  233. // the color is not preceded by either ", " or =. Indeed, the property
  234. // filter: chroma(color="#FFFFFF");
  235. // would become
  236. // filter: chroma(color="#FFF");
  237. // which makes the filter break in IE.
  238. // We also want to make sure we're only compressing #AABBCC patterns inside { }, not id selectors ( #FAABAC {} )
  239. // We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD)
  240. 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{][^{]*?\\})");
  241. m = p.matcher(css);
  242. sb = new StringBuffer();
  243. int index = 0;
  244. while (m.find(index)) {
  245. sb.append(css.substring(index, m.start()));
  246. boolean isFilter = (m.group(1) != null && !"".equals(m.group(1)));
  247. if (isFilter) {
  248. // Restore, as is. Compression will break filters
  249. sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7));
  250. } else {
  251. if( m.group(2).equalsIgnoreCase(m.group(3)) &&
  252. m.group(4).equalsIgnoreCase(m.group(5)) &&
  253. m.group(6).equalsIgnoreCase(m.group(7))) {
  254. // #AABBCC pattern
  255. sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase());
  256. } else {
  257. // Non-compressible color, restore, but lower case.
  258. sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase());
  259. }
  260. }
  261. index = m.end(7);
  262. }
  263. sb.append(css.substring(index));
  264. css = sb.toString();
  265. // border: none -> border:0
  266. sb = new StringBuffer();
  267. p = Pattern.compile("(?i)(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|})");
  268. m = p.matcher(css);
  269. while (m.find()) {
  270. m.appendReplacement(sb, m.group(1).toLowerCase() + ":0" + m.group(2));
  271. }
  272. m.appendTail(sb);
  273. css = sb.toString();
  274. // shorter opacity IE filter
  275. css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity=");
  276. // Remove empty rules.
  277. css = css.replaceAll("[^\\}\\{/;]+\\{\\}", "");
  278. // TODO: Should this be after we re-insert tokens. These could alter the break points. However then
  279. // we'd need to make sure we don't break in the middle of a string etc.
  280. if (linebreakpos >= 0) {
  281. // Some source control tools don't like it when files containing lines longer
  282. // than, say 8000 characters, are checked in. The linebreak option is used in
  283. // that case to split long lines after a specific column.
  284. i = 0;
  285. int linestartpos = 0;
  286. sb = new StringBuffer(css);
  287. while (i < sb.length()) {
  288. char c = sb.charAt(i++);
  289. if (c == '}' && i - linestartpos > linebreakpos) {
  290. sb.insert(i, '\n');
  291. linestartpos = i;
  292. }
  293. }
  294. css = sb.toString();
  295. }
  296. // Replace multiple semi-colons in a row by a single one
  297. // See SF bug #1980989
  298. css = css.replaceAll(";;+", ";");
  299. // restore preserved comments and strings
  300. for(i = 0, max = preservedTokens.size(); i < max; i++) {
  301. css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens.get(i).toString());
  302. }
  303. // Trim the final string (for any leading or trailing white spaces)
  304. css = css.trim();
  305. // Write the output...
  306. out.write(css);
  307. }
  308. }