diff options
Diffstat (limited to '')
-rw-r--r-- | infrastructure/ace/.gitignore (renamed from trunk/infrastructure/ace/.gitignore) | 0 | ||||
-rw-r--r-- | infrastructure/ace/README (renamed from trunk/infrastructure/ace/README) | 0 | ||||
-rwxr-xr-x | infrastructure/ace/bin/make | 339 | ||||
-rwxr-xr-x | infrastructure/ace/bin/serve (renamed from trunk/infrastructure/ace/bin/serve) | 0 | ||||
-rw-r--r-- | infrastructure/ace/blog.txt (renamed from trunk/infrastructure/ace/blog.txt) | 0 | ||||
-rw-r--r-- | infrastructure/ace/build/.gitignore (renamed from trunk/infrastructure/ace/build/.gitignore) | 0 | ||||
-rw-r--r-- | infrastructure/ace/build/index.html (renamed from trunk/infrastructure/ace/build/index.html) | 0 | ||||
-rw-r--r-- | infrastructure/ace/build/jquery-1.2.1.js (renamed from trunk/infrastructure/ace/build/jquery-1.2.1.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/build/testcode.js (renamed from trunk/infrastructure/ace/build/testcode.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/easysync-notes.txt (renamed from trunk/infrastructure/ace/easysync-notes.txt) | 0 | ||||
l--------- | infrastructure/ace/lib/rhino-js-1.7r1.jar (renamed from trunk/infrastructure/ace/lib/rhino-js-1.7r1.jar) | 0 | ||||
l--------- | infrastructure/ace/lib/yuicompressor-2.4-appjet.jar (renamed from trunk/infrastructure/ace/lib/yuicompressor-2.4-appjet.jar) | 0 | ||||
-rw-r--r-- | infrastructure/ace/notes.txt (renamed from trunk/infrastructure/ace/notes.txt) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/ace2_common.js (renamed from trunk/infrastructure/ace/www/ace2_common.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/ace2_common_dev.js (renamed from trunk/infrastructure/ace/www/ace2_common_dev.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/ace2_inner.js | 4809 | ||||
-rw-r--r-- | infrastructure/ace/www/ace2_outer.js | 234 | ||||
-rw-r--r-- | infrastructure/ace/www/ace2_wrapper.js (renamed from trunk/infrastructure/ace/www/ace2_wrapper.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/bbtree.js (renamed from trunk/infrastructure/ace/www/bbtree.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/changesettracker.js (renamed from trunk/infrastructure/ace/www/changesettracker.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/colorutils.js (renamed from trunk/infrastructure/ace/www/colorutils.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/contentcollector.js (renamed from trunk/infrastructure/ace/www/contentcollector.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/cssmanager.js (renamed from trunk/infrastructure/ace/www/cssmanager.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/dev.html (renamed from trunk/infrastructure/ace/www/dev.html) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/domline.js | 232 | ||||
-rw-r--r-- | infrastructure/ace/www/easy_sync.js (renamed from trunk/infrastructure/ace/www/easy_sync.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/easysync2.js (renamed from trunk/infrastructure/ace/www/easysync2.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/easysync2_tests.js (renamed from trunk/infrastructure/ace/www/easysync2_tests.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/editor.css (renamed from trunk/infrastructure/ace/www/editor.css) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/firebug/errorIcon.png (renamed from trunk/infrastructure/ace/www/firebug/errorIcon.png) | bin | 457 -> 457 bytes | |||
-rw-r--r-- | infrastructure/ace/www/firebug/firebug.css (renamed from trunk/infrastructure/ace/www/firebug/firebug.css) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/firebug/firebug.html (renamed from trunk/infrastructure/ace/www/firebug/firebug.html) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/firebug/firebug.js (renamed from trunk/infrastructure/ace/www/firebug/firebug.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/firebug/firebugx.js (renamed from trunk/infrastructure/ace/www/firebug/firebugx.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/firebug/infoIcon.png (renamed from trunk/infrastructure/ace/www/firebug/infoIcon.png) | bin | 524 -> 524 bytes | |||
-rw-r--r-- | infrastructure/ace/www/firebug/warningIcon.png (renamed from trunk/infrastructure/ace/www/firebug/warningIcon.png) | bin | 516 -> 516 bytes | |||
-rw-r--r-- | infrastructure/ace/www/index.html (renamed from trunk/infrastructure/ace/www/index.html) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/inner.css (renamed from trunk/infrastructure/ace/www/inner.css) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/jquery-1.2.1.js (renamed from trunk/infrastructure/ace/www/jquery-1.2.1.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/lang_html.js (renamed from trunk/infrastructure/ace/www/lang_html.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/lang_js.js (renamed from trunk/infrastructure/ace/www/lang_js.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/lexer_support.js (renamed from trunk/infrastructure/ace/www/lexer_support.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/linestylefilter.js | 287 | ||||
-rw-r--r-- | infrastructure/ace/www/magicdom.js (renamed from trunk/infrastructure/ace/www/magicdom.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/multilang_lexer.js (renamed from trunk/infrastructure/ace/www/multilang_lexer.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/processing.js (renamed from trunk/infrastructure/ace/www/processing.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/profiler.js (renamed from trunk/infrastructure/ace/www/profiler.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/skiplist.js (renamed from trunk/infrastructure/ace/www/skiplist.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/spanlist.js (renamed from trunk/infrastructure/ace/www/spanlist.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/syntax-new.css (renamed from trunk/infrastructure/ace/www/syntax-new.css) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/syntax.css (renamed from trunk/infrastructure/ace/www/syntax.css) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/test.html (renamed from trunk/infrastructure/ace/www/test.html) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/testcode.js (renamed from trunk/infrastructure/ace/www/testcode.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/toSource.js (renamed from trunk/infrastructure/ace/www/toSource.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/undomodule.js (renamed from trunk/infrastructure/ace/www/undomodule.js) | 0 | ||||
-rw-r--r-- | infrastructure/ace/www/virtual_lines.js (renamed from trunk/infrastructure/ace/www/virtual_lines.js) | 0 |
56 files changed, 5901 insertions, 0 deletions
diff --git a/trunk/infrastructure/ace/.gitignore b/infrastructure/ace/.gitignore index 4083037..4083037 100644 --- a/trunk/infrastructure/ace/.gitignore +++ b/infrastructure/ace/.gitignore diff --git a/trunk/infrastructure/ace/README b/infrastructure/ace/README index 275684f..275684f 100644 --- a/trunk/infrastructure/ace/README +++ b/infrastructure/ace/README diff --git a/infrastructure/ace/bin/make b/infrastructure/ace/bin/make new file mode 100755 index 0000000..98a48f4 --- /dev/null +++ b/infrastructure/ace/bin/make @@ -0,0 +1,339 @@ +#!/bin/sh +mkdir -p ../../etherpad/src/etherpad/collab/ace +mkdir -p ../../etherpad/src/static/js +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; +} diff --git a/trunk/infrastructure/ace/bin/serve b/infrastructure/ace/bin/serve index e02e042..e02e042 100755 --- a/trunk/infrastructure/ace/bin/serve +++ b/infrastructure/ace/bin/serve diff --git a/trunk/infrastructure/ace/blog.txt b/infrastructure/ace/blog.txt index 0b095a2..0b095a2 100644 --- a/trunk/infrastructure/ace/blog.txt +++ b/infrastructure/ace/blog.txt diff --git a/trunk/infrastructure/ace/build/.gitignore b/infrastructure/ace/build/.gitignore index 4dc709e..4dc709e 100644 --- a/trunk/infrastructure/ace/build/.gitignore +++ b/infrastructure/ace/build/.gitignore diff --git a/trunk/infrastructure/ace/build/index.html b/infrastructure/ace/build/index.html index b8c8505..b8c8505 100644 --- a/trunk/infrastructure/ace/build/index.html +++ b/infrastructure/ace/build/index.html diff --git a/trunk/infrastructure/ace/build/jquery-1.2.1.js b/infrastructure/ace/build/jquery-1.2.1.js index b4eb132..b4eb132 100644 --- a/trunk/infrastructure/ace/build/jquery-1.2.1.js +++ b/infrastructure/ace/build/jquery-1.2.1.js diff --git a/trunk/infrastructure/ace/build/testcode.js b/infrastructure/ace/build/testcode.js index f393335..f393335 100644 --- a/trunk/infrastructure/ace/build/testcode.js +++ b/infrastructure/ace/build/testcode.js diff --git a/trunk/infrastructure/ace/easysync-notes.txt b/infrastructure/ace/easysync-notes.txt index 6808f40..6808f40 100644 --- a/trunk/infrastructure/ace/easysync-notes.txt +++ b/infrastructure/ace/easysync-notes.txt diff --git a/trunk/infrastructure/ace/lib/rhino-js-1.7r1.jar b/infrastructure/ace/lib/rhino-js-1.7r1.jar index f41e23b..f41e23b 120000 --- a/trunk/infrastructure/ace/lib/rhino-js-1.7r1.jar +++ b/infrastructure/ace/lib/rhino-js-1.7r1.jar diff --git a/trunk/infrastructure/ace/lib/yuicompressor-2.4-appjet.jar b/infrastructure/ace/lib/yuicompressor-2.4-appjet.jar index 3953af5..3953af5 120000 --- a/trunk/infrastructure/ace/lib/yuicompressor-2.4-appjet.jar +++ b/infrastructure/ace/lib/yuicompressor-2.4-appjet.jar diff --git a/trunk/infrastructure/ace/notes.txt b/infrastructure/ace/notes.txt index d9e1fda..d9e1fda 100644 --- a/trunk/infrastructure/ace/notes.txt +++ b/infrastructure/ace/notes.txt diff --git a/trunk/infrastructure/ace/www/ace2_common.js b/infrastructure/ace/www/ace2_common.js index 4a08de6..4a08de6 100644 --- a/trunk/infrastructure/ace/www/ace2_common.js +++ b/infrastructure/ace/www/ace2_common.js diff --git a/trunk/infrastructure/ace/www/ace2_common_dev.js b/infrastructure/ace/www/ace2_common_dev.js index 8fb88b0..8fb88b0 100644 --- a/trunk/infrastructure/ace/www/ace2_common_dev.js +++ b/infrastructure/ace/www/ace2_common_dev.js diff --git a/infrastructure/ace/www/ace2_inner.js b/infrastructure/ace/www/ace2_inner.js new file mode 100644 index 0000000..33c13e8 --- /dev/null +++ b/infrastructure/ace/www/ace2_inner.js @@ -0,0 +1,4809 @@ +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function OUTER(gscope) { + + var DEBUG=true;//$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;" + + var isSetUp = false; + + var THE_TAB = ' ';//4 + var MAX_LIST_LEVEL = 8; + + var LINE_NUMBER_PADDING_RIGHT = 4; + var LINE_NUMBER_PADDING_LEFT = 4; + var MIN_LINEDIV_WIDTH = 20; + var EDIT_BODY_PADDING_TOP = 8; + var EDIT_BODY_PADDING_LEFT = 8; + + var caughtErrors = []; + + var thisAuthor = ''; + + var disposed = false; + + var editorInfo = parent.editorInfo; + + var iframe = window.frameElement; + var outerWin = iframe.ace_outerWin; + iframe.ace_outerWin = null; // prevent IE 6 memory leak + var sideDiv = iframe.nextSibling; + var lineMetricsDiv = sideDiv.nextSibling; + var overlaysdiv = lineMetricsDiv.nextSibling; + initLineNumbers(); + + var outsideKeyDown = function(evt) {}; + var outsideKeyPress = function(evt) { return true; }; + var outsideNotifyDirty = function() {}; + + // selFocusAtStart -- determines whether the selection extends "backwards", so that the focus + // point (controlled with the arrow keys) is at the beginning; not supported in IE, though + // native IE selections have that behavior (which we try not to interfere with). + // Must be false if selection is collapsed! + var rep = { lines: newSkipList(), selStart: null, selEnd: null, selFocusAtStart: false, + alltext: "", alines: [], + apool: new AttribPool() }; + // lines, alltext, alines, and DOM are set up in setup() + if (undoModule.enabled) { + undoModule.apool = rep.apool; + } + + var root, doc; // set in setup() + + var isEditable = true; + var doesWrap = true; + var hasLineNumbers = true; + var isStyled = true; + + // space around the innermost iframe element + var iframePadLeft = MIN_LINEDIV_WIDTH + LINE_NUMBER_PADDING_RIGHT + EDIT_BODY_PADDING_LEFT; + var iframePadTop = EDIT_BODY_PADDING_TOP; + var iframePadBottom = 0, iframePadRight = 0; + + var console = (DEBUG && top.console); + if (! console) { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", + "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; + console = {}; + for (var i = 0; i < names.length; ++i) + console[names[i]] = function() {}; + //console.error = function(str) { alert(str); }; + } + var PROFILER = window.PROFILER; + if (!PROFILER) { + PROFILER = function() { return {start:noop, mark:noop, literal:noop, end:noop, cancel:noop}; }; + } + function noop() {} + function identity(x) { return x; } + + // "dmesg" is for displaying messages in the in-page output pane + // visible when "?djs=1" is appended to the pad URL. It generally + // remains a no-op unless djs is enabled, but we make a habit of + // only calling it in error cases or while debugging. + var dmesg = noop; + window.dmesg = noop; + + var scheduler = parent; + + var textFace = 'monospace'; + var textSize = 12; + function textLineHeight() { return Math.round(textSize * 4/3); } + + var dynamicCSS = null; + function initDynamicCSS() { + dynamicCSS = makeCSSManager("dynamicsyntax"); + } + + var changesetTracker = makeChangesetTracker(scheduler, rep.apool, { + withCallbacks: function(operationName, f) { + inCallStackIfNecessary(operationName, function() { + fastIncorp(1); + f({ + setDocumentAttributedText: function(atext) { + setDocAText(atext); + }, + applyChangesetToDocument: function(changeset, preferInsertionAfterCaret) { + var oldEventType = currentCallStack.editEvent.eventType; + currentCallStack.startNewEvent("nonundoable"); + + performDocumentApplyChangeset(changeset, preferInsertionAfterCaret); + + currentCallStack.startNewEvent(oldEventType); + } + }); + }); + } + }); + + var authorInfos = {}; // presence of key determines if author is present in doc + + function setAuthorInfo(author, info) { + if ((typeof author) != "string") { + throw new Error("setAuthorInfo: author ("+author+") is not a string"); + } + if (! info) { + delete authorInfos[author]; + if (dynamicCSS) { + dynamicCSS.removeSelectorStyle(getAuthorColorClassSelector(getAuthorClassName(author))); + } + } + else { + authorInfos[author] = info; + if (info.bgcolor) { + if (dynamicCSS) { + var bgcolor = info.bgcolor; + if ((typeof info.fade) == "number") { + bgcolor = fadeColor(bgcolor, info.fade); + } + + dynamicCSS.selectorStyle(getAuthorColorClassSelector( + getAuthorClassName(author))).backgroundColor = bgcolor; + } + } + } + } + + function getAuthorClassName(author) { + return "author-"+author.replace(/[^a-y0-9]/g, function(c) { + if (c == ".") return "-"; + return 'z'+c.charCodeAt(0)+'z'; + }); + } + function className2Author(className) { + if (className.substring(0,7) == "author-") { + return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, function(cc) { + if (cc == '-') return '.'; + else if (cc.charAt(0) == 'z') { + return String.fromCharCode(Number(cc.slice(1,-1))); + } + else { + return cc; + } + }); + } + return null; + } + function getAuthorColorClassSelector(oneClassName) { + return ".authorColors ."+oneClassName; + } + function setUpTrackingCSS() { + if (dynamicCSS) { + var backgroundHeight = lineMetricsDiv.offsetHeight; + var lineHeight = textLineHeight(); + var extraBodding = 0; + var extraTodding = 0; + if (backgroundHeight < lineHeight) { + extraBodding = Math.ceil((lineHeight - backgroundHeight)/2); + extraTodding = lineHeight - backgroundHeight - extraBodding; + } + var spanStyle = dynamicCSS.selectorStyle("#innerdocbody span"); + spanStyle.paddingTop = extraTodding+"px"; + spanStyle.paddingBottom = extraBodding+"px"; + } + } + function boldColorFromColor(lightColorCSS) { + var color = colorutils.css2triple(lightColorCSS); + + // amp up the saturation to full + color = colorutils.saturate(color); + + // normalize brightness based on luminosity + color = colorutils.scaleColor(color, 0, 0.5 / colorutils.luminosity(color)); + + return colorutils.triple2css(color); + } + function fadeColor(colorCSS, fadeFrac) { + var color = colorutils.css2triple(colorCSS); + color = colorutils.blend(color, [1,1,1], fadeFrac); + return colorutils.triple2css(color); + } + + function doAlert(str) { + scheduler.setTimeout(function() { alert(str); }, 0); + } + + var currentCallStack = null; + function inCallStack(type, action) { + if (disposed) return; + + if (currentCallStack) { + console.error("Can't enter callstack "+type+", already in "+ + currentCallStack.type); + } + + var profiling = false; + function profileRest() { + profiling = true; + console.profile(); + } + + function newEditEvent(eventType) { + return {eventType:eventType, backset: null}; + } + + function submitOldEvent(evt) { + if (rep.selStart && rep.selEnd) { + var selStartChar = + rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; + var selEndChar = + rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; + evt.selStart = selStartChar; + evt.selEnd = selEndChar; + evt.selFocusAtStart = rep.selFocusAtStart; + } + if (undoModule.enabled) { + var undoWorked = false; + try { + if (evt.eventType == "setup" || evt.eventType == "importText" || + evt.eventType == "setBaseText") { + undoModule.clearHistory(); + } + else if (evt.eventType == "nonundoable") { + if (evt.changeset) { + undoModule.reportExternalChange(evt.changeset); + } + } + else { + undoModule.reportEvent(evt); + } + undoWorked = true; + } + finally { + if (! undoWorked) { + undoModule.enabled = false; // for safety + } + } + } + } + + function startNewEvent(eventType, dontSubmitOld) { + var oldEvent = currentCallStack.editEvent; + if (! dontSubmitOld) { + submitOldEvent(oldEvent); + } + currentCallStack.editEvent = newEditEvent(eventType); + return oldEvent; + } + + currentCallStack = {type: type, docTextChanged: false, selectionAffected: false, + userChangedSelection: false, + domClean: false, profileRest:profileRest, + isUserChange: false, // is this a "user change" type of call-stack + repChanged: false, editEvent: newEditEvent(type), + startNewEvent:startNewEvent}; + var cleanExit = false; + var result; + try { + result = action(); + //console.log("Just did action for: "+type); + cleanExit = true; + } + catch (e) { + caughtErrors.push({error: e, time: +new Date()}); + dmesg(e.toString()); + throw e; + } + finally { + var cs = currentCallStack; + //console.log("Finished action for: "+type); + if (cleanExit) { + submitOldEvent(cs.editEvent); + if (cs.domClean && cs.type != "setup") { + if (cs.isUserChange) { + if (cs.repChanged) parenModule.notifyChange(); + else parenModule.notifyTick(); + } + recolorModule.recolorLines(); + if (cs.selectionAffected) { + updateBrowserSelectionFromRep(); + } + if ((cs.docTextChanged || cs.userChangedSelection) && cs.type != "applyChangesToBase") { + scrollSelectionIntoView(); + } + if (cs.docTextChanged && cs.type.indexOf("importText") < 0) { + outsideNotifyDirty(); + } + } + } + else { + // non-clean exit + if (currentCallStack.type == "idleWorkTimer") { + idleWorkTimer.atLeast(1000); + } + } + currentCallStack = null; + if (profiling) console.profileEnd(); + } + return result; + } + + function inCallStackIfNecessary(type, action) { + if (! currentCallStack) { + inCallStack(type, action); + } + else { + action(); + } + } + + function recolorLineByKey(key) { + if (rep.lines.containsKey(key)) { + var offset = rep.lines.offsetOfKey(key); + var width = rep.lines.atKey(key).width; + recolorLinesInRange(offset, offset + width); + } + } + + function getLineKeyForOffset(charOffset) { + return rep.lines.atOffset(charOffset).key; + } + + var recolorModule = (function() { + var dirtyLineKeys = {}; + + var module = {}; + module.setCharNeedsRecoloring = function(offset) { + if (offset >= rep.alltext.length) { + offset = rep.alltext.length-1; + } + dirtyLineKeys[getLineKeyForOffset(offset)] = true; + } + + module.setCharRangeNeedsRecoloring = function(offset1, offset2) { + if (offset1 >= rep.alltext.length) { + offset1 = rep.alltext.length-1; + } + if (offset2 >= rep.alltext.length) { + offset2 = rep.alltext.length-1; + } + var firstEntry = rep.lines.atOffset(offset1); + var lastKey = rep.lines.atOffset(offset2).key; + dirtyLineKeys[lastKey] = true; + var entry = firstEntry; + while (entry && entry.key != lastKey) { + dirtyLineKeys[entry.key] = true; + entry = rep.lines.next(entry); + } + } + + module.recolorLines = function() { + for(var k in dirtyLineKeys) { + recolorLineByKey(k); + } + dirtyLineKeys = {}; + } + + return module; + })(); + + var parenModule = (function() { + var module = {}; + module.notifyTick = function() { handleFlashing(false); }; + module.notifyChange = function() { handleFlashing(true); }; + module.shouldNormalizeOnChar = function (c) { + if (parenFlashRep.active) { + // avoid highlight style from carrying on to typed text + return true; + } + c = String.fromCharCode(c); + return !! (bracketMap[c]); + } + + var parenFlashRep = { active: false, whichChars: null, whichLineKeys: null, expireTime: null }; + var bracketMap = {'(': 1, ')':-1, '[':2, ']':-2, '{':3, '}':-3}; + var bracketRegex = /[{}\[\]()]/g; + function handleFlashing(docChanged) { + function getSearchRange(aroundLoc) { + var rng = getVisibleCharRange(); + var d = 100; // minimum radius + var e = 3000; // maximum radius; + if (rng[0] > aroundLoc-d) rng[0] = aroundLoc-d; + if (rng[0] < aroundLoc-e) rng[0] = aroundLoc-e; + if (rng[0] < 0) rng[0] = 0; + if (rng[1] < aroundLoc+d) rng[1] = aroundLoc+d; + if (rng[1] > aroundLoc+e) rng[1] = aroundLoc+e; + if (rng[1] > rep.lines.totalWidth()) rng[1] = rep.lines.totalWidth(); + return rng; + } + function findMatchingVisibleBracket(startLoc, forwards) { + var rng = getSearchRange(startLoc); + var str = rep.alltext.substring(rng[0], rng[1]); + var bstr = str.replace(bracketRegex, '('); // handy for searching + var loc = startLoc - rng[0]; + var bracketState = []; + var foundParen = false; + var goodParen = false; + function nextLoc() { + if (loc < 0) return; + if (forwards) loc++; else loc--; + if (loc < 0 || loc >= str.length) loc = -1; + if (loc >= 0) { + if (forwards) loc = bstr.indexOf('(', loc); + else loc = bstr.lastIndexOf('(', loc); + } + } + while ((! foundParen) && (loc >= 0)) { + if (getCharType(loc + rng[0]) == "p") { + var b = bracketMap[str.charAt(loc)]; // -1, 1, -2, 2, -3, 3 + var into = forwards; + var typ = b; + if (typ < 0) { into = ! into; typ = -typ; } + if (into) bracketState.push(typ); + else { + var recent = bracketState.pop(); + if (recent != typ) { + foundParen = true; goodParen = false; + } + else if (bracketState.length == 0) { + foundParen = true; goodParen = true; + } + } + } + //console.log(bracketState.toSource()); + if ((! foundParen) && (loc >= 0)) nextLoc(); + } + if (! foundParen) return null; + return {chr: (loc + rng[0]), good: goodParen}; + } + + var r = parenFlashRep; + var charsToHighlight = null; + var linesToUnhighlight = null; + if (r.active && (docChanged || (now() > r.expireTime))) { + linesToUnhighlight = r.whichLineKeys; + r.active = false; + } + if ((! r.active) && docChanged && isCaret() && caretColumn() > 0) { + var caret = caretDocChar(); + if (caret > 0 && getCharType(caret-1) == "p") { + var charBefore = rep.alltext.charAt(caret-1); + if (bracketMap[charBefore]) { + var lookForwards = (bracketMap[charBefore] > 0); + var findResult = findMatchingVisibleBracket(caret-1, lookForwards); + if (findResult) { + var mateLoc = findResult.chr; + var mateGood = findResult.good; + r.active = true; + charsToHighlight = {}; + charsToHighlight[caret-1] = 'flash'; + charsToHighlight[mateLoc] = (mateGood ? 'flash' : 'flashbad'); + r.whichLineKeys = []; + r.whichLineKeys.push(getLineKeyForOffset(caret-1)); + r.whichLineKeys.push(getLineKeyForOffset(mateLoc)); + r.expireTime = now() + 4000; + newlyActive = true; + } + } + } + + } + if (linesToUnhighlight) { + recolorLineByKey(linesToUnhighlight[0]); + recolorLineByKey(linesToUnhighlight[1]); + } + if (r.active && charsToHighlight) { + function f(txt, cls, next, ofst) { + var flashClass = charsToHighlight[ofst]; + if (cls) { + next(txt, cls+" "+flashClass); + } + else next(txt, cls); + } + for(var c in charsToHighlight) { + recolorLinesInRange((+c), (+c)+1, null, f); + } + } + } + + return module; + })(); + + function dispose() { + disposed = true; + if (idleWorkTimer) idleWorkTimer.never(); + teardown(); + } + + function checkALines() { + return; // disable for speed + function error() { throw new Error("checkALines"); } + if (rep.alines.length != rep.lines.length()) { + error(); + } + for(var i=0;i<rep.alines.length;i++) { + var aline = rep.alines[i]; + var lineText = rep.lines.atIndex(i).text+"\n"; + var lineTextLength = lineText.length; + var opIter = Changeset.opIterator(aline); + var alineLength = 0; + while (opIter.hasNext()) { + var o = opIter.next(); + alineLength += o.chars; + if (opIter.hasNext()) { + if (o.lines != 0) error(); + } + else { + if (o.lines != 1) error(); + } + } + if (alineLength != lineTextLength) { + error(); + } + } + } + + function setWraps(newVal) { + doesWrap = newVal; + var dwClass = "doesWrap"; + setClassPresence(root, "doesWrap", doesWrap); + scheduler.setTimeout(function() { + inCallStackIfNecessary("setWraps", function() { + fastIncorp(7); + recreateDOM(); + fixView(); + }); + }, 0); + } + + function setStyled(newVal) { + var oldVal = isStyled; + isStyled = !!newVal; + + if (newVal != oldVal) { + if (! newVal) { + // clear styles + inCallStackIfNecessary("setStyled", function() { + fastIncorp(12); + var clearStyles = []; + for(var k in STYLE_ATTRIBS) { + clearStyles.push([k,'']); + } + performDocumentApplyAttributesToCharRange(0, rep.alltext.length, clearStyles); + }); + } + } + } + + function setTextFace(face) { + textFace = face; + root.style.fontFamily = textFace; + lineMetricsDiv.style.fontFamily = textFace; + scheduler.setTimeout(function() { + setUpTrackingCSS(); + }, 0); + } + + function setTextSize(size) { + textSize = size; + root.style.fontSize = textSize+"px"; + root.style.lineHeight = textLineHeight()+"px"; + sideDiv.style.lineHeight = textLineHeight()+"px"; + lineMetricsDiv.style.fontSize = textSize+"px"; + scheduler.setTimeout(function() { + setUpTrackingCSS(); + }, 0); + } + + function recreateDOM() { + // precond: normalized + recolorLinesInRange(0, rep.alltext.length); + } + + function setEditable(newVal) { + isEditable = newVal; + + // the following may fail, e.g. if iframe is hidden + if (! isEditable) { + setDesignMode(false); + } + else { + setDesignMode(true); + } + setClassPresence(root, "static", ! isEditable); + } + + function enforceEditability() { + setEditable(isEditable); + } + + function importText(text, undoable, dontProcess) { + var lines; + if (dontProcess) { + if (text.charAt(text.length-1) != "\n") { + throw new Error("new raw text must end with newline"); + } + if (/[\r\t\xa0]/.exec(text)) { + throw new Error("new raw text must not contain CR, tab, or nbsp"); + } + lines = text.substring(0, text.length-1).split('\n'); + } + else { + lines = map(text.split('\n'), textify); + } + var newText = "\n"; + if (lines.length > 0) { + newText = lines.join('\n')+'\n'; + } + + inCallStackIfNecessary("importText"+(undoable?"Undoable":""), function() { + setDocText(newText); + }); + + if (dontProcess && rep.alltext != text) { + throw new Error("mismatch error setting raw text in importText"); + } + } + + function importAText(atext, apoolJsonObj, undoable) { + atext = Changeset.cloneAText(atext); + if (apoolJsonObj) { + var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); + atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool); + } + inCallStackIfNecessary("importText"+(undoable?"Undoable":""), function() { + setDocAText(atext); + }); + } + + function setDocAText(atext) { + fastIncorp(8); + + var oldLen = rep.lines.totalWidth(); + var numLines = rep.lines.length(); + var upToLastLine = rep.lines.offsetOfIndex(numLines-1); + var lastLineLength = rep.lines.atIndex(numLines-1).text.length; + var assem = Changeset.smartOpAssembler(); + var o = Changeset.newOp('-'); + o.chars = upToLastLine; + o.lines = numLines-1; + assem.append(o); + o.chars = lastLineLength; + o.lines = 0; + assem.append(o); + Changeset.appendATextToAssembler(atext, assem); + var newLen = oldLen + assem.getLengthChange(); + var changeset = Changeset.checkRep( + Changeset.pack(oldLen, newLen, assem.toString(), + atext.text.slice(0, -1))); + performDocumentApplyChangeset(changeset); + + performSelectionChange([0,rep.lines.atIndex(0).lineMarker], + [0,rep.lines.atIndex(0).lineMarker]); + + idleWorkTimer.atMost(100); + + if (rep.alltext != atext.text) { + dmesg(htmlPrettyEscape(rep.alltext)); + dmesg(htmlPrettyEscape(atext.text)); + throw new Error("mismatch error setting raw text in setDocAText"); + } + } + + function setDocText(text) { + setDocAText(Changeset.makeAText(text)); + } + + function getDocText() { + var alltext = rep.alltext; + var len = alltext.length; + if (len > 0) len--; // final extra newline + return alltext.substring(0, len); + } + + function exportText() { + if (currentCallStack && ! currentCallStack.domClean) { + inCallStackIfNecessary("exportText", function() { fastIncorp(2); }); + } + return getDocText(); + } + + function editorChangedSize() { + fixView(); + } + + function setOnKeyPress(handler) { + outsideKeyPress = handler; + } + + function setOnKeyDown(handler) { + outsideKeyDown = handler; + } + + function setNotifyDirty(handler) { + outsideNotifyDirty = handler; + } + + function getFormattedCode() { + if (currentCallStack && ! currentCallStack.domClean) { + inCallStackIfNecessary("getFormattedCode", incorporateUserChanges); + } + var buf = []; + if (rep.lines.length() > 0) { + // should be the case, even for empty file + var entry = rep.lines.atIndex(0); + while (entry) { + var domInfo = entry.domInfo; + buf.push((domInfo && domInfo.getInnerHTML()) || + domline.processSpaces(domline.escapeHTML(entry.text), + doesWrap) || + ' ' /*empty line*/); + entry = rep.lines.next(entry); + } + } + return '<div class="syntax"><div>'+buf.join('</div>\n<div>')+ + '</div></div>'; + } + + var CMDS = { + bold: function() { toggleAttributeOnSelection('bold'); }, + italic: function() { toggleAttributeOnSelection('italic'); }, + underline: function() { toggleAttributeOnSelection('underline'); }, + strikethrough: function() { toggleAttributeOnSelection('strikethrough'); }, + h1: function() { toggleAttributeOnSelection('h1'); }, + h2: function() { toggleAttributeOnSelection('h2'); }, + h3: function() { toggleAttributeOnSelection('h3'); }, + h4: function() { toggleAttributeOnSelection('h4'); }, + h5: function() { toggleAttributeOnSelection('h5'); }, + h6: function() { toggleAttributeOnSelection('h6'); }, + undo: function() { doUndoRedo('undo'); }, + redo: function() { doUndoRedo('redo'); }, + clearauthorship: function(prompt) { + if ((!(rep.selStart && rep.selEnd)) || isCaret()) { + if (prompt) { + prompt(); + } + else { + performDocumentApplyAttributesToCharRange(0, rep.alltext.length, + [['author', '']]); + } + } + else { + setAttributeOnSelection('author', ''); + } + }, + insertunorderedlist: doInsertUnorderedList, + indent: function() { + if (! doIndentOutdent(false)) { + doInsertUnorderedList(); + } + }, + outdent: function() { doIndentOutdent(true); } + }; + + function execCommand(cmd) { + cmd = cmd.toLowerCase(); + var cmdArgs = Array.prototype.slice.call(arguments, 1); + if (CMDS[cmd]) { + inCallStack(cmd, function() { + fastIncorp(9); + CMDS[cmd].apply(CMDS, cmdArgs); + }); + } + } + + editorInfo.ace_focus = focus; + editorInfo.ace_importText = importText; + editorInfo.ace_importAText = importAText; + editorInfo.ace_exportText = exportText; + editorInfo.ace_editorChangedSize = editorChangedSize; + editorInfo.ace_setOnKeyPress = setOnKeyPress; + editorInfo.ace_setOnKeyDown = setOnKeyDown; + editorInfo.ace_setNotifyDirty = setNotifyDirty; + editorInfo.ace_dispose = dispose; + editorInfo.ace_getFormattedCode = getFormattedCode; + editorInfo.ace_setEditable = setEditable; + editorInfo.ace_execCommand = execCommand; + + editorInfo.ace_setProperty = function(key, value) { + var k = key.toLowerCase(); + if (k == "wraps") { + setWraps(value); + } + else if (k == "showsauthorcolors") { + setClassPresence(root, "authorColors", !!value); + } + else if (k == "showsuserselections") { + setClassPresence(root, "userSelections", !!value); + } + else if (k == "showslinenumbers") { + hasLineNumbers = !!value; + setClassPresence(sideDiv, "sidedivhidden", ! hasLineNumbers); + fixView(); + } + else if (k == "grayedout") { + setClassPresence(outerWin.document.body, "grayedout", !!value); + } + else if (k == "dmesg") { + dmesg = value; + window.dmesg = value; + } + else if (k == 'userauthor') { + thisAuthor = String(value); + } + else if (k == 'styled') { + setStyled(value); + } + else if (k == 'textface') { + setTextFace(value); + } + else if (k == 'textsize') { + setTextSize(value); + } + } + + editorInfo.ace_setBaseText = function(txt) { + changesetTracker.setBaseText(txt); + }; + editorInfo.ace_setBaseAttributedText = function(atxt, apoolJsonObj) { + setUpTrackingCSS(); + changesetTracker.setBaseAttributedText(atxt, apoolJsonObj); + }; + editorInfo.ace_applyChangesToBase = function(c, optAuthor, apoolJsonObj) { + changesetTracker.applyChangesToBase(c, optAuthor, apoolJsonObj); + }; + editorInfo.ace_prepareUserChangeset = function() { + return changesetTracker.prepareUserChangeset(); + }; + editorInfo.ace_applyPreparedChangesetToBase = function() { + changesetTracker.applyPreparedChangesetToBase(); + }; + editorInfo.ace_setUserChangeNotificationCallback = function(f) { + changesetTracker.setUserChangeNotificationCallback(f); + }; + editorInfo.ace_setAuthorInfo = function(author, info) { + setAuthorInfo(author, info); + }; + editorInfo.ace_setAuthorSelectionRange = function(author, start, end) { + changesetTracker.setAuthorSelectionRange(author, start, end); + }; + + editorInfo.ace_getUnhandledErrors = function() { + return caughtErrors.slice(); + }; + + editorInfo.ace_getDebugProperty = function(prop) { + if (prop == "debugger") { + // obfuscate "eval" so as not to scare yuicompressor + window['ev'+'al']("debugger"); + } + else if (prop == "rep") { + return rep; + } + else if (prop == "window") { + return window; + } + else if (prop == "document") { + return document; + } + return undefined; + }; + + function now() { return (new Date()).getTime(); } + + function newTimeLimit(ms) { + //console.debug("new time limit"); + var startTime = now(); + var lastElapsed = 0; + var exceededAlready = false; + var printedTrace = false; + var isTimeUp = function () { + if (exceededAlready) { + if ((! printedTrace)) {// && now() - startTime - ms > 300) { + //console.trace(); + printedTrace = true; + } + return true; + } + var elapsed = now() - startTime; + if (elapsed > ms) { + exceededAlready = true; + //console.debug("time limit hit, before was %d/%d", lastElapsed, ms); + //console.trace(); + return true; + } + else { + lastElapsed = elapsed; + return false; + } + } + isTimeUp.elapsed = function() { return now() - startTime; } + return isTimeUp; + } + + + function makeIdleAction(func) { + var scheduledTimeout = null; + var scheduledTime = 0; + function unschedule() { + if (scheduledTimeout) { + scheduler.clearTimeout(scheduledTimeout); + scheduledTimeout = null; + } + } + function reschedule(time) { + unschedule(); + scheduledTime = time; + var delay = time - now(); + if (delay < 0) delay = 0; + scheduledTimeout = scheduler.setTimeout(callback, delay); + } + function callback() { + scheduledTimeout = null; + // func may reschedule the action + func(); + } + return { + atMost: function (ms) { + var latestTime = now() + ms; + if ((! scheduledTimeout) || scheduledTime > latestTime) { + reschedule(latestTime); + } + }, + // atLeast(ms) will schedule the action if not scheduled yet. + // In other words, "infinity" is replaced by ms, even though + // it is technically larger. + atLeast: function (ms) { + var earliestTime = now()+ms; + if ((! scheduledTimeout) || scheduledTime < earliestTime) { + reschedule(earliestTime); + } + }, + never: function() { + unschedule(); + } + } + } + + function fastIncorp(n) { + // normalize but don't do any lexing or anything + incorporateUserChanges(newTimeLimit(0)); + } + + function incorpIfQuick() { + var me = incorpIfQuick; + var failures = (me.failures || 0); + if (failures < 5) { + var isTimeUp = newTimeLimit(40); + var madeChanges = incorporateUserChanges(isTimeUp); + if (isTimeUp()) { + me.failures = failures+1; + } + return true; + } + else { + var skipCount = (me.skipCount || 0); + skipCount++; + if (skipCount == 20) { + skipCount = 0; + me.failures = 0; + } + me.skipCount = skipCount; + } + return false; + } + + var idleWorkTimer = makeIdleAction(function() { + + //if (! top.BEFORE) top.BEFORE = []; + //top.BEFORE.push(magicdom.root.dom.innerHTML); + + if (! isEditable) return; // and don't reschedule + + if (inInternationalComposition) { + // don't do idle input incorporation during international input composition + idleWorkTimer.atLeast(500); + return; + } + + inCallStack("idleWorkTimer", function() { + + var isTimeUp = newTimeLimit(250); + + //console.time("idlework"); + + var finishedImportantWork = false; + var finishedWork = false; + + try { + + // isTimeUp() is a soft constraint for incorporateUserChanges, + // which always renormalizes the DOM, no matter how long it takes, + // but doesn't necessarily lex and highlight it + incorporateUserChanges(isTimeUp); + + if (isTimeUp()) return; + + updateLineNumbers(); // update line numbers if any time left + + if (isTimeUp()) return; + + var visibleRange = getVisibleCharRange(); + var docRange = [0, rep.lines.totalWidth()]; + //console.log("%o %o", docRange, visibleRange); + + finishedImportantWork = true; + finishedWork = true; + } + finally { + //console.timeEnd("idlework"); + if (finishedWork) { + idleWorkTimer.atMost(1000); + } + else if (finishedImportantWork) { + // if we've finished highlighting the view area, + // more highlighting could be counter-productive, + // e.g. if the user just opened a triple-quote and will soon close it. + idleWorkTimer.atMost(500); + } + else { + var timeToWait = Math.round(isTimeUp.elapsed() / 2); + if (timeToWait < 100) timeToWait = 100; + idleWorkTimer.atMost(timeToWait); + } + } + }); + + //if (! top.AFTER) top.AFTER = []; + //top.AFTER.push(magicdom.root.dom.innerHTML); + + }); + + var _nextId = 1; + function uniqueId(n) { + // not actually guaranteed to be unique, e.g. if user copy-pastes + // nodes with ids + var nid = n.id; + if (nid) return nid; + return (n.id = "magicdomid"+(_nextId++)); + } + + + function recolorLinesInRange(startChar, endChar, isTimeUp, optModFunc) { + if (endChar <= startChar) return; + if (startChar < 0 || startChar >= rep.lines.totalWidth()) return; + var lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary + var lineStart = rep.lines.offsetOfEntry(lineEntry); + var lineIndex = rep.lines.indexOfEntry(lineEntry); + var selectionNeedsResetting = false; + var firstLine = null; + var lastLine = null; + isTimeUp = (isTimeUp || noop); + + // tokenFunc function; accesses current value of lineEntry and curDocChar, + // also mutates curDocChar + var curDocChar; + var tokenFunc = function(tokenText, tokenClass) { + lineEntry.domInfo.appendSpan(tokenText, tokenClass); + }; + if (optModFunc) { + var f = tokenFunc; + tokenFunc = function(tokenText, tokenClass) { + optModFunc(tokenText, tokenClass, f, curDocChar); + curDocChar += tokenText.length; + }; + } + + while (lineEntry && lineStart < endChar && ! isTimeUp()) { + //var timer = newTimeLimit(200); + var lineEnd = lineStart + lineEntry.width; + + curDocChar = lineStart; + lineEntry.domInfo.clearSpans(); + getSpansForLine(lineEntry, tokenFunc, lineStart); + lineEntry.domInfo.finishUpdate(); + + markNodeClean(lineEntry.lineNode); + + if (rep.selStart && rep.selStart[0] == lineIndex || + rep.selEnd && rep.selEnd[0] == lineIndex) { + selectionNeedsResetting = true; + } + + //if (timer()) console.dirxml(lineEntry.lineNode.dom); + + if (firstLine === null) firstLine = lineIndex; + lastLine = lineIndex; + lineStart = lineEnd; + lineEntry = rep.lines.next(lineEntry); + lineIndex++; + } + if (selectionNeedsResetting) { + currentCallStack.selectionAffected = true; + } + //console.debug("Recolored line range %d-%d", firstLine, lastLine); + } + + // like getSpansForRange, but for a line, and the func takes (text,class) + // instead of (width,class); excludes the trailing '\n' from + // consideration by func + function getSpansForLine(lineEntry, textAndClassFunc, lineEntryOffsetHint) { + var lineEntryOffset = lineEntryOffsetHint; + if ((typeof lineEntryOffset) != "number") { + lineEntryOffset = rep.lines.offsetOfEntry(lineEntry); + } + var text = lineEntry.text; + var width = lineEntry.width; // text.length+1 + + if (text.length == 0) { + // allow getLineStyleFilter to set line-div styles + var func = linestylefilter.getLineStyleFilter( + 0, '', textAndClassFunc, rep.apool); + func('', ''); + } + else { + var offsetIntoLine = 0; + var filteredFunc = linestylefilter.getFilterStack(text, textAndClassFunc, browser); + var lineNum = rep.lines.indexOfEntry(lineEntry); + var aline = rep.alines[lineNum]; + filteredFunc = linestylefilter.getLineStyleFilter( + text.length, aline, filteredFunc, rep.apool); + filteredFunc(text, ''); + } + } + + + function getCharType(charIndex) { + return ''; + } + + var observedChanges; + function clearObservedChanges() { + observedChanges = { cleanNodesNearChanges: {} }; + } + clearObservedChanges(); + + function getCleanNodeByKey(key) { + var p = PROFILER("getCleanNodeByKey", false); + p.extra = 0; + var n = doc.getElementById(key); + // copying and pasting can lead to duplicate ids + while (n && isNodeDirty(n)) { + p.extra++; + n.id = ""; + n = doc.getElementById(key); + } + p.literal(p.extra, "extra"); + p.end(); + return n; + } + + function observeChangesAroundNode(node) { + // Around this top-level DOM node, look for changes to the document + // (from how it looks in our representation) and record them in a way + // that can be used to "normalize" the document (apply the changes to our + // representation, and put the DOM in a canonical form). + + //top.console.log("observeChangesAroundNode(%o)", node); + + var cleanNode; + var hasAdjacentDirtyness; + if (! isNodeDirty(node)) { + cleanNode = node; + var prevSib = cleanNode.previousSibling; + var nextSib = cleanNode.nextSibling; + hasAdjacentDirtyness = ((prevSib && isNodeDirty(prevSib)) + || (nextSib && isNodeDirty(nextSib))); + } + else { + // node is dirty, look for clean node above + var upNode = node.previousSibling; + while (upNode && isNodeDirty(upNode)) { + upNode = upNode.previousSibling; + } + if (upNode) { + cleanNode = upNode; + } + else { + var downNode = node.nextSibling; + while (downNode && isNodeDirty(downNode)) { + downNode = downNode.nextSibling; + } + if (downNode) { + cleanNode = downNode; + } + } + if (! cleanNode) { + // Couldn't find any adjacent clean nodes! + // Since top and bottom of doc is dirty, the dirty area will be detected. + return; + } + hasAdjacentDirtyness = true; + } + + if (hasAdjacentDirtyness) { + // previous or next line is dirty + observedChanges.cleanNodesNearChanges['$'+uniqueId(cleanNode)] = true; + } + else { + // next and prev lines are clean (if they exist) + var lineKey = uniqueId(cleanNode); + var prevSib = cleanNode.previousSibling; + var nextSib = cleanNode.nextSibling; + var actualPrevKey = ((prevSib && uniqueId(prevSib)) || null); + var actualNextKey = ((nextSib && uniqueId(nextSib)) || null); + var repPrevEntry = rep.lines.prev(rep.lines.atKey(lineKey)); + var repNextEntry = rep.lines.next(rep.lines.atKey(lineKey)); + var repPrevKey = ((repPrevEntry && repPrevEntry.key) || null); + var repNextKey = ((repNextEntry && repNextEntry.key) || null); + if (actualPrevKey != repPrevKey || actualNextKey != repNextKey) { + observedChanges.cleanNodesNearChanges['$'+uniqueId(cleanNode)] = true; + } + } + } + + function observeChangesAroundSelection() { + if (currentCallStack.observedSelection) return; + currentCallStack.observedSelection = true; + + var p = PROFILER("getSelection", false); + var selection = getSelection(); + p.end(); + if (selection) { + function topLevel(n) { + if ((!n) || n == root) return null; + while (n.parentNode != root) { + n = n.parentNode; + } + return n; + } + var node1 = topLevel(selection.startPoint.node); + var node2 = topLevel(selection.endPoint.node); + if (node1) observeChangesAroundNode(node1); + if (node2 && node1 != node2) { + observeChangesAroundNode(node2); + } + } + } + + function observeSuspiciousNodes() { + // inspired by Firefox bug #473255, where pasting formatted text + // causes the cursor to jump away, making the new HTML never found. + if (root.getElementsByTagName) { + var nds = root.getElementsByTagName("style"); + for(var i=0;i<nds.length;i++) { + var n = nds[i]; + while (n.parentNode && n.parentNode != root) { + n = n.parentNode; + } + if (n.parentNode == root) { + observeChangesAroundNode(n); + } + } + } + } + + function incorporateUserChanges(isTimeUp) { + + if (currentCallStack.domClean) return false; + + inInternationalComposition = false; // if we need the document normalized, so be it + + currentCallStack.isUserChange = true; + + isTimeUp = (isTimeUp || function() { return false; }); + + if (DEBUG && top.DONT_INCORP || window.DEBUG_DONT_INCORP) return false; + + var p = PROFILER("incorp", false); + + //if (doc.body.innerHTML.indexOf("AppJet") >= 0) + //dmesg(htmlPrettyEscape(doc.body.innerHTML)); + //if (top.RECORD) top.RECORD.push(doc.body.innerHTML); + + // returns true if dom changes were made + + if (! root.firstChild) { + root.innerHTML = "<div><!-- --></div>"; + } + + p.mark("obs"); + observeChangesAroundSelection(); + observeSuspiciousNodes(); + p.mark("dirty"); + var dirtyRanges = getDirtyRanges(); + //console.log("dirtyRanges: "+toSource(dirtyRanges)); + + var dirtyRangesCheckOut = true; + var j = 0; + var a,b; + while (j < dirtyRanges.length) { + a = dirtyRanges[j][0]; + b = dirtyRanges[j][1]; + if (! ((a == 0 || getCleanNodeByKey(rep.lines.atIndex(a-1).key)) && + (b == rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key)))) { + dirtyRangesCheckOut = false; + break; + } + j++; + } + if (! dirtyRangesCheckOut) { + var numBodyNodes = root.childNodes.length; + for(var k=0;k<numBodyNodes;k++) { + var bodyNode = root.childNodes.item(k); + if ((bodyNode.tagName) && ((! bodyNode.id) || (! rep.lines.containsKey(bodyNode.id)))) { + observeChangesAroundNode(bodyNode); + } + } + dirtyRanges = getDirtyRanges(); + } + + clearObservedChanges(); + + p.mark("getsel"); + var selection = getSelection(); + + //console.log(magicdom.root.dom.innerHTML); + //console.log("got selection: %o", selection); + var selStart, selEnd; // each one, if truthy, has [line,char] needed to set selection + + var i = 0; + var splicesToDo = []; + var netNumLinesChangeSoFar = 0; + var toDeleteAtEnd = []; + p.mark("ranges"); + p.literal(dirtyRanges.length, "numdirt"); + var domInsertsNeeded = []; // each entry is [nodeToInsertAfter, [info1, info2, ...]] + while (i < dirtyRanges.length) { + var range = dirtyRanges[i]; + a = range[0]; + b = range[1]; + var firstDirtyNode = (((a == 0) && root.firstChild) || + getCleanNodeByKey(rep.lines.atIndex(a-1).key).nextSibling); + firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode); + var lastDirtyNode = (((b == rep.lines.length()) && root.lastChild) || + getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling); + lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode); + if (firstDirtyNode && lastDirtyNode) { + var cc = makeContentCollector(isStyled, browser, rep.apool, null, + className2Author); + cc.notifySelection(selection); + var dirtyNodes = []; + for(var n = firstDirtyNode; n && ! (n.previousSibling && + n.previousSibling == lastDirtyNode); + n = n.nextSibling) { + if (browser.msie) { + // try to undo IE's pesky and overzealous linkification + try { n.createTextRange().execCommand("unlink", false, null); } + catch (e) {} + } + cc.collectContent(n); + dirtyNodes.push(n); + } + cc.notifyNextNode(lastDirtyNode.nextSibling); + var lines = cc.getLines(); + if ((lines.length <= 1 || lines[lines.length-1] !== "") + && lastDirtyNode.nextSibling) { + // dirty region doesn't currently end a line, even taking the following node + // (or lack of node) into account, so include the following clean node. + // It could be SPAN or a DIV; basically this is any case where the contentCollector + // decides it isn't done. + // Note that this clean node might need to be there for the next dirty range. + //console.log("inclusive of "+lastDirtyNode.next().dom.tagName); + b++; + var cleanLine = lastDirtyNode.nextSibling; + cc.collectContent(cleanLine); + toDeleteAtEnd.push(cleanLine); + cc.notifyNextNode(cleanLine.nextSibling); + } + + var ccData = cc.finish(); + var ss = ccData.selStart; + var se = ccData.selEnd; + lines = ccData.lines; + var lineAttribs = ccData.lineAttribs; + var linesWrapped = ccData.linesWrapped; + + if (linesWrapped > 0) { + doAlert("Editor warning: "+linesWrapped+" long line"+ + (linesWrapped == 1 ? " was" : "s were")+" hard-wrapped into "+ + ccData.numLinesAfter + +" lines."); + } + + if (ss[0] >= 0) selStart = [ss[0]+a+netNumLinesChangeSoFar, ss[1]]; + if (se[0] >= 0) selEnd = [se[0]+a+netNumLinesChangeSoFar, se[1]]; + + /*var oldLines = rep.alltext.substring(rep.lines.offsetOfIndex(a), + rep.lines.offsetOfIndex(b)); + var newLines = lines.join('\n')+'\n'; + dmesg("OLD: "+htmlPrettyEscape(oldLines)); + dmesg("NEW: "+htmlPrettyEscape(newLines));*/ + + var entries = []; + var nodeToAddAfter = lastDirtyNode; + var lineNodeInfos = new Array(lines.length); + for(var k=0;k<lines.length;k++) { + var lineString = lines[k]; + var newEntry = createDomLineEntry(lineString); + entries.push(newEntry); + lineNodeInfos[k] = newEntry.domInfo; + } + //var fragment = magicdom.wrapDom(document.createDocumentFragment()); + domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]); + forEach(dirtyNodes, function (n) { toDeleteAtEnd.push(n); }); + var spliceHints = {}; + if (selStart) spliceHints.selStart = selStart; + if (selEnd) spliceHints.selEnd = selEnd; + splicesToDo.push([a+netNumLinesChangeSoFar, b-a, entries, lineAttribs, spliceHints]); + netNumLinesChangeSoFar += (lines.length - (b-a)); + } + else if (b > a) { + splicesToDo.push([a+netNumLinesChangeSoFar, b-a, [], []]); + } + i++; + } + + var domChanges = (splicesToDo.length > 0); + + // update the representation + p.mark("splice"); + forEach(splicesToDo, function (splice) { + doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]); + }); + + //p.mark("relex"); + //rep.lexer.lexCharRange(getVisibleCharRange(), function() { return false; }); + //var isTimeUp = newTimeLimit(100); + + // do DOM inserts + p.mark("insert"); + forEach(domInsertsNeeded, function (ins) { + insertDomLines(ins[0], ins[1], isTimeUp); + }); + + p.mark("del"); + // delete old dom nodes + forEach(toDeleteAtEnd, function (n) { + //var id = n.uniqueId(); + + // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf) + n.parentNode.removeChild(n); + + //dmesg(htmlPrettyEscape(htmlForRemovedChild(n))); + //console.log("removed: "+id); + }); + + p.mark("findsel"); + // if the nodes that define the selection weren't encountered during + // content collection, figure out where those nodes are now. + if (selection && !selStart) { + //if (domChanges) dmesg("selection not collected"); + selStart = getLineAndCharForPoint(selection.startPoint); + } + if (selection && !selEnd) { + selEnd = getLineAndCharForPoint(selection.endPoint); + } + + // selection from content collection can, in various ways, extend past final + // BR in firefox DOM, so cap the line + var numLines = rep.lines.length(); + if (selStart && selStart[0] >= numLines) { + selStart[0] = numLines-1; + selStart[1] = rep.lines.atIndex(selStart[0]).text.length; + } + if (selEnd && selEnd[0] >= numLines) { + selEnd[0] = numLines-1; + selEnd[1] = rep.lines.atIndex(selEnd[0]).text.length; + } + + p.mark("repsel"); + // update rep + repSelectionChange(selStart, selEnd, selection && selection.focusAtStart); + // update browser selection + p.mark("browsel"); + if (selection && (domChanges || isCaret())) { + // if no DOM changes (not this case), want to treat range selection delicately, + // e.g. in IE not lose which end of the selection is the focus/anchor; + // on the other hand, we may have just noticed a press of PageUp/PageDown + currentCallStack.selectionAffected = true; + } + + currentCallStack.domClean = true; + + p.mark("fixview"); + + fixView(); + + p.end("END"); + + return domChanges; + } + + function htmlForRemovedChild(n) { + var div = doc.createElement("DIV"); + div.appendChild(n); + return div.innerHTML; + } + + var STYLE_ATTRIBS = {bold: true, italic: true, underline: true, + strikethrough: true, h1: true, h2: true, + h3: true, h4: true, h5: true, h6: true, + list: true}; + var OTHER_INCORPED_ATTRIBS = {insertorder: true, author: true}; + + function isStyleAttribute(aname) { + return !! STYLE_ATTRIBS[aname]; + } + function isIncorpedAttribute(aname) { + return (!! STYLE_ATTRIBS[aname]) || (!! OTHER_INCORPED_ATTRIBS[aname]); + } + + function insertDomLines(nodeToAddAfter, infoStructs, isTimeUp) { + isTimeUp = (isTimeUp || function() { return false; }); + + var lastEntry; + var lineStartOffset; + if (infoStructs.length < 1) return; + var startEntry = rep.lines.atKey(uniqueId(infoStructs[0].node)); + var endEntry = rep.lines.atKey(uniqueId(infoStructs[infoStructs.length-1].node)); + var charStart = rep.lines.offsetOfEntry(startEntry); + var charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width; + + //rep.lexer.lexCharRange([charStart, charEnd], isTimeUp); + + forEach(infoStructs, function (info) { + var p2 = PROFILER("insertLine", false); + var node = info.node; + var key = uniqueId(node); + var entry; + p2.mark("findEntry"); + if (lastEntry) { + // optimization to avoid recalculation + var next = rep.lines.next(lastEntry); + if (next && next.key == key) { + entry = next; + lineStartOffset += lastEntry.width; + } + } + if (! entry) { + p2.literal(1, "nonopt"); + entry = rep.lines.atKey(key); + lineStartOffset = rep.lines.offsetOfKey(key); + } + else p2.literal(0, "nonopt"); + lastEntry = entry; + p2.mark("spans"); + getSpansForLine(entry, function (tokenText, tokenClass) { + info.appendSpan(tokenText, tokenClass); + }, lineStartOffset, isTimeUp()); + //else if (entry.text.length > 0) { + //info.appendSpan(entry.text, 'dirty'); + //} + p2.mark("addLine"); + info.prepareForAdd(); + entry.lineMarker = info.lineMarker; + if (! nodeToAddAfter) { + root.insertBefore(node, root.firstChild); + } + else { + root.insertBefore(node, nodeToAddAfter.nextSibling); + } + nodeToAddAfter = node; + info.notifyAdded(); + p2.mark("markClean"); + markNodeClean(node); + p2.end(); + }); + } + + function isCaret() { + return (rep.selStart && rep.selEnd && rep.selStart[0] == rep.selEnd[0] && + rep.selStart[1] == rep.selEnd[1]); + } + + // prereq: isCaret() + function caretLine() { return rep.selStart[0]; } + function caretColumn() { return rep.selStart[1]; } + function caretDocChar() { + return rep.lines.offsetOfIndex(caretLine()) + caretColumn(); + } + + function handleReturnIndentation() { + // on return, indent to level of previous line + if (isCaret() && caretColumn() == 0 && caretLine() > 0) { + var lineNum = caretLine(); + var thisLine = rep.lines.atIndex(lineNum); + var prevLine = rep.lines.prev(thisLine); + var prevLineText = prevLine.text; + var theIndent = /^ *(?:)/.exec(prevLineText)[0]; + if (/[\[\(\{]\s*$/.exec(prevLineText)) theIndent += THE_TAB; + var cs = Changeset.builder(rep.lines.totalWidth()).keep( + rep.lines.offsetOfIndex(lineNum), lineNum).insert( + theIndent, [['author',thisAuthor]], rep.apool).toString(); + performDocumentApplyChangeset(cs); + performSelectionChange([lineNum, theIndent.length], [lineNum, theIndent.length]); + } + } + + + function setupMozillaCaretHack(lineNum) { + // This is really ugly, but by god, it works! + // Fixes annoying Firefox caret artifact (observed in 2.0.0.12 + // and unfixed in Firefox 2 as of now) where mutating the DOM + // and then moving the caret to the beginning of a line causes + // an image of the caret to be XORed at the top of the iframe. + // The previous solution involved remembering to set the selection + // later, in response to the next event in the queue, which was hugely + // annoying. + // This solution: add a space character (0x20) to the beginning of the line. + // After setting the selection, remove the space. + var lineNode = rep.lines.atIndex(lineNum).lineNode; + + var fc = lineNode.firstChild; + while (isBlockElement(fc) && fc.firstChild) { + fc = fc.firstChild; + } + var textNode; + if (isNodeText(fc)) { + fc.nodeValue = " "+fc.nodeValue; + textNode = fc; + } + else { + textNode = doc.createTextNode(" "); + fc.parentNode.insertBefore(textNode, fc); + } + markNodeClean(lineNode); + return { unhack: function() { + if (textNode.nodeValue == " ") { + textNode.parentNode.removeChild(textNode); + } + else { + textNode.nodeValue = textNode.nodeValue.substring(1); + } + markNodeClean(lineNode); + } }; + } + + + function getPointForLineAndChar(lineAndChar) { + var line = lineAndChar[0]; + var charsLeft = lineAndChar[1]; + //console.log("line: %d, key: %s, node: %o", line, rep.lines.atIndex(line).key, + //getCleanNodeByKey(rep.lines.atIndex(line).key)); + var lineEntry = rep.lines.atIndex(line); + charsLeft -= lineEntry.lineMarker; + if (charsLeft < 0) { + charsLeft = 0; + } + var lineNode = lineEntry.lineNode; + var n = lineNode; + var after = false; + if (charsLeft == 0) { + var index = 0; + if (browser.msie && line == (rep.lines.length()-1) && lineNode.childNodes.length == 0) { + // best to stay at end of last empty div in IE + index = 1; + } + return {node: lineNode, index:index, maxIndex:1}; + } + while (!(n == lineNode && after)) { + if (after) { + if (n.nextSibling) { + n = n.nextSibling; + after = false; + } + else n = n.parentNode; + } + else { + if (isNodeText(n)) { + var len = n.nodeValue.length; + if (charsLeft <= len) { + return {node: n, index:charsLeft, maxIndex:len}; + } + charsLeft -= len; + after = true; + } + else { + if (n.firstChild) n = n.firstChild; + else after = true; + } + } + } + return {node: lineNode, index:1, maxIndex:1}; + } + + function nodeText(n) { + return n.innerText || n.textContent || n.nodeValue || ''; + } + + function getLineAndCharForPoint(point) { + // Turn DOM node selection into [line,char] selection. + // This method has to work when the DOM is not pristine, + // assuming the point is not in a dirty node. + if (point.node == root) { + if (point.index == 0) { + return [0, 0]; + } + else { + var N = rep.lines.length(); + var ln = rep.lines.atIndex(N-1); + return [N-1, ln.text.length]; + } + } + else { + var n = point.node; + var col = 0; + // if this part fails, it probably means the selection node + // was dirty, and we didn't see it when collecting dirty nodes. + if (isNodeText(n)) { + col = point.index; + } + else if (point.index > 0) { + col = nodeText(n).length; + } + var parNode, prevSib; + while ((parNode = n.parentNode) != root) { + if ((prevSib = n.previousSibling)) { + n = prevSib; + col += nodeText(n).length; + } + else { + n = parNode; + } + } + if (n.id == "") console.debug("BAD"); + if (n.firstChild && isBlockElement(n.firstChild)) { + col += 1; // lineMarker + } + var lineEntry = rep.lines.atKey(n.id); + var lineNum = rep.lines.indexOfEntry(lineEntry); + return [lineNum, col]; + } + } + + function createDomLineEntry(lineString) { + var info = doCreateDomLine(lineString.length > 0); + var newNode = info.node; + return {key: uniqueId(newNode), text: lineString, lineNode: newNode, + domInfo: info, lineMarker: 0}; + } + + function canApplyChangesetToDocument(changes) { + return Changeset.oldLen(changes) == rep.alltext.length; + } + + function performDocumentApplyChangeset(changes, insertsAfterSelection) { + doRepApplyChangeset(changes, insertsAfterSelection); + + var requiredSelectionSetting = null; + if (rep.selStart && rep.selEnd) { + var selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; + var selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; + var result = Changeset.characterRangeFollow(changes, selStartChar, selEndChar, + insertsAfterSelection); + requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart]; + } + + var linesMutatee = { + splice: function(start, numRemoved, newLinesVA) { + domAndRepSplice(start, numRemoved, + map(Array.prototype.slice.call(arguments, 2), + function(s) { return s.slice(0,-1); }), + null); + }, + get: function(i) { return rep.lines.atIndex(i).text+'\n'; }, + length: function() { return rep.lines.length(); }, + slice_notused: function(start, end) { + return map(rep.lines.slice(start, end), function(e) { return e.text+'\n'; }); + } + }; + + Changeset.mutateTextLines(changes, linesMutatee); + + checkALines(); + + if (requiredSelectionSetting) { + performSelectionChange(lineAndColumnFromChar(requiredSelectionSetting[0]), + lineAndColumnFromChar(requiredSelectionSetting[1]), + requiredSelectionSetting[2]); + } + + function domAndRepSplice(startLine, deleteCount, newLineStrings, isTimeUp) { + // dgreensp 3/2009: the spliced lines may be in the middle of a dirty region, + // so if no explicit time limit, don't spend a lot of time highlighting + isTimeUp = (isTimeUp || newTimeLimit(50)); + + var keysToDelete = []; + if (deleteCount > 0) { + var entryToDelete = rep.lines.atIndex(startLine); + for(var i=0;i<deleteCount;i++) { + keysToDelete.push(entryToDelete.key); + entryToDelete = rep.lines.next(entryToDelete); + } + } + + var lineEntries = map(newLineStrings, createDomLineEntry); + + doRepLineSplice(startLine, deleteCount, lineEntries); + + var nodeToAddAfter; + if (startLine > 0) { + nodeToAddAfter = getCleanNodeByKey(rep.lines.atIndex(startLine-1).key); + } + else nodeToAddAfter = null; + + insertDomLines(nodeToAddAfter, map(lineEntries, function (entry) { return entry.domInfo; }), + isTimeUp); + + forEach(keysToDelete, function (k) { + var n = doc.getElementById(k); + n.parentNode.removeChild(n); + }); + + if ((rep.selStart && rep.selStart[0] >= startLine && rep.selStart[0] <= startLine+deleteCount) || + (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine+deleteCount)) { + currentCallStack.selectionAffected = true; + } + } + } + + function checkChangesetLineInformationAgainstRep(changes) { + return true; // disable for speed + var opIter = Changeset.opIterator(Changeset.unpack(changes).ops); + var curOffset = 0; + var curLine = 0; + var curCol = 0; + while (opIter.hasNext()) { + var o = opIter.next(); + if (o.opcode == '-' || o.opcode == '=') { + curOffset += o.chars; + if (o.lines) { + curLine += o.lines; + curCol = 0; + } + else { + curCol += o.chars; + } + } + var calcLine = rep.lines.indexOfOffset(curOffset); + var calcLineStart = rep.lines.offsetOfIndex(calcLine); + var calcCol = curOffset - calcLineStart; + if (calcCol != curCol || calcLine != curLine) { + return false; + } + } + return true; + } + + function doRepApplyChangeset(changes, insertsAfterSelection) { + Changeset.checkRep(changes); + + if (Changeset.oldLen(changes) != rep.alltext.length) + throw new Error("doRepApplyChangeset length mismatch: "+ + Changeset.oldLen(changes)+"/"+rep.alltext.length); + + if (! checkChangesetLineInformationAgainstRep(changes)) { + throw new Error("doRepApplyChangeset line break mismatch"); + } + + (function doRecordUndoInformation(changes) { + var editEvent = currentCallStack.editEvent; + if (editEvent.eventType == "nonundoable") { + if (! editEvent.changeset) { + editEvent.changeset = changes; + } + else { + editEvent.changeset = Changeset.compose(editEvent.changeset, changes, + rep.apool); + } + } + else { + var inverseChangeset = Changeset.inverse(changes, {get: function(i) { + return rep.lines.atIndex(i).text+'\n'; + }, length: function() { return rep.lines.length(); }}, + rep.alines, rep.apool); + + if (! editEvent.backset) { + editEvent.backset = inverseChangeset; + } + else { + editEvent.backset = Changeset.compose(inverseChangeset, + editEvent.backset, rep.apool); + } + } + })(changes); + + //rep.alltext = Changeset.applyToText(changes, rep.alltext); + Changeset.mutateAttributionLines(changes, rep.alines, rep.apool); + + if (changesetTracker.isTracking()) { + changesetTracker.composeUserChangeset(changes); + } + + } + + function lineAndColumnFromChar(x) { + var lineEntry = rep.lines.atOffset(x); + var lineStart = rep.lines.offsetOfEntry(lineEntry); + var lineNum = rep.lines.indexOfEntry(lineEntry); + return [lineNum, x - lineStart]; + } + + function performDocumentReplaceCharRange(startChar, endChar, newText) { + if (startChar == endChar && newText.length == 0) { + return; + } + // Requires that the replacement preserve the property that the + // internal document text ends in a newline. Given this, we + // rewrite the splice so that it doesn't touch the very last + // char of the document. + if (endChar == rep.alltext.length) { + if (startChar == endChar) { + // an insert at end + startChar--; + endChar--; + newText = '\n'+newText.substring(0, newText.length-1); + } + else if (newText.length == 0) { + // a delete at end + startChar--; + endChar--; + } + else { + // a replace at end + endChar--; + newText = newText.substring(0, newText.length-1); + } + } + performDocumentReplaceRange(lineAndColumnFromChar(startChar), + lineAndColumnFromChar(endChar), + newText); + } + + function performDocumentReplaceRange(start, end, newText) { + //dmesg(String([start.toSource(),end.toSource(),newText.toSource()])); + + // start[0]: <--- start[1] --->CCCCCCCCCCC\n + // CCCCCCCCCCCCCCCCCCCC\n + // CCCC\n + // end[0]: <CCC end[1] CCC>-------\n + + var builder = Changeset.builder(rep.lines.totalWidth()); + buildKeepToStartOfRange(builder, start); + buildRemoveRange(builder, start, end); + builder.insert(newText, [['author',thisAuthor]], rep.apool); + var cs = builder.toString(); + + performDocumentApplyChangeset(cs); + } + + function performDocumentApplyAttributesToCharRange(start, end, attribs) { + if (end >= rep.alltext.length) { + end = rep.alltext.length-1; + } + performDocumentApplyAttributesToRange(lineAndColumnFromChar(start), + lineAndColumnFromChar(end), attribs); + } + + function performDocumentApplyAttributesToRange(start, end, attribs) { + var builder = Changeset.builder(rep.lines.totalWidth()); + buildKeepToStartOfRange(builder, start); + buildKeepRange(builder, start, end, attribs, rep.apool); + var cs = builder.toString(); + performDocumentApplyChangeset(cs); + } + + function buildKeepToStartOfRange(builder, start) { + var startLineOffset = rep.lines.offsetOfIndex(start[0]); + + builder.keep(startLineOffset, start[0]); + builder.keep(start[1]); + } + function buildRemoveRange(builder, start, end) { + var startLineOffset = rep.lines.offsetOfIndex(start[0]); + var endLineOffset = rep.lines.offsetOfIndex(end[0]); + + if (end[0] > start[0]) { + builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]); + builder.remove(end[1]); + } + else { + builder.remove(end[1] - start[1]); + } + } + function buildKeepRange(builder, start, end, attribs, pool) { + var startLineOffset = rep.lines.offsetOfIndex(start[0]); + var endLineOffset = rep.lines.offsetOfIndex(end[0]); + + if (end[0] > start[0]) { + builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool); + builder.keep(end[1], 0, attribs, pool); + } + else { + builder.keep(end[1] - start[1], 0, attribs, pool); + } + } + + function setAttributeOnSelection(attributeName, attributeValue) { + if (!(rep.selStart && rep.selEnd)) return; + + performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, + [[attributeName, attributeValue]]); + } + + function toggleAttributeOnSelection(attributeName) { + if (!(rep.selStart && rep.selEnd)) return; + + var selectionAllHasIt = true; + var withIt = Changeset.makeAttribsString('+', [[attributeName, 'true']], rep.apool); + var withItRegex = new RegExp(withIt.replace(/\*/g,'\\*')+"(\\*|$)"); + function hasIt(attribs) { return withItRegex.test(attribs); } + + var selStartLine = rep.selStart[0]; + var selEndLine = rep.selEnd[0]; + for(var n=selStartLine; n<=selEndLine; n++) { + var opIter = Changeset.opIterator(rep.alines[n]); + var indexIntoLine = 0; + var selectionStartInLine = 0; + var selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline + if (n == selStartLine) { + selectionStartInLine = rep.selStart[1]; + } + if (n == selEndLine) { + selectionEndInLine = rep.selEnd[1]; + } + while (opIter.hasNext()) { + var op = opIter.next(); + var opStartInLine = indexIntoLine; + var opEndInLine = opStartInLine + op.chars; + if (! hasIt(op.attribs)) { + // does op overlap selection? + if (! (opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine)) { + selectionAllHasIt = false; + break; + } + } + indexIntoLine = opEndInLine; + } + if (! selectionAllHasIt) { + break; + } + } + + if (selectionAllHasIt) { + performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, + [[attributeName,'']]); + } + else { + var settings = [[attributeName, 'true']]; + + if (attributeName == 'h1' || attributeName == 'h2' || attributeName == 'h3' || + attributeName == 'h4' || attributeName == 'h5' || attributeName == 'h6') { + + settings = [['h1',''], ['h2',''], ['h3',''], + ['h4',''], ['h5',''], ['h6','']]; + if (attributeName == 'h1') { + settings[0] = [attributeName, 'true']; + } + else if (attributeName == 'h2') { + settings[1] = [attributeName, 'true']; + } + else if (attributeName == 'h3') { + settings[2] = [attributeName, 'true']; + } + else if (attributeName == 'h4') { + settings[3] = [attributeName, 'true']; + } + else if (attributeName == 'h5') { + settings[4] = [attributeName, 'true']; + } + else if (attributeName == 'h6') { + settings[5] = [attributeName, 'true']; + } + } + + performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, settings); + } + } + + function performDocumentReplaceSelection(newText) { + if (!(rep.selStart && rep.selEnd)) return; + performDocumentReplaceRange(rep.selStart, rep.selEnd, newText); + } + + // Change the abstract representation of the document to have a different set of lines. + // Must be called after rep.alltext is set. + function doRepLineSplice(startLine, deleteCount, newLineEntries) { + + forEach(newLineEntries, function (entry) { entry.width = entry.text.length+1; }); + + var startOldChar = rep.lines.offsetOfIndex(startLine); + var endOldChar = rep.lines.offsetOfIndex(startLine+deleteCount); + + var oldRegionStart = rep.lines.offsetOfIndex(startLine); + var oldRegionEnd = rep.lines.offsetOfIndex(startLine+deleteCount); + rep.lines.splice(startLine, deleteCount, newLineEntries); + currentCallStack.docTextChanged = true; + currentCallStack.repChanged = true; + var newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length); + + var newText = map(newLineEntries, function (e) { return e.text+'\n'; }).join(''); + + rep.alltext = rep.alltext.substring(0, startOldChar) + newText + + rep.alltext.substring(endOldChar, rep.alltext.length); + + //var newTotalLength = rep.alltext.length; + + //rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart, + //newRegionEnd - oldRegionStart); + } + + function doIncorpLineSplice(startLine, deleteCount, newLineEntries, lineAttribs, hints) { + + var startOldChar = rep.lines.offsetOfIndex(startLine); + var endOldChar = rep.lines.offsetOfIndex(startLine+deleteCount); + + var oldRegionStart = rep.lines.offsetOfIndex(startLine); + + var selStartHintChar, selEndHintChar; + if (hints && hints.selStart) { + selStartHintChar = rep.lines.offsetOfIndex(hints.selStart[0]) + hints.selStart[1] - + oldRegionStart; + } + if (hints && hints.selEnd) { + selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - + oldRegionStart; + } + + var newText = map(newLineEntries, function (e) { return e.text+'\n'; }).join(''); + var oldText = rep.alltext.substring(startOldChar, endOldChar); + var oldAttribs = rep.alines.slice(startLine, startLine+deleteCount).join(''); + var newAttribs = lineAttribs.join('|1+1')+'|1+1'; // not valid in a changeset + var analysis = analyzeChange(oldText, newText, oldAttribs, newAttribs, + selStartHintChar, selEndHintChar); + var commonStart = analysis[0]; + var commonEnd = analysis[1]; + var shortOldText = oldText.substring(commonStart, oldText.length - commonEnd); + var shortNewText = newText.substring(commonStart, newText.length - commonEnd); + var spliceStart = startOldChar+commonStart; + var spliceEnd = endOldChar-commonEnd; + var shiftFinalNewlineToBeforeNewText = false; + + // adjust the splice to not involve the final newline of the document; + // be very defensive + if (shortOldText.charAt(shortOldText.length-1) == '\n' && + shortNewText.charAt(shortNewText.length-1) == '\n') { + // replacing text that ends in newline with text that also ends in newline + // (still, after analysis, somehow) + shortOldText = shortOldText.slice(0,-1); + shortNewText = shortNewText.slice(0,-1); + spliceEnd--; + commonEnd++; + } + if (shortOldText.length == 0 && spliceStart == rep.alltext.length + && shortNewText.length > 0) { + // inserting after final newline, bad + spliceStart--; + spliceEnd--; + shortNewText = '\n'+shortNewText.slice(0,-1); + shiftFinalNewlineToBeforeNewText = true; + } + if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && + shortNewText.length == 0) { + // deletion at end of rep.alltext + if (rep.alltext.charAt(spliceStart-1) == '\n') { + // (if not then what the heck? it will definitely lead + // to a rep.alltext without a final newline) + spliceStart--; + spliceEnd--; + } + } + + if (! (shortOldText.length == 0 && shortNewText.length == 0)) { + var oldDocText = rep.alltext; + var oldLen = oldDocText.length; + + var spliceStartLine = rep.lines.indexOfOffset(spliceStart); + var spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine); + function startBuilder() { + var builder = Changeset.builder(oldLen); + builder.keep(spliceStartLineStart, spliceStartLine); + builder.keep(spliceStart - spliceStartLineStart); + return builder; + } + + function eachAttribRun(attribs, func/*(startInNewText, endInNewText, attribs)*/) { + var attribsIter = Changeset.opIterator(attribs); + var textIndex = 0; + var newTextStart = commonStart; + var newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0); + while (attribsIter.hasNext()) { + var op = attribsIter.next(); + var nextIndex = textIndex + op.chars; + if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { + func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); + } + textIndex = nextIndex; + } + } + + var justApplyStyles = (shortNewText == shortOldText); + var theChangeset; + + if (justApplyStyles) { + // create changeset that clears the incorporated styles on + // the existing text. we compose this with the + // changeset the applies the styles found in the DOM. + // This allows us to incorporate, e.g., Safari's native "unbold". + + var incorpedAttribClearer = cachedStrFunc(function (oldAtts) { + return Changeset.mapAttribNumbers(oldAtts, function(n) { + var k = rep.apool.getAttribKey(n); + if (isStyleAttribute(k)) { + return rep.apool.putAttrib([k,'']); + } + return false; + }); + }); + + var builder1 = startBuilder(); + if (shiftFinalNewlineToBeforeNewText) { + builder1.keep(1, 1); + } + eachAttribRun(oldAttribs, function(start, end, attribs) { + builder1.keepText(newText.substring(start, end), incorpedAttribClearer(attribs)); + }); + var clearer = builder1.toString(); + + var builder2 = startBuilder(); + if (shiftFinalNewlineToBeforeNewText) { + builder2.keep(1, 1); + } + eachAttribRun(newAttribs, function(start, end, attribs) { + builder2.keepText(newText.substring(start, end), attribs); + }); + var styler = builder2.toString(); + + theChangeset = Changeset.compose(clearer, styler, rep.apool); + } + else { + var builder = startBuilder(); + + var spliceEndLine = rep.lines.indexOfOffset(spliceEnd); + var spliceEndLineStart = rep.lines.offsetOfIndex(spliceEndLine); + if (spliceEndLineStart > spliceStart) { + builder.remove(spliceEndLineStart - spliceStart, spliceEndLine - spliceStartLine); + builder.remove(spliceEnd - spliceEndLineStart); + } + else { + builder.remove(spliceEnd - spliceStart); + } + + var isNewTextMultiauthor = false; + var authorAtt = Changeset.makeAttribsString( + '+', (thisAuthor ? [['author', thisAuthor]] : []), rep.apool); + var authorizer = cachedStrFunc(function(oldAtts) { + if (isNewTextMultiauthor) { + // prefer colors from DOM + return Changeset.composeAttributes(authorAtt, oldAtts, true, rep.apool); + } + else { + // use this author's color + return Changeset.composeAttributes(oldAtts, authorAtt, true, rep.apool); + } + }); + + var foundDomAuthor = ''; + eachAttribRun(newAttribs, function(start, end, attribs) { + var a = Changeset.attribsAttributeValue(attribs, 'author', rep.apool); + if (a && a != foundDomAuthor) { + if (! foundDomAuthor) { + foundDomAuthor = a; + } + else { + isNewTextMultiauthor = true; // multiple authors in DOM! + } + } + }); + + if (shiftFinalNewlineToBeforeNewText) { + builder.insert('\n', authorizer('')); + } + + eachAttribRun(newAttribs, function(start, end, attribs) { + builder.insert(newText.substring(start, end), authorizer(attribs)); + }); + theChangeset = builder.toString(); + } + + //dmesg(htmlPrettyEscape(theChangeset)); + + doRepApplyChangeset(theChangeset); + } + + // do this no matter what, because we need to get the right + // line keys into the rep. + doRepLineSplice(startLine, deleteCount, newLineEntries); + + checkALines(); + } + + function cachedStrFunc(func) { + var cache = {}; + return function(s) { + if (! cache[s]) { + cache[s] = func(s); + } + return cache[s]; + }; + } + + function analyzeChange(oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint) { + function incorpedAttribFilter(anum) { + return isStyleAttribute(rep.apool.getAttribKey(anum)); + } + function attribRuns(attribs) { + var lengs = []; + var atts = []; + var iter = Changeset.opIterator(attribs); + while (iter.hasNext()) { + var op = iter.next(); + lengs.push(op.chars); + atts.push(op.attribs); + } + return [lengs,atts]; + } + function attribIterator(runs, backward) { + var lengs = runs[0]; + var atts = runs[1]; + var i = (backward ? lengs.length-1 : 0); + var j = 0; + return function next() { + while (j >= lengs[i]) { + if (backward) i--; else i++; + j = 0; + } + var a = atts[i]; + j++; + return a; + }; + } + + var oldLen = oldText.length; + var newLen = newText.length; + var minLen = Math.min(oldLen, newLen); + + var oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter)); + var newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter)); + + var commonStart = 0; + var oldStartIter = attribIterator(oldARuns, false); + var newStartIter = attribIterator(newARuns, false); + while (commonStart < minLen) { + if (oldText.charAt(commonStart) == newText.charAt(commonStart) && + oldStartIter() == newStartIter()) { + commonStart++; + } + else break; + } + + var commonEnd = 0; + var oldEndIter = attribIterator(oldARuns, true); + var newEndIter = attribIterator(newARuns, true); + while (commonEnd < minLen) { + if (commonEnd == 0) { + // assume newline in common + oldEndIter(); newEndIter(); + commonEnd++; + } + else if (oldText.charAt(oldLen-1-commonEnd) == newText.charAt(newLen-1-commonEnd) && + oldEndIter() == newEndIter()) { + commonEnd++; + } + else break; + } + + var hintedCommonEnd = -1; + if ((typeof optSelEndHint) == "number") { + hintedCommonEnd = newLen - optSelEndHint; + } + + + if (commonStart + commonEnd > oldLen) { + // ambiguous insertion + var minCommonEnd = oldLen - commonStart; + var maxCommonEnd = commonEnd; + if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) { + commonEnd = hintedCommonEnd; + } + else { + commonEnd = minCommonEnd; + } + commonStart = oldLen - commonEnd; + } + if (commonStart + commonEnd > newLen) { + // ambiguous deletion + var minCommonEnd = newLen - commonStart; + var maxCommonEnd = commonEnd; + if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) { + commonEnd = hintedCommonEnd; + } + else { + commonEnd = minCommonEnd; + } + commonStart = newLen - commonEnd; + } + + return [commonStart, commonEnd]; + } + + function equalLineAndChars(a, b) { + if (!a) return !b; + if (!b) return !a; + return (a[0] == b[0] && a[1] == b[1]); + } + + function performSelectionChange(selectStart, selectEnd, focusAtStart) { + if (repSelectionChange(selectStart, selectEnd, focusAtStart)) { + currentCallStack.selectionAffected = true; + } + } + + // Change the abstract representation of the document to have a different selection. + // Should not rely on the line representation. Should not affect the DOM. + function repSelectionChange(selectStart, selectEnd, focusAtStart) { + focusAtStart = !! focusAtStart; + + var newSelFocusAtStart = (focusAtStart && + ((! selectStart) || (! selectEnd) || + (selectStart[0] != selectEnd[0]) || + (selectStart[1] != selectEnd[1]))); + + if ((! equalLineAndChars(rep.selStart, selectStart)) || + (! equalLineAndChars(rep.selEnd, selectEnd)) || + (rep.selFocusAtStart != newSelFocusAtStart)) { + rep.selStart = selectStart; + rep.selEnd = selectEnd; + rep.selFocusAtStart = newSelFocusAtStart; + if (mozillaFakeArrows) mozillaFakeArrows.notifySelectionChanged(); + currentCallStack.repChanged = true; + + return true; + //console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd, + //String(!!rep.selFocusAtStart)); + } + return false; + //console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart); + } + + /*function escapeHTML(s) { + var re = /[&<>'"]/g; /']/; // stupid indentation thing + if (! re.MAP) { + // persisted across function calls! + re.MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + } + return s.replace(re, function(c) { return re.MAP[c]; }); + }*/ + + function doCreateDomLine(nonEmpty) { + if (browser.msie && (! nonEmpty)) { + var result = { node: null, + appendSpan: noop, + prepareForAdd: noop, + notifyAdded: noop, + clearSpans: noop, + finishUpdate: noop, + lineMarker: 0 }; + + var lineElem = doc.createElement("div"); + result.node = lineElem; + + result.notifyAdded = function() { + // magic -- settng an empty div's innerHTML to the empty string + // keeps it from collapsing. Apparently innerHTML must be set *after* + // adding the node to the DOM. + // Such a div is what IE 6 creates naturally when you make a blank line + // in a document of divs. However, when copy-and-pasted the div will + // contain a space, so we note its emptiness with a property. + lineElem.innerHTML = ""; + // a primitive-valued property survives copy-and-paste + setAssoc(lineElem, "shouldBeEmpty", true); + // an object property doesn't + setAssoc(lineElem, "unpasted", {}); + }; + var lineClass = 'ace-line'; + result.appendSpan = function(txt, cls) { + if ((! txt) && cls) { + // gain a whole-line style (currently to show insertion point in CSS) + lineClass = domline.addToLineClass(lineClass, cls); + } + // otherwise, ignore appendSpan, this is an empty line + }; + result.clearSpans = function() { + lineClass = ''; // non-null to cause update + }; + function writeClass() { + if (lineClass !== null) lineElem.className = lineClass; + } + result.prepareForAdd = writeClass; + result.finishUpdate = writeClass; + result.getInnerHTML = function() { return ""; }; + + return result; + } + else { + return domline.createDomLine(nonEmpty, doesWrap, browser, doc); + } + } + + function textify(str) { + return str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '); + } + + var _blockElems = { "div":1, "p":1, "pre":1, "li":1, "ol":1, "ul":1 }; + function isBlockElement(n) { + return !!_blockElems[(n.tagName || "").toLowerCase()]; + } + + function getDirtyRanges() { + // based on observedChanges, return a list of ranges of original lines + // that need to be removed or replaced with new user content to incorporate + // the user's changes into the line representation. ranges may be zero-length, + // indicating inserted content. for example, [0,0] means content was inserted + // at the top of the document, while [3,4] means line 3 was deleted, modified, + // or replaced with one or more new lines of content. ranges do not touch. + + var p = PROFILER("getDirtyRanges", false); + p.forIndices = 0; + p.consecutives = 0; + p.corrections = 0; + + var cleanNodeForIndexCache = {}; + var N = rep.lines.length(); // old number of lines + function cleanNodeForIndex(i) { + // if line (i) in the un-updated line representation maps to a clean node + // in the document, return that node. + // if (i) is out of bounds, return true. else return false. + if (cleanNodeForIndexCache[i] === undefined) { + p.forIndices++; + var result; + if (i < 0 || i >= N) { + result = true; // truthy, but no actual node + } + else { + var key = rep.lines.atIndex(i).key; + result = (getCleanNodeByKey(key) || false); + } + cleanNodeForIndexCache[i] = result; + } + return cleanNodeForIndexCache[i]; + } + var isConsecutiveCache = {}; + function isConsecutive(i) { + if (isConsecutiveCache[i] === undefined) { + p.consecutives++; + isConsecutiveCache[i] = (function() { + // returns whether line (i) and line (i-1), assumed to be map to clean DOM nodes, + // or document boundaries, are consecutive in the changed DOM + var a = cleanNodeForIndex(i-1); + var b = cleanNodeForIndex(i); + if ((!a) || (!b)) return false; // violates precondition + if ((a === true) && (b === true)) return ! root.firstChild; + if ((a === true) && b.previousSibling) return false; + if ((b === true) && a.nextSibling) return false; + if ((a === true) || (b === true)) return true; + return a.nextSibling == b; + })(); + } + return isConsecutiveCache[i]; + } + function isClean(i) { + // returns whether line (i) in the un-updated representation maps to a clean node, + // or is outside the bounds of the document + return !! cleanNodeForIndex(i); + } + // list of pairs, each representing a range of lines that is clean and consecutive + // in the changed DOM. lines (-1) and (N) are always clean, but may or may not + // be consecutive with lines in the document. pairs are in sorted order. + var cleanRanges = [[-1,N+1]]; + function rangeForLine(i) { + // returns index of cleanRange containing i, or -1 if none + var answer = -1; + forEach(cleanRanges, function (r, idx) { + if (i >= r[1]) return false; // keep looking + if (i < r[0]) return true; // not found, stop looking + answer = idx; + return true; // found, stop looking + }); + return answer; + } + function removeLineFromRange(rng, line) { + // rng is index into cleanRanges, line is line number + // precond: line is in rng + var a = cleanRanges[rng][0]; + var b = cleanRanges[rng][1]; + if ((a+1) == b) cleanRanges.splice(rng, 1); + else if (line == a) cleanRanges[rng][0]++; + else if (line == (b-1)) cleanRanges[rng][1]--; + else cleanRanges.splice(rng, 1, [a,line], [line+1,b]); + } + function splitRange(rng, pt) { + // precond: pt splits cleanRanges[rng] into two non-empty ranges + var a = cleanRanges[rng][0]; + var b = cleanRanges[rng][1]; + cleanRanges.splice(rng, 1, [a,pt], [pt,b]); + } + var correctedLines = {}; + function correctlyAssignLine(line) { + if (correctedLines[line]) return true; + p.corrections++; + correctedLines[line] = true; + // "line" is an index of a line in the un-updated rep. + // returns whether line was already correctly assigned (i.e. correctly + // clean or dirty, according to cleanRanges, and if clean, correctly + // attached or not attached (i.e. in the same range as) the prev and next lines). + //console.log("correctly assigning: %d", line); + var rng = rangeForLine(line); + var lineClean = isClean(line); + if (rng < 0) { + if (lineClean) { + console.debug("somehow lost clean line"); + } + return true; + } + if (! lineClean) { + // a clean-range includes this dirty line, fix it + removeLineFromRange(rng, line); + return false; + } + else { + // line is clean, but could be wrongly connected to a clean line + // above or below + var a = cleanRanges[rng][0]; + var b = cleanRanges[rng][1]; + var didSomething = false; + // we'll leave non-clean adjacent nodes in the clean range for the caller to + // detect and deal with. we deal with whether the range should be split + // just above or just below this line. + if (a < line && isClean(line-1) && ! isConsecutive(line)) { + splitRange(rng, line); + didSomething = true; + } + if (b > (line+1) && isClean(line+1) && ! isConsecutive(line+1)) { + splitRange(rng, line+1); + didSomething = true; + } + return ! didSomething; + } + } + function detectChangesAroundLine(line, reqInARow) { + // make sure cleanRanges is correct about line number "line" and the surrounding + // lines; only stops checking at end of document or after no changes need + // making for several consecutive lines. note that iteration is over old lines, + // so this operation takes time proportional to the number of old lines + // that are changed or missing, not the number of new lines inserted. + var correctInARow = 0; + var currentIndex = line; + while (correctInARow < reqInARow && currentIndex >= 0) { + if (correctlyAssignLine(currentIndex)) { + correctInARow++; + } + else correctInARow = 0; + currentIndex--; + } + correctInARow = 0; + currentIndex = line; + while (correctInARow < reqInARow && currentIndex < N) { + if (correctlyAssignLine(currentIndex)) { + correctInARow++; + } + else correctInARow = 0; + currentIndex++; + } + } + + if (N == 0) { + p.cancel(); + if (! isConsecutive(0)) { + splitRange(0, 0); + } + } + else { + p.mark("topbot"); + detectChangesAroundLine(0,1); + detectChangesAroundLine(N-1,1); + + p.mark("obs"); + //console.log("observedChanges: "+toSource(observedChanges)); + for (var k in observedChanges.cleanNodesNearChanges) { + var key = k.substring(1); + if (rep.lines.containsKey(key)) { + var line = rep.lines.indexOfKey(key); + detectChangesAroundLine(line,2); + } + } + p.mark("stats&calc"); + p.literal(p.forIndices, "byidx"); + p.literal(p.consecutives, "cons"); + p.literal(p.corrections, "corr"); + } + + var dirtyRanges = []; + for(var r=0;r<cleanRanges.length-1;r++) { + dirtyRanges.push([cleanRanges[r][1], cleanRanges[r+1][0]]); + } + + p.end(); + + return dirtyRanges; + } + + function markNodeClean(n) { + // clean nodes have knownHTML that matches their innerHTML + var dirtiness = {}; + dirtiness.nodeId = uniqueId(n); + dirtiness.knownHTML = n.innerHTML; + if (browser.msie) { + // adding a space to an "empty" div in IE designMode doesn't + // change the innerHTML of the div's parent; also, other + // browsers don't support innerText + dirtiness.knownText = n.innerText; + } + setAssoc(n, "dirtiness", dirtiness); + } + + function isNodeDirty(n) { + var p = PROFILER("cleanCheck", false); + if (n.parentNode != root) return true; + var data = getAssoc(n, "dirtiness"); + if (!data) return true; + if (n.id !== data.nodeId) return true; + if (browser.msie) { + if (n.innerText !== data.knownText) return true; + } + if (n.innerHTML !== data.knownHTML) return true; + p.end(); + return false; + } + + function getLineEntryTopBottom(entry, destObj) { + var dom = entry.lineNode; + var top = dom.offsetTop; + var height = dom.offsetHeight; + var obj = (destObj || {}); + obj.top = top; + obj.bottom = (top+height); + return obj; + } + + function getViewPortTopBottom() { + var theTop = getScrollY(); + var doc = outerWin.document; + var height = doc.documentElement.clientHeight; + return {top:theTop, bottom:(theTop+height)}; + } + + function getVisibleLineRange() { + var viewport = getViewPortTopBottom(); + //console.log("viewport top/bottom: %o", viewport); + var obj = {}; + var start = rep.lines.search(function (e) { + return getLineEntryTopBottom(e, obj).bottom > viewport.top; + }); + var end = rep.lines.search(function(e) { + return getLineEntryTopBottom(e, obj).top >= viewport.bottom; + }); + if (end < start) end = start; // unlikely + //console.log(start+","+end); + return [start,end]; + } + + function getVisibleCharRange() { + var lineRange = getVisibleLineRange(); + return [rep.lines.offsetOfIndex(lineRange[0]), + rep.lines.offsetOfIndex(lineRange[1])]; + } + + function handleClick(evt) { + inCallStack("handleClick", function() { + idleWorkTimer.atMost(200); + }); + + // only want to catch left-click + if ((! evt.ctrlKey) && (evt.button != 2) && (evt.button != 3)) { + // find A tag with HREF + function isLink(n) { return (n.tagName||'').toLowerCase() == "a" && n.href; } + var n = evt.target; + while (n && n.parentNode && ! isLink(n)) { n = n.parentNode; } + if (n && isLink(n)) { + try { + var newWindow = window.open(n.href, '_blank'); + newWindow.focus(); + } + catch (e) { + // absorb "user canceled" error in IE for certain prompts + } + evt.preventDefault(); + } + } + } + + function doReturnKey() { + if (! (rep.selStart && rep.selEnd)) { + return; + } + var lineNum = rep.selStart[0]; + var listType = getLineListType(lineNum); + + performDocumentReplaceSelection('\n'); + if (listType) { + if (lineNum+1 < rep.lines.length()) { + setLineListType(lineNum+1, listType); + } + } + else { + handleReturnIndentation(); + } + } + + function doIndentOutdent(isOut) { + if (! (rep.selStart && rep.selEnd)) { + return false; + } + + var firstLine, lastLine; + firstLine = rep.selStart[0]; + lastLine = Math.max(firstLine, + rep.selEnd[0] - ((rep.selEnd[1] == 0) ? 1 : 0)); + + var mods = []; + var foundLists = false; + for(var n=firstLine;n<=lastLine;n++) { + var listType = getLineListType(n); + if (listType) { + listType = /([a-z]+)([12345678])/.exec(listType); + if (listType) { + foundLists = true; + var t = listType[1]; + var level = Number(listType[2]); + var newLevel = + Math.max(1, Math.min(MAX_LIST_LEVEL, + level + (isOut ? -1 : 1))); + if (level != newLevel) { + mods.push([n, t+newLevel]); + } + } + } + } + + if (mods.length > 0) { + setLineListTypes(mods); + } + + return foundLists; + } + + function doTabKey(shiftDown) { + if (! doIndentOutdent(shiftDown)) { + performDocumentReplaceSelection(THE_TAB); + } + } + + function doDeleteKey(optEvt) { + var evt = optEvt || {}; + var handled = false; + if (rep.selStart) { + if (isCaret()) { + var lineNum = caretLine(); + var col = caretColumn(); + var lineEntry = rep.lines.atIndex(lineNum); + var lineText = lineEntry.text; + var lineMarker = lineEntry.lineMarker; + if (/^ +$/.exec(lineText.substring(lineMarker, col))) { + var col2 = col - lineMarker; + var tabSize = THE_TAB.length; + var toDelete = ((col2 - 1) % tabSize)+1; + performDocumentReplaceRange([lineNum,col-toDelete], + [lineNum,col], ''); + //scrollSelectionIntoView(); + handled = true; + } + } + if (! handled) { + if (isCaret()) { + var theLine = caretLine(); + var lineEntry = rep.lines.atIndex(theLine); + if (caretColumn() <= lineEntry.lineMarker) { + // delete at beginning of line + var action = 'delete_newline'; + var prevLineListType = + (theLine > 0 ? getLineListType(theLine-1) : ''); + var thisLineListType = getLineListType(theLine); + var prevLineEntry = (theLine > 0 && + rep.lines.atIndex(theLine-1)); + var prevLineBlank = (prevLineEntry && + prevLineEntry.text.length == + prevLineEntry.lineMarker); + if (thisLineListType) { + // this line is a list + /*if (prevLineListType) { + // prev line is a list too, remove this bullet + performDocumentReplaceRange([theLine-1, prevLineEntry.text.length], + [theLine, lineEntry.lineMarker], ''); + } + else*/ if (prevLineBlank && ! prevLineListType) { + // previous line is blank, remove it + performDocumentReplaceRange([theLine-1, prevLineEntry.text.length], + [theLine, 0], ''); + } + else { + // delistify + performDocumentReplaceRange([theLine, 0], + [theLine, lineEntry.lineMarker], ''); + } + } + else if (theLine > 0) { + // remove newline + performDocumentReplaceRange([theLine-1, prevLineEntry.text.length], + [theLine, 0], ''); + } + } + else { + var docChar = caretDocChar(); + if (docChar > 0) { + if (evt.metaKey || evt.ctrlKey || evt.altKey) { + // delete as many unicode "letters or digits" in a row as possible; + // always delete one char, delete further even if that first char + // isn't actually a word char. + var deleteBackTo = docChar-1; + while (deleteBackTo > lineEntry.lineMarker && + isWordChar(rep.alltext.charAt(deleteBackTo-1))) { + deleteBackTo--; + } + performDocumentReplaceCharRange(deleteBackTo, docChar, ''); + } + else { + // normal delete + performDocumentReplaceCharRange(docChar-1, docChar, ''); + } + } + } + } + else { + performDocumentReplaceSelection(''); + } + } + } + } + + // set of "letter or digit" chars is based on section 20.5.16 of the original Java Language Spec + var REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; + var REGEX_SPACE = /\s/; + + function isWordChar(c) { + return !! REGEX_WORDCHAR.exec(c); + } + function isSpaceChar(c) { + return !! REGEX_SPACE.exec(c); + } + + function moveByWordInLine(lineText, initialIndex, forwardNotBack) { + var i = initialIndex; + function nextChar() { + if (forwardNotBack) return lineText.charAt(i); + else return lineText.charAt(i-1); + } + function advance() { if (forwardNotBack) i++; else i--; } + function isDone() { + if (forwardNotBack) return i >= lineText.length; + else return i <= 0; + } + + // On Mac and Linux, move right moves to end of word and move left moves to start; + // on Windows, always move to start of word. + // On Windows, Firefox and IE disagree on whether to stop for punctuation (FF says no). + if (browser.windows && forwardNotBack) { + while ((! isDone()) && isWordChar(nextChar())) { advance(); } + while ((! isDone()) && ! isWordChar(nextChar())) { advance(); } + } + else { + while ((! isDone()) && ! isWordChar(nextChar())) { advance(); } + while ((! isDone()) && isWordChar(nextChar())) { advance(); } + } + + return i; + } + + function handleKeyEvent(evt) { + if (DEBUG && top.DONT_INCORP) return; + + /*if (evt.which == 48) { + //setEditable(! isEditable); + //doAlert(getInnerWidth()); + //doAlert(doc.documentElement.innerWidth) + alert(eval(prompt())); + evt.preventDefault(); + return; + }*/ + /*if (evt.which == 48) { + alert(doc.body.innerHTML); + }*/ + /*if (evt.which == 48 && evt.type == "keydown") { + var lineHeights = []; + function eachChild(node, func) { + if (node.firstChild) { + var n = node.firstChild; + while (n) { + func(n); + n = n.nextSibling; + } + } + } + eachChild(doc.body, function (n) { + if (n.clientHeight) { + lineHeights.push(n.clientHeight); + } + }); + alert(lineHeights.join(',')); + }*/ + /*if (evt.which == 48) { + top.DONT_INCORP = true; + var cmdTarget = doc; + if (browser.msie) { + if (doc.selection) { + cmdTarget = doc.selection.createRange(); + } + else cmdTarget = null; + } + if (cmdTarget) { + cmdTarget.execCommand("Bold", false, null); + } + alert(doc.body.innerHTML); + evt.preventDefault(); + return; + }*/ + /*if (evt.which == 48) { + if (evt.type == "keypress") { + top.console.log(window.getSelection().getRangeAt(0)); + evt.preventDefault(); + } + return; + }*/ + /*if (evt.which == 48) { + if (evt.type == "keypress") { + inCallStack("bold", function() { + fastIncorp(9); + toggleAttributeOnSelection('bold'); + }); + evt.preventDefault(); + } + return; + }*/ + /*if (evt.which == 48) { + if (evt.type == "keypress") { + inCallStack("insertunorderedlist", function() { + fastIncorp(9); + doInsertUnorderedList(); + }); + evt.preventDefault(); + } + return; + }*/ + + if (! isEditable) return; + + var type = evt.type; + var charCode = evt.charCode; + var keyCode = evt.keyCode; + var mods = ""; + if (evt.altKey) mods = mods+"A"; + if (evt.ctrlKey) mods = mods+"C"; + if (evt.shiftKey) mods = mods+"S"; + if (evt.metaKey) mods = mods+"M"; + var modsPrfx = ""; + if (mods) modsPrfx = mods+"-"; + var which = evt.which; + + //dmesg("keyevent type: "+type+", which: "+which); + + // Don't take action based on modifier keys going up and down. + // Modifier keys do not generate "keypress" events. + // 224 is the command-key under Mac Firefox. + // 91 is the Windows key in IE; it is ASCII for open-bracket but isn't the keycode for that key + // 20 is capslock in IE. + var isModKey = ((!charCode) && + ((type == "keyup") || (type == "keydown")) && + (keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 20 || keyCode == 224 + || keyCode == 91)); + if (isModKey) return; + + var specialHandled = false; + var isTypeForSpecialKey = ((browser.msie || browser.safari) ? + (type == "keydown") : (type == "keypress")); + var isTypeForCmdKey = ((browser.msie || browser.safari) ? (type == "keydown") : (type == "keypress")); + + var stopped = false; + + inCallStack("handleKeyEvent", function() { + + if (type == "keypress" || + (isTypeForSpecialKey && keyCode == 13/*return*/)) { + // in IE, special keys don't send keypress, the keydown does the action + if (! outsideKeyPress(evt)) { + evt.preventDefault(); + stopped = true; + } + } + else if (type == "keydown") { + outsideKeyDown(evt); + } + + if (! stopped) { + if (isTypeForSpecialKey && keyCode == 8) { + // "delete" key; in mozilla, if we're at the beginning of a line, normalize now, + // or else deleting a blank line can take two delete presses. + // -- + // we do deletes completely customly now: + // - allows consistent (and better) meta-delete behavior + // - normalizing and then allowing default behavior confused IE + // - probably eliminates a few minor quirks + fastIncorp(3); + evt.preventDefault(); + doDeleteKey(evt); + specialHandled = true; + } + if ((!specialHandled) && isTypeForSpecialKey && keyCode == 13) { + // return key, handle specially; + // note that in mozilla we need to do an incorporation for proper return behavior anyway. + fastIncorp(4); + evt.preventDefault(); + doReturnKey(); + //scrollSelectionIntoView(); + scheduler.setTimeout(function() {outerWin.scrollBy(-100,0);}, 0); + specialHandled = true; + } + if ((!specialHandled) && isTypeForSpecialKey && keyCode == 9 && + ! (evt.metaKey || evt.ctrlKey)) { + // tab + fastIncorp(5); + evt.preventDefault(); + doTabKey(evt.shiftKey); + //scrollSelectionIntoView(); + specialHandled = true; + } + if ((!specialHandled) && isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() == "z" && + (evt.metaKey || evt.ctrlKey)) { + // cmd-Z (undo) + fastIncorp(6); + evt.preventDefault(); + if (evt.shiftKey) { + doUndoRedo("redo"); + } + else { + doUndoRedo("undo"); + } + specialHandled = true; + } + if ((!specialHandled) && isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() == "y" && + (evt.metaKey || evt.ctrlKey)) { + // cmd-Y (redo) + fastIncorp(10); + evt.preventDefault(); + doUndoRedo("redo"); + specialHandled = true; + } + if ((!specialHandled) && isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() == "b" && + (evt.metaKey || evt.ctrlKey)) { + // cmd-B (bold) + fastIncorp(13); + evt.preventDefault(); + toggleAttributeOnSelection('bold'); + specialHandled = true; + } + if ((!specialHandled) && isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() == "i" && + (evt.metaKey || evt.ctrlKey)) { + // cmd-I (italic) + fastIncorp(14); + evt.preventDefault(); + toggleAttributeOnSelection('italic'); + specialHandled = true; + } + if ((!specialHandled) && isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() == "u" && + (evt.metaKey || evt.ctrlKey)) { + // cmd-U (underline) + fastIncorp(15); + evt.preventDefault(); + toggleAttributeOnSelection('underline'); + specialHandled = true; + } + if ((!specialHandled) && isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() == "h" && + (evt.ctrlKey)) { + // cmd-H (backspace) + fastIncorp(20); + evt.preventDefault(); + doDeleteKey(); + specialHandled = true; + } + /*if ((!specialHandled) && isTypeForCmdKey && + String.fromCharCode(which).toLowerCase() == "u" && + (evt.metaKey || evt.ctrlKey)) { + // cmd-U + doc.body.innerHTML = ''; + evt.preventDefault(); + specialHandled = true; + }*/ + + if (mozillaFakeArrows && mozillaFakeArrows.handleKeyEvent(evt)) { + evt.preventDefault(); + specialHandled = true; + } + } + + if (type == "keydown") { + idleWorkTimer.atLeast(500); + } + else if (type == "keypress") { + if ((! specialHandled) && parenModule.shouldNormalizeOnChar(charCode)) { + idleWorkTimer.atMost(0); + } + else { + idleWorkTimer.atLeast(500); + } + } + else if (type == "keyup") { + var wait = 200; + idleWorkTimer.atLeast(wait); + idleWorkTimer.atMost(wait); + } + + // Is part of multi-keystroke international character on Firefox Mac + var isFirefoxHalfCharacter = + (browser.mozilla && evt.altKey && charCode == 0 && keyCode == 0); + + // Is part of multi-keystroke international character on Safari Mac + var isSafariHalfCharacter = + (browser.safari && evt.altKey && keyCode == 229); + + if (thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter) { + idleWorkTimer.atLeast(3000); // give user time to type + // if this is a keydown, e.g., the keyup shouldn't trigger a normalize + thisKeyDoesntTriggerNormalize = true; + } + + if ((! specialHandled) && (! thisKeyDoesntTriggerNormalize) && + (! inInternationalComposition)) { + if (type != "keyup" || ! incorpIfQuick()) { + observeChangesAroundSelection(); + } + } + + if (type == "keyup") { + thisKeyDoesntTriggerNormalize = false; + } + }); + } + + var thisKeyDoesntTriggerNormalize = false; + + function doUndoRedo(which) { + // precond: normalized DOM + if (undoModule.enabled) { + var whichMethod; + if (which == "undo") whichMethod = 'performUndo'; + if (which == "redo") whichMethod = 'performRedo'; + if (whichMethod) { + var oldEventType = currentCallStack.editEvent.eventType; + currentCallStack.startNewEvent(which); + undoModule[whichMethod](function(backset, selectionInfo) { + if (backset) { + performDocumentApplyChangeset(backset); + } + if (selectionInfo) { + performSelectionChange(lineAndColumnFromChar(selectionInfo.selStart), + lineAndColumnFromChar(selectionInfo.selEnd), + selectionInfo.selFocusAtStart); + } + var oldEvent = currentCallStack.startNewEvent(oldEventType, true); + return oldEvent; + }); + } + } + } + + /*function enforceNewTextTypedStyle() { + var sel = getSelection(); + var n = (sel && sel.startPoint && sel.startPoint.node); + if (!n) return; + var isInOurNode = false; + while (n) { + if (n.tagName) { + var tag = n.tagName.toLowerCase(); + if (tag == "b" || tag == "strong") { + isInOurNode = true; + break; + } + if (((typeof n.className) == "string") && + n.className.toLowerCase().indexOf("Apple-style-span") >= 0) { + isInOurNode = true; + break; + } + } + n = n.parentNode; + } + + if (! isInOurNode) { + doc.execCommand("Bold", false, null); + } + + if (! browser.msie) { + var browserSelection = window.getSelection(); + if (browserSelection && browserSelection.type != "None" && + browserSelection.rangeCount !== 0) { + var range = browserSelection.getRangeAt(0); + var surrounder = doc.createElement("B"); + range.surroundContents(surrounder); + range.selectNodeContents(surrounder); + browserSelection.removeAllRanges(); + browserSelection.addRange(range); + } + } + }*/ + + function updateBrowserSelectionFromRep() { + // requires normalized DOM! + var selStart = rep.selStart, selEnd = rep.selEnd; + + if (!(selStart && selEnd)) { + setSelection(null); + return; + } + + var mozillaCaretHack = (false && browser.mozilla && selStart && selEnd && + selStart[0] == selEnd[0] + && selStart[1] == rep.lines.atIndex(selStart[0]).lineMarker + && selEnd[1] == rep.lines.atIndex(selEnd[0]).lineMarker && + setupMozillaCaretHack(selStart[0])); + + var selection = {}; + + var ss = [selStart[0], selStart[1]]; + if (mozillaCaretHack) ss[1] += 1; + selection.startPoint = getPointForLineAndChar(ss); + + var se = [selEnd[0], selEnd[1]]; + if (mozillaCaretHack) se[1] += 1; + selection.endPoint = getPointForLineAndChar(se); + + selection.focusAtStart = !!rep.selFocusAtStart; + + setSelection(selection); + + if (mozillaCaretHack) { + mozillaCaretHack.unhack(); + } + } + + function getRepHTML() { + /*function lineWithSelection(text, lineNum) { + var haveSelStart = (rep.selStart && rep.selStart[0] == lineNum); + var haveSelEnd = (rep.selEnd && rep.selEnd[0] == lineNum); + var startCol = (haveSelStart && rep.selStart[1]); + var endCol = (haveSelEnd && rep.selEnd[1]); + var len = text.length; + if (haveSelStart && haveSelEnd && startCol == endCol) { + var color = "#000"; + if (endCol == len) { + return '<span style="border-right: 1px solid '+color+'">'+ + htmlEscape(text)+'</span>'; + } + else { + return htmlEscape + } + } + }*/ + + return map(rep.lines.slice(), function (entry) { + var text = entry.text; + var content; + if (text.length == 0) { + content = '<span style="color: #aaa">--</span>'; + } + else { + content = htmlPrettyEscape(text); + } + return '<div><code>'+content+'</div></code>'; + }).join(''); + } + + function nodeMaxIndex(nd) { + if (isNodeText(nd)) return nd.nodeValue.length; + else return 1; + } + + function hasIESelection() { + var browserSelection; + try { browserSelection = doc.selection; } catch (e) {} + if (! browserSelection) return false; + var origSelectionRange; + try { origSelectionRange = browserSelection.createRange(); } catch (e) {} + if (! origSelectionRange) return false; + var selectionParent = origSelectionRange.parentElement(); + if (selectionParent.ownerDocument != doc) return false; + return true; + } + + function getSelection() { + // returns null, or a structure containing startPoint and endPoint, + // each of which has node (a magicdom node), index, and maxIndex. If the node + // is a text node, maxIndex is the length of the text; else maxIndex is 1. + // index is between 0 and maxIndex, inclusive. + if (browser.msie) { + var browserSelection; + try { browserSelection = doc.selection; } catch (e) {} + if (! browserSelection) return null; + var origSelectionRange; + try { origSelectionRange = browserSelection.createRange(); } catch (e) {} + if (! origSelectionRange) return null; + var selectionParent = origSelectionRange.parentElement(); + if (selectionParent.ownerDocument != doc) return null; + function newRange() { + return doc.body.createTextRange(); + } + function rangeForElementNode(nd) { + var rng = newRange(); + // doesn't work on text nodes + rng.moveToElementText(nd); + return rng; + } + function pointFromCollapsedRange(rng) { + var parNode = rng.parentElement(); + var elemBelow = -1; + var elemAbove = parNode.childNodes.length; + var rangeWithin = rangeForElementNode(parNode); + + if (rng.compareEndPoints("StartToStart", rangeWithin) == 0) { + return {node:parNode, index:0, maxIndex:1}; + } + else if (rng.compareEndPoints("EndToEnd", rangeWithin) == 0) { + if (isBlockElement(parNode) && parNode.nextSibling) { + // caret after block is not consistent across browsers + // (same line vs next) so put caret before next node + return {node:parNode.nextSibling, index:0, maxIndex:1}; + } + return {node:parNode, index:1, maxIndex:1}; + } + else if (parNode.childNodes.length == 0) { + return {node:parNode, index:0, maxIndex:1}; + } + + for(var i=0;i<parNode.childNodes.length;i++) { + var n = parNode.childNodes.item(i); + if (! isNodeText(n)) { + var nodeRange = rangeForElementNode(n); + var startComp = rng.compareEndPoints("StartToStart", nodeRange); + var endComp = rng.compareEndPoints("EndToEnd", nodeRange); + if (startComp >= 0 && endComp <= 0) { + var index = 0; + if (startComp > 0) { + index = 1; + } + return {node:n, index:index, maxIndex:1}; + } + else if (endComp > 0) { + if (i > elemBelow) { + elemBelow = i; + rangeWithin.setEndPoint("StartToEnd", nodeRange); + } + } + else if (startComp < 0) { + if (i < elemAbove) { + elemAbove = i; + rangeWithin.setEndPoint("EndToStart", nodeRange); + } + } + } + } + if ((elemAbove - elemBelow) == 1) { + if (elemBelow >= 0) { + return {node:parNode.childNodes.item(elemBelow), index:1, maxIndex:1}; + } + else { + return {node:parNode.childNodes.item(elemAbove), index:0, maxIndex:1}; + } + } + var idx = 0; + var r = rng.duplicate(); + // infinite stateful binary search! call function for values 0 to inf, + // expecting the answer to be about 40. return index of smallest + // true value. + var indexIntoRange = binarySearchInfinite(40, function (i) { + // the search algorithm whips the caret back and forth, + // though it has to be moved relatively and may hit + // the end of the buffer + var delta = i-idx; + var moved = Math.abs(r.move("character", -delta)); + // next line is work-around for fact that when moving left, the beginning + // of a text node is considered to be after the start of the parent element: + if (r.move("character", -1)) r.move("character", 1); + if (delta < 0) idx -= moved; + else idx += moved; + return (r.compareEndPoints("StartToStart", rangeWithin) <= 0); + }); + // iterate over consecutive text nodes, point is in one of them + var textNode = elemBelow+1; + var indexLeft = indexIntoRange; + while (textNode < elemAbove) { + var tn = parNode.childNodes.item(textNode); + if (indexLeft <= tn.nodeValue.length) { + return {node:tn, index:indexLeft, maxIndex:tn.nodeValue.length}; + } + indexLeft -= tn.nodeValue.length; + textNode++; + } + var tn = parNode.childNodes.item(textNode-1); + return {node:tn, index:tn.nodeValue.length, maxIndex:tn.nodeValue.length}; + } + var selection = {}; + if (origSelectionRange.compareEndPoints("StartToEnd", origSelectionRange) == 0) { + // collapsed + var pnt = pointFromCollapsedRange(origSelectionRange); + selection.startPoint = pnt; + selection.endPoint = {node:pnt.node, index:pnt.index, maxIndex:pnt.maxIndex}; + } + else { + var start = origSelectionRange.duplicate(); + start.collapse(true); + var end = origSelectionRange.duplicate(); + end.collapse(false); + selection.startPoint = pointFromCollapsedRange(start); + selection.endPoint = pointFromCollapsedRange(end); + /*if ((!selection.startPoint.node.isText) && (!selection.endPoint.node.isText)) { + console.log(selection.startPoint.node.uniqueId()+","+ + selection.startPoint.index+" / "+ + selection.endPoint.node.uniqueId()+","+ + selection.endPoint.index); + }*/ + } + return selection; + } + else { + // non-IE browser + var browserSelection = window.getSelection(); + if (browserSelection && browserSelection.type != "None" && + browserSelection.rangeCount !== 0) { + var range = browserSelection.getRangeAt(0); + function isInBody(n) { + while (n && ! (n.tagName && n.tagName.toLowerCase() == "body")) { + n = n.parentNode; + } + return !!n; + } + function pointFromRangeBound(container, offset) { + if (! isInBody(container)) { + // command-click in Firefox selects whole document, HEAD and BODY! + return {node:root, index:0, maxIndex:1}; + } + var n = container; + var childCount = n.childNodes.length; + if (isNodeText(n)) { + return {node:n, index:offset, maxIndex:n.nodeValue.length}; + } + else if (childCount == 0) { + return {node:n, index:0, maxIndex:1}; + } + // treat point between two nodes as BEFORE the second (rather than after the first) + // if possible; this way point at end of a line block-element is treated as + // at beginning of next line + else if (offset == childCount) { + var nd = n.childNodes.item(childCount-1); + var max = nodeMaxIndex(nd); + return {node:nd, index:max, maxIndex:max}; + } + else { + var nd = n.childNodes.item(offset); + var max = nodeMaxIndex(nd); + return {node:nd, index:0, maxIndex:max}; + } + } + var selection = {}; + selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset); + selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset); + selection.focusAtStart = (((range.startContainer != range.endContainer) || + (range.startOffset != range.endOffset)) && + browserSelection.anchorNode && + (browserSelection.anchorNode == range.endContainer) && + (browserSelection.anchorOffset == range.endOffset)); + return selection; + } + else return null; + } + } + + function setSelection(selection) { + function copyPoint(pt) { + return {node:pt.node, index:pt.index, maxIndex:pt.maxIndex}; + } + if (browser.msie) { + // Oddly enough, accessing scrollHeight fixes return key handling on IE 8, + // presumably by forcing some kind of internal DOM update. + doc.body.scrollHeight; + + function moveToElementText(s, n) { + while (n.firstChild && ! isNodeText(n.firstChild)) { + n = n.firstChild; + } + s.moveToElementText(n); + } + function newRange() { + return doc.body.createTextRange(); + } + function setCollapsedBefore(s, n) { + // s is an IE TextRange, n is a dom node + if (isNodeText(n)) { + // previous node should not also be text, but prevent inf recurs + if (n.previousSibling && ! isNodeText(n.previousSibling)) { + setCollapsedAfter(s, n.previousSibling); + } + else { + setCollapsedBefore(s, n.parentNode); + } + } + else { + moveToElementText(s, n); + // work around for issue that caret at beginning of line + // somehow ends up at end of previous line + if (s.move('character', 1)) { + s.move('character', -1); + } + s.collapse(true); // to start + } + } + function setCollapsedAfter(s, n) { + // s is an IE TextRange, n is a magicdom node + if (isNodeText(n)) { + // can't use end of container when no nextSibling (could be on next line), + // so use previousSibling or start of container and move forward. + setCollapsedBefore(s, n); + s.move("character", n.nodeValue.length); + } + else { + moveToElementText(s, n); + s.collapse(false); // to end + } + } + function getPointRange(point) { + var s = newRange(); + var n = point.node; + if (isNodeText(n)) { + setCollapsedBefore(s, n); + s.move("character", point.index); + } + else if (point.index == 0) { + setCollapsedBefore(s, n); + } + else { + setCollapsedAfter(s, n); + } + return s; + } + + if (selection) { + if (! hasIESelection()) { + return; // don't steal focus + } + + var startPoint = copyPoint(selection.startPoint); + var endPoint = copyPoint(selection.endPoint); + + // fix issue where selection can't be extended past end of line + // with shift-rightarrow or shift-downarrow + if (endPoint.index == endPoint.maxIndex && endPoint.node.nextSibling) { + endPoint.node = endPoint.node.nextSibling; + endPoint.index = 0; + endPoint.maxIndex = nodeMaxIndex(endPoint.node); + } + var range = getPointRange(startPoint); + range.setEndPoint("EndToEnd", getPointRange(endPoint)); + + // setting the selection in IE causes everything to scroll + // so that the selection is visible. if setting the selection + // definitely accomplishes nothing, don't do it. + function isEqualToDocumentSelection(rng) { + var browserSelection; + try { browserSelection = doc.selection; } catch (e) {} + if (! browserSelection) return false; + var rng2 = browserSelection.createRange(); + if (rng2.parentElement().ownerDocument != doc) return false; + if (rng.compareEndPoints("StartToStart", rng2) !== 0) return false; + if (rng.compareEndPoints("EndToEnd", rng2) !== 0) return false; + return true; + } + if (! isEqualToDocumentSelection(range)) { + //dmesg(toSource(selection)); + //dmesg(escapeHTML(doc.body.innerHTML)); + range.select(); + } + } + else { + try { doc.selection.empty(); } catch (e) {} + } + } + else { + // non-IE browser + var isCollapsed; + function pointToRangeBound(pt) { + var p = copyPoint(pt); + // Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level, + // and also problem where cut/copy of a whole line selected with fake arrow-keys + // copies the next line too. + if (isCollapsed) { + function diveDeep() { + while (p.node.childNodes.length > 0) { + //&& (p.node == root || p.node.parentNode == root)) { + if (p.index == 0) { + p.node = p.node.firstChild; + p.maxIndex = nodeMaxIndex(p.node); + } + else if (p.index == p.maxIndex) { + p.node = p.node.lastChild; + p.maxIndex = nodeMaxIndex(p.node); + p.index = p.maxIndex; + } + else break; + } + } + // now fix problem where cursor at end of text node at end of span-like element + // with background doesn't seem to show up... + if (isNodeText(p.node) && p.index == p.maxIndex) { + var n = p.node; + while ((! n.nextSibling) && (n != root) && (n.parentNode != root)) { + n = n.parentNode; + } + if (n.nextSibling && + (! ((typeof n.nextSibling.tagName) == "string" && + n.nextSibling.tagName.toLowerCase() == "br")) && + (n != p.node) && (n != root) && (n.parentNode != root)) { + // found a parent, go to next node and dive in + p.node = n.nextSibling; + p.maxIndex = nodeMaxIndex(p.node); + p.index = 0; + diveDeep(); + } + } + // try to make sure insertion point is styled; + // also fixes other FF problems + if (! isNodeText(p.node)) { + diveDeep(); + } + } + /*// make sure Firefox cursor is shallow enough; + // to fix problem where "return" between two spans doesn't move the caret to + // the next line + // (decided against) + while (!(p.node.isRoot || p.node.parent().isRoot || p.node.parent().parent().isRoot)) { + if (p.index == 0 && ! p.node.prev()) { + p.node = p.node.parent(); + p.maxIndex = 1; + } + else if (p.index == p.maxIndex && ! p.node.next()) { + p.node = p.node.parent(); + p.maxIndex = 1; + p.index = 1; + } + else break; + } + if ((! p.node.isRoot) && (!p.node.parent().isRoot) && + (p.index == p.maxIndex) && p.node.next()) { + p.node = p.node.next(); + p.maxIndex = nodeMaxIndex(p.node); + p.index = 0; + }*/ + if (isNodeText(p.node)) { + return { container: p.node, offset: p.index }; + } + else { + // p.index in {0,1} + return { container: p.node.parentNode, offset: childIndex(p.node) + p.index }; + } + } + var browserSelection = window.getSelection(); + if (browserSelection) { + browserSelection.removeAllRanges(); + if (selection) { + isCollapsed = (selection.startPoint.node === selection.endPoint.node && + selection.startPoint.index === selection.endPoint.index); + var start = pointToRangeBound(selection.startPoint); + var end = pointToRangeBound(selection.endPoint); + + if ((!isCollapsed) && selection.focusAtStart && browserSelection.collapse && browserSelection.extend) { + // can handle "backwards"-oriented selection, shift-arrow-keys move start + // of selection + browserSelection.collapse(end.container, end.offset); + //console.trace(); + //console.log(htmlPrettyEscape(rep.alltext)); + //console.log("%o %o", rep.selStart, rep.selEnd); + //console.log("%o %d", start.container, start.offset); + browserSelection.extend(start.container, start.offset); + } + else { + var range = doc.createRange(); + range.setStart(start.container, start.offset); + range.setEnd(end.container, end.offset); + browserSelection.removeAllRanges(); + browserSelection.addRange(range); + } + } + } + } + } + + function childIndex(n) { + var idx = 0; + while (n.previousSibling) { + idx++; + n = n.previousSibling; + } + return idx; + } + + function fixView() { + // calling this method repeatedly should be fast + + if (getInnerWidth() == 0 || getInnerHeight() == 0) { + return; + } + + function setIfNecessary(obj, prop, value) { + if (obj[prop] != value) { + obj[prop] = value; + } + } + + var lineNumberWidth = sideDiv.firstChild.offsetWidth; + var newSideDivWidth = lineNumberWidth + LINE_NUMBER_PADDING_LEFT; + if (newSideDivWidth < MIN_LINEDIV_WIDTH) newSideDivWidth = MIN_LINEDIV_WIDTH; + iframePadLeft = EDIT_BODY_PADDING_LEFT; + if (hasLineNumbers) iframePadLeft += newSideDivWidth + LINE_NUMBER_PADDING_RIGHT; + setIfNecessary(iframe.style, "left", iframePadLeft+"px"); + setIfNecessary(sideDiv.style, "width", newSideDivWidth+"px"); + + for(var i=0;i<2;i++) { + var newHeight = root.clientHeight; + var newWidth = (browser.msie ? root.createTextRange().boundingWidth : root.clientWidth); + var viewHeight = getInnerHeight() - iframePadBottom - iframePadTop; + var viewWidth = getInnerWidth() - iframePadLeft - iframePadRight; + if (newHeight < viewHeight) { + newHeight = viewHeight; + if (browser.msie) setIfNecessary(outerWin.document.documentElement.style, 'overflowY', 'auto'); + } + else { + if (browser.msie) setIfNecessary(outerWin.document.documentElement.style, 'overflowY', 'scroll'); + } + if (doesWrap) { + newWidth = viewWidth; + } + else { + if (newWidth < viewWidth) newWidth = viewWidth; + } + if (newHeight > 32000) newHeight = 32000; + if (newWidth > 32000) newWidth = 32000; + setIfNecessary(iframe.style, "height", newHeight+"px"); + setIfNecessary(iframe.style, "width", newWidth+"px"); + setIfNecessary(sideDiv.style, "height", newHeight+"px"); + } + if (browser.mozilla) { + if (! doesWrap) { + // the body:display:table-cell hack makes mozilla do scrolling + // correctly by shrinking the <body> to fit around its content, + // but mozilla won't act on clicks below the body. We keep the + // style.height property set to the viewport height (editor height + // not including scrollbar), so it will never shrink so that part of + // the editor isn't clickable. + var body = root; + var styleHeight = viewHeight+"px"; + setIfNecessary(body.style, "height", styleHeight); + } + else { + setIfNecessary(root.style, "height", ""); + } + } + // if near edge, scroll to edge + var scrollX = getScrollX(); + var scrollY = getScrollY(); + var win = outerWin; + var r = 20; + /*if (scrollX <= iframePadLeft+r) win.scrollBy(-iframePadLeft-r, 0); + else if (getPageWidth() - scrollX - getInnerWidth() <= iframePadRight+r) + scrollBy(iframePadRight+r, 0);*/ + /*if (scrollY <= iframePadTop+r) win.scrollBy(0, -iframePadTop-r); + else if (getPageHeight() - scrollY - getInnerHeight() <= iframePadBottom+r) + scrollBy(0, iframePadBottom+r);*/ + + enforceEditability(); + + addClass(sideDiv, 'sidedivdelayed'); + } + + function getScrollXY() { + var win = outerWin; + var odoc = outerWin.document; + if (typeof(win.pageYOffset) == "number") { + return {x: win.pageXOffset, y: win.pageYOffset}; + } + var docel = odoc.documentElement; + if (docel && typeof(docel.scrollTop) == "number") { + return {x:docel.scrollLeft, y:docel.scrollTop}; + } + } + + function getScrollX() { + return getScrollXY().x; + } + + function getScrollY() { + return getScrollXY().y; + } + + function setScrollX(x) { + outerWin.scrollTo(x, getScrollY()); + } + + function setScrollY(y) { + outerWin.scrollTo(getScrollX(), y); + } + + function setScrollXY(x, y) { + outerWin.scrollTo(x, y); + } + + var _teardownActions = []; + function teardown() { + forEach(_teardownActions, function (a) { a(); }); + } + + bindEventHandler(window, "load", setup); + + function setDesignMode(newVal) { + try { + function setIfNecessary(target, prop, val) { + if (String(target[prop]).toLowerCase() != val) { + target[prop] = val; + return true; + } + return false; + } + if (browser.msie || browser.safari) { + setIfNecessary(root, 'contentEditable', (newVal ? 'true' : 'false')); + } + else { + var wasSet = setIfNecessary(doc, 'designMode', (newVal ? 'on' : 'off')); + if (wasSet && newVal && browser.opera) { + // turning on designMode clears event handlers + bindTheEventHandlers(); + } + } + return true; + } + catch (e) { + return false; + } + } + + var iePastedLines = null; + function handleIEPaste(evt) { + // Pasting in IE loses blank lines in a way that loses information; + // "one\n\ntwo\nthree" becomes "<p>one</p><p>two</p><p>three</p>", + // which becomes "one\ntwo\nthree". We can get the correct text + // from the clipboard directly, but we still have to let the paste + // happen to get the style information. + + var clipText = window.clipboardData && window.clipboardData.getData("Text"); + if (clipText && doc.selection) { + // this "paste" event seems to mess with the selection whether we try to + // stop it or not, so can't really do document-level manipulation now + // or in an idle call-stack. instead, use IE native manipulation + //function escapeLine(txt) { + //return processSpaces(escapeHTML(textify(txt))); + //} + //var newHTML = map(clipText.replace(/\r/g,'').split('\n'), escapeLine).join('<br>'); + //doc.selection.createRange().pasteHTML(newHTML); + //evt.preventDefault(); + + //iePastedLines = map(clipText.replace(/\r/g,'').split('\n'), textify); + } + } + + var inInternationalComposition = false; + + function handleCompositionEvent(evt) { + // international input events, fired in FF3, at least; allow e.g. Japanese input + if (evt.type == "compositionstart") { + inInternationalComposition = true; + } + else if (evt.type == "compositionend") { + inInternationalComposition = false; + } + } + + /*function handleTextEvent(evt) { + top.console.log("TEXT EVENT"); + inCallStackIfNecessary("handleTextEvent", function() { + observeChangesAroundSelection(); + }); + }*/ + + function bindTheEventHandlers() { + bindEventHandler(window, "unload", teardown); + bindEventHandler(document, "keydown", handleKeyEvent); + bindEventHandler(document, "keypress", handleKeyEvent); + bindEventHandler(document, "keyup", handleKeyEvent); + bindEventHandler(document, "click", handleClick); + bindEventHandler(root, "blur", handleBlur); + if (browser.msie) { + bindEventHandler(document, "click", handleIEOuterClick); + } + if (browser.msie) bindEventHandler(root, "paste", handleIEPaste); + if ((! browser.msie) && document.documentElement) { + bindEventHandler(document.documentElement, "compositionstart", handleCompositionEvent); + bindEventHandler(document.documentElement, "compositionend", handleCompositionEvent); + } + + /*bindEventHandler(window, "mousemove", function(e) { + if (e.pageX < 10) { + window.DEBUG_DONT_INCORP = (e.pageX < 2); + } + });*/ + } + + function handleIEOuterClick(evt) { + if ((evt.target.tagName||'').toLowerCase() != "html") { + return; + } + if (!(evt.pageY > root.clientHeight)) { + return; + } + + // click below the body + inCallStack("handleOuterClick", function() { + // put caret at bottom of doc + fastIncorp(11); + if (isCaret()) { // don't interfere with drag + var lastLine = rep.lines.length()-1; + var lastCol = rep.lines.atIndex(lastLine).text.length; + performSelectionChange([lastLine,lastCol],[lastLine,lastCol]); + } + }); + } + + function getClassArray(elem, optFilter) { + var bodyClasses = []; + (elem.className || '').replace(/\S+/g, function (c) { + if ((! optFilter) || (optFilter(c))) { + bodyClasses.push(c); + } + }); + return bodyClasses; + } + function setClassArray(elem, array) { + elem.className = array.join(' '); + } + function addClass(elem, className) { + var seen = false; + var cc = getClassArray(elem, function(c) { if (c == className) seen = true; return true; }); + if (! seen) { + cc.push(className); + setClassArray(elem, cc); + } + } + function removeClass(elem, className) { + var seen = false; + var cc = getClassArray(elem, function(c) { + if (c == className) { seen = true; return false; } return true; }); + if (seen) { + setClassArray(elem, cc); + } + } + function setClassPresence(elem, className, present) { + if (present) addClass(elem, className); + else removeClass(elem, className); + } + + function setup() { + doc = document; // defined as a var in scope outside + inCallStack("setup", function() { + var body = doc.getElementById("innerdocbody"); + root = body; // defined as a var in scope outside + + if (browser.mozilla) addClass(root, "mozilla"); + if (browser.safari) addClass(root, "safari"); + if (browser.msie) addClass(root, "msie"); + if (browser.msie) { + // cache CSS background images + try { + doc.execCommand("BackgroundImageCache", false, true); + } + catch (e) { + /* throws an error in some IE 6 but not others! */ + } + } + setClassPresence(root, "authorColors", true); + setClassPresence(root, "doesWrap", doesWrap); + + initDynamicCSS(); + + enforceEditability(); + + // set up dom and rep + while (root.firstChild) root.removeChild(root.firstChild); + var oneEntry = createDomLineEntry(""); + doRepLineSplice(0, rep.lines.length(), [oneEntry]); + insertDomLines(null, [oneEntry.domInfo], null); + rep.alines = Changeset.splitAttributionLines( + Changeset.makeAttribution("\n"), "\n"); + + bindTheEventHandlers(); + + }); + + scheduler.setTimeout(function() { + parent.readyFunc(); // defined in code that sets up the inner iframe + }, 0); + + isSetUp = true; + } + + function focus() { + window.focus(); + } + + function handleBlur(evt) { + if (browser.msie) { + // a fix: in IE, clicking on a control like a button outside the + // iframe can "blur" the editor, causing it to stop getting + // events, though typing still affects it(!). + setSelection(null); + } + } + + function bindEventHandler(target, type, func) { + var handler; + if ((typeof func._wrapper) != "function") { + func._wrapper = function(event) { + func(fixEvent(event || window.event || {})); + } + } + var handler = func._wrapper; + if (target.addEventListener) + target.addEventListener(type, handler, false); + else + target.attachEvent("on" + type, handler); + _teardownActions.push(function() { + unbindEventHandler(target, type, func); + }); + } + + function unbindEventHandler(target, type, func) { + var handler = func._wrapper; + if (target.removeEventListener) + target.removeEventListener(type, handler, false); + else + target.detachEvent("on" + type, handler); + } + + /*forEach(['rep', 'getCleanNodeByKey', 'getDirtyRanges', 'isNodeDirty', + 'getSelection', 'setSelection', 'updateBrowserSelectionFromRep', + 'makeRecentSet', 'resetProfiler', 'getScrollXY', 'makeIdleAction'], function (k) { + top['_'+k] = eval(k); + });*/ + + function getSelectionPointX(point) { + // doesn't work in wrap-mode + var node = point.node; + var index = point.index; + function leftOf(n) { return n.offsetLeft; } + function rightOf(n) { return n.offsetLeft + n.offsetWidth; } + if (! isNodeText(node)) { + if (index == 0) return leftOf(node); + else return rightOf(node); + } + else { + // we can get bounds of element nodes, so look for those. + // allow consecutive text nodes for robustness. + var charsToLeft = index; + var charsToRight = node.nodeValue.length - index; + var n; + for(n = node.previousSibling; n && isNodeText(n); n = n.previousSibling) + charsToLeft += n.nodeValue; + var leftEdge = (n ? rightOf(n) : leftOf(node.parentNode)); + for(n = node.nextSibling; n && isNodeText(n); n = n.nextSibling) + charsToRight += n.nodeValue; + var rightEdge = (n ? leftOf(n) : rightOf(node.parentNode)); + var frac = (charsToLeft / (charsToLeft + charsToRight)); + var pixLoc = leftEdge + frac*(rightEdge - leftEdge); + return Math.round(pixLoc); + } + } + + function getPageHeight() { + var win = outerWin; + var odoc = win.document; + if (win.innerHeight && win.scrollMaxY) return win.innerHeight + win.scrollMaxY; + else if (odoc.body.scrollHeight > odoc.body.offsetHeight) return odoc.body.scrollHeight; + else return odoc.body.offsetHeight; + } + + function getPageWidth() { + var win = outerWin; + var odoc = win.document; + if (win.innerWidth && win.scrollMaxX) return win.innerWidth + win.scrollMaxX; + else if (odoc.body.scrollWidth > odoc.body.offsetWidth) return odoc.body.scrollWidth; + else return odoc.body.offsetWidth; + } + + function getInnerHeight() { + var win = outerWin; + var odoc = win.document; + var h; + if (browser.opera) h = win.innerHeight; + else h = odoc.documentElement.clientHeight; + if (h) return h; + + // deal with case where iframe is hidden, hope that + // style.height of iframe container is set in px + return Number(editorInfo.frame.parentNode.style.height.replace(/[^0-9]/g,'') + || 0); + } + + function getInnerWidth() { + var win = outerWin; + var odoc = win.document; + return odoc.documentElement.clientWidth; + } + + function scrollNodeVerticallyIntoView(node) { + // requires element (non-text) node; + // if node extends above top of viewport or below bottom of viewport (or top of scrollbar), + // scroll it the minimum distance needed to be completely in view. + var win = outerWin; + var odoc = outerWin.document; + var distBelowTop = node.offsetTop + iframePadTop - win.scrollY; + var distAboveBottom = win.scrollY + getInnerHeight() - + (node.offsetTop +iframePadTop + node.offsetHeight); + + if (distBelowTop < 0) { + win.scrollBy(0, distBelowTop); + } + else if (distAboveBottom < 0) { + win.scrollBy(0, -distAboveBottom); + } + } + + function scrollXHorizontallyIntoView(pixelX) { + var win = outerWin; + var odoc = outerWin.document; + pixelX += iframePadLeft; + var distInsideLeft = pixelX - win.scrollX; + var distInsideRight = win.scrollX + getInnerWidth() - pixelX; + if (distInsideLeft < 0) { + win.scrollBy(distInsideLeft, 0); + } + else if (distInsideRight < 0) { + win.scrollBy(-distInsideRight+1, 0); + } + } + + function scrollSelectionIntoView() { + if (! rep.selStart) return; + fixView(); + var focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]); + scrollNodeVerticallyIntoView(rep.lines.atIndex(focusLine).lineNode); + if (! doesWrap) { + var browserSelection = getSelection(); + if (browserSelection) { + var focusPoint = (browserSelection.focusAtStart ? browserSelection.startPoint : + browserSelection.endPoint); + var selectionPointX = getSelectionPointX(focusPoint); + scrollXHorizontallyIntoView(selectionPointX); + fixView(); + } + } + } + + function getLineListType(lineNum) { + // get "list" attribute of first char of line + var aline = rep.alines[lineNum]; + if (aline) { + var opIter = Changeset.opIterator(aline); + if (opIter.hasNext()) { + return Changeset.opAttributeValue(opIter.next(), 'list', rep.apool) || ''; + } + } + return ''; + } + + function setLineListType(lineNum, listType) { + setLineListTypes([[lineNum, listType]]); + } + + function setLineListTypes(lineNumTypePairsInOrder) { + var loc = [0,0]; + var builder = Changeset.builder(rep.lines.totalWidth()); + for(var i=0;i<lineNumTypePairsInOrder.length;i++) { + var pair = lineNumTypePairsInOrder[i]; + var lineNum = pair[0]; + var listType = pair[1]; + buildKeepRange(builder, loc, (loc = [lineNum,0])); + if (getLineListType(lineNum)) { + // already a line marker + if (listType) { + // make different list type + buildKeepRange(builder, loc, (loc = [lineNum,1]), + [['list',listType]], rep.apool); + } + else { + // remove list marker + buildRemoveRange(builder, loc, (loc = [lineNum,1])); + } + } + else { + // currently no line marker + if (listType) { + // add a line marker + builder.insert('*', [['author', thisAuthor], + ['insertorder', 'first'], + ['list', listType]], rep.apool); + } + } + } + + var cs = builder.toString(); + if (! Changeset.isIdentity(cs)) { + performDocumentApplyChangeset(cs); + } + } + + function doInsertUnorderedList() { + if (! (rep.selStart && rep.selEnd)) { + return; + } + + var firstLine, lastLine; + firstLine = rep.selStart[0]; + lastLine = Math.max(firstLine, + rep.selEnd[0] - ((rep.selEnd[1] == 0) ? 1 : 0)); + + var allLinesAreList = true; + for(var n=firstLine;n<=lastLine;n++) { + if (! getLineListType(n)) { + allLinesAreList = false; + break; + } + } + + var mods = []; + for(var n=firstLine;n<=lastLine;n++) { + var t = getLineListType(n); + mods.push([n, allLinesAreList ? '' : (t ? t : 'bullet1')]); + } + setLineListTypes(mods); + } + + var mozillaFakeArrows = (browser.mozilla && (function() { + // In Firefox 2, arrow keys are unstable while DOM-manipulating + // operations are going on. Specifically, if an operation + // (computation that ties up the event queue) is going on (in the + // call-stack of some event, like a timeout) that at some point + // mutates nodes involved in the selection, then the arrow + // keypress may (randomly) move the caret to the beginning or end + // of the document. If the operation also mutates the selection + // range, the old selection or the new selection may be used, or + // neither. + + // As long as the arrow is pressed during the busy operation, it + // doesn't seem to matter that the keydown and keypress events + // aren't generated until afterwards, or that the arrow movement + // can still be stopped (meaning it hasn't been performed yet); + // Firefox must be preserving some old information about the + // selection or the DOM from when the key was initially pressed. + // However, it also doesn't seem to matter when the key was + // actually pressed relative to the time of the mutation within + // the prolonged operation. Also, even in very controlled tests + // (like a mutation followed by a long period of busyWaiting), the + // problem shows up often but not every time, with no discernable + // pattern. Who knows, it could have something to do with the + // caret-blinking timer, or DOM changes not being applied + // immediately. + + // This problem, mercifully, does not show up at all in IE or + // Safari. My solution is to have my own, full-featured arrow-key + // implementation for Firefox. + + // Note that the problem addressed here is potentially very subtle, + // especially if the operation is quick and is timed to usually happen + // when the user is idle. + + // features: + // - 'up' and 'down' arrows preserve column when passing through shorter lines + // - shift-arrows extend the "focus" point, which may be start or end of range + // - the focus point is kept horizontally and vertically scrolled into view + // - arrows without shift cause caret to move to beginning or end of selection (left,right) + // or move focus point up or down a line (up,down) + // - command-(left,right,up,down) on Mac acts like (line-start, line-end, doc-start, doc-end) + // - takes wrapping into account when doesWrap is true, i.e. up-arrow and down-arrow move + // between the virtual lines within a wrapped line; this was difficult, and unfortunately + // requires mutating the DOM to get the necessary information + + var savedFocusColumn = 0; // a value of 0 has no effect + var updatingSelectionNow = false; + + function getVirtualLineView(lineNum) { + var lineNode = rep.lines.atIndex(lineNum).lineNode; + while (lineNode.firstChild && isBlockElement(lineNode.firstChild)) { + lineNode = lineNode.firstChild; + } + return makeVirtualLineView(lineNode); + } + + function markerlessLineAndChar(line, chr) { + return [line, chr - rep.lines.atIndex(line).lineMarker]; + } + function markerfulLineAndChar(line, chr) { + return [line, chr + rep.lines.atIndex(line).lineMarker]; + } + + return { + notifySelectionChanged: function() { + if (! updatingSelectionNow) { + savedFocusColumn = 0; + } + }, + handleKeyEvent: function(evt) { + // returns "true" if handled + if (evt.type != "keypress") return false; + var keyCode = evt.keyCode; + if (keyCode < 37 || keyCode > 40) return false; + incorporateUserChanges(); + + if (!(rep.selStart && rep.selEnd)) return true; + + // {byWord,toEnd,normal} + var moveMode = (evt.altKey ? "byWord" : + (evt.ctrlKey ? "byWord" : + (evt.metaKey ? "toEnd" : + "normal"))); + + var anchorCaret = + markerlessLineAndChar(rep.selStart[0], rep.selStart[1]); + var focusCaret = + markerlessLineAndChar(rep.selEnd[0], rep.selEnd[1]); + var wasCaret = isCaret(); + if (rep.selFocusAtStart) { + var tmp = anchorCaret; anchorCaret = focusCaret; focusCaret = tmp; + } + var K_UP = 38, K_DOWN = 40, K_LEFT = 37, K_RIGHT = 39; + var dontMove = false; + if (wasCaret && ! evt.shiftKey) { + // collapse, will mutate both together + anchorCaret = focusCaret; + } + else if ((! wasCaret) && (! evt.shiftKey)) { + if (keyCode == K_LEFT) { + // place caret at beginning + if (rep.selFocusAtStart) anchorCaret = focusCaret; + else focusCaret = anchorCaret; + if (moveMode == "normal") dontMove = true; + } + else if (keyCode == K_RIGHT) { + // place caret at end + if (rep.selFocusAtStart) focusCaret = anchorCaret; + else anchorCaret = focusCaret; + if (moveMode == "normal") dontMove = true; + } + else { + // collapse, will mutate both together + anchorCaret = focusCaret; + } + } + if (! dontMove) { + function lineLength(i) { + var entry = rep.lines.atIndex(i); + return entry.text.length - entry.lineMarker; + } + function lineText(i) { + var entry = rep.lines.atIndex(i); + return entry.text.substring(entry.lineMarker); + } + + if (keyCode == K_UP || keyCode == K_DOWN) { + var up = (keyCode == K_UP); + var canChangeLines = ((up && focusCaret[0]) || + ((!up) && focusCaret[0] < rep.lines.length()-1)); + var virtualLineView, virtualLineSpot, canChangeVirtualLines = false; + if (doesWrap) { + virtualLineView = getVirtualLineView(focusCaret[0]); + virtualLineSpot = virtualLineView.getVLineAndOffsetForChar(focusCaret[1]); + canChangeVirtualLines = ((up && virtualLineSpot.vline > 0) || + ((!up) && virtualLineSpot.vline < ( + virtualLineView.getNumVirtualLines() - 1))); + } + var newColByVirtualLineChange; + if (moveMode == "toEnd") { + if (up) { + focusCaret[0] = 0; + focusCaret[1] = 0; + } + else { + focusCaret[0] = rep.lines.length()-1; + focusCaret[1] = lineLength(focusCaret[0]); + } + } + else if (moveMode == "byWord") { + // move by "paragraph", a feature that Firefox lacks but IE and Safari both have + if (up) { + if (focusCaret[1] == 0 && canChangeLines) { + focusCaret[0]--; + focusCaret[1] = 0; + } + else focusCaret[1] = 0; + } + else { + var lineLen = lineLength(focusCaret[0]); + if (browser.windows) { + if (canChangeLines) { + focusCaret[0]++; + focusCaret[1] = 0; + } + else { + focusCaret[1] = lineLen; + } + } + else { + if (focusCaret[1] == lineLen && canChangeLines) { + focusCaret[0]++; + focusCaret[1] = lineLength(focusCaret[0]); + } + else { + focusCaret[1] = lineLen; + } + } + } + savedFocusColumn = 0; + } + else if (canChangeVirtualLines) { + var vline = virtualLineSpot.vline; + var offset = virtualLineSpot.offset; + if (up) vline--; + else vline++; + if (savedFocusColumn > offset) offset = savedFocusColumn; + else { + savedFocusColumn = offset; + } + var newSpot = virtualLineView.getCharForVLineAndOffset(vline, offset); + focusCaret[1] = newSpot.lineChar; + } + else if (canChangeLines) { + if (up) focusCaret[0]--; + else focusCaret[0]++; + var offset = focusCaret[1]; + if (doesWrap) { + offset = virtualLineSpot.offset; + } + if (savedFocusColumn > offset) offset = savedFocusColumn; + else { + savedFocusColumn = offset; + } + if (doesWrap) { + var newLineView = getVirtualLineView(focusCaret[0]); + var vline = (up ? newLineView.getNumVirtualLines()-1 : 0); + var newSpot = newLineView.getCharForVLineAndOffset(vline, offset); + focusCaret[1] = newSpot.lineChar; + } + else { + var lineLen = lineLength(focusCaret[0]); + if (offset > lineLen) offset = lineLen; + focusCaret[1] = offset; + } + } + else { + if (up) focusCaret[1] = 0; + else focusCaret[1] = lineLength(focusCaret[0]); + savedFocusColumn = 0; + } + } + else if (keyCode == K_LEFT || keyCode == K_RIGHT) { + var left = (keyCode == K_LEFT); + if (left) { + if (moveMode == "toEnd") focusCaret[1] = 0; + else if (focusCaret[1] > 0) { + if (moveMode == "byWord") { + focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], false); + } + else { + focusCaret[1]--; + } + } + else if (focusCaret[0] > 0) { + focusCaret[0]--; + focusCaret[1] = lineLength(focusCaret[0]); + if (moveMode == "byWord") { + focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], false); + } + } + } + else { + var lineLen = lineLength(focusCaret[0]); + if (moveMode == "toEnd") focusCaret[1] = lineLen; + else if (focusCaret[1] < lineLen) { + if (moveMode == "byWord") { + focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], true); + } + else { + focusCaret[1]++; + } + } + else if (focusCaret[0] < rep.lines.length()-1) { + focusCaret[0]++; + focusCaret[1] = 0; + if (moveMode == "byWord") { + focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], true); + } + } + } + savedFocusColumn = 0; + } + } + + var newSelFocusAtStart = ((focusCaret[0] < anchorCaret[0]) || + (focusCaret[0] == anchorCaret[0] && + focusCaret[1] < anchorCaret[1])); + var newSelStart = (newSelFocusAtStart ? focusCaret : anchorCaret); + var newSelEnd = (newSelFocusAtStart ? anchorCaret : focusCaret); + updatingSelectionNow = true; + performSelectionChange(markerfulLineAndChar(newSelStart[0], + newSelStart[1]), + markerfulLineAndChar(newSelEnd[0], + newSelEnd[1]), + newSelFocusAtStart); + updatingSelectionNow = false; + currentCallStack.userChangedSelection = true; + return true; + } + }; + })()); + + + // stolen from jquery-1.2.1 + function fixEvent(event) { + // store a copy of the original event object + // and clone to set read-only properties + var originalEvent = event; + event = extend({}, originalEvent); + + // add preventDefault and stopPropagation since + // they will not work on the clone + event.preventDefault = function() { + // if preventDefault exists run it on the original event + if (originalEvent.preventDefault) + originalEvent.preventDefault(); + // otherwise set the returnValue property of the original event to false (IE) + originalEvent.returnValue = false; + }; + event.stopPropagation = function() { + // if stopPropagation exists run it on the original event + if (originalEvent.stopPropagation) + originalEvent.stopPropagation(); + // otherwise set the cancelBubble property of the original event to true (IE) + originalEvent.cancelBubble = true; + }; + + // Fix target property, if necessary + if ( !event.target && event.srcElement ) + event.target = event.srcElement; + + // check if target is a textnode (safari) + if (browser.safari && event.target.nodeType == 3) + event.target = originalEvent.target.parentNode; + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && event.fromElement ) + event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && event.clientX != null ) { + var e = document.documentElement, b = document.body; + event.pageX = event.clientX + (e && e.scrollLeft || b.scrollLeft || 0); + event.pageY = event.clientY + (e && e.scrollTop || b.scrollTop || 0); + } + + // Add which for key events + if ( !event.which && (event.charCode || event.keyCode) ) + event.which = event.charCode || event.keyCode; + + // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) + if ( !event.metaKey && event.ctrlKey ) + event.metaKey = event.ctrlKey; + + // Add which for click: 1 == left; 2 == middle; 3 == right + // Note: button is not normalized, so don't use it + if ( !event.which && event.button ) + event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); + + return event; + } + + var lineNumbersShown; + var sideDivInner; + function initLineNumbers() { + lineNumbersShown = 1; + sideDiv.innerHTML = + '<table border="0" cellpadding="0" cellspacing="0" align="right">'+ + '<tr><td id="sidedivinner"><div>1</div></td></tr></table>'; + sideDivInner = outerWin.document.getElementById("sidedivinner"); + } + + function updateLineNumbers() { + var newNumLines = rep.lines.length(); + if (newNumLines < 1) newNumLines = 1; + if (newNumLines != lineNumbersShown) { + var container = sideDivInner; + var odoc = outerWin.document; + while (lineNumbersShown < newNumLines) { + lineNumbersShown++; + var n = lineNumbersShown; + var div = odoc.createElement("DIV"); + div.appendChild(odoc.createTextNode(String(n))); + container.appendChild(div); + } + while (lineNumbersShown > newNumLines) { + container.removeChild(container.lastChild); + lineNumbersShown--; + } + } + + if (currentCallStack && currentCallStack.domClean) { + var a = sideDivInner.firstChild; + var b = doc.body.firstChild; + while (a && b) { + var h = (b.clientHeight || b.offsetHeight); + if (b.nextSibling) { + // when text is zoomed in mozilla, divs have fractional + // heights (though the properties are always integers) + // and the line-numbers don't line up unless we pay + // attention to where the divs are actually placed... + // (also: padding on TTs/SPANs in IE...) + h = b.nextSibling.offsetTop - b.offsetTop; + } + if (h) { + var hpx = h+"px"; + if (a.style.height != hpx) + a.style.height = hpx; + } + a = a.nextSibling; + b = b.nextSibling; + } + + // fix if first line has margin (f.e. h1 in first line) + sideDivInner.firstChild.style.marginTop = + (doc.body.firstChild.offsetTop - sideDivInner.firstChild.offsetTop + + parseInt(sideDivInner.firstChild.style.marginTop + "0")) + "px"; + } + } + +}; + +OUTER(this); diff --git a/infrastructure/ace/www/ace2_outer.js b/infrastructure/ace/www/ace2_outer.js new file mode 100644 index 0000000..f947534 --- /dev/null +++ b/infrastructure/ace/www/ace2_outer.js @@ -0,0 +1,234 @@ +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// requires: top +// requires: plugins +// requires: undefined + + +Ace2Editor.registry = { nextId: 1 }; + +function Ace2Editor() { + var thisFunctionsName = "Ace2Editor"; + var ace2 = Ace2Editor; + + var editor = {}; + var info = { editor: editor, id: (ace2.registry.nextId++) }; + var loaded = false; + + var actionsPendingInit = []; + function pendingInit(func, optDoNow) { + return function() { + var that = this; + var args = arguments; + function action() { + func.apply(that, args); + } + if (optDoNow) { + optDoNow.apply(that, args); + } + if (loaded) { + action(); + } + else { + actionsPendingInit.push(action); + } + }; + } + function doActionsPendingInit() { + for(var i=0;i<actionsPendingInit.length;i++) { + actionsPendingInit[i](); + } + actionsPendingInit = []; + } + + ace2.registry[info.id] = info; + + editor.importText = pendingInit(function(newCode, undoable) { + info.ace_importText(newCode, undoable); }); + editor.importAText = pendingInit(function(newCode, apoolJsonObj, undoable) { + info.ace_importAText(newCode, apoolJsonObj, undoable); }); + editor.exportText = function() { + if (! loaded) return "(awaiting init)\n"; + return info.ace_exportText(); + }; + editor.getFrame = function() { return info.frame || null; }; + editor.focus = pendingInit(function() { info.ace_focus(); }); + editor.adjustSize = pendingInit(function() { + var frameParent = info.frame.parentNode; + var parentHeight = frameParent.clientHeight; + // deal with case where iframe is hidden, no clientHeight + info.frame.style.height = (parentHeight ? parentHeight+"px" : + frameParent.style.height); + info.ace_editorChangedSize(); + }); + editor.setEditable = pendingInit(function(newVal) { info.ace_setEditable(newVal); }); + editor.getFormattedCode = function() { return info.ace_getFormattedCode(); }; + editor.setOnKeyPress = pendingInit(function (handler) { info.ace_setOnKeyPress(handler); }); + editor.setOnKeyDown = pendingInit(function (handler) { info.ace_setOnKeyDown(handler); }); + editor.setNotifyDirty = pendingInit(function (handler) { info.ace_setNotifyDirty(handler); }); + + editor.setProperty = pendingInit(function(key, value) { info.ace_setProperty(key, value); }); + editor.getDebugProperty = function(prop) { return info.ace_getDebugProperty(prop); }; + + editor.setBaseText = pendingInit(function(txt) { info.ace_setBaseText(txt); }); + editor.setBaseAttributedText = pendingInit(function(atxt, apoolJsonObj) { + info.ace_setBaseAttributedText(atxt, apoolJsonObj); }); + editor.applyChangesToBase = pendingInit(function (changes, optAuthor,apoolJsonObj) { + info.ace_applyChangesToBase(changes, optAuthor, apoolJsonObj); }); + // prepareUserChangeset: + // Returns null if no new changes or ACE not ready. Otherwise, bundles up all user changes + // to the latest base text into a Changeset, which is returned (as a string if encodeAsString). + // If this method returns a truthy value, then applyPreparedChangesetToBase can be called + // at some later point to consider these changes part of the base, after which prepareUserChangeset + // must be called again before applyPreparedChangesetToBase. Multiple consecutive calls + // to prepareUserChangeset will return an updated changeset that takes into account the + // latest user changes, and modify the changeset to be applied by applyPreparedChangesetToBase + // accordingly. + editor.prepareUserChangeset = function() { + if (! loaded) return null; + return info.ace_prepareUserChangeset(); + }; + editor.applyPreparedChangesetToBase = pendingInit( + function() { info.ace_applyPreparedChangesetToBase(); }); + editor.setUserChangeNotificationCallback = pendingInit(function(callback) { + info.ace_setUserChangeNotificationCallback(callback); + }); + editor.setAuthorInfo = pendingInit(function(author, authorInfo) { + info.ace_setAuthorInfo(author, authorInfo); + }); + editor.setAuthorSelectionRange = pendingInit(function(author, start, end) { + info.ace_setAuthorSelectionRange(author, start, end); + }); + + editor.getUnhandledErrors = function() { + if (! loaded) return []; + // returns array of {error: <browser Error object>, time: +new Date()} + return info.ace_getUnhandledErrors(); + }; + editor.execCommand = pendingInit(function(cmd, arg1) { + info.ace_execCommand(cmd, arg1); + }); + + // calls to these functions ($$INCLUDE_...) are replaced when this file is processed + // and compressed, putting the compressed code from the named file directly into the + // source here. + + var $$INCLUDE_CSS = function(fileName) { + return '<link rel="stylesheet" type="text/css" href="'+fileName+'"/>'; + }; + var $$INCLUDE_JS = function(fileName) { + return '\x3cscript type="text/javascript" src="'+fileName+'">\x3c/script>'; + }; + var $$INCLUDE_JS_DEV = $$INCLUDE_JS; + var $$INCLUDE_CSS_DEV = $$INCLUDE_CSS; + + var $$INCLUDE_CSS_Q = function(fileName) { + return '\'<link rel="stylesheet" type="text/css" href="'+fileName+'"/>\''; + }; + var $$INCLUDE_JS_Q = function(fileName) { + return '\'\\x3cscript type="text/javascript" src="'+fileName+'">\\x3c/script>\''; + }; + var $$INCLUDE_JS_Q_DEV = $$INCLUDE_JS_Q; + var $$INCLUDE_CSS_Q_DEV = $$INCLUDE_CSS_Q; + + editor.destroy = pendingInit(function() { + info.ace_dispose(); + info.frame.parentNode.removeChild(info.frame); + delete ace2.registry[info.id]; + info = null; // prevent IE 6 closure memory leaks + }); + + editor.init = function(containerId, initialCode, doneFunc) { + + editor.importText(initialCode); + + info.onEditorReady = function() { + loaded = true; + doActionsPendingInit(); + doneFunc(); + }; + + (function() { + var doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '+ + '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'; + + var iframeHTML = ["'"+doctype+"<html><head>'"]; + + plugins.callHook( + "aceInitInnerdocbodyHead", {iframeHTML:iframeHTML}); + + // these lines must conform to a specific format because they are passed by the build script: + iframeHTML.push($$INCLUDE_CSS_Q("editor.css syntax.css inner.css")); + //iframeHTML.push(INCLUDE_JS_Q_DEV("ace2_common_dev.js")); + //iframeHTML.push(INCLUDE_JS_Q_DEV("profiler.js")); + iframeHTML.push($$INCLUDE_JS_Q("ace2_common.js skiplist.js virtual_lines.js easysync2.js cssmanager.js colorutils.js undomodule.js contentcollector.js changesettracker.js linestylefilter.js domline.js")); + iframeHTML.push($$INCLUDE_JS_Q("ace2_inner.js")); + iframeHTML.push('\'\\n<style type="text/css" title="dynamicsyntax"></style>\\n\''); + iframeHTML.push('\'</head><body id="innerdocbody" class="syntax" spellcheck="false"> </body></html>\''); + + var outerScript = 'editorId = "'+info.id+'"; editorInfo = parent.'+ + thisFunctionsName+'.registry[editorId]; '+ + 'window.onload = function() '+ + '{ window.onload = null; setTimeout'+ + '(function() '+ + '{ var iframe = document.createElement("IFRAME"); '+ + 'iframe.scrolling = "no"; var outerdocbody = document.getElementById("outerdocbody"); '+ + 'iframe.frameBorder = 0; iframe.allowTransparency = true; '+ // for IE + 'outerdocbody.insertBefore(iframe, outerdocbody.firstChild); '+ + 'iframe.ace_outerWin = window; '+ + 'readyFunc = function() { editorInfo.onEditorReady(); readyFunc = null; editorInfo = null; }; '+ + 'var doc = iframe.contentWindow.document; doc.open(); doc.write('+ + iframeHTML.join('+')+'); doc.close(); '+ + '}, 0); }'; + + var outerHTML = [doctype, '<html><head>', + $$INCLUDE_CSS("editor.css"), + // bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly + // (throbs busy while typing) + '<link rel="stylesheet" type="text/css" href="data:text/css,"/>', + '\x3cscript>', outerScript, '\x3c/script>', + '</head><body id="outerdocbody"><div id="sidediv"><!-- --></div><div id="linemetricsdiv">x</div><div id="overlaysdiv"><!-- --></div></body></html>']; + + + if (!Array.prototype.map) Array.prototype.map = function(fun) { //needed for IE + if (typeof fun != "function") throw new TypeError(); + var len = this.length; + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in this) res[i] = fun.call(thisp, this[i], i, this); + } + return res; + }; + + var outerFrame = document.createElement("IFRAME"); + outerFrame.frameBorder = 0; // for IE + info.frame = outerFrame; + document.getElementById(containerId).appendChild(outerFrame); + + var editorDocument = outerFrame.contentWindow.document; + + editorDocument.open(); + editorDocument.write(outerHTML.join('')); + editorDocument.close(); + + editor.adjustSize(); + })(); + }; + + return editor; +} diff --git a/trunk/infrastructure/ace/www/ace2_wrapper.js b/infrastructure/ace/www/ace2_wrapper.js index b62e09d..b62e09d 100644 --- a/trunk/infrastructure/ace/www/ace2_wrapper.js +++ b/infrastructure/ace/www/ace2_wrapper.js diff --git a/trunk/infrastructure/ace/www/bbtree.js b/infrastructure/ace/www/bbtree.js index 70cb8c0..70cb8c0 100644 --- a/trunk/infrastructure/ace/www/bbtree.js +++ b/infrastructure/ace/www/bbtree.js diff --git a/trunk/infrastructure/ace/www/changesettracker.js b/infrastructure/ace/www/changesettracker.js index d6fe018..d6fe018 100644 --- a/trunk/infrastructure/ace/www/changesettracker.js +++ b/infrastructure/ace/www/changesettracker.js diff --git a/trunk/infrastructure/ace/www/colorutils.js b/infrastructure/ace/www/colorutils.js index bb61de3..bb61de3 100644 --- a/trunk/infrastructure/ace/www/colorutils.js +++ b/infrastructure/ace/www/colorutils.js diff --git a/trunk/infrastructure/ace/www/contentcollector.js b/infrastructure/ace/www/contentcollector.js index 573672e..573672e 100644 --- a/trunk/infrastructure/ace/www/contentcollector.js +++ b/infrastructure/ace/www/contentcollector.js diff --git a/trunk/infrastructure/ace/www/cssmanager.js b/infrastructure/ace/www/cssmanager.js index a5c549b..a5c549b 100644 --- a/trunk/infrastructure/ace/www/cssmanager.js +++ b/infrastructure/ace/www/cssmanager.js diff --git a/trunk/infrastructure/ace/www/dev.html b/infrastructure/ace/www/dev.html index 0a9768e..0a9768e 100644 --- a/trunk/infrastructure/ace/www/dev.html +++ b/infrastructure/ace/www/dev.html diff --git a/infrastructure/ace/www/domline.js b/infrastructure/ace/www/domline.js new file mode 100644 index 0000000..f1d19e4 --- /dev/null +++ b/infrastructure/ace/www/domline.js @@ -0,0 +1,232 @@ +// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline +// %APPJET%: import("etherpad.admin.plugins"); + +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// requires: top +// requires: plugins +// requires: undefined + +var domline = {}; +domline.noop = function() {}; +domline.identity = function(x) { return x; }; + +domline.addToLineClass = function(lineClass, cls) { + // an "empty span" at any point can be used to add classes to + // the line, using line:className. otherwise, we ignore + // the span. + cls.replace(/\S+/g, function (c) { + if (c.indexOf("line:") == 0) { + // add class to line + lineClass = (lineClass ? lineClass+' ' : '')+c.substring(5); + } + }); + return lineClass; +} + +// if "document" is falsy we don't create a DOM node, just +// an object with innerHTML and className +domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) { + var result = { node: null, + appendSpan: domline.noop, + prepareForAdd: domline.noop, + notifyAdded: domline.noop, + clearSpans: domline.noop, + finishUpdate: domline.noop, + lineMarker: 0 }; + + var browser = (optBrowser || {}); + var document = optDocument; + + if (document) { + result.node = document.createElement("div"); + } + else { + result.node = {innerHTML: '', className: ''}; + } + + var html = []; + var preHtml, postHtml; + var curHTML = null; + function processSpaces(s) { + return domline.processSpaces(s, doesWrap); + } + var identity = domline.identity; + var perTextNodeProcess = (doesWrap ? identity : processSpaces); + var perHtmlLineProcess = (doesWrap ? processSpaces : identity); + var lineClass = 'ace-line'; + result.appendSpan = function(txt, cls) { + if (cls.indexOf('list') >= 0) { + var listType = /(?:^| )list:(\S+)/.exec(cls); + if (listType) { + listType = listType[1]; + if (listType) { + preHtml = '<ul class="list-'+listType+'"><li>'; + postHtml = '</li></ul>'; + } + result.lineMarker += txt.length; + return; // don't append any text + } + } + var href = null; + var simpleTags = null; + if (cls.indexOf('url') >= 0) { + cls = cls.replace(/(^| )url:(\S+)/g, function(x0, space, url) { + href = url; + return space+"url"; + }); + } + if (cls.indexOf('tag') >= 0) { + cls = cls.replace(/(^| )tag:(\S+)/g, function(x0, space, tag) { + if (! simpleTags) simpleTags = []; + simpleTags.push(tag.toLowerCase()); + return space+tag; + }); + } + + var extraOpenTags = ""; + var extraCloseTags = ""; + + var plugins_; + if (typeof(plugins)!='undefined') { + plugins_ = plugins; + } else { + plugins_ = parent.parent.plugins; + } + + plugins_.callHook( + "aceCreateDomLine", {domline:domline, cls:cls} + ).map(function (modifier) { + cls = modifier.cls; + extraOpenTags = extraOpenTags+modifier.extraOpenTags; + extraCloseTags = modifier.extraCloseTags+extraCloseTags; + }); + + if ((! txt) && cls) { + lineClass = domline.addToLineClass(lineClass, cls); + } + else if (txt) { + if (href) { + extraOpenTags = extraOpenTags+'<a href="'+ + href.replace(/\"/g, '"')+'">'; + extraCloseTags = '</a>'+extraCloseTags; + } + if (simpleTags) { + simpleTags.sort(); + extraOpenTags = extraOpenTags+'<'+simpleTags.join('><')+'>'; + simpleTags.reverse(); + extraCloseTags = '</'+simpleTags.join('></')+'>'+extraCloseTags; + } + html.push('<span class="',cls||'','">',extraOpenTags, + perTextNodeProcess(domline.escapeHTML(txt)), + extraCloseTags,'</span>'); + } + }; + result.clearSpans = function() { + html = []; + lineClass = ''; // non-null to cause update + result.lineMarker = 0; + }; + function writeHTML() { + var newHTML = perHtmlLineProcess(html.join('')); + if (! newHTML) { + if ((! document) || (! optBrowser)) { + newHTML += ' '; + } + else if (! browser.msie) { + newHTML += '<br/>'; + } + } + if (nonEmpty) { + newHTML = (preHtml||'')+newHTML+(postHtml||''); + } + html = preHtml = postHtml = null; // free memory + if (newHTML !== curHTML) { + curHTML = newHTML; + result.node.innerHTML = curHTML; + } + if (lineClass !== null) result.node.className = lineClass; + } + result.prepareForAdd = writeHTML; + result.finishUpdate = writeHTML; + result.getInnerHTML = function() { return curHTML || ''; }; + + return result; +}; + +domline.escapeHTML = function(s) { + var re = /[&<>'"]/g; /']/; // stupid indentation thing + if (! re.MAP) { + // persisted across function calls! + re.MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + } + return s.replace(re, function(c) { return re.MAP[c]; }); +}; + +domline.processSpaces = function(s, doesWrap) { + if (s.indexOf("<") < 0 && ! doesWrap) { + // short-cut + return s.replace(/ /g, ' '); + } + var parts = []; + s.replace(/<[^>]*>?| |[^ <]+/g, function(m) { parts.push(m); }); + if (doesWrap) { + var endOfLine = true; + var beforeSpace = false; + // last space in a run is normal, others are nbsp, + // end of line is nbsp + for(var i=parts.length-1;i>=0;i--) { + var p = parts[i]; + if (p == " ") { + if (endOfLine || beforeSpace) + parts[i] = ' '; + endOfLine = false; + beforeSpace = true; + } + else if (p.charAt(0) != "<") { + endOfLine = false; + beforeSpace = false; + } + } + // beginning of line is nbsp + for(var i=0;i<parts.length;i++) { + var p = parts[i]; + if (p == " ") { + parts[i] = ' '; + break; + } + else if (p.charAt(0) != "<") { + break; + } + } + } + else { + for(var i=0;i<parts.length;i++) { + var p = parts[i]; + if (p == " ") { + parts[i] = ' '; + } + } + } + return parts.join(''); +}; diff --git a/trunk/infrastructure/ace/www/easy_sync.js b/infrastructure/ace/www/easy_sync.js index 86a4327..86a4327 100644 --- a/trunk/infrastructure/ace/www/easy_sync.js +++ b/infrastructure/ace/www/easy_sync.js diff --git a/trunk/infrastructure/ace/www/easysync2.js b/infrastructure/ace/www/easysync2.js index efc5b99..efc5b99 100644 --- a/trunk/infrastructure/ace/www/easysync2.js +++ b/infrastructure/ace/www/easysync2.js diff --git a/trunk/infrastructure/ace/www/easysync2_tests.js b/infrastructure/ace/www/easysync2_tests.js index 2fcf202..2fcf202 100644 --- a/trunk/infrastructure/ace/www/easysync2_tests.js +++ b/infrastructure/ace/www/easysync2_tests.js diff --git a/trunk/infrastructure/ace/www/editor.css b/infrastructure/ace/www/editor.css index 9df127d..9df127d 100644 --- a/trunk/infrastructure/ace/www/editor.css +++ b/infrastructure/ace/www/editor.css diff --git a/trunk/infrastructure/ace/www/firebug/errorIcon.png b/infrastructure/ace/www/firebug/errorIcon.png Binary files differindex 2d75261..2d75261 100644 --- a/trunk/infrastructure/ace/www/firebug/errorIcon.png +++ b/infrastructure/ace/www/firebug/errorIcon.png diff --git a/trunk/infrastructure/ace/www/firebug/firebug.css b/infrastructure/ace/www/firebug/firebug.css index 1f041c4..1f041c4 100644 --- a/trunk/infrastructure/ace/www/firebug/firebug.css +++ b/infrastructure/ace/www/firebug/firebug.css diff --git a/trunk/infrastructure/ace/www/firebug/firebug.html b/infrastructure/ace/www/firebug/firebug.html index 861e639..861e639 100644 --- a/trunk/infrastructure/ace/www/firebug/firebug.html +++ b/infrastructure/ace/www/firebug/firebug.html diff --git a/trunk/infrastructure/ace/www/firebug/firebug.js b/infrastructure/ace/www/firebug/firebug.js index d3c1978..d3c1978 100644 --- a/trunk/infrastructure/ace/www/firebug/firebug.js +++ b/infrastructure/ace/www/firebug/firebug.js diff --git a/trunk/infrastructure/ace/www/firebug/firebugx.js b/infrastructure/ace/www/firebug/firebugx.js index b2cc49c..b2cc49c 100644 --- a/trunk/infrastructure/ace/www/firebug/firebugx.js +++ b/infrastructure/ace/www/firebug/firebugx.js diff --git a/trunk/infrastructure/ace/www/firebug/infoIcon.png b/infrastructure/ace/www/firebug/infoIcon.png Binary files differindex da1e533..da1e533 100644 --- a/trunk/infrastructure/ace/www/firebug/infoIcon.png +++ b/infrastructure/ace/www/firebug/infoIcon.png diff --git a/trunk/infrastructure/ace/www/firebug/warningIcon.png b/infrastructure/ace/www/firebug/warningIcon.png Binary files differindex de51084..de51084 100644 --- a/trunk/infrastructure/ace/www/firebug/warningIcon.png +++ b/infrastructure/ace/www/firebug/warningIcon.png diff --git a/trunk/infrastructure/ace/www/index.html b/infrastructure/ace/www/index.html index a1e6e96..a1e6e96 100644 --- a/trunk/infrastructure/ace/www/index.html +++ b/infrastructure/ace/www/index.html diff --git a/trunk/infrastructure/ace/www/inner.css b/infrastructure/ace/www/inner.css index 7479cfe..7479cfe 100644 --- a/trunk/infrastructure/ace/www/inner.css +++ b/infrastructure/ace/www/inner.css diff --git a/trunk/infrastructure/ace/www/jquery-1.2.1.js b/infrastructure/ace/www/jquery-1.2.1.js index b4eb132..b4eb132 100644 --- a/trunk/infrastructure/ace/www/jquery-1.2.1.js +++ b/infrastructure/ace/www/jquery-1.2.1.js diff --git a/trunk/infrastructure/ace/www/lang_html.js b/infrastructure/ace/www/lang_html.js index f9eff8e..f9eff8e 100644 --- a/trunk/infrastructure/ace/www/lang_html.js +++ b/infrastructure/ace/www/lang_html.js diff --git a/trunk/infrastructure/ace/www/lang_js.js b/infrastructure/ace/www/lang_js.js index 4bbc5b4..4bbc5b4 100644 --- a/trunk/infrastructure/ace/www/lang_js.js +++ b/infrastructure/ace/www/lang_js.js diff --git a/trunk/infrastructure/ace/www/lexer_support.js b/infrastructure/ace/www/lexer_support.js index 3d54f5c..3d54f5c 100644 --- a/trunk/infrastructure/ace/www/lexer_support.js +++ b/infrastructure/ace/www/lexer_support.js diff --git a/infrastructure/ace/www/linestylefilter.js b/infrastructure/ace/www/linestylefilter.js new file mode 100644 index 0000000..f772ce3 --- /dev/null +++ b/infrastructure/ace/www/linestylefilter.js @@ -0,0 +1,287 @@ +// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.linestylefilter +// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset"); +// %APPJET%: import("etherpad.admin.plugins"); + +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// requires: easysync2.Changeset +// requires: top +// requires: plugins +// requires: undefined + +var linestylefilter = {}; + +linestylefilter.ATTRIB_CLASSES = { + 'bold':'tag:b', + 'italic':'tag:i', + 'underline':'tag:u', + 'strikethrough':'tag:s', + 'h1':'tag:h1', + 'h2':'tag:h2', + 'h3':'tag:h3', + 'h4':'tag:h4', + 'h5':'tag:h5', + 'h6':'tag:h6' +}; + +linestylefilter.getAuthorClassName = function(author) { + return "author-"+author.replace(/[^a-y0-9]/g, function(c) { + if (c == ".") return "-"; + return 'z'+c.charCodeAt(0)+'z'; + }); +}; + +// lineLength is without newline; aline includes newline, +// but may be falsy if lineLength == 0 +linestylefilter.getLineStyleFilter = function(lineLength, aline, + textAndClassFunc, apool) { + + if (lineLength == 0) return textAndClassFunc; + + var nextAfterAuthorColors = textAndClassFunc; + + var authorColorFunc = (function() { + var lineEnd = lineLength; + var curIndex = 0; + var extraClasses; + var leftInAuthor; + + function attribsToClasses(attribs) { + var classes = ''; + Changeset.eachAttribNumber(attribs, function(n) { + var key = apool.getAttribKey(n); + if (key) { + var value = apool.getAttribValue(n); + if (value) { + if (key == 'author') { + classes += ' '+linestylefilter.getAuthorClassName(value); + } + else if (key == 'list') { + classes += ' list:'+value; + } + else if (linestylefilter.ATTRIB_CLASSES[key]) { + classes += ' '+linestylefilter.ATTRIB_CLASSES[key]; + } + } + } + }); + return classes.substring(1); + } + + var attributionIter = Changeset.opIterator(aline); + var nextOp, nextOpClasses; + function goNextOp() { + nextOp = attributionIter.next(); + nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs)); + } + goNextOp(); + function nextClasses() { + if (curIndex < lineEnd) { + extraClasses = nextOpClasses; + leftInAuthor = nextOp.chars; + goNextOp(); + while (nextOp.opcode && nextOpClasses == extraClasses) { + leftInAuthor += nextOp.chars; + goNextOp(); + } + } + } + nextClasses(); + + return function(txt, cls) { + while (txt.length > 0) { + if (leftInAuthor <= 0) { + // prevent infinite loop if something funny's going on + return nextAfterAuthorColors(txt, cls); + } + var spanSize = txt.length; + if (spanSize > leftInAuthor) { + spanSize = leftInAuthor; + } + var curTxt = txt.substring(0, spanSize); + txt = txt.substring(spanSize); + nextAfterAuthorColors(curTxt, (cls&&cls+" ")+extraClasses); + curIndex += spanSize; + leftInAuthor -= spanSize; + if (leftInAuthor == 0) { + nextClasses(); + } + } + }; + })(); + return authorColorFunc; +}; + +linestylefilter.getAtSignSplitterFilter = function(lineText, + textAndClassFunc) { + var at = /@/g; + at.lastIndex = 0; + var splitPoints = null; + var execResult; + while ((execResult = at.exec(lineText))) { + if (! splitPoints) { + splitPoints = []; + } + splitPoints.push(execResult.index); + } + + if (! splitPoints) return textAndClassFunc; + + return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, + splitPoints); +}; + +linestylefilter.getRegexpFilter = function (regExp, tag) { + return function (lineText, textAndClassFunc) { + regExp.lastIndex = 0; + var regExpMatchs = null; + var splitPoints = null; + var execResult; + while ((execResult = regExp.exec(lineText))) { + if (! regExpMatchs) { + regExpMatchs = []; + splitPoints = []; + } + var startIndex = execResult.index; + var regExpMatch = execResult[0]; + regExpMatchs.push([startIndex, regExpMatch]); + splitPoints.push(startIndex, startIndex + regExpMatch.length); + } + + if (! regExpMatchs) return textAndClassFunc; + + function regExpMatchForIndex(idx) { + for(var k=0; k<regExpMatchs.length; k++) { + var u = regExpMatchs[k]; + if (idx >= u[0] && idx < u[0]+u[1].length) { + return u[1]; + } + } + return false; + } + + var handleRegExpMatchsAfterSplit = (function() { + var curIndex = 0; + return function(txt, cls) { + var txtlen = txt.length; + var newCls = cls; + var regExpMatch = regExpMatchForIndex(curIndex); + if (regExpMatch) { + newCls += " "+tag+":"+regExpMatch; + } + textAndClassFunc(txt, newCls); + curIndex += txtlen; + }; + })(); + + return linestylefilter.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit, + splitPoints); + }; +}; + + +linestylefilter.REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; +linestylefilter.REGEX_URLCHAR = new RegExp('('+/[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source+'|'+linestylefilter.REGEX_WORDCHAR.source+')'); +linestylefilter.REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source+linestylefilter.REGEX_URLCHAR.source+'*(?![:.,;])'+linestylefilter.REGEX_URLCHAR.source, 'g'); +linestylefilter.getURLFilter = linestylefilter.getRegexpFilter( + linestylefilter.REGEX_URL, 'url'); + +linestylefilter.textAndClassFuncSplitter = function(func, splitPointsOpt) { + var nextPointIndex = 0; + var idx = 0; + + // don't split at 0 + while (splitPointsOpt && + nextPointIndex < splitPointsOpt.length && + splitPointsOpt[nextPointIndex] == 0) { + nextPointIndex++; + } + + function spanHandler(txt, cls) { + if ((! splitPointsOpt) || nextPointIndex >= splitPointsOpt.length) { + func(txt, cls); + idx += txt.length; + } + else { + var splitPoints = splitPointsOpt; + var pointLocInSpan = splitPoints[nextPointIndex] - idx; + var txtlen = txt.length; + if (pointLocInSpan >= txtlen) { + func(txt, cls); + idx += txt.length; + if (pointLocInSpan == txtlen) { + nextPointIndex++; + } + } + else { + if (pointLocInSpan > 0) { + func(txt.substring(0, pointLocInSpan), cls); + idx += pointLocInSpan; + } + nextPointIndex++; + // recurse + spanHandler(txt.substring(pointLocInSpan), cls); + } + } + } + return spanHandler; +}; + +linestylefilter.getFilterStack = function(lineText, textAndClassFunc, browser) { + var func = linestylefilter.getURLFilter(lineText, textAndClassFunc); + + var plugins_; + if (typeof(plugins)!='undefined') { + plugins_ = plugins; + } else { + plugins_ = parent.parent.plugins; + } + + var hookFilters = plugins_.callHook( + "aceGetFilterStack", {linestylefilter:linestylefilter, browser:browser}); + hookFilters.map(function (hookFilter) { + func = hookFilter(lineText, func); + }); + + if (browser !== undefined && browser.msie) { + // IE7+ will take an e-mail address like <foo@bar.com> and linkify it to foo@bar.com. + // We then normalize it back to text with no angle brackets. It's weird. So always + // break spans at an "at" sign. + func = linestylefilter.getAtSignSplitterFilter( + lineText, func); + } + return func; +}; + +// domLineObj is like that returned by domline.createDomLine +linestylefilter.populateDomLine = function(textLine, aline, apool, + domLineObj) { + // remove final newline from text if any + var text = textLine; + if (text.slice(-1) == '\n') { + text = text.substring(0, text.length-1); + } + + function textAndClassFunc(tokenText, tokenClass) { + domLineObj.appendSpan(tokenText, tokenClass); + } + + var func = linestylefilter.getFilterStack(text, textAndClassFunc); + func = linestylefilter.getLineStyleFilter(text.length, aline, + func, apool); + func(text, ''); +}; diff --git a/trunk/infrastructure/ace/www/magicdom.js b/infrastructure/ace/www/magicdom.js index 4bad3d4..4bad3d4 100644 --- a/trunk/infrastructure/ace/www/magicdom.js +++ b/infrastructure/ace/www/magicdom.js diff --git a/trunk/infrastructure/ace/www/multilang_lexer.js b/infrastructure/ace/www/multilang_lexer.js index 9617981..9617981 100644 --- a/trunk/infrastructure/ace/www/multilang_lexer.js +++ b/infrastructure/ace/www/multilang_lexer.js diff --git a/trunk/infrastructure/ace/www/processing.js b/infrastructure/ace/www/processing.js index 988ef76..988ef76 100644 --- a/trunk/infrastructure/ace/www/processing.js +++ b/infrastructure/ace/www/processing.js diff --git a/trunk/infrastructure/ace/www/profiler.js b/infrastructure/ace/www/profiler.js index 24b68a2..24b68a2 100644 --- a/trunk/infrastructure/ace/www/profiler.js +++ b/infrastructure/ace/www/profiler.js diff --git a/trunk/infrastructure/ace/www/skiplist.js b/infrastructure/ace/www/skiplist.js index e6c2e04..e6c2e04 100644 --- a/trunk/infrastructure/ace/www/skiplist.js +++ b/infrastructure/ace/www/skiplist.js diff --git a/trunk/infrastructure/ace/www/spanlist.js b/infrastructure/ace/www/spanlist.js index 756a411..756a411 100644 --- a/trunk/infrastructure/ace/www/spanlist.js +++ b/infrastructure/ace/www/spanlist.js diff --git a/trunk/infrastructure/ace/www/syntax-new.css b/infrastructure/ace/www/syntax-new.css index 30f1823..30f1823 100644 --- a/trunk/infrastructure/ace/www/syntax-new.css +++ b/infrastructure/ace/www/syntax-new.css diff --git a/trunk/infrastructure/ace/www/syntax.css b/infrastructure/ace/www/syntax.css index e018320..e018320 100644 --- a/trunk/infrastructure/ace/www/syntax.css +++ b/infrastructure/ace/www/syntax.css diff --git a/trunk/infrastructure/ace/www/test.html b/infrastructure/ace/www/test.html index 73fa45c..73fa45c 100644 --- a/trunk/infrastructure/ace/www/test.html +++ b/infrastructure/ace/www/test.html diff --git a/trunk/infrastructure/ace/www/testcode.js b/infrastructure/ace/www/testcode.js index f393335..f393335 100644 --- a/trunk/infrastructure/ace/www/testcode.js +++ b/infrastructure/ace/www/testcode.js diff --git a/trunk/infrastructure/ace/www/toSource.js b/infrastructure/ace/www/toSource.js index bf96df7..bf96df7 100644 --- a/trunk/infrastructure/ace/www/toSource.js +++ b/infrastructure/ace/www/toSource.js diff --git a/trunk/infrastructure/ace/www/undomodule.js b/infrastructure/ace/www/undomodule.js index b8a56f9..b8a56f9 100644 --- a/trunk/infrastructure/ace/www/undomodule.js +++ b/infrastructure/ace/www/undomodule.js diff --git a/trunk/infrastructure/ace/www/virtual_lines.js b/infrastructure/ace/www/virtual_lines.js index 86e3dea..86e3dea 100644 --- a/trunk/infrastructure/ace/www/virtual_lines.js +++ b/infrastructure/ace/www/virtual_lines.js |