aboutsummaryrefslogblamecommitdiffstats
path: root/trunk/infrastructure/ace/bin/make
blob: 0a39d91e92bf19ffab114c91cddf887ebe6a2f95 (plain) (tree)
1
2
3
4
5
6
7
8
9








                                                                                   
                                                             






























































































































































































































































                                                                                                                                                                                                                                                                                                                                                                                                                  

                                                     




                                                                      

                                                     






























































                                                                                             
#!/bin/sh
exec scala -classpath lib/yuicompressor-2.4-appjet.jar:lib/rhino-js-1.7r1.jar $0 $@
!#

import java.io._;

def superpack(input: String): String = {
  // this function is self-contained;  takes a string, returns an expression
  // that evaluates to that string
  // XXX (This compresses well but decompression is too slow)

  // constraints on special chars:
  // - this string must be able to go in a character class
  // - each char must be able to go in single quotes
  val specialChars = "-~@%$#*^_`()|abcdefghijklmnopqrstuvwxyz=!+,.;:?{}";
  val specialCharsSet:Set[Char] = Set(specialChars:_*);
  def containsSpecialChar(str: String) = str.exists(specialCharsSet.contains(_));
  
  val toks:Array[String] = (
    "@|[a-zA-Z0-9]+|[^@a-zA-Z0-9]{1,3}").r.findAllIn(input).collect.toArray;
  
  val stringCounts = {
    val m = new scala.collection.mutable.HashMap[String,Int];
    def incrementCount(s: String) = { m(s) = m.getOrElse(s, 0) + 1; }
    for(s <- toks) incrementCount(s);
    m;
  }
  
  val estimatedSavings = scala.util.Sorting.stableSort(
    for((s,n) <- stringCounts.toArray; savings = s.length*n
	if (savings > 8 || containsSpecialChar(s)))
    yield (s,n,savings),
    (x:(String,Int,Int))=> -x._3);
  
  def strLast(str: String, n: Int) = str.substring(str.length - n, str.length);
  // order of encodeNames is very important!
  val encodeNames = for(n <- 0 until (36*36); c <- specialChars) yield c.toString+strLast("0"+Integer.toString(n, 36).toUpperCase, 2);
  
  val thingsToReplace:Seq[String] = estimatedSavings.map(_._1);
  assert(encodeNames.length >= thingsToReplace.length);
  
  val replacements = Map(thingsToReplace.elements.zipWithIndex.map({
    case (str, i) => (str, encodeNames(i));
  }).collect:_*);
  def encode(tk: String) = if (replacements.contains(tk)) replacements(tk) else tk;
  
  val afterReplace = toks.map(encode(_)).mkString.replaceAll(
    "(["+specialChars+"])(?=..[^0-9A-Z])(00|0)", "$1");
  
  def makeSingleQuotedContents(str: String): String = {
    str.replace("\\", "\\\\").replace("'", "\\'").replace("<", "\\x3c").replace("\n", "\\n").
    replace("\r", "\\n").replace("\t", "\\t");
  }

  val expansionMap = new scala.collection.mutable.HashMap[Char,scala.collection.mutable.ArrayBuffer[String]];
  for(i <- 0 until thingsToReplace.length; sc = encodeNames(i).charAt(0);
      e = thingsToReplace(i)) {
    expansionMap.getOrElseUpdate(sc, new scala.collection.mutable.ArrayBuffer[String]) +=
      (if (e == "@") "" else e);
  }
  val expansionMapLiteral = "{"+(for((sc,strs) <- expansionMap) yield {
    "'"+sc+"':'"+makeSingleQuotedContents(strs.mkString("@"))+"'";
  }).mkString(",")+"}";
  
  val expr = ("(function(m){m="+expansionMapLiteral+
    ";for(var k in m){if(m.hasOwnProperty(k))m[k]=m[k].split('@')};return '"+
      makeSingleQuotedContents(afterReplace)+
	"'.replace(/(["+specialChars+
	  "])([0-9A-Z]{0,2})/g,function(a,b,c){return m[b][parseInt(c||'0',36)]||'@'})}())");
  /*val expr = ("(function(m){m="+expansionMapLiteral+
    ";for(var k in m){if(m.hasOwnProperty(k))m[k]=m[k].split('@')};"+
      "var result=[];var i=0;var s='"+makeSingleQuotedContents(afterReplace)+
	"';var len=s.length;while (i<len) {var x=s.charAt(i); var L=m[x],a = s.charAt(i+1),b = s.charAt(i+2);if (L) { var c;if (!(a >= 'A' && a <= 'Z' || a >= '0' && a <= '9')) {c=L[0];i++} else if (!(b >= 'A' && b <= 'Z' || b >= '0' && b <= '9')) {c = L[parseInt(a,36)]; i+=2} else {c = L[parseInt(a+b,36)]; i+=3}; result.push(c||'@'); } else {result.push(x); i++} }; return result.join(''); }())");*/

  def evaluateString(js: String): String = {
    import org.mozilla.javascript._;
    ContextFactory.getGlobal.call(new ContextAction {
      def run(cx: Context) = {
	val scope = cx.initStandardObjects;
	cx.evaluateString(scope, js, "<cmd>", 1, null) } }).asInstanceOf[String];
  }

  def putFile(str: String, path: String): Unit = {
    import java.io._;
    val writer = new FileWriter(path);
    writer.write(str);
    writer.close;
  }
  
  val exprOut = evaluateString(expr);
  if (exprOut != input) {
    putFile(input, "/tmp/superpack.input");
    putFile(expr, "/tmp/superpack.expr");
    putFile(exprOut, "/tmp/superpack.output");
    error("Superpacked string does not evaluate to original string; check /tmp/superpack.*");
  }
  
  val singleLiteral = "'"+makeSingleQuotedContents(input)+"'";
  if (singleLiteral.length < expr.length) {
    singleLiteral;
  }
  else {
    expr;
  }
}

