Codemirror 0.8.
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Mon, 6 Sep 2010 09:12:07 +0000 (11:12 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Mon, 6 Sep 2010 09:12:07 +0000 (11:12 +0200)
fixes #816: editor and error msgs line numbers match,
fixes #744: preserve whitespace after slash

13 files changed:
redakcja/static/css/xmlcolors_15032010.css [deleted file]
redakcja/static/css/xmlcolors_20100906.css [new file with mode: 0644]
redakcja/static/js/lib/codemirror/codemirror.js
redakcja/static/js/lib/codemirror/editor.js
redakcja/static/js/lib/codemirror/parsexml.js
redakcja/static/js/lib/codemirror/select.js
redakcja/static/js/lib/codemirror/stringstream.js
redakcja/static/js/lib/codemirror/tokenize.js
redakcja/static/js/lib/codemirror/undo.js
redakcja/static/js/lib/codemirror/util.js
redakcja/static/js/wiki/view_editor_source.js
redakcja/static/js/wiki/view_search.js
redakcja/static/js/wiki/xslt.js

diff --git a/redakcja/static/css/xmlcolors_15032010.css b/redakcja/static/css/xmlcolors_15032010.css
deleted file mode 100644 (file)
index 17277f8..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-.editbox {
-  margin: .4em;
-  margin-top: 5px;
-  margin-left: 5px;
-  padding: 0;
-  font-family:"Lucida Console", monospace;
-  font-size: 13px;
-  line-height: 18px;
-  color: black;
-}
-
-.editbox p {
-  margin: 0;
-}
-
-.editbox span {
-       display: inline;
-       font-size: 13px;
-    line-height: 18px;
-}
-
-span.xml-tagname {
-  color: #0e5f0e;
-}
-
-span.xml-attribute {
-  color: #8b8b8b;
-}
-
-span.xml-punctuation {
-  color: black;
-}
-
-span.xml-attname {
-  color: #817aff;
-}
-
-span.xml-comment {
-  color: #A70;
-}
-
-span.xml-cdata {
-  color: #48A;
-}
-
-span.xml-processing {
-  color: #999;
-}
-
-span.xml-entity {
-  color: #A22;
-}
-
-span.xml-error {
-  color: #F00;
-  font-weight: bolder;
-}
-
-span.xml-text {
-  color: black;
-}
diff --git a/redakcja/static/css/xmlcolors_20100906.css b/redakcja/static/css/xmlcolors_20100906.css
new file mode 100644 (file)
index 0000000..7446450
--- /dev/null
@@ -0,0 +1,63 @@
+html {
+  cursor: text;
+}
+
+.editbox {
+  margin: .4em;
+  padding: 0;
+  /* fnp changes */
+  margin-top: 5px;
+  margin-left: 5px;
+  font-family: "Lucida Console", monospace;
+  font-size: 13px;
+  line-height: 18px;
+  /* end fnp changes */
+  color: black;
+}
+
+.editbox p {
+  margin: 0;
+}
+
+span.xml-tagname {
+  color: #A0B;
+}
+
+span.xml-attribute {
+  color: #281;
+}
+
+span.xml-punctuation {
+  color: black;
+}
+
+span.xml-attname {
+  color: #00F;
+}
+
+span.xml-comment {
+  color: #A70;
+}
+
+span.xml-cdata {
+  color: #48A;
+}
+
+span.xml-processing {
+  color: #999;
+}
+
+span.xml-entity {
+  color: #A22;
+}
+
+span.xml-error {
+ color: #F00 !important;
+  /* fnp changes */
+  font-weight: bolder;
+  /* end fnp changes */
+}
+
+span.xml-text {
+  color: black;
+}
index 8475989..57e44be 100644 (file)
@@ -26,13 +26,15 @@ var CodeMirror = (function(){
   // options to a specific CodeMirror constructor. See manual.html for
   // their meaning.
   setDefaults(CodeMirrorConfig, {
-    stylesheet: "",
+    stylesheet: [],
     path: "",
     parserfile: [],
     basefiles: ["util.js", "stringstream.js", "select.js", "undo.js", "editor.js", "tokenize.js"],
     iframeClass: null,
     passDelay: 200,
     passTime: 50,
+    lineNumberDelay: 200,
+    lineNumberTime: 50,
     continuousScanning: false,
     saveFunction: null,
     onChange: null,
@@ -41,8 +43,9 @@ var CodeMirror = (function(){
     disableSpellcheck: true,
     textWrapping: true,
     readOnly: false,
-    width: "100%",
+    width: "",
     height: "300px",
+    minHeight: 100,
     autoMatchParens: false,
     parserConfig: null,
     tabMode: "indent", // or "spaces", "default", "shift"
@@ -50,124 +53,131 @@ var CodeMirror = (function(){
     activeTokens: null,
     cursorActivity: null,
     lineNumbers: false,
-    indentUnit: 2
+    indentUnit: 2,
+    domain: null
   });
 
-  function wrapLineNumberDiv(place) {
-    return function(node) {
-      var container = document.createElement("div"),
-          nums = document.createElement("div"),
-          scroller = document.createElement("div");
-      container.style.position = "relative";
-      nums.style.position = "absolute";
-      nums.style.height = "100%";
-      if (nums.style.setExpression) {
-        try {nums.style.setExpression("height", "this.previousSibling.offsetHeight + 'px'");}
-        catch(e) {} // Seems to throw 'Not Implemented' on some IE8 versions
-      }
-      nums.style.top = "0px";
-      nums.style.overflow = "hidden";
-      place(container);
-      container.appendChild(node);
-      container.appendChild(nums);
-      scroller.className = "CodeMirror-line-numbers";
-      nums.appendChild(scroller);
+  function addLineNumberDiv(container) {
+    var nums = document.createElement("DIV"),
+        scroller = document.createElement("DIV");
+    nums.style.position = "absolute";
+    nums.style.height = "100%";
+    if (nums.style.setExpression) {
+      try {nums.style.setExpression("height", "this.previousSibling.offsetHeight + 'px'");}
+      catch(e) {} // Seems to throw 'Not Implemented' on some IE8 versions
     }
+    nums.style.top = "0px";
+    nums.style.left = "0px";
+    nums.style.overflow = "hidden";
+    container.appendChild(nums);
+    scroller.className = "CodeMirror-line-numbers";
+    nums.appendChild(scroller);
+    scroller.innerHTML = "<div>1</div>";
+    return nums;
   }
 
-  function applyLineNumbers(frame) {
-    var win = frame.contentWindow, doc = win.document,
-        nums = frame.nextSibling, scroller = nums.firstChild;
-
-    var nextNum = 1;
-
-    function update() {
-      var diff = 20 + Math.max(doc.body.offsetHeight, frame.offsetHeight) - scroller.offsetHeight;
-      for (var n = Math.ceil(diff / 10); n > 0; n--) {
-        var div = document.createElement("div");
-        div.appendChild(document.createTextNode(nextNum++));
-        scroller.appendChild(div);
-      }
-      nums.scrollTop = doc.body.scrollTop || doc.documentElement.scrollTop || 0;
-    }
+  function frameHTML(options) {
+    if (typeof options.parserfile == "string")
+      options.parserfile = [options.parserfile];
+    if (typeof options.stylesheet == "string")
+      options.stylesheet = [options.stylesheet];
 
-    update();
-    win.addEventHandler(win, "scroll", update);
-    win.addEventHandler(win, "resize", update);
+    var html = ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\"><html><head>"];
+    // Hack to work around a bunch of IE8-specific problems.
+    html.push("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=EmulateIE7\"/>");
+    forEach(options.stylesheet, function(file) {
+      html.push("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + file + "\"/>");
+    });
+    forEach(options.basefiles.concat(options.parserfile), function(file) {
+      if (!/^https?:/.test(file)) file = options.path + file;
+      html.push("<script type=\"text/javascript\" src=\"" + file + "\"><" + "/script>");
+    });
+    html.push("</head><body style=\"border-width: 0;\" class=\"editbox\" spellcheck=\"" +
+              (options.disableSpellcheck ? "false" : "true") + "\"></body></html>");
+    return html.join("");
   }
 
-  function CodeMirror(place, options) {
-    // Backward compatibility for deprecated options.
-    if (options.dumbTabs) options.tabMode = "spaces";
-    else if (options.normalTab) options.tabMode = "default";
+  var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
 
+  function CodeMirror(place, options) {
     // Use passed options, if any, to override defaults.
     this.options = options = options || {};
     setDefaults(options, CodeMirrorConfig);
 
-    var frame = this.frame = document.createElement("iframe");
+    // Backward compatibility for deprecated options.
+    if (options.dumbTabs) options.tabMode = "spaces";
+    else if (options.normalTab) options.tabMode = "default";
+
+    var frame = this.frame = document.createElement("IFRAME");
     if (options.iframeClass) frame.className = options.iframeClass;
     frame.frameBorder = 0;
-    frame.src = "javascript:false;";
     frame.style.border = "0";
-    frame.style.width = options.width;
-    frame.style.height = options.height;
+    frame.style.width = '100%';
+    frame.style.height = '100%';
     // display: block occasionally suppresses some Firefox bugs, so we
     // always add it, redundant as it sounds.
     frame.style.display = "block";
 
-    if (place.appendChild) {
-      var node = place;
-      place = function(n){node.appendChild(n);};
-    }
-    if (options.lineNumbers) place = wrapLineNumberDiv(place);
-    place(frame);
+    var div = this.wrapping = document.createElement("DIV");
+    div.style.position = "relative";
+    div.className = "CodeMirror-wrapping";
+    div.style.width = options.width;
+    div.style.height = (options.height == "dynamic") ? options.minHeight + "px" : options.height;
+    // This is used by Editor.reroutePasteEvent
+    var teHack = this.textareaHack = document.createElement("TEXTAREA");
+    div.appendChild(teHack);
+    teHack.style.position = "absolute";
+    teHack.style.left = "-10000px";
+    teHack.style.width = "10px";
 
     // Link back to this object, so that the editor can fetch options
     // and add a reference to itself.
     frame.CodeMirror = this;
-    this.win = frame.contentWindow;
+    if (options.domain && internetExplorer) {
+      this.html = frameHTML(options);
+      frame.src = "javascript:(function(){document.open();" +
+        (options.domain ? "document.domain=\"" + options.domain + "\";" : "") +
+        "document.write(window.frameElement.CodeMirror.html);document.close();})()";
+    }
+    else {
+      frame.src = "javascript:false";
+    }
 
-    if (typeof options.parserfile == "string")
-      options.parserfile = [options.parserfile];
-    if (typeof options.stylesheet == "string")
-      options.stylesheet = [options.stylesheet];
+    if (place.appendChild) place.appendChild(div);
+    else place(div);
+    div.appendChild(frame);
+    if (options.lineNumbers) this.lineNumbers = addLineNumberDiv(div);
 
-    var html = ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\"><html><head>"];
-    // Hack to work around a bunch of IE8-specific problems.
-    html.push("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=EmulateIE7\"/>");
-    forEach(options.stylesheet, function(file) {
-      html.push("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + file + "\"/>");
-    });
-    forEach(options.basefiles.concat(options.parserfile), function(file) {
-      html.push("<script type=\"text/javascript\" src=\"" + options.path + file + "\"></script>");
-    });
-    html.push("</head><body style=\"border-width: 0;\" class=\"editbox\" spellcheck=\"" +
-              (options.disableSpellcheck ? "false" : "true") + "\"></body></html>");
-
-    var doc = this.win.document;
-    doc.open();
-    doc.write(html.join(""));
-    doc.close();
+    this.win = frame.contentWindow;
+    if (!options.domain || !internetExplorer) {
+      this.win.document.open();
+      this.win.document.write(frameHTML(options));
+      this.win.document.close();
+    }
   }
 
   CodeMirror.prototype = {
     init: function() {
       if (this.options.initCallback) this.options.initCallback(this);
-      if (this.options.lineNumbers) applyLineNumbers(this.frame);
+      if (this.options.lineNumbers) this.activateLineNumbers();
       if (this.options.reindentOnLoad) this.reindent();
+      if (this.options.height == "dynamic") this.setDynamicHeight();
     },
 
     getCode: function() {return this.editor.getCode();},
     setCode: function(code) {this.editor.importCode(code);},
-    selection: function() {return this.editor.selectedText();},
+    selection: function() {this.focusIfIE(); return this.editor.selectedText();},
     reindent: function() {this.editor.reindent();},
-    reindentSelection: function() {this.editor.reindentSelection(null);},
+    reindentSelection: function() {this.focusIfIE(); this.editor.reindentSelection(null);},
 
+    focusIfIE: function() {
+      // in IE, a lot of selection-related functionality only works when the frame is focused
+      if (this.win.select.ie_selection) this.focus();
+    },
     focus: function() {
       this.win.focus();
       if (this.editor.selectionSnapshot) // IE hack
-        this.win.select.selectCoords(this.win, this.editor.selectionSnapshot);
+        this.win.select.setBookmark(this.win.document.body, this.editor.selectionSnapshot);
     },
     replaceSelection: function(text) {
       this.focus();
@@ -177,8 +187,8 @@ var CodeMirror = (function(){
     replaceChars: function(text, start, end) {
       this.editor.replaceChars(text, start, end);
     },
-    getSearchCursor: function(string, fromCursor, regexp, case_sensitive) {
-      return this.editor.getSearchCursor(string, fromCursor, regexp, case_sensitive);
+    getSearchCursor: function(string, fromCursor, caseFold) {
+      return this.editor.getSearchCursor(string, fromCursor, caseFold);
     },
 
     undo: function() {this.editor.history.undo();},
@@ -189,18 +199,76 @@ var CodeMirror = (function(){
     grabKeys: function(callback, filter) {this.editor.grabKeys(callback, filter);},
     ungrabKeys: function() {this.editor.ungrabKeys();},
 
-    setParser: function(name) {this.editor.setParser(name);},
-
-    cursorPosition: function(start) {
-      if (this.win.select.ie_selection) this.focus();
-      return this.editor.cursorPosition(start);
+    setParser: function(name, parserConfig) {this.editor.setParser(name, parserConfig);},
+    setSpellcheck: function(on) {this.win.document.body.spellcheck = on;},
+    setStylesheet: function(names) {
+      if (typeof names === "string") names = [names];
+      var activeStylesheets = {};
+      var matchedNames = {};
+      var links = this.win.document.getElementsByTagName("link");
+      // Create hashes of active stylesheets and matched names.
+      // This is O(n^2) but n is expected to be very small.
+      for (var x = 0, link; link = links[x]; x++) {
+        if (link.rel.indexOf("stylesheet") !== -1) {
+          for (var y = 0; y < names.length; y++) {
+            var name = names[y];
+            if (link.href.substring(link.href.length - name.length) === name) {
+              activeStylesheets[link.href] = true;
+              matchedNames[name] = true;
+            }
+          }
+        }
+      }
+      // Activate the selected stylesheets and disable the rest.
+      for (var x = 0, link; link = links[x]; x++) {
+        if (link.rel.indexOf("stylesheet") !== -1) {
+          link.disabled = !(link.href in activeStylesheets);
+        }
+      }
+      // Create any new stylesheets.
+      for (var y = 0; y < names.length; y++) {
+        var name = names[y];
+        if (!(name in matchedNames)) {
+          var link = this.win.document.createElement("link");
+          link.rel = "stylesheet";
+          link.type = "text/css";
+          link.href = name;
+          this.win.document.getElementsByTagName('head')[0].appendChild(link);
+        }
+      }
+    },
+    setTextWrapping: function(on) {
+      if (on == this.options.textWrapping) return;
+      this.win.document.body.style.whiteSpace = on ? "" : "nowrap";
+      this.options.textWrapping = on;
+      if (this.lineNumbers) {
+        this.setLineNumbers(false);
+        this.setLineNumbers(true);
+      }
     },
+    setIndentUnit: function(unit) {this.win.indentUnit = unit;},
+    setUndoDepth: function(depth) {this.editor.history.maxDepth = depth;},
+    setTabMode: function(mode) {this.options.tabMode = mode;},
+    setLineNumbers: function(on) {
+      if (on && !this.lineNumbers) {
+        this.lineNumbers = addLineNumberDiv(this.wrapping);
+        this.activateLineNumbers();
+      }
+      else if (!on && this.lineNumbers) {
+        this.wrapping.removeChild(this.lineNumbers);
+        this.wrapping.style.marginLeft = "";
+        this.lineNumbers = null;
+      }
+    },
+
+    cursorPosition: function(start) {this.focusIfIE(); return this.editor.cursorPosition(start);},
     firstLine: function() {return this.editor.firstLine();},
     lastLine: function() {return this.editor.lastLine();},
     nextLine: function(line) {return this.editor.nextLine(line);},
     prevLine: function(line) {return this.editor.prevLine(line);},
     lineContent: function(line) {return this.editor.lineContent(line);},
     setLineContent: function(line, content) {this.editor.setLineContent(line, content);},
+    removeLine: function(line){this.editor.removeLine(line);},
     insertIntoLine: function(line, position, content) {this.editor.insertIntoLine(line, position, content);},
     selectLines: function(startLine, startOffset, endLine, endOffset) {
       this.win.focus();
@@ -220,14 +288,168 @@ var CodeMirror = (function(){
       }
       return num;
     },
-
-    // Old number-based line interface
-    jumpToLine: function(n) {
-      this.selectLines(this.nthLine(n), 0);
+    jumpToLine: function(line) {
+      if (typeof line == "number") line = this.nthLine(line);
+      this.selectLines(line, 0);
       this.win.focus();
     },
-    currentLine: function() {
-      return this.lineNumber(this.cursorPosition().line);
+    currentLine: function() { // Deprecated, but still there for backward compatibility
+      return this.lineNumber(this.cursorLine());
+    },
+    cursorLine: function() {
+      return this.cursorPosition().line;
+    },
+    cursorCoords: function(start) {return this.editor.cursorCoords(start);},
+
+    activateLineNumbers: function() {
+      var frame = this.frame, win = frame.contentWindow, doc = win.document, body = doc.body,
+          nums = this.lineNumbers, scroller = nums.firstChild, self = this;
+      var barWidth = null;
+
+      function sizeBar() {
+        if (frame.offsetWidth == 0) return;
+        for (var root = frame; root.parentNode; root = root.parentNode);
+        if (!nums.parentNode || root != document || !win.Editor) {
+          // Clear event handlers (their nodes might already be collected, so try/catch)
+          try{clear();}catch(e){}
+          clearInterval(sizeInterval);
+          return;
+        }
+
+        /*if (nums.offsetWidth != barWidth) {
+          barWidth = nums.offsetWidth;
+          frame.parentNode.style.paddingLeft = barWidth + "px";
+        }*/
+      }
+      function doScroll() {
+        nums.scrollTop = body.scrollTop || doc.documentElement.scrollTop || 0;
+      }
+      // Cleanup function, registered by nonWrapping and wrapping.
+      var clear = function(){};
+      sizeBar();
+      var sizeInterval = setInterval(sizeBar, 500);
+
+      function ensureEnoughLineNumbers(fill) {
+        var lineHeight = scroller.firstChild.offsetHeight;
+        if (lineHeight == 0) return;
+        var targetHeight = 50 + Math.max(body.offsetHeight, Math.max(frame.offsetHeight, body.scrollHeight || 0)),
+            lastNumber = Math.ceil(targetHeight / lineHeight);
+        for (var i = scroller.childNodes.length; i <= lastNumber; i++) {
+          var div = document.createElement("DIV");
+          div.appendChild(document.createTextNode(fill ? String(i + 1) : "\u00a0"));
+          scroller.appendChild(div);
+        }
+      }
+
+      function nonWrapping() {
+        function update() {
+          ensureEnoughLineNumbers(true);
+          doScroll();
+        }
+        self.updateNumbers = update;
+        var onScroll = win.addEventHandler(win, "scroll", doScroll, true),
+            onResize = win.addEventHandler(win, "resize", update, true);
+        clear = function(){
+          onScroll(); onResize();
+          if (self.updateNumbers == update) self.updateNumbers = null;
+        };
+        update();
+      }
+
+      function wrapping() {
+        var node, lineNum, next, pos, changes = [], styleNums = self.options.styleNumbers;
+
+        function setNum(n, node) {
+          // Does not typically happen (but can, if you mess with the
+          // document during the numbering)
+          if (!lineNum) lineNum = scroller.appendChild(document.createElement("DIV"));
+          if (styleNums) styleNums(lineNum, node, n);
+          // Changes are accumulated, so that the document layout
+          // doesn't have to be recomputed during the pass
+          changes.push(lineNum); changes.push(n);
+          pos = lineNum.offsetHeight + lineNum.offsetTop;
+          lineNum = lineNum.nextSibling;
+        }
+        function commitChanges() {
+          for (var i = 0; i < changes.length; i += 2)
+            changes[i].innerHTML = changes[i + 1];
+          changes = [];
+        }
+        function work() {
+          if (!scroller.parentNode || scroller.parentNode != self.lineNumbers) return;
+
+          var endTime = new Date().getTime() + self.options.lineNumberTime;
+          while (node) {
+            setNum(next++, node.previousSibling);
+            for (; node && !win.isBR(node); node = node.nextSibling) {
+              var bott = node.offsetTop + node.offsetHeight;
+              while (scroller.offsetHeight && bott - 3 > pos) setNum("&nbsp;");
+            }
+            if (node) node = node.nextSibling;
+            if (new Date().getTime() > endTime) {
+              commitChanges();
+              pending = setTimeout(work, self.options.lineNumberDelay);
+              return;
+            }
+          }
+          while (lineNum) setNum(next++);
+          commitChanges();
+          doScroll();
+        }
+        function start(firstTime) {
+          doScroll();
+          ensureEnoughLineNumbers(firstTime);
+          node = body.firstChild;
+          lineNum = scroller.firstChild;
+          pos = 0;
+          next = 1;
+          work();
+        }
+
+        start(true);
+        var pending = null;
+        function update() {
+          if (pending) clearTimeout(pending);
+          if (self.editor.allClean()) start();
+          else pending = setTimeout(update, 200);
+        }
+        self.updateNumbers = update;
+        var onScroll = win.addEventHandler(win, "scroll", doScroll, true),
+            onResize = win.addEventHandler(win, "resize", update, true);
+        clear = function(){
+          if (pending) clearTimeout(pending);
+          if (self.updateNumbers == update) self.updateNumbers = null;
+          onScroll();
+          onResize();
+        };
+      }
+      (this.options.textWrapping || this.options.styleNumbers ? wrapping : nonWrapping)();
+    },
+
+    setDynamicHeight: function() {
+      var self = this, activity = self.options.cursorActivity, win = self.win, body = win.document.body,
+          lineHeight = null, timeout = null, vmargin = 2 * self.frame.offsetTop;
+      body.style.overflowY = "hidden";
+      win.document.documentElement.style.overflowY = "hidden";
+      this.frame.scrolling = "no";
+
+      function updateHeight() {
+        for (var span = body.firstChild, sawBR = false; span; span = span.nextSibling)
+          if (win.isSpan(span) && span.offsetHeight) {
+            lineHeight = span.offsetHeight;
+            if (!sawBR) vmargin = 2 * (self.frame.offsetTop + span.offsetTop + body.offsetTop + (internetExplorer ? 10 : 0));
+            break;
+          }
+        if (lineHeight)
+          self.wrapping.style.height = Math.max(vmargin + lineHeight * (body.getElementsByTagName("BR").length + 1),
+                                                self.options.minHeight) + "px";
+      }
+      setTimeout(updateHeight, 100);
+      self.options.cursorActivity = function(x) {
+        if (activity) activity(x);
+        clearTimeout(timeout);
+        timeout = setTimeout(updateHeight, 200);
+      };
     }
   };
 
@@ -260,6 +482,15 @@ var CodeMirror = (function(){
         area.form.addEventListener("submit", updateField, false);
       else
         area.form.attachEvent("onsubmit", updateField);
+      var realSubmit = area.form.submit;
+      function wrapSubmit() {
+        updateField();
+        // Can't use realSubmit.apply because IE6 is too stupid
+        area.form.submit = realSubmit;
+        area.form.submit();
+        area.form.submit = wrapSubmit;
+      }
+      area.form.submit = wrapSubmit;
     }
 
     function insert(frame) {
@@ -271,6 +502,18 @@ var CodeMirror = (function(){
 
     area.style.display = "none";
     var mirror = new CodeMirror(insert, options);
+    mirror.toTextArea = function() {
+      area.parentNode.removeChild(mirror.wrapping);
+      area.style.display = "";
+      if (area.form) {
+        area.form.submit = realSubmit;
+        if (typeof area.form.removeEventListener == "function")
+          area.form.removeEventListener("submit", updateField, false);
+        else
+          area.form.detachEvent("onsubmit", updateField);
+      }
+    };
+
     return mirror;
   };
 
index 4eb6d09..07410d2 100644 (file)
@@ -4,13 +4,22 @@
  * plain sequences of <span> and <br> elements
  */
 
+var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
+var webkit = /AppleWebKit/.test(navigator.userAgent);
+var safari = /Apple Computers, Inc/.test(navigator.vendor);
+var gecko = /gecko\/(\d{8})/i.test(navigator.userAgent);
+// TODO this is related to the backspace-at-end-of-line bug. Remove
+// this if Opera gets their act together, make the version check more
+// broad if they don't.
+var brokenOpera = window.opera && /Version\/10.[56]/.test(navigator.userAgent);
+
 // Make sure a string does not contain two consecutive 'collapseable'
 // whitespace characters.
 function makeWhiteSpace(n) {
   var buffer = [], nb = true;
   for (; n > 0; n--) {
     buffer.push((nb || n == 1) ? nbsp : " ");
-    nb = !nb;
+    nb ^= true;
   }
   return buffer.join("");
 }
@@ -19,12 +28,12 @@ function makeWhiteSpace(n) {
 // by the browser, but will not break text-wrapping either.
 function fixSpaces(string) {
   if (string.charAt(0) == " ") string = nbsp + string.slice(1);
-  return string.replace(/\t/g, function(){return makeWhiteSpace(indentUnit);})
+  return string.replace(/\t/g, function() {return makeWhiteSpace(indentUnit);})
     .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
 }
 
 function cleanText(text) {
-  return text.replace(/\u00a0/g, " ").replace(/\u200b/g, "");
+  return text.replace(/\u00a0/g, " ");
 }
 
 // Create a SPAN node with the expected properties for document part
@@ -41,22 +50,6 @@ function makePartSpan(value, doc) {
   return span;
 }
 
-// On webkit, when the last BR of the document does not have text
-// behind it, the cursor can not be put on the line after it. This
-// makes pressing enter at the end of the document occasionally do
-// nothing (or at least seem to do nothing). To work around it, this
-// function makes sure the document ends with a span containing a
-// zero-width space character. The traverseDOM iterator filters such
-// character out again, so that the parsers won't see them. This
-// function is called from a few strategic places to make sure the
-// zwsp is restored after the highlighting process eats it.
-var webkitLastLineHack = webkit ?
-  function(container) {
-    var last = container.lastChild;
-    if (!last || !last.isPart || last.textContent != "\u200b")
-      container.appendChild(makePartSpan("\u200b", container.ownerDocument));
-  } : function() {};
-
 var Editor = (function(){
   // The HTML elements whose content should be suffixed by a newline
   // when converting them to flat text.
@@ -76,17 +69,17 @@ var Editor = (function(){
 
     function simplifyNode(node, top) {
       if (node.nodeType == 3) {
-        var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " "));
+        var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/\r/g, "").replace(/\n/g, " "));
         if (text.length) leaving = false;
         result.push(node);
       }
-      else if (node.nodeName == "BR" && node.childNodes.length == 0) {
+      else if (isBR(node) && node.childNodes.length == 0) {
         leaving = true;
         result.push(node);
       }
       else {
-        forEach(node.childNodes, simplifyNode);
-        if (!leaving && newlineElements.hasOwnProperty(node.nodeName)) {
+        for (var n = node.firstChild; n; n = n.nextSibling) simplifyNode(n);
+        if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) {
           leaving = true;
           if (!atEnd || !top)
             result.push(doc.createElement("BR"));
@@ -103,13 +96,7 @@ var Editor = (function(){
   // the nodes. It makes sure that all nodes up to and including the
   // one whose text is being yielded have been 'normalized' to be just
   // <span> and <br> elements.
-  // See the story.html file for some short remarks about the use of
-  // continuation-passing style in this iterator.
   function traverseDOM(start){
-    function yield(value, c){cc = c; return value;}
-    function push(fun, arg, c){return function(){return fun(arg, c);};}
-    function stop(){cc = stop; throw StopIteration;};
-    var cc = push(scanNode, start, stop);
     var owner = start.ownerDocument;
     var nodeQueue = [];
 
@@ -124,6 +111,11 @@ var Editor = (function(){
     }
     var point = null;
 
+    // This an Opera-specific hack -- always insert an empty span
+    // between two BRs, because Opera's cursor code gets terribly
+    // confused when the cursor is between two BRs.
+    var afterBR = true;
+
     // Insert a normalized node at the current point. If it is a text
     // node, wrap it in a <span>, and give that span a currentText
     // property -- this is used to cache the nodeValue, because
@@ -136,6 +128,12 @@ var Editor = (function(){
         select.snapshotChanged();
         part = makePartSpan(part, owner);
         text = part.currentText;
+        afterBR = false;
+      }
+      else {
+        if (afterBR && window.opera)
+          point(makePartSpan("", owner));
+        afterBR = true;
       }
       part.dirty = true;
       nodeQueue.push(part);
@@ -144,14 +142,13 @@ var Editor = (function(){
     }
 
     // Extract the text and newlines from a DOM node, insert them into
-    // the document, and yield the textual content. Used to replace
+    // the document, and return the textual content. Used to replace
     // non-normalized nodes.
-    function writeNode(node, c, end) {
-      var toYield = [];
-      forEach(simplifyDOM(node, end), function(part) {
-        toYield.push(insertPart(part));
-      });
-      return yield(toYield.join(""), c);
+    function writeNode(node, end) {
+      var simplified = simplifyDOM(node, end);
+      for (var i = 0; i < simplified.length; i++)
+        simplified[i] = insertPart(simplified[i]);
+      return simplified.join("");
     }
 
     // Check whether a node is a normalized <span> element.
@@ -163,55 +160,54 @@ var Editor = (function(){
       return false;
     }
 
-    // Handle a node. Add its successor to the continuation if there
-    // is one, find out whether the node is normalized. If it is,
-    // yield its content, otherwise, normalize it (writeNode will take
-    // care of yielding).
-    function scanNode(node, c){
-      if (node.nextSibling)
-        c = push(scanNode, node.nextSibling, c);
+    // Advance to next node, return string for current node.
+    function next() {
+      if (!start) throw StopIteration;
+      var node = start;
+      start = node.nextSibling;
 
       if (partNode(node)){
         nodeQueue.push(node);
-        return yield(node.currentText, c);
+        afterBR = false;
+        return node.currentText;
       }
-      else if (node.nodeName == "BR") {
+      else if (isBR(node)) {
+        if (afterBR && window.opera)
+          node.parentNode.insertBefore(makePartSpan("", owner), node);
         nodeQueue.push(node);
-        return yield("\n", c);
+        afterBR = true;
+        return "\n";
       }
       else {
         var end = !node.nextSibling;
         point = pointAt(node);
         removeElement(node);
-        return writeNode(node, c, end);
+        return writeNode(node, end);
       }
     }
 
     // MochiKit iterators are objects with a next function that
     // returns the next value or throws StopIteration when there are
     // no more values.
-    return {next: function(){return cc();}, nodes: nodeQueue};
+    return {next: next, nodes: nodeQueue};
   }
 
   // Determine the text size of a processed node.
   function nodeSize(node) {
-    if (node.nodeName == "BR")
-      return 1;
-    else
-      return node.currentText.length;
+    return isBR(node) ? 1 : node.currentText.length;
   }
 
   // Search backwards through the top-level nodes until the next BR or
   // the start of the frame.
   function startOfLine(node) {
-    while (node && node.nodeName != "BR") node = node.previousSibling;
+    while (node && !isBR(node)) node = node.previousSibling;
     return node;
   }
   function endOfLine(node, container) {
     if (!node) node = container.firstChild;
-    else if (node.nodeName == "BR") node = node.nextSibling;
+    else if (isBR(node)) node = node.nextSibling;
 
-    while (node && node.nodeName != "BR") node = node.nextSibling;
+    while (node && !isBR(node)) node = node.nextSibling;
     return node;
   }
 
@@ -223,30 +219,13 @@ var Editor = (function(){
   // indicating whether anything was found, and can be called again to
   // skip to the next find. Use the select and replace methods to
   // actually do something with the found locations.
-  function SearchCursor(editor, string, fromCursor, regexp, case_sensitive) {
-
-    function casedIndexOf(hay, needle, case_sensitive) {
-        if (case_sensitive)
-            return hay.indexOf(needle);
-        else
-            return hay.toLowerCase().indexOf(needle.toLowerCase())
-    }
-
-    function casedLastIndexOf(hay, needle, case_sensitive) {
-        if (case_sensitive)
-            return hay.lastIndexOf(needle);
-        else
-            return hay.toLowerCase().lastIndexOf(needle.toLowerCase());
-    }
-
-    function casedEqual(a, b, case_sensitive) {
-        if (case_sensitive)
-            return a == b;
-        else
-            return a.toLowerCase() == b.toLowerCase();
-    }
-
+  function SearchCursor(editor, string, fromCursor, caseFold) {
     this.editor = editor;
+    if (caseFold == undefined) {
+      caseFold = (string == string.toLowerCase());
+    }
+    this.caseFold = caseFold;
+    if (caseFold) string = string.toLowerCase();
     this.history = editor.history;
     this.history.commit();
 
@@ -274,8 +253,8 @@ var Editor = (function(){
       // For one-line strings, searching can be done simply by calling
       // indexOf on the current line.
       function() {
-        var match = casedIndexOf(cleanText(self.history.textAfter(self.line).slice(self.offset)),
-                string, case_sensitive);
+        var line = cleanText(self.history.textAfter(self.line).slice(self.offset));
+        var match = (self.caseFold ? line.toLowerCase() : line).indexOf(string);
         if (match > -1)
           return {from: {node: self.line, offset: self.offset + match},
                   to: {node: self.line, offset: self.offset + match + string.length}};
@@ -285,19 +264,21 @@ var Editor = (function(){
       // end of the line and the last match starts at the start.
       function() {
         var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
-        var match = casedLastIndexOf(firstLine, target[0], case_sensitive);
+        var match = (self.caseFold ? firstLine.toLowerCase() : firstLine).lastIndexOf(target[0]);
         if (match == -1 || match != firstLine.length - target[0].length)
           return false;
         var startOffset = self.offset + match;
 
         var line = self.history.nodeAfter(self.line);
         for (var i = 1; i < target.length - 1; i++) {
-          if (!casedEqual(cleanText(self.history.textAfter(line)), target[i], case_sensitive))
+          var lineText = cleanText(self.history.textAfter(line));
+          if ((self.caseFold ? lineText.toLowerCase() : lineText) != target[i])
             return false;
           line = self.history.nodeAfter(line);
         }
 
-        if (casedIndexOf(cleanText(self.history.textAfter(line)), target[target.length - 1], case_sensitive) != 0)
+        var lastLine = cleanText(self.history.textAfter(line));
+        if ((self.caseFold ? lastLine.toLowerCase() : lastLine).indexOf(target[target.length - 1]) != 0)
           return false;
 
         return {from: {node: self.line, offset: startOffset},
@@ -374,8 +355,7 @@ var Editor = (function(){
     this.doc = document;
     var container = this.container = this.doc.body;
     this.win = window;
-    this.history = new History(container, options.undoDepth, options.undoDelay,
-                               this, options.onChange);
+    this.history = new UndoHistory(container, options.undoDepth, options.undoDelay, this);
     var self = this;
 
     if (!Editor.Parser)
@@ -387,10 +367,8 @@ var Editor = (function(){
       select.setCursorPos(container, {node: null, offset: 0});
 
     this.dirty = [];
-    if (options.content)
-      this.importCode(options.content);
-    else // FF acts weird when the editable document is completely empty
-      container.appendChild(this.doc.createElement("BR"));
+    this.importCode(options.content || "");
+    this.history.onChange = options.onChange;
 
     if (!options.readOnly) {
       if (options.continuousScanning !== false) {
@@ -399,8 +377,10 @@ var Editor = (function(){
       }
 
       function setEditable() {
-        // In IE, designMode frames can not run any scripts, so we use
-        // contentEditable instead.
+        // Use contentEditable instead of designMode on IE, since designMode frames
+        // can not run any scripts. It would be nice if we could use contentEditable
+        // everywhere, but it is significantly flakier than designMode on every
+        // single non-IE browser.
         if (document.body.contentEditable != undefined && internetExplorer)
           document.body.contentEditable = "true";
         else
@@ -432,6 +412,11 @@ var Editor = (function(){
       addEventHandler(document.body, "mouseup", cursorActivity);
       addEventHandler(document.body, "cut", cursorActivity);
 
+      // workaround for a gecko bug [?] where going forward and then
+      // back again breaks designmode (no more cursor)
+      if (gecko)
+        addEventHandler(this.win, "pagehide", function(){self.unloaded = true;});
+
       addEventHandler(document.body, "paste", function(event) {
         cursorActivity();
         var text = null;
@@ -441,15 +426,14 @@ var Editor = (function(){
         }
         catch(e) {}
         if (text !== null) {
-          self.replaceSelection(text);
           event.stop();
+          self.replaceSelection(text);
+          select.scrollToCursor(self.container);
         }
       });
 
-      addEventHandler(document.body, "beforepaste", method(this, "reroutePasteEvent"));
-
       if (this.options.autoMatchParens)
-        addEventHandler(document.body, "click", method(this, "scheduleParenBlink"));
+        addEventHandler(document.body, "click", method(this, "scheduleParenHighlight"));
     }
     else if (!options.textWrapping) {
       container.style.whiteSpace = "nowrap";
@@ -476,7 +460,6 @@ var Editor = (function(){
       var accum = [];
       select.markSelection(this.win);
       forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
-      webkitLastLineHack(this.container);
       select.selectMarked();
       return cleanText(accum.join(""));
     },
@@ -514,6 +497,16 @@ var Editor = (function(){
       return startOfLine(line.previousSibling);
     },
 
+    visibleLineCount: function() {
+      var line = this.container.firstChild;
+      while (line && isBR(line)) line = line.nextSibling; // BR heights are unreliable
+      if (!line) return false;
+      var innerHeight = (window.innerHeight
+                         || document.documentElement.clientHeight
+                         || document.body.clientHeight);
+      return Math.floor(innerHeight / line.offsetHeight);
+    },
+
     selectLines: function(startLine, startOffset, endLine, endOffset) {
       this.checkLine(startLine);
       var start = {node: startLine, offset: startOffset}, end = null;
@@ -526,10 +519,9 @@ var Editor = (function(){
     },
 
     lineContent: function(line) {
-      this.checkLine(line);
       var accum = [];
       for (line = line ? line.nextSibling : this.container.firstChild;
-           line && line.nodeName != "BR"; line = line.nextSibling)
+           line && !isBR(line); line = line.nextSibling)
         accum.push(nodeText(line));
       return cleanText(accum.join(""));
     },
@@ -543,6 +535,18 @@ var Editor = (function(){
       this.scheduleHighlight();
     },
 
+    removeLine: function(line) {
+      var node = line ? line.nextSibling : this.container.firstChild;
+      while (node) {
+        var next = node.nextSibling;
+        removeElement(node);
+        if (isBR(node)) break;
+        node = next;
+      }
+      this.addDirtyNode(line);
+      this.scheduleHighlight();
+    },
+
     insertIntoLine: function(line, position, content) {
       var before = null;
       if (position == "end") {
@@ -554,7 +558,7 @@ var Editor = (function(){
             before = cur;
             break;
           }
-          var text = (cur.innerText || cur.textContent || cur.nodeValue || "");
+          var text = nodeText(cur);
           if (text.length > position) {
             before = cur.nextSibling;
             content = text.slice(0, position) + content + text.slice(position);
@@ -593,8 +597,7 @@ var Editor = (function(){
       return cleanText(text.join("\n"));
     },
 
-    // Replace the selection with another p
-    // iece of text.
+    // Replace the selection with another piece of text.
     replaceSelection: function(text) {
       this.history.commit();
 
@@ -603,20 +606,58 @@ var Editor = (function(){
       if (!start || !end) return;
 
       end = this.replaceRange(start, end, text);
-      // select.setCursorPos(this.container, end);
-      webkitLastLineHack(this.container);
+      select.setCursorPos(this.container, end);
+    },
+
+    cursorCoords: function(start) {
+      var sel = select.cursorPos(this.container, start);
+      if (!sel) return null;
+      var off = sel.offset, node = sel.node, doc = this.win.document, self = this;
+      function measureFromNode(node, xOffset) {
+        var y = -(self.win.document.body.scrollTop || self.win.document.documentElement.scrollTop || 0),
+            x = -(self.win.document.body.scrollLeft || self.win.document.documentElement.scrollLeft || 0) + xOffset;
+        forEach([node, self.win.frameElement], function(n) {
+          while (n) {x += n.offsetLeft; y += n.offsetTop;n = n.offsetParent;}
+        });
+        return {x: x, y: y, yBot: y + node.offsetHeight};
+      }
+      function withTempNode(text, f) {
+        var node = doc.createElement("SPAN");
+        node.appendChild(doc.createTextNode(text));
+        try {return f(node);}
+        finally {if (node.parentNode) node.parentNode.removeChild(node);}
+      }
+
+      while (off) {
+        node = node ? node.nextSibling : this.container.firstChild;
+        var txt = nodeText(node);
+        if (off < txt.length)
+          return withTempNode(txt.substr(0, off), function(tmp) {
+            tmp.style.position = "absolute"; tmp.style.visibility = "hidden";
+            tmp.className = node.className;
+            self.container.appendChild(tmp);
+            return measureFromNode(node, tmp.offsetWidth);
+          });
+        off -= txt.length;
+      }
+      if (node && isSpan(node))
+        return measureFromNode(node, node.offsetWidth);
+      else if (node && node.nextSibling && isSpan(node.nextSibling))
+        return measureFromNode(node.nextSibling, 0);
+      else
+        return withTempNode("\u200b", function(tmp) {
+          if (node) node.parentNode.insertBefore(tmp, node.nextSibling);
+          else self.container.insertBefore(tmp, self.container.firstChild);
+          return measureFromNode(tmp, 0);
+        });
     },
 
     reroutePasteEvent: function() {
       if (this.capturingPaste || window.opera) return;
       this.capturingPaste = true;
-      var te = parent.document.createElement("TEXTAREA");
-      te.style.position = "absolute";
-      te.style.left = "-500px";
-      te.style.width = "10px";
-      te.style.top = nodeTop(frameElement) + "px";
-      parent.document.body.appendChild(te);
+      var te = window.frameElement.CodeMirror.textareaHack;
       parent.focus();
+      te.value = "";
       te.focus();
 
       var self = this;
@@ -624,10 +665,12 @@ var Editor = (function(){
         self.capturingPaste = false;
         self.win.focus();
         if (self.selectionSnapshot) // IE hack
-          self.win.select.selectCoords(self.win, self.selectionSnapshot);
+          self.win.select.setBookmark(self.container, self.selectionSnapshot);
         var text = te.value;
-        if (text) self.replaceSelection(text);
-        removeElement(te);
+        if (text) {
+          self.replaceSelection(text);
+          select.scrollToCursor(self.container);
+        }
       }, 10);
     },
 
@@ -642,8 +685,8 @@ var Editor = (function(){
               offset: lastLine.length};
     },
 
-    getSearchCursor: function(string, fromCursor, regexp, case_sensitive) {
-      return new SearchCursor(this, string, fromCursor, regexp, case_sensitive);
+    getSearchCursor: function(string, fromCursor, caseFold) {
+      return new SearchCursor(this, string, fromCursor, caseFold);
     },
 
     // Re-indent the whole buffer
@@ -670,11 +713,14 @@ var Editor = (function(){
     },
     ungrabKeys: function() {
       this.frozen = "leave";
-      this.keyFilter = null;
     },
 
-    setParser: function(name) {
+    setParser: function(name, parserConfig) {
       Editor.Parser = window[name];
+      parserConfig = parserConfig || this.options.parserConfig;
+      if (parserConfig && Editor.Parser.configure)
+        Editor.Parser.configure(parserConfig);
+
       if (this.container.firstChild) {
         forEach(this.container.childNodes, function(n) {
           if (n.nodeType != 3) n.dirty = true;
@@ -686,19 +732,19 @@ var Editor = (function(){
 
     // Intercept enter and tab, and assign their new functions.
     keyDown: function(event) {
-      if (this.frozen == "leave") this.frozen = null;
-      if (this.frozen && (!this.keyFilter || this.keyFilter(event))) {
+      if (this.frozen == "leave") {this.frozen = null; this.keyFilter = null;}
+      if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode, event))) {
         event.stop();
         this.frozen(event);
         return;
       }
 
-      var code = event.keyCode;
+      var code = this.lastKeyDownCode = event.keyCode;
       // Don't scan when the user is typing.
       this.delayScanning();
       // Schedule a paren-highlight event, if configured.
       if (this.options.autoMatchParens)
-        this.scheduleParenBlink();
+        this.scheduleParenHighlight();
 
       // The various checks for !altKey are there because AltGr sets both
       // ctrlKey and altKey to true, and should not be recognised as
@@ -714,8 +760,8 @@ var Editor = (function(){
         }
         event.stop();
       }
-      else if (code == 9 && this.options.tabMode != "default") { // tab
-        this.handleTab(!event.ctrlKey && !event.shiftKey);
+      else if (code == 9 && this.options.tabMode != "default" && !event.ctrlKey) { // tab
+        this.handleTab(!event.shiftKey);
         event.stop();
       }
       else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
@@ -723,23 +769,32 @@ var Editor = (function(){
         event.stop();
       }
       else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home
-        if (this.home())
-          event.stop();
+        if (this.home()) event.stop();
+      }
+      else if (code == 35 && !event.shiftKey && !event.ctrlKey) { // end
+        if (this.end()) event.stop();
+      }
+      // Only in Firefox is the default behavior for PgUp/PgDn correct.
+      else if (code == 33 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgUp
+        if (this.pageUp()) event.stop();
+      }
+      else if (code == 34 && !event.shiftKey && !event.ctrlKey && !gecko) {  // PgDn
+        if (this.pageDown()) event.stop();
       }
       else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
-        this.blinkParens(event.shiftKey);
+        this.highlightParens(event.shiftKey, true);
         event.stop();
       }
       else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right
         var cursor = select.selectionTopNode(this.container);
-        if (cursor === false || !this.container.firstChild) return;
-
-        if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
-        else {
-          var end = endOfLine(cursor, this.container);
-          select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
+        if (cursor !== false && this.container.firstChild) {
+          if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
+          else {
+            var end = endOfLine(cursor, this.container);
+            select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
+          }
+          event.stop();
         }
-        event.stop();
       }
       else if ((event.ctrlKey || event.metaKey) && !event.altKey) {
         if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y
@@ -754,31 +809,49 @@ var Editor = (function(){
           this.options.saveFunction();
           event.stop();
         }
+        else if (internetExplorer && code == 86) {
+          this.reroutePasteEvent();
+        }
       }
+      this.keyUpOrPressAfterLastKeyDown = false;
     },
 
     // Check for characters that should re-indent the current line,
     // and prevent Opera from handling enter and tab anyway.
     keyPress: function(event) {
+      this.keyUpOrPressAfterLastKeyDown = true;
       var electric = Editor.Parser.electricChars, self = this;
       // Hack for Opera, and Firefox on OS X, in which stopping a
       // keydown event does not prevent the associated keypress event
       // from happening, so we have to cancel enter and tab again
       // here.
-      if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode))) ||
+      if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode || event.code, event))) ||
           event.code == 13 || (event.code == 9 && this.options.tabMode != "default") ||
-          (event.keyCode == 32 && event.shiftKey && this.options.tabMode == "default"))
+          (event.code == 32 && event.shiftKey && this.options.tabMode == "default"))
         event.stop();
       else if (electric && electric.indexOf(event.character) != -1)
         this.parent.setTimeout(function(){self.indentAtCursor(null);}, 0);
       else if ((event.character == "v" || event.character == "V")
                && (event.ctrlKey || event.metaKey) && !event.altKey) // ctrl-V
         this.reroutePasteEvent();
+      // Work around a bug where pressing backspace at the end of a
+      // line often causes the cursor to jump to the start of the line
+      // in Opera 10.60.
+      else if (brokenOpera && event.code == 8) {
+        var sel = select.selectionTopNode(this.container), self = this,
+            next = sel ? sel.nextSibling : this.container.firstChild;
+        if (sel !== false && next && isBR(next))
+          this.parent.setTimeout(function(){
+            if (select.selectionTopNode(self.container) == next)
+              select.focusAfterNode(next.previousSibling, self.container);
+          }, 20);
+      }
     },
 
     // Mark the node at the cursor dirty when a non-safe key is
     // released.
     keyUp: function(event) {
+      this.keyUpOrPressAfterLastKeyDown = true;
       this.cursorActivity(isSafeKey(event.keyCode));
     },
 
@@ -828,6 +901,7 @@ var Editor = (function(){
         if (whiteSpace) {
           whiteSpace.currentText = makeWhiteSpace(newIndent);
           whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
+          select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
         }
         // Otherwise, we have to add a new whitespace node.
         else {
@@ -835,18 +909,18 @@ var Editor = (function(){
           whiteSpace.className = "whitespace";
           if (start) insertAfter(whiteSpace, start);
           else this.container.insertBefore(whiteSpace, this.container.firstChild);
+          select.snapshotMove(firstText && (firstText.firstChild || firstText),
+                              whiteSpace.firstChild, newIndent, false, true);
         }
-        if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
       }
       if (indentDiff != 0) this.addDirtyNode(start);
-      return whiteSpace;
     },
 
     // Re-highlight the selected part of the document.
     highlightAtCursor: function() {
       var pos = select.selectionTopNode(this.container, true);
       var to = select.selectionTopNode(this.container, false);
-      if (pos === false || to === false) return;
+      if (pos === false || to === false) return false;
 
       select.markSelection(this.win);
       if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
@@ -865,12 +939,14 @@ var Editor = (function(){
         this.reindentSelection(direction);
     },
 
+    // Custom home behaviour that doesn't land the cursor in front of
+    // leading whitespace unless pressed twice.
     home: function() {
       var cur = select.selectionTopNode(this.container, true), start = cur;
-      if (cur === false || !(!cur || cur.isPart || cur.nodeName == "BR") || !this.container.firstChild)
+      if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild)
         return false;
 
-      while (cur && cur.nodeName != "BR") cur = cur.previousSibling;
+      while (cur && !isBR(cur)) cur = cur.previousSibling;
       var next = cur ? cur.nextSibling : this.container.firstChild;
       if (next && next != start && next.isPart && hasClass(next, "whitespace"))
         select.focusAfterNode(next, this.container);
@@ -881,18 +957,88 @@ var Editor = (function(){
       return true;
     },
 
-    // Delay (or initiate) the next paren blink event.
-    scheduleParenBlink: function() {
+    // Some browsers (Opera) don't manage to handle the end key
+    // properly in the face of vertical scrolling.
+    end: function() {
+      var cur = select.selectionTopNode(this.container, true);
+      if (cur === false) return false;
+      cur = endOfLine(cur, this.container);
+      if (!cur) return false;
+      select.focusAfterNode(cur.previousSibling, this.container);
+      select.scrollToCursor(this.container);
+      return true;
+    },
+
+    pageUp: function() {
+      var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
+      if (line === false || scrollAmount === false) return false;
+      // Try to keep one line on the screen.
+      scrollAmount -= 2;
+      for (var i = 0; i < scrollAmount; i++) {
+        line = this.prevLine(line);
+        if (line === false) break;
+      }
+      if (i == 0) return false; // Already at first line
+      select.setCursorPos(this.container, {node: line, offset: 0});
+      select.scrollToCursor(this.container);
+      return true;
+    },
+
+    pageDown: function() {
+      var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
+      if (line === false || scrollAmount === false) return false;
+      // Try to move to the last line of the current page.
+      scrollAmount -= 2;
+      for (var i = 0; i < scrollAmount; i++) {
+        var nextLine = this.nextLine(line);
+        if (nextLine === false) break;
+        line = nextLine;
+      }
+      if (i == 0) return false; // Already at last line
+      select.setCursorPos(this.container, {node: line, offset: 0});
+      select.scrollToCursor(this.container);
+      return true;
+    },
+
+    // Delay (or initiate) the next paren highlight event.
+    scheduleParenHighlight: function() {
       if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
       var self = this;
-      this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300);
+      this.parenEvent = this.parent.setTimeout(function(){self.highlightParens();}, 300);
     },
 
     // Take the token before the cursor. If it contains a character in
     // '()[]{}', search for the matching paren/brace/bracket, and
     // highlight them in green for a moment, or red if no proper match
     // was found.
-    blinkParens: function(jump) {
+    highlightParens: function(jump, fromKey) {
+      var self = this;
+      // give the relevant nodes a colour.
+      function highlight(node, ok) {
+        if (!node) return;
+        if (self.options.markParen) {
+          self.options.markParen(node, ok);
+        }
+        else {
+          node.style.fontWeight = "bold";
+          node.style.color = ok ? "#8F8" : "#F88";
+        }
+      }
+      function unhighlight(node) {
+        if (!node) return;
+        if (self.options.unmarkParen) {
+          self.options.unmarkParen(node);
+        }
+        else {
+          node.style.fontWeight = "";
+          node.style.color = "";
+        }
+      }
+      if (!fromKey && self.highlighted) {
+        unhighlight(self.highlighted[0]);
+        unhighlight(self.highlighted[1]);
+      }
+
       if (!window.select) return;
       // Clear the event property.
       if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
@@ -910,7 +1056,7 @@ var Editor = (function(){
         return /[\(\[\{]/.test(ch);
       }
 
-      var ch, self = this, cursor = select.selectionTopNode(this.container, true);
+      var ch, cursor = select.selectionTopNode(this.container, true);
       if (!cursor || !this.highlightAtCursor()) return;
       cursor = select.selectionTopNode(this.container, true);
       if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor)))))
@@ -923,9 +1069,9 @@ var Editor = (function(){
       // have to scan, we just try, and when we find dirty nodes we
       // abort, parse them, and re-try.
       function tryFindMatch() {
-        var stack = [], ch, ok = true;;
+        var stack = [], ch, ok = true;
         for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
-          if (runner.className == className && runner.nodeName == "SPAN" && (ch = paren(runner))) {
+          if (runner.className == className && isSpan(runner) && (ch = paren(runner))) {
             if (forward(ch) == dir)
               stack.push(ch);
             else if (!stack.length)
@@ -934,18 +1080,12 @@ var Editor = (function(){
               ok = false;
             if (!stack.length) break;
           }
-          else if (runner.dirty || runner.nodeName != "SPAN" && runner.nodeName != "BR") {
+          else if (runner.dirty || !isSpan(runner) && !isBR(runner)) {
             return {node: runner, status: "dirty"};
           }
         }
         return {node: runner, status: runner && ok};
       }
-      // Temporarily give the relevant nodes a colour.
-      function blink(node, ok) {
-        node.style.fontWeight = "bold";
-        node.style.color = ok ? "#8F8" : "#F88";
-        self.parent.setTimeout(function() {node.style.fontWeight = ""; node.style.color = "";}, 500);
-      }
 
       while (true) {
         var found = tryFindMatch();
@@ -957,11 +1097,14 @@ var Editor = (function(){
           continue;
         }
         else {
-          blink(cursor, found.status);
-          if (found.node) {
-            blink(found.node, found.status);
-            if (jump) select.focusAfterNode(found.node.previousSibling, this.container);
-          }
+          highlight(cursor, found.status);
+          highlight(found.node, found.status);
+          if (fromKey)
+            self.parent.setTimeout(function() {unhighlight(cursor); unhighlight(found.node);}, 500);
+          else
+            self.highlighted = [cursor, found.node];
+          if (jump && found.node)
+            select.focusAfterNode(found.node.previousSibling, this.container);
           break;
         }
       }
@@ -979,20 +1122,17 @@ var Editor = (function(){
       // there's nothing to indent.
       if (cursor === false)
         return;
-      var lineStart = startOfLine(cursor);
-      var whiteSpace = this.indentLineAfter(lineStart, direction);
-      if (cursor == lineStart && whiteSpace)
-          cursor = whiteSpace;
-      // This means the indentation has probably messed up the cursor.
-      if (cursor == whiteSpace)
-        select.focusAfterNode(cursor, this.container);
+      select.markSelection(this.win);
+      this.indentLineAfter(startOfLine(cursor), direction);
+      select.selectMarked();
     },
 
     // Indent all lines whose start falls inside of the current
     // selection.
     indentRegion: function(start, end, direction) {
       var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling);
-      if (end.nodeName != "BR") end = endOfLine(end, this.container);
+      if (!isBR(end)) end = endOfLine(end, this.container);
+      this.addDirtyNode(start);
 
       do {
         var next = endOfLine(current, this.container);
@@ -1007,9 +1147,16 @@ var Editor = (function(){
     // Find the node that the cursor is in, mark it as dirty, and make
     // sure a highlight pass is scheduled.
     cursorActivity: function(safe) {
+      // pagehide event hack above
+      if (this.unloaded) {
+        this.win.document.designMode = "off";
+        this.win.document.designMode = "on";
+        this.unloaded = false;
+      }
+
       if (internetExplorer) {
         this.container.createTextRange().execCommand("unlink");
-        this.selectionSnapshot = select.selectionCoords(this.win);
+        this.selectionSnapshot = select.getBookmark(this.container);
       }
 
       var activity = this.options.cursorActivity;
@@ -1045,6 +1192,10 @@ var Editor = (function(){
       this.dirty.push(node);
     },
 
+    allClean: function() {
+      return !this.dirty.length;
+    },
+
     // Cause a highlight pass to happen in options.passDelay
     // milliseconds. Clear the existing timeout, if one exists. This
     // way, the passes do not happen while the user is typing, and
@@ -1067,7 +1218,7 @@ var Editor = (function(){
           // If the node has been coloured in the meantime, or is no
           // longer in the document, it should not be returned.
           while (found && found.parentNode != this.container)
-            found = found.parentNode
+            found = found.parentNode;
           if (found && (found.dirty || found.nodeType == 3))
             return found;
         } catch (e) {}
@@ -1084,7 +1235,7 @@ var Editor = (function(){
     highlightDirty: function(force) {
       // Prevent FF from raising an error when it is firing timeouts
       // on a page that's no longer loaded.
-      if (!window.select) return;
+      if (!window.select) return false;
 
       if (!this.options.readOnly) select.markSelection(this.win);
       var start, endTime = force ? null : time() + this.options.passTime;
@@ -1127,6 +1278,12 @@ var Editor = (function(){
       }
     },
 
+    isIMEOn: function() {
+      // chrome:  keyDown keyCode is 229 while IME on
+      // firefox: no keyUps or keyPresses fires after first keyDown while IME on
+      return this.lastKeyDownCode == 229 || this.keyUpOrPressAfterLastKeyDown === false;
+    },
+
     // The function that does the actual highlighting/colouring (with
     // help from the parser and the DOM normalizer). Its interface is
     // rather overcomplicated, because it is used in different
@@ -1146,18 +1303,18 @@ var Editor = (function(){
       var container = this.container, self = this, active = this.options.activeTokens;
       var endTime = (typeof target == "number" ? target : null);
 
-      if (!container.firstChild)
-        return;
+      if (!container.firstChild || this.isIMEOn())
+        return false;
       // Backtrack to the first node before from that has a partial
       // parse stored.
       while (from && (!from.parserFromHere || from.dirty)) {
-        if (maxBacktrack != null && from.nodeName == "BR" && (--maxBacktrack) < 0)
+        if (maxBacktrack != null && isBR(from) && (--maxBacktrack) < 0)
           return false;
         from = from.previousSibling;
       }
       // If we are at the end of the document, do nothing.
       if (from && !from.nextSibling)
-        return;
+        return false;
 
       // Check whether a part (<span> node) and the corresponding token
       // match.
@@ -1175,7 +1332,7 @@ var Editor = (function(){
       }
       // Create a part corresponding to a given token.
       function tokenPart(token){
-        var part = makePartSpan(token.value, self.doc);
+        var part = makePartSpan(token.value, self.doc);     
         part.className = token.style;
         return part;
       }
@@ -1202,6 +1359,11 @@ var Editor = (function(){
           stream = stringStream(traversal),
           parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
 
+      function surroundedByBRs(node) {
+        return (node.previousSibling == null || isBR(node.previousSibling)) &&
+               (node.nextSibling == null || isBR(node.nextSibling));
+      }
+
       // parts is an interface to make it possible to 'delay' fetching
       // the next DOM node until we are completely done with the one
       // before it. This is necessary because often the next node is
@@ -1232,13 +1394,23 @@ var Editor = (function(){
           // Allow empty nodes when they are alone on a line, needed
           // for the FF cursor bug workaround (see select.js,
           // insertNewlineAtCursor).
-          while (part && part.nodeName == "SPAN" && part.currentText == "") {
-            var old = part;
-            this.remove();
-            part = this.get();
-            // Adjust selection information, if any. See select.js for details.
-            select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
+          while (part && isSpan(part) && part.currentText == "") {
+            // Leave empty nodes that are alone on a line alone in
+            // Opera, since that browsers doesn't deal well with
+            // having 2 BRs in a row.
+            if (window.opera && surroundedByBRs(part)) {
+              this.next();
+              part = this.get();
+            }
+            else {
+              var old = part;
+              this.remove();
+              part = this.get();
+              // Adjust selection information, if any. See select.js for details.
+              select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
+            }
           }
+          
           return part;
         }
       };
@@ -1254,7 +1426,7 @@ var Editor = (function(){
         if (token.value == "\n"){
           // The idea of the two streams actually staying synchronized
           // is such a long shot that we explicitly check.
-          if (part.nodeName != "BR")
+          if (!isBR(part))
             throw "Parser out of sync. Expected BR.";
 
           if (part.dirty || !part.indentation) lineDirty = true;
@@ -1282,7 +1454,7 @@ var Editor = (function(){
           parts.next();
         }
         else {
-          if (part.nodeName != "SPAN")
+          if (!isSpan(part))
             throw "Parser out of sync. Expected SPAN.";
           if (part.dirty)
             lineDirty = true;
@@ -1323,7 +1495,6 @@ var Editor = (function(){
         }
       });
       maybeTouch(from);
-      webkitLastLineHack(this.container);
 
       // The function returns some status information that is used by
       // hightlightDirty to determine whether and where it has to
@@ -1338,6 +1509,6 @@ var Editor = (function(){
 
 addEventHandler(window, "load", function() {
   var CodeMirror = window.frameElement.CodeMirror;
-  CodeMirror.editor = new Editor(CodeMirror.options);
+  var e = CodeMirror.editor = new Editor(CodeMirror.options);
   this.parent.setTimeout(method(CodeMirror, "init"), 0);
 });
index 87fbf0c..994efd3 100644 (file)
@@ -125,12 +125,12 @@ var XMLParser = Editor.Parser = (function() {
   // parseJavaScript in parsejavascript.js (there is actually a bit more
   // shared code than I'd like), but it is quite a bit simpler.
   function parseXML(source) {
-    var tokens = tokenizeXML(source);
+    var tokens = tokenizeXML(source), token;
     var cc = [base];
     var tokenNr = 0, indented = 0;
     var currentTag = null, context = null;
-    var consume, marked;
-
+    var consume;
+    
     function push(fs) {
       for (var i = fs.length - 1; i >= 0; i--)
         cc.push(fs[i]);
@@ -144,13 +144,13 @@ var XMLParser = Editor.Parser = (function() {
       consume = false;
     }
 
-    function mark(style) {
-      marked = style;
+    function markErr() {
+      token.style += " xml-error";
     }
     function expect(text) {
       return function(style, content) {
         if (content == text) cont();
-        else mark("xml-error") || cont(arguments.callee);
+        else {markErr(); cont(arguments.callee);}
       };
     }
 
@@ -192,12 +192,12 @@ var XMLParser = Editor.Parser = (function() {
         cont();
       }
       else if (harmlessTokens.hasOwnProperty(style)) cont();
-      else mark("xml-error") || cont();
+      else {markErr(); cont();}
     }
     function tagname(style, content) {
       if (style == "xml-name") {
         currentTag = content.toLowerCase();
-        mark("xml-tagname");
+        token.style = "xml-tagname";
         cont();
       }
       else {
@@ -206,24 +206,22 @@ var XMLParser = Editor.Parser = (function() {
       }
     }
     function closetagname(style, content) {
-      if (style == "xml-name" && context && content.toLowerCase() == context.name) {
-        popContext();
-        mark("xml-tagname");
-      }
-      else {
-        mark("xml-error");
+      if (style == "xml-name") {
+        token.style = "xml-tagname";
+        if (context && content.toLowerCase() == context.name) popContext();
+        else markErr();
       }
       cont();
     }
     function endtag(startOfLine) {
       return function(style, content) {
         if (content == "/>" || (content == ">" && UseKludges.autoSelfClosers.hasOwnProperty(currentTag))) cont();
-        else if (content == ">") pushContext(currentTag, startOfLine) || cont();
-        else mark("xml-error") || cont(arguments.callee);
+        else if (content == ">") {pushContext(currentTag, startOfLine); cont();}
+        else {markErr(); cont(arguments.callee);}
       };
     }
     function attributes(style) {
-      if (style == "xml-name") mark("xml-attname") || cont(attribute, attributes);
+      if (style == "xml-name") {token.style = "xml-attname"; cont(attribute, attributes);}
       else pass();
     }
     function attribute(style, content) {
@@ -240,7 +238,7 @@ var XMLParser = Editor.Parser = (function() {
       indentation: function() {return indented;},
 
       next: function(){
-        var token = tokens.next();
+        token = tokens.next();
         if (token.style == "whitespace" && tokenNr == 0)
           indented = token.value.length;
         else
@@ -254,20 +252,16 @@ var XMLParser = Editor.Parser = (function() {
           return token;
 
         while(true){
-          consume = marked = false;
+          consume = false;
           cc.pop()(token.style, token.content);
-          if (consume){
-            if (marked)
-              token.style = marked;
-            return token;
-          }
+          if (consume) return token;
         }
       },
 
       copy: function(){
         var _cc = cc.concat([]), _tokenState = tokens.state, _context = context;
         var parser = this;
-
+        
         return function(input){
           cc = _cc.concat([]);
           tokenNr = indented = 0;
index d513ba5..01fe996 100644 (file)
@@ -29,15 +29,16 @@ var select = {};
 
   var fourSpaces = "\u00a0\u00a0\u00a0\u00a0";
 
-  select.scrollToNode = function(element) {
-    if (!element) return;
-    var doc = element.ownerDocument, body = doc.body,
+  select.scrollToNode = function(node, cursor) {
+    if (!node) return;
+    var element = node,
+        doc = element.ownerDocument, body = doc.body,
         win = (doc.defaultView || doc.parentWindow),
         html = doc.documentElement,
         atEnd = !element.nextSibling || !element.nextSibling.nextSibling
                 || !element.nextSibling.nextSibling.nextSibling;
     // In Opera (and recent Webkit versions), BR elements *always*
-    // have a scrollTop property of zero.
+    // have a offsetTop property of zero.
     var compensateHack = 0;
     while (element && !element.offsetTop) {
       compensateHack++;
@@ -48,32 +49,47 @@ var select = {};
     // offset, just scroll to the end.
     if (compensateHack == 0) atEnd = false;
 
-    var y = compensateHack * (element ? element.offsetHeight : 0), x = 0, pos = element;
+    // WebKit has a bad habit of (sometimes) happily returning bogus
+    // offsets when the document has just been changed. This seems to
+    // always be 5/5, so we don't use those.
+    if (webkit && element && element.offsetTop == 5 && element.offsetLeft == 5)
+      return;
+
+    var y = compensateHack * (element ? element.offsetHeight : 0), x = 0,
+        width = (node ? node.offsetWidth : 0), pos = element;
     while (pos && pos.offsetParent) {
       y += pos.offsetTop;
       // Don't count X offset for <br> nodes
-      if (pos.nodeName != "BR")
+      if (!isBR(pos))
         x += pos.offsetLeft;
       pos = pos.offsetParent;
     }
 
     var scroll_x = body.scrollLeft || html.scrollLeft || 0,
         scroll_y = body.scrollTop || html.scrollTop || 0,
-        screen_x = x - scroll_x, screen_y = y - scroll_y, scroll = false;
+        scroll = false, screen_width = win.innerWidth || html.clientWidth || 0;
 
-    if (screen_x < 0 || screen_x > (win.innerWidth || html.clientWidth || 0)) {
-      scroll_x = x;
-      scroll = true;
+    if (cursor || width < screen_width) {
+      if (cursor) {
+        var off = select.offsetInNode(win, node), size = nodeText(node).length;
+        if (size) x += width * (off / size);
+      }
+      var screen_x = x - scroll_x;
+      if (screen_x < 0 || screen_x > screen_width) {
+        scroll_x = x;
+        scroll = true;
+      }
     }
+    var screen_y = y - scroll_y;
     if (screen_y < 0 || atEnd || screen_y > (win.innerHeight || html.clientHeight || 0) - 50) {
-      scroll_y = atEnd ? 1e10 : y;
+      scroll_y = atEnd ? 1e6 : y;
       scroll = true;
     }
     if (scroll) win.scrollTo(scroll_x, scroll_y);
   };
 
   select.scrollToCursor = function(container) {
-    select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild);
+    select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild, true);
   };
 
   // Used to prevent restoring a selection when we do not need to.
@@ -218,6 +234,15 @@ var select = {};
       start.select();
     };
 
+    select.offsetInNode = function(win, node) {
+      var sel = win.document.selection;
+      if (!sel) return 0;
+      var range = sel.createRange(), range2 = range.duplicate();
+      try {range2.moveToElementText(node);} catch(e){return 0;}
+      range.setEndPoint("StartToStart", range2);
+      return range.text.length;
+    };
+
     // Get the top-level node that one end of the cursor is inside or
     // after. Note that this returns false for 'no cursor', and null
     // for 'start of document'.
@@ -247,13 +272,17 @@ var select = {};
           }
           if (cur) {
             try{range.moveToElementText(cur);}
-            catch(e){}
+            catch(e){return false;}
             range.collapse(false);
           }
           else range.moveToElementText(node.parentNode);
           if (count) range.move("character", count);
         }
-        else range.moveToElementText(node);
+        else {
+          try{range.moveToElementText(node);}
+          catch(e){return false;}
+        }
+        return true;
       }
 
       // Do a binary search through the container object, comparing
@@ -262,7 +291,7 @@ var select = {};
       while (start < end) {
         var middle = Math.ceil((end + start) / 2), node = container.childNodes[middle];
         if (!node) return false; // Don't ask. IE6 manages this sometimes.
-        moveToNodeStart(range2, node);
+        if (!moveToNodeStart(range2, node)) return false;
         if (range.compareEndPoints("StartToStart", range2) == 1)
           start = middle;
         else
@@ -314,7 +343,7 @@ var select = {};
       if (!selection) return null;
 
       var topNode = select.selectionTopNode(container, start);
-      while (topNode && topNode.nodeName != "BR")
+      while (topNode && !isBR(topNode))
         topNode = topNode.previousSibling;
 
       var range = selection.createRange(), range2 = range.duplicate();
@@ -356,36 +385,36 @@ var select = {};
     }
 
     // Some hacks for storing and re-storing the selection when the editor loses and regains focus.
-    select.selectionCoords = function (win) {
-      var selection = win.document.selection;
-      if (!selection) return null;
-      var start = selection.createRange(), end = start.duplicate();
-      start.collapse(true);
-      end.collapse(false);
-
-      var body = win.document.body;
-      return {start: {x: start.boundingLeft + body.scrollLeft - 1,
-                      y: start.boundingTop + body.scrollTop},
-              end: {x: end.boundingLeft + body.scrollLeft - 1,
-                    y: end.boundingTop + body.scrollTop}};
+    select.getBookmark = function (container) {
+      var from = select.cursorPos(container, true), to = select.cursorPos(container, false);
+      if (from && to) return {from: from, to: to};
     };
 
     // Restore a stored selection.
-    select.selectCoords = function(win, coords) {
-      if (!coords) return;
-
-      var range1 = win.document.body.createTextRange(), range2 = range1.duplicate();
-      // This can fail for various hard-to-handle reasons.
-      try {
-        range1.moveToPoint(coords.start.x, coords.start.y);
-        range2.moveToPoint(coords.end.x, coords.end.y);
-        range1.setEndPoint("EndToStart", range2);
-        range1.select();
-      } catch(e) {}
+    select.setBookmark = function(container, mark) {
+      if (!mark) return;
+      select.setCursorPos(container, mark.from, mark.to);
     };
   }
   // W3C model
   else {
+    // Find the node right at the cursor, not one of its
+    // ancestors with a suitable offset. This goes down the DOM tree
+    // until a 'leaf' is reached (or is it *up* the DOM tree?).
+    function innerNode(node, offset) {
+      while (node.nodeType != 3 && !isBR(node)) {
+        var newNode = node.childNodes[offset] || node.nextSibling;
+        offset = 0;
+        while (!newNode && node.parentNode) {
+          node = node.parentNode;
+          newNode = node.nextSibling;
+        }
+        node = newNode;
+        if (!newNode) break;
+      }
+      return {node: node, offset: offset};
+    }
+
     // Store start and end nodes, and offsets within these, and refer
     // back to the selection object from those nodes, so that this
     // object can be updated when the nodes are replaced before the
@@ -397,36 +426,30 @@ var select = {};
       var range = selection.getRangeAt(0);
 
       currentSelection = {
-        start: {node: range.startContainer, offset: range.startOffset},
-        end: {node: range.endContainer, offset: range.endOffset},
+        start: innerNode(range.startContainer, range.startOffset),
+        end: innerNode(range.endContainer, range.endOffset),
         window: win,
         changed: false
       };
-
-      // We want the nodes right at the cursor, not one of their
-      // ancestors with a suitable offset. This goes down the DOM tree
-      // until a 'leaf' is reached (or is it *up* the DOM tree?).
-      function normalize(point){
-        while (point.node.nodeType != 3 && point.node.nodeName != "BR") {
-          var newNode = point.node.childNodes[point.offset] || point.node.nextSibling;
-          point.offset = 0;
-          while (!newNode && point.node.parentNode) {
-            point.node = point.node.parentNode;
-            newNode = point.node.nextSibling;
-          }
-          point.node = newNode;
-          if (!newNode)
-            break;
-        }
-      }
-
-      normalize(currentSelection.start);
-      normalize(currentSelection.end);
     };
 
     select.selectMarked = function () {
-      if (!currentSelection || !currentSelection.changed) return;
-      var win = currentSelection.window, range = win.document.createRange();
+      var cs = currentSelection;
+      // on webkit-based browsers, it is apparently possible that the
+      // selection gets reset even when a node that is not one of the
+      // endpoints get messed with. the most common situation where
+      // this occurs is when a selection is deleted or overwitten. we
+      // check for that here.
+      function focusIssue() {
+        if (cs.start.node == cs.end.node && cs.start.offset == cs.end.offset) {
+          var selection = cs.window.getSelection();
+          if (!selection || selection.rangeCount == 0) return true;
+          var range = selection.getRangeAt(0), point = innerNode(range.startContainer, range.startOffset);
+          return cs.start.node != point.node || cs.start.offset != point.offset;
+        }
+      }
+      if (!cs || !(cs.changed || (webkit && focusIssue()))) return;
+      var win = cs.window, range = win.document.createRange();
 
       function setPoint(point, which) {
         if (point.node) {
@@ -442,17 +465,18 @@ var select = {};
         }
       }
 
-      setPoint(currentSelection.end, "End");
-      setPoint(currentSelection.start, "Start");
+      setPoint(cs.end, "End");
+      setPoint(cs.start, "Start");
       selectRange(range, win);
     };
 
     // Helper for selecting a range object.
     function selectRange(range, window) {
       var selection = window.getSelection();
+      if (!selection) return;
       selection.removeAllRanges();
       selection.addRange(range);
-    };
+    }
     function selectionRange(window) {
       var selection = window.getSelection();
       if (!selection || selection.rangeCount == 0)
@@ -471,7 +495,7 @@ var select = {};
       var offset = start ? range.startOffset : range.endOffset;
       // Work around (yet another) bug in Opera's selection model.
       if (window.opera && !start && range.endContainer == container && range.endOffset == range.startOffset + 1 &&
-          container.childNodes[range.startOffset] && container.childNodes[range.startOffset].nodeName == "BR")
+          container.childNodes[range.startOffset] && isBR(container.childNodes[range.startOffset]))
         offset--;
 
       // For text nodes, we look at the node itself if the cursor is
@@ -486,7 +510,7 @@ var select = {};
       // Occasionally, browsers will return the HTML node as
       // selection. If the offset is 0, we take the start of the frame
       // ('after null'), otherwise, we take the last node.
-      else if (node.nodeName == "HTML") {
+      else if (node.nodeName.toUpperCase() == "HTML") {
         return (offset == 1 ? null : container.lastChild);
       }
       // If the given node is our 'container', we just look up the
@@ -531,13 +555,34 @@ var select = {};
       return range && !range.collapsed;
     };
 
+    select.offsetInNode = function(win, node) {
+      var range = selectionRange(win);
+      if (!range) return 0;
+      range = range.cloneRange();
+      range.setStartBefore(node);
+      return range.toString().length;
+    };
+
     function insertNodeAtCursor(window, node) {
       var range = selectionRange(window);
       if (!range) return;
 
       range.deleteContents();
       range.insertNode(node);
-      webkitLastLineHack(window.document.body);
+
+      // work around weirdness where Opera will magically insert a new
+      // BR node when a BR node inside a span is moved around. makes
+      // sure the BR ends up outside of spans.
+      if (window.opera && isBR(node) && isSpan(node.parentNode)) {
+        var next = node.nextSibling, p = node.parentNode, outer = p.parentNode;
+        outer.insertBefore(node, p.nextSibling);
+        var textAfter = "";
+        for (; next && next.nodeType == 3; next = next.nextSibling) {
+          textAfter += next.nodeValue;
+          removeElement(next);
+        }
+        outer.insertBefore(makePartSpan(textAfter, window.document), node.nextSibling);
+      }
       range = window.document.createRange();
       range.selectNode(node);
       range.collapse(false);
@@ -545,7 +590,10 @@ var select = {};
     }
 
     select.insertNewlineAtCursor = function(window) {
-      insertNodeAtCursor(window, window.document.createElement("BR"));
+      if (webkit)
+        document.execCommand('insertLineBreak');
+      else
+        insertNodeAtCursor(window, window.document.createElement("BR"));
     };
 
     select.insertTabAtCursor = function(window) {
@@ -557,7 +605,7 @@ var select = {};
       if (!range) return;
 
       var topNode = select.selectionTopNode(container, start);
-      while (topNode && topNode.nodeName != "BR")
+      while (topNode && !isBR(topNode))
         topNode = topNode.previousSibling;
 
       range = range.cloneRange();
@@ -566,6 +614,7 @@ var select = {};
         range.setStartAfter(topNode);
       else
         range.setStartBefore(container);
+
       return {node: topNode, offset: range.toString().length};
     };
 
@@ -574,13 +623,17 @@ var select = {};
           range = win.document.createRange();
 
       function setPoint(node, offset, side) {
+        if (offset == 0 && node && !node.nextSibling) {
+          range["set" + side + "After"](node);
+          return true;
+        }
+
         if (!node)
           node = container.firstChild;
         else
           node = node.nextSibling;
 
-        if (!node)
-          return;
+        if (!node) return;
 
         if (offset == 0) {
           range["set" + side + "Before"](node);
index 8c1c042..4f5bc61 100644 (file)
@@ -14,7 +14,7 @@
 // Make a stringstream stream out of an iterator that returns strings.
 // This is applied to the result of traverseDOM (see codemirror.js),
 // and the resulting stream is fed to the parser.
-window.stringStream = function(source){
+var stringStream = function(source){
   // String that's currently being iterated over.
   var current = "";
   // Position in that string.
index b0c9545..071970c 100644 (file)
@@ -43,7 +43,7 @@ function tokenizer(source, state) {
         source.next();
         return this.take("whitespace");
       }
-
+      
       if (source.applies(isWhiteSpace))
         type = "whitespace";
       else
index 5f717a9..4ea2385 100644 (file)
@@ -9,7 +9,7 @@
  * complexity and hackery.
  *
  * In short, the editor 'touches' BR elements as it parses them, and
- * the History stores these. When nothing is touched in commitDelay
+ * the UndoHistory stores these. When nothing is touched in commitDelay
  * milliseconds, the changes are committed: It goes over all touched
  * nodes, throws out the ones that did not change since last commit or
  * are no longer in the document, and assembles the rest into zero or
 // delay (of no input) after which it commits a set of changes, and,
 // unfortunately, the 'parent' window -- a window that is not in
 // designMode, and on which setTimeout works in every browser.
-function History(container, maxDepth, commitDelay, editor, onChange) {
+function UndoHistory(container, maxDepth, commitDelay, editor) {
   this.container = container;
   this.maxDepth = maxDepth; this.commitDelay = commitDelay;
   this.editor = editor; this.parent = editor.parent;
-  this.onChange = onChange;
   // This line object represents the initial, empty editor.
   var initial = {text: "", from: null, to: null};
   // As the borders between lines are represented by BR elements, the
@@ -48,7 +47,7 @@ function History(container, maxDepth, commitDelay, editor, onChange) {
   this.history = []; this.redoHistory = []; this.touched = [];
 }
 
-History.prototype = {
+UndoHistory.prototype = {
   // Schedule a commit (if no other touches come in for commitDelay
   // milliseconds).
   scheduleCommit: function() {
@@ -73,7 +72,7 @@ History.prototype = {
       // shadow in the redo history.
       var item = this.history.pop();
       this.redoHistory.push(this.updateTo(item, "applyChain"));
-      if (this.onChange) this.onChange();
+      this.notifyEnvironment();
       return this.chainNode(item);
     }
   },
@@ -85,7 +84,7 @@ History.prototype = {
       // The inverse of undo, basically.
       var item = this.redoHistory.pop();
       this.addUndoLevel(this.updateTo(item, "applyChain"));
-      if (this.onChange) this.onChange();
+      this.notifyEnvironment();
       return this.chainNode(item);
     }
   },
@@ -109,6 +108,7 @@ History.prototype = {
       from = end;
     }
     this.pushChains([chain], from == null && to == null);
+    this.notifyEnvironment();
   },
 
   pushChains: function(chains, doNotHighlight) {
@@ -145,7 +145,7 @@ History.prototype = {
 
   // Commit unless there are pending dirty nodes.
   tryCommit: function() {
-    if (!window.History) return; // Stop when frame has been unloaded
+    if (!window.UndoHistory) return; // Stop when frame has been unloaded
     if (this.editor.highlightDirty()) this.commit(true);
     else this.scheduleCommit();
   },
@@ -162,7 +162,7 @@ History.prototype = {
     if (chains.length) {
       this.addUndoLevel(this.updateTo(chains, "linkChain"));
       this.redoHistory = [];
-      if (this.onChange) this.onChange();
+      this.notifyEnvironment();
     }
   },
 
@@ -191,6 +191,13 @@ History.prototype = {
     this.editor.scheduleHighlight();
   },
 
+  notifyEnvironment: function() {
+    if (this.onChange) this.onChange();
+    // Used by the line-wrapping line-numbering code.
+    if (window.frameElement && window.frameElement.CodeMirror.updateNumbers)
+      window.frameElement.CodeMirror.updateNumbers();
+  },
+
   // Link a chain into the DOM nodes (or the first/last links for null
   // nodes).
   linkChain: function(chain) {
@@ -250,7 +257,7 @@ History.prototype = {
     function buildLine(node) {
       var text = [];
       for (var cur = node ? node.nextSibling : self.container.firstChild;
-           cur && cur.nodeName != "BR"; cur = cur.nextSibling)
+           cur && !isBR(cur); cur = cur.nextSibling)
         if (cur.currentText) text.push(cur.currentText);
       return {from: node, to: cur, text: cleanText(text.join(""))};
     }
@@ -275,7 +282,7 @@ History.prototype = {
     // Get the BR element after/before the given node.
     function nextBR(node, dir) {
       var link = dir + "Sibling", search = node[link];
-      while (search && search.nodeName != "BR")
+      while (search && !isBR(search))
         search = search[link];
       return search;
     }
index 796025e..c7021c2 100644 (file)
@@ -1,9 +1,5 @@
 /* A few useful utility functions. */
 
-var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
-var webkit = /AppleWebKit/.test(navigator.userAgent);
-var safari = /Apple Computers, Inc/.test(navigator.vendor);
-
 // Capture a method on an object.
 function method(obj, name) {
   return function() {obj[name].apply(obj, arguments);};
@@ -123,3 +119,12 @@ function nodeTop(node) {
   }
   return top;
 }
+
+function isBR(node) {
+  var nn = node.nodeName;
+  return nn == "BR" || nn == "br";
+}
+function isSpan(node) {
+  var nn = node.nodeName;
+  return nn == "SPAN" || nn == "span";
+}
index 0597140..6975f9b 100644 (file)
@@ -10,7 +10,7 @@
                        this.codemirror = CodeMirror.fromTextArea('codemirror_placeholder', {
                                parserfile: 'parsexml.js',
                                path: STATIC_URL + "js/lib/codemirror/",
-                               stylesheet: STATIC_URL + "css/xmlcolors_15032010.css",
+                               stylesheet: STATIC_URL + "css/xmlcolors_20100906.css",
                                parserConfig: {
                                        useHTMLKludges: false
                                },
@@ -18,6 +18,7 @@
                                textWrapping: true,
                                lineNumbers: true,
                                width: "100%",
+                               height: "100%",
                                tabMode: 'spaces',
                                indentUnit: 0,
                                readOnly: CurrentDocument.readonly || false,
index 34393dc..740c913 100644 (file)
@@ -64,8 +64,7 @@
             self.searchCursor = self.editor.getSearchCursor(
                 self.$searchInput.val(), 
                 self.options['search-from-cursor'], 
-                self.options['search-regexp'],
-                self.options['search-case-sensitive']
+                !self.options['search-case-sensitive']
             );
         }
         if (self.searchCursor.findNext()) {
index 86c6d36..791cf1a 100644 (file)
@@ -61,7 +61,7 @@ function withThemes(code_block, onError)
 
 function xml2html(options) {
     withStylesheets(function() {
-        var xml = options.xml.replace(/\/\s+/g, '<br />');
+        var xml = options.xml.replace(/\/(\s+)/g, '<br />$1');
         var parser = new DOMParser();
         var serializer = new XMLSerializer();
         var doc = parser.parseFromString(xml, 'text/xml');
@@ -271,7 +271,7 @@ HTMLSerializer.prototype.serialize = function(rootElement, stripOuter)
                                if(xnode === 'wers') {
                                        /* push children */
                                        if(self._verseBefore(token.node))
-                                               self.result += '/\n';
+                                               self.result += '/';
                                        self._pushChildren(token.node);
                                        break;
                                };