content: /*
   * YUI Compressor
   * http://developer.yahoo.com/yui/compressor/
   * Author: Julien Lecomte -  http://www.julienlecomte.net/
   * Author: Isaac Schlueter - http://foohack.com/
   * Author: Stoyan Stefanov - http://phpied.com/
   * Copyright (c) 2011 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 com.yahoo.platform.yui.compressor;
  
  import java.io.IOException;
  import java.io.Reader;
  import java.io.Writer;
  import java.util.regex.Pattern;
  import java.util.regex.Matcher;
  import java.util.ArrayList;
  
  public class CssCompressor {
  
      private StringBuffer srcsb = new StringBuffer();
  
      public CssCompressor(Reader in) throws IOException {
          // Read the stream...
          int c;
          while ((c = in.read()) != -1) {
              srcsb.append((char) c);
          }
      }
  
      // Leave data urls alone to increase parse performance.
      protected String extractDataUrls(String css, ArrayList preservedTokens) {
  
      	int maxIndex = css.length() - 1;
          int appendIndex = 0;
  
      	StringBuffer sb = new StringBuffer();
  
          Pattern p = Pattern.compile("url\\(\\s*([\"']?)data\\:");
          Matcher m = p.matcher(css);
          
          /* 
           * Since we need to account for non-base64 data urls, we need to handle 
           * ' and ) being part of the data string. Hence switching to indexOf,
           * to determine whether or not we have matching string terminators and
           * handling sb appends directly, instead of using matcher.append* methods.
           */
  
          while (m.find()) {
  
          	int startIndex = m.start() + 4;  	// "url(".length()
      		String terminator = m.group(1);     // ', " or empty (not quoted)
      		
      		if (terminator.length() == 0) {
      		 	terminator = ")";
      		}
  
      		boolean foundTerminator = false;
  
      		int endIndex = m.end() - 1;
      		while(foundTerminator == false && endIndex+1 <= maxIndex) {
      			endIndex = css.indexOf(terminator, endIndex+1);
  
      			if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) {
      				foundTerminator = true;
      				if (!")".equals(terminator)) {
      					endIndex = css.indexOf(")", endIndex); 
      				}
      			}
      		}
  
      		// Enough searching, start moving stuff over to the buffer
  			sb.append(css.substring(appendIndex, m.start()));
  
      		if (foundTerminator) {
      			String token = css.substring(startIndex, endIndex);
      			token = token.replaceAll("\\s+", "");
  	    		preservedTokens.add(token);
  
  	    		String preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)";
  	    		sb.append(preserver);
  
  	    		appendIndex = endIndex + 1;
      		} else {
      			// No end terminator found, re-add the whole match. Should we throw/warn here?
      			sb.append(css.substring(m.start(), m.end()));
      			appendIndex = m.end();
      		}
          }
  
          sb.append(css.substring(appendIndex));
  
          return sb.toString();
      }
  
      public void compress(Writer out, int linebreakpos)
              throws IOException {
  
          Pattern p;
          Matcher m;
          String css = srcsb.toString();
  
          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;
  
          css = this.extractDataUrls(css, preservedTokens);
  
          StringBuffer sb = new StringBuffer(css);
  
          // 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;
          // same for transform-origin
          sb = new StringBuffer();
          p = Pattern.compile("(?i)(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|})");
          m = p.matcher(css);
          while (m.find()) {
              m.appendReplacement(sb, m.group(1).toLowerCase() + ":0 0" + m.group(2));
          }
          m.appendTail(sb);
          css = sb.toString();
  
          // 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.
          // We also want to make sure we're only compressing #AABBCC patterns inside { }, not id selectors ( #FAABAC {} )
          // We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD)
          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{][^{]*?\\})");
  
          m = p.matcher(css);
          sb = new StringBuffer();
          int index = 0;
  
          while (m.find(index)) {
  
          	sb.append(css.substring(index, m.start()));
  
          	boolean isFilter = (m.group(1) != null && !"".equals(m.group(1))); 
  
          	if (isFilter) {
          		// Restore, as is. Compression will break filters
          		sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7));
          	} else {
          		if( m.group(2).equalsIgnoreCase(m.group(3)) &&
                      m.group(4).equalsIgnoreCase(m.group(5)) &&
                      m.group(6).equalsIgnoreCase(m.group(7))) {
  
  	        		// #AABBCC pattern
  	                sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase());
  
          		} else {
  
          			// Non-compressible color, restore, but lower case.
          			sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase());
          		}
              }
  
          	index = m.end(7);
          }
  
          sb.append(css.substring(index));
          css = sb.toString();
  
          // border: none -> border:0
          sb = new StringBuffer();
          p = Pattern.compile("(?i)(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|})");
          m = p.matcher(css);
          while (m.find()) {
              m.appendReplacement(sb, m.group(1).toLowerCase() + ":0" + m.group(2));
          }
          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("[^\\}\\{/;]+\\{\\}", "");
  
          // TODO: Should this be after we re-insert tokens. These could alter the break points. However then
          // we'd need to make sure we don't break in the middle of a string etc.
          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);
      }
  }
  