def doMake {

  lazy val isEtherPad = (args.length >= 2 && args(1) == "etherpad");
  lazy val isNoHelma = (args.length >= 2 && args(1) == "nohelma");
    
  def getFile(path:String): String = {
    val builder = new StringBuilder(1000);
    val reader = new BufferedReader(new FileReader(path));
    val buf = new Array[Char](1024);
    var numRead = 0;
    while({ numRead = reader.read(buf); numRead } != -1) {
      builder.append(buf, 0, numRead);
    }
    reader.close;
    return builder.toString;
  }

  def putFile(str: String, path: String): Unit = {
    val writer = new FileWriter(path);
    writer.write(str);
    writer.close;
  }

  def writeToString(func:(Writer=>Unit)): String = {
    val writer = new StringWriter;
    func(writer);
    return writer.toString;
  }

  def compressJS(code: String, wrap: Boolean): String = {
    import yuicompressor.org.mozilla.javascript.{ErrorReporter, EvaluatorException};
    object MyErrorReporter extends ErrorReporter {
      def warning(message:String, sourceName:String, line:Int, lineSource:String, lineOffset:Int) {
	if (message startsWith "Try to use a single 'var' statement per scope.") return;
	if (line < 0) System.err.println("\n[WARNING] " + message);
	else System.err.println("\n[WARNING] " + line + ':' + lineOffset + ':' + message);
      }
      def error(message:String, sourceName:String, line:Int, lineSource:String, lineOffset:Int) {
	if (line < 0) System.err.println("\n[ERROR] " + message);
	else System.err.println("\n[ERROR] " + line + ':' + lineOffset + ':' + message);
      }
      def runtimeError(message:String, sourceName:String, line:Int, lineSource:String, lineOffset:Int): EvaluatorException = {
	error(message, sourceName, line, lineSource, lineOffset);
	return new EvaluatorException(message);
      }
    }
    
    val munge = true;
    val verbose = false;
    val optimize = true;
    val compressor = new com.yahoo.platform.yui.compressor.JavaScriptCompressor(new StringReader(code), MyErrorReporter);
    return writeToString(compressor.compress(_, if (wrap) 100 else -1, munge, verbose, true, !optimize));
  }

  def compressCSS(code: String, wrap: Boolean): String = {
    val compressor = new com.yahoo.platform.yui.compressor.CssCompressor(new StringReader(code));
    return writeToString(compressor.compress(_, if (wrap) 100 else -1));  
  }

  import java.util.regex.{Pattern, Matcher, MatchResult};

  def stringReplace(orig: String, regex: String, groupReferences:Boolean, func:(MatchResult=>String)): String = {
    val buf = new StringBuffer;
    val m = Pattern.compile(regex).matcher(orig);
    while (m.find) {
      var str = func(m);
      if (! groupReferences) {
	str = str.replace("\\", "\\\\").replace("$", "\\$");
      }
      m.appendReplacement(buf, str);
    }
    m.appendTail(buf);
    return buf.toString;
  }

  def stringToExpression(str: String): String = {
    var contents = str.replace("\\", "\\\\").replace("'", "\\'").replace("<", "\\x3c").replace("\n", "\\n").
    replace("\r", "\\n").replace("\t", "\\t");
    contents = contents.replace("\\/", "\\\\x2f"); // for Norton Internet Security
    val result = "'"+contents+"'";
    result;
  }

  val srcDir = "www";
  val destDir = "build";
  var code = getFile(srcDir+"/ace2_outer.js");

  val useCompression = true; //if (isEtherPad) false else true;

  code = stringReplace(code, "\\$\\$INCLUDE_([A-Z_]+)\\([\"']([^\"']+)[\"']\\)", false, (m:MatchResult) => {
    val includeType = m.group(1);
    val paths = m.group(2);
    val pathsArray = paths.replaceAll("""/\*.*?\*/""", "").split(" +").filter(_.length > 0);
    def getSubcode = pathsArray.map(p => getFile(srcDir+"/"+p)).mkString("\n");
    val doPack = (stringToExpression _);
    includeType match {
      case "JS" => {
	var subcode = getSubcode;
	subcode = subcode.replaceAll("var DEBUG=true;//\\$\\$[^\n\r]*", "var DEBUG=false;");
	if (useCompression) subcode = compressJS(subcode, true);
	"('\\x3cscript type=\"text/javascript\">//<!--\\n'+" + doPack(subcode) +
	  "+'//-->\\n</script>')";
      }
      case "CSS" => {
	var subcode = getSubcode;
	if (useCompression) subcode = compressCSS(subcode, false);
	"('<style type=\"text/css\">'+" + doPack(subcode) + "+'</style>')";
      }
      case "JS_Q" => {
	var subcode = getSubcode
	subcode = subcode.replaceAll("var DEBUG=true;//\\$\\$[^\n\r]*", "var DEBUG=false;");
	if (useCompression) subcode = compressJS(subcode, true);
	"('(\\'\\\\x3cscript type=\"text/javascript\">//<!--\\\\n\\'+'+" +
	  doPack(stringToExpression(subcode)) +
	    "+'+\\'//-->\\\\n\\\\x3c/script>\\')')";
      }
      case "CSS_Q" => {
	var subcode = getSubcode;
	if (useCompression) subcode = compressCSS(subcode, false);
	"('(\\'<style type=\"text/css\">\\'+'+" + doPack(stringToExpression(subcode)) +
	  "+'+\\'\\\\x3c/style>\\')')";
      }
      case ("JS_DEV" | "CSS_DEV") => "''";
      case ("JS_Q_DEV" | "CSS_Q_DEV") => "'\\'\\''";
      //case _ => "$$INCLUDE_"+includeType+"(\"../www/"+path+"\")";
    }
  });

  if (useCompression) code = compressJS(code, true);

  putFile(code, destDir+"/ace2bare.js");

  //var wrapper = getFile(srcDir+"/ace2_wrapper.js");
  //if (useCompression) wrapper = compressJS(wrapper, true);
  putFile(/*wrapper+"\n"+*/code, destDir+"/ace2.js");

  var index = getFile(srcDir+"/index.html");
  index = index.replaceAll("<!--\\s*DEBUG\\s*-->\\s*([\\s\\S]+?)\\s*<!--\\s*/DEBUG\\s*-->", "");
  index = index.replaceAll("<!--\\s*PROD:\\s*([\\s\\S]+?)\\s*-->", "$1");
  putFile(index, destDir+"/index.html");

  putFile(getFile(srcDir+"/testcode.js"), destDir+"/testcode.js");

  def copyFile(fromFile: String, toFile: String) {
    if (0 != Runtime.getRuntime.exec("cp "+fromFile+" "+toFile).waitFor) {
      printf("copy failed (%s -> %s).\n", fromFile, toFile);
    }      
  }

  def replaceFirstLine(txt: String, newFirstLine: String): String = {
    var newlinePos = txt.indexOf('\n');
    newFirstLine + txt.substring(newlinePos);
  }
  
  if (isEtherPad) {
    copyFile("build/ace2.js", "../../etherpad/src/static/js/ace.js");

    def copyFileToEtherpad(fromName: String, toName: String) {
      var code = getFile(srcDir+"/"+fromName);
      code = "// DO NOT EDIT THIS FILE, edit "+
        "infrastructure/ace/www/"+fromName+"\n"+code;
      code = code.replaceAll("""(?<=\n)\s*//\s*%APPJET%:\s*""", "");
      putFile(code, "../../etherpad/src/etherpad/collab/ace/"+toName);
    }
    def copyFileToClientSide(fromName: String, toName: String) {
      var code = getFile(srcDir+"/"+fromName);
      code = "// DO NOT EDIT THIS FILE, edit "+
        "infrastructure/ace/www/"+fromName+"\n"+code;
      code = code.replaceAll("""(?<=\n)\s*//\s*%APPJET%:.*?\n""", "");
      code = code.replaceAll("""(?<=\n)\s*//\s*%CLIENT FILE ENDS HERE%[\s\S]*""",
                             "");
      putFile(code, "../../etherpad/src/static/js/"+toName);
    }

    copyFileToEtherpad("easy_sync.js", "easysync1.js");
    copyFileToEtherpad("easysync2.js", "easysync2.js");
    copyFileToEtherpad("contentcollector.js", "contentcollector.js");
    copyFileToEtherpad("easysync2_tests.js", "easysync2_tests.js");
    copyFileToClientSide("colorutils.js", "colorutils.js");
    copyFileToClientSide("easysync2.js", "easysync2_client.js");
    copyFileToEtherpad("linestylefilter.js", "linestylefilter.js");
    copyFileToClientSide("linestylefilter.js", "linestylefilter_client.js");
    copyFileToEtherpad("domline.js", "domline.js");
    copyFileToClientSide("domline.js", "domline_client.js");
    copyFileToClientSide("cssmanager.js", "cssmanager_client.js");
  }
  /*else if (! isNoHelma) {
    copyFile("build/ace2.js", "../helma_apps/appjet/protectedStatic/js/ace.js");	     
  }*/
}

def remakeLoop {
  
  def getStamp: Long = {
    return (new java.io.File("www").listFiles.
	    filter(! _.getName.endsWith("~")).
	    filter(! _.getName.endsWith("#")).
	    filter(! _.getName.startsWith(".")).map(_.lastModified).
	    reduceLeft(Math.max(_:Long,_:Long)));
  }
  
  var madeStamp:Long = 0;
  var errorStamp:Long = 0;
  while (true) {
    Thread.sleep(500);
    val s = getStamp;
    if (s > madeStamp && s != errorStamp) {
      Thread.sleep(1000);
      if (getStamp == s) {
	madeStamp = s;
	print("Remaking...  ");
	try {
	  doMake;
	  println("OK");
	}
	catch { case e => {
	  println("ERROR");
	  errorStamp = s;
	} }
      }
    }
  }

}

if (args.length >= 1 && args(0) == "auto") {
  remakeLoop;
}
else {
  doMake;
}