[ Index ]

MailPress 7.2

[ Index ]     [ Classes ]     [ Functions ]     [ Variables ]     [ Constants ]     [ Statistics ]    

title

Body

[close]

/mp-includes/js/codemirror/js/ -> editor.js (source)

   1  /* The Editor object manages the content of the editable frame. It
   2   * catches events, colours nodes, and indents lines. This file also
   3   * holds some functions for transforming arbitrary DOM structures into
   4   * plain sequences of <span> and <br> elements
   5   */
   6  
   7  var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
   8  var webkit = /AppleWebKit/.test(navigator.userAgent);
   9  var safari = /Apple Computers, Inc/.test(navigator.vendor);
  10  var gecko = /gecko\/(\d{8})/i.test(navigator.userAgent);
  11  var mac = /Mac/.test(navigator.platform);
  12  
  13  // TODO this is related to the backspace-at-end-of-line bug. Remove
  14  // this if Opera gets their act together, make the version check more
  15  // broad if they don't.
  16  var brokenOpera = window.opera && /Version\/10.[56]/.test(navigator.userAgent);
  17  // TODO remove this once WebKit 533 becomes less common.
  18  var slowWebkit = /AppleWebKit\/533/.test(navigator.userAgent);
  19  
  20  // Make sure a string does not contain two consecutive 'collapseable'
  21  // whitespace characters.
  22  function makeWhiteSpace(n) {
  23    var buffer = [], nb = true;
  24    for (; n > 0; n--) {
  25      buffer.push((nb || n == 1) ? nbsp : " ");
  26      nb ^= true;
  27    }
  28    return buffer.join("");
  29  }
  30  
  31  // Create a set of white-space characters that will not be collapsed
  32  // by the browser, but will not break text-wrapping either.
  33  function fixSpaces(string) {
  34    if (string.charAt(0) == " ") string = nbsp + string.slice(1);
  35    return string.replace(/\t/g, function() {return makeWhiteSpace(indentUnit);})
  36      .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
  37  }
  38  
  39  function cleanText(text) {
  40    return text.replace(/\u00a0/g, " ");
  41  }
  42  
  43  // Create a SPAN node with the expected properties for document part
  44  // spans.
  45  function makePartSpan(value) {
  46    var text = value;
  47    if (value.nodeType == 3) text = value.nodeValue;
  48    else value = document.createTextNode(text);
  49  
  50    var span = document.createElement("SPAN");
  51    span.isPart = true;
  52    span.appendChild(value);
  53    span.currentText = text;
  54    return span;
  55  }
  56  
  57  // On webkit, when the last BR of the document does not have text
  58  // behind it, the cursor can not be put on the line after it. This
  59  // makes pressing enter at the end of the document occasionally do
  60  // nothing (or at least seem to do nothing). To work around it, this
  61  // function makes sure the document ends with a span containing a
  62  // zero-width space character. The traverseDOM iterator filters such
  63  // character out again, so that the parsers won't see them. This
  64  // function is called from a few strategic places to make sure the
  65  // zwsp is restored after the highlighting process eats it.
  66  var webkitLastLineHack = webkit ?
  67    function(container) {
  68      var last = container.lastChild;
  69      if (!last || !last.hackBR) {
  70        var br = document.createElement("BR");
  71        br.hackBR = true;
  72        container.appendChild(br);
  73      }
  74    } : function() {};
  75  
  76  var Editor = (function(){
  77    // The HTML elements whose content should be suffixed by a newline
  78    // when converting them to flat text.
  79    var newlineElements = {"P": true, "DIV": true, "LI": true};
  80  
  81    function asEditorLines(string) {
  82      var tab = makeWhiteSpace(indentUnit);
  83      return map(string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n").split("\n"), fixSpaces);
  84    }
  85  
  86    // Helper function for traverseDOM. Flattens an arbitrary DOM node
  87    // into an array of textnodes and <br> tags.
  88    function simplifyDOM(root, atEnd) {
  89      var result = [];
  90      var leaving = true;
  91  
  92      function simplifyNode(node, top) {
  93        if (node.nodeType == 3) {
  94          var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/\r/g, "").replace(/\n/g, " "));
  95          if (text.length) leaving = false;
  96          result.push(node);
  97        }
  98        else if (isBR(node) && node.childNodes.length == 0) {
  99          leaving = true;
 100          result.push(node);
 101        }
 102        else {
 103          for (var n = node.firstChild; n; n = n.nextSibling) simplifyNode(n);
 104          if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) {
 105            leaving = true;
 106            if (!atEnd || !top)
 107              result.push(document.createElement("BR"));
 108          }
 109        }
 110      }
 111  
 112      simplifyNode(root, true);
 113      return result;
 114    }
 115  
 116    // Creates a MochiKit-style iterator that goes over a series of DOM
 117    // nodes. The values it yields are strings, the textual content of
 118    // the nodes. It makes sure that all nodes up to and including the
 119    // one whose text is being yielded have been 'normalized' to be just
 120    // <span> and <br> elements.
 121    function traverseDOM(start){
 122      var nodeQueue = [];
 123  
 124      // Create a function that can be used to insert nodes after the
 125      // one given as argument.
 126      function pointAt(node){
 127        var parent = node.parentNode;
 128        var next = node.nextSibling;
 129        return function(newnode) {
 130          parent.insertBefore(newnode, next);
 131        };
 132      }
 133      var point = null;
 134  
 135      // This an Opera-specific hack -- always insert an empty span
 136      // between two BRs, because Opera's cursor code gets terribly
 137      // confused when the cursor is between two BRs.
 138      var afterBR = true;
 139  
 140      // Insert a normalized node at the current point. If it is a text
 141      // node, wrap it in a <span>, and give that span a currentText
 142      // property -- this is used to cache the nodeValue, because
 143      // directly accessing nodeValue is horribly slow on some browsers.
 144      // The dirty property is used by the highlighter to determine
 145      // which parts of the document have to be re-highlighted.
 146      function insertPart(part){
 147        var text = "\n";
 148        if (part.nodeType == 3) {
 149          select.snapshotChanged();
 150          part = makePartSpan(part);
 151          text = part.currentText;
 152          afterBR = false;
 153        }
 154        else {
 155          if (afterBR && window.opera)
 156            point(makePartSpan(""));
 157          afterBR = true;
 158        }
 159        part.dirty = true;
 160        nodeQueue.push(part);
 161        point(part);
 162        return text;
 163      }
 164  
 165      // Extract the text and newlines from a DOM node, insert them into
 166      // the document, and return the textual content. Used to replace
 167      // non-normalized nodes.
 168      function writeNode(node, end) {
 169        var simplified = simplifyDOM(node, end);
 170        for (var i = 0; i < simplified.length; i++)
 171          simplified[i] = insertPart(simplified[i]);
 172        return simplified.join("");
 173      }
 174  
 175      // Check whether a node is a normalized <span> element.
 176      function partNode(node){
 177        if (node.isPart && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
 178          node.currentText = node.firstChild.nodeValue;
 179          return !/[\n\t\r]/.test(node.currentText);
 180        }
 181        return false;
 182      }
 183  
 184      // Advance to next node, return string for current node.
 185      function next() {
 186        if (!start) throw StopIteration;
 187        var node = start;
 188        start = node.nextSibling;
 189  
 190        if (partNode(node)){
 191          nodeQueue.push(node);
 192          afterBR = false;
 193          return node.currentText;
 194        }
 195        else if (isBR(node)) {
 196          if (afterBR && window.opera)
 197            node.parentNode.insertBefore(makePartSpan(""), node);
 198          nodeQueue.push(node);
 199          afterBR = true;
 200          return "\n";
 201        }
 202        else {
 203          var end = !node.nextSibling;
 204          point = pointAt(node);
 205          removeElement(node);
 206          return writeNode(node, end);
 207        }
 208      }
 209  
 210      // MochiKit iterators are objects with a next function that
 211      // returns the next value or throws StopIteration when there are
 212      // no more values.
 213      return {next: next, nodes: nodeQueue};
 214    }
 215  
 216    // Determine the text size of a processed node.
 217    function nodeSize(node) {
 218      return isBR(node) ? 1 : node.currentText.length;
 219    }
 220  
 221    // Search backwards through the top-level nodes until the next BR or
 222    // the start of the frame.
 223    function startOfLine(node) {
 224      while (node && !isBR(node)) node = node.previousSibling;
 225      return node;
 226    }
 227    function endOfLine(node, container) {
 228      if (!node) node = container.firstChild;
 229      else if (isBR(node)) node = node.nextSibling;
 230  
 231      while (node && !isBR(node)) node = node.nextSibling;
 232      return node;
 233    }
 234  
 235    function time() {return new Date().getTime();}
 236  
 237    // Client interface for searching the content of the editor. Create
 238    // these by calling CodeMirror.getSearchCursor. To use, call
 239    // findNext on the resulting object -- this returns a boolean
 240    // indicating whether anything was found, and can be called again to
 241    // skip to the next find. Use the select and replace methods to
 242    // actually do something with the found locations.
 243    function SearchCursor(editor, string, from, caseFold) {
 244      this.editor = editor;
 245      this.history = editor.history;
 246      this.history.commit();
 247      this.valid = !!string;
 248      this.atOccurrence = false;
 249      if (caseFold == undefined) caseFold = string == string.toLowerCase();
 250  
 251      function getText(node){
 252        var line = cleanText(editor.history.textAfter(node));
 253        return (caseFold ? line.toLowerCase() : line);
 254      }
 255  
 256      var topPos = {node: null, offset: 0};
 257      if (from && typeof from == "object" && typeof from.character == "number") {
 258        editor.checkLine(from.line);
 259        var pos = {node: from.line, offset: from.character};
 260        this.pos = {from: pos, to: pos};
 261      }
 262      else if (from) {
 263        this.pos = {from: select.cursorPos(editor.container, true) || topPos,
 264                    to: select.cursorPos(editor.container, false) || topPos};
 265      }
 266      else {
 267        this.pos = {from: topPos, to: topPos};
 268      }
 269  
 270      if (caseFold) string = string.toLowerCase();
 271      // Create a matcher function based on the kind of string we have.
 272      var target = string.split("\n");
 273      this.matches = (target.length == 1) ?
 274        // For one-line strings, searching can be done simply by calling
 275        // indexOf or lastIndexOf on the current line.
 276        function(reverse, node, offset) {
 277          var line = getText(node), len = string.length, match;
 278          if (reverse ? (offset >= len && (match = line.lastIndexOf(string, offset - len)) != -1)
 279                      : (match = line.indexOf(string, offset)) != -1)
 280            return {from: {node: node, offset: match},
 281                    to: {node: node, offset: match + len}};
 282        } :
 283        // Multi-line strings require internal iteration over lines, and
 284        // some clunky checks to make sure the first match ends at the
 285        // end of the line and the last match starts at the start.
 286        function(reverse, node, offset) {
 287          var idx = (reverse ? target.length - 1 : 0), match = target[idx], line = getText(node);
 288          var offsetA = (reverse ? line.indexOf(match) + match.length : line.lastIndexOf(match));
 289          if (reverse ? offsetA >= offset || offsetA != match.length
 290                      : offsetA <= offset || offsetA != line.length - match.length)
 291            return;
 292  
 293          var pos = node;
 294          while (true) {
 295            if (reverse && !pos) return;
 296            pos = (reverse ? this.history.nodeBefore(pos) : this.history.nodeAfter(pos) );
 297            if (!reverse && !pos) return;
 298  
 299            line = getText(pos);
 300            match = target[reverse ? --idx : ++idx];
 301  
 302            if (idx > 0 && idx < target.length - 1) {
 303              if (line != match) return;
 304              else continue;
 305            }
 306            var offsetB = (reverse ? line.lastIndexOf(match) : line.indexOf(match) + match.length);
 307            if (reverse ? offsetB != line.length - match.length : offsetB != match.length)
 308              return;
 309            return {from: {node: reverse ? pos : node, offset: reverse ? offsetB : offsetA},
 310                    to: {node: reverse ? node : pos, offset: reverse ? offsetA : offsetB}};
 311          }
 312        };
 313    }
 314  
 315    SearchCursor.prototype = {
 316      findNext: function() {return this.find(false);},
 317      findPrevious: function() {return this.find(true);},
 318  
 319      find: function(reverse) {
 320        if (!this.valid) return false;
 321  
 322        var self = this, pos = reverse ? this.pos.from : this.pos.to,
 323            node = pos.node, offset = pos.offset;
 324        // Reset the cursor if the current line is no longer in the DOM tree.
 325        if (node && !node.parentNode) {
 326          node = null; offset = 0;
 327        }
 328        function savePosAndFail() {
 329          var pos = {node: node, offset: offset};
 330          self.pos = {from: pos, to: pos};
 331          self.atOccurrence = false;
 332          return false;
 333        }
 334  
 335        while (true) {
 336          if (this.pos = this.matches(reverse, node, offset)) {
 337            this.atOccurrence = true;
 338            return true;
 339          }
 340  
 341          if (reverse) {
 342            if (!node) return savePosAndFail();
 343            node = this.history.nodeBefore(node);
 344            offset = this.history.textAfter(node).length;
 345          }
 346          else {
 347            var next = this.history.nodeAfter(node);
 348            if (!next) {
 349              offset = this.history.textAfter(node).length;
 350              return savePosAndFail();
 351            }
 352            node = next;
 353            offset = 0;
 354          }        
 355        }
 356      },
 357  
 358      select: function() {
 359        if (this.atOccurrence) {
 360          select.setCursorPos(this.editor.container, this.pos.from, this.pos.to);
 361          select.scrollToCursor(this.editor.container);
 362        }
 363      },
 364  
 365      replace: function(string) {
 366        if (this.atOccurrence) {
 367          var end = this.editor.replaceRange(this.pos.from, this.pos.to, string);
 368          this.pos.to = end;
 369          this.atOccurrence = false;
 370        }
 371      },
 372  
 373      position: function() {
 374        if (this.atOccurrence)
 375          return {line: this.pos.from.node, character: this.pos.from.offset};
 376      }
 377    };
 378  
 379    // The Editor object is the main inside-the-iframe interface.
 380    function Editor(options) {
 381      this.options = options;
 382      window.indentUnit = options.indentUnit;
 383      this.parent = parent;
 384      var container = this.container = document.body;
 385      this.history = new UndoHistory(container, options.undoDepth, options.undoDelay, this);
 386      var self = this;
 387  
 388      if (!Editor.Parser)
 389        throw "No parser loaded.";
 390      if (options.parserConfig && Editor.Parser.configure)
 391        Editor.Parser.configure(options.parserConfig);
 392  
 393      if (!options.readOnly)
 394        select.setCursorPos(container, {node: null, offset: 0});
 395  
 396      this.dirty = [];
 397      this.importCode(options.content || "");
 398      this.history.onChange = options.onChange;
 399  
 400      if (!options.readOnly) {
 401        if (options.continuousScanning !== false) {
 402          this.scanner = this.documentScanner(options.passTime);
 403          this.delayScanning();
 404        }
 405  
 406        function setEditable() {
 407          // Use contentEditable instead of designMode on IE, since designMode frames
 408          // can not run any scripts. It would be nice if we could use contentEditable
 409          // everywhere, but it is significantly flakier than designMode on every
 410          // single non-IE browser.
 411          if (document.body.contentEditable != undefined && internetExplorer)
 412            document.body.contentEditable = "true";
 413          else
 414            document.designMode = "on";
 415  
 416          // Work around issue where you have to click on the actual
 417          // body of the document to focus it in IE, making focusing
 418          // hard when the document is small.
 419          if (internetExplorer && options.height != "dynamic")
 420            document.body.style.minHeight = (frameElement.clientHeight - 2 * document.body.offsetTop - 5) + "px";
 421  
 422          document.documentElement.style.borderWidth = "0";
 423          if (!options.textWrapping)
 424            container.style.whiteSpace = "nowrap";
 425        }
 426  
 427        // If setting the frame editable fails, try again when the user
 428        // focus it (happens when the frame is not visible on
 429        // initialisation, in Firefox).
 430        try {
 431          setEditable();
 432        }
 433        catch(e) {
 434          var focusEvent = addEventHandler(document, "focus", function() {
 435            focusEvent();
 436            setEditable();
 437          }, true);
 438        }
 439  
 440        addEventHandler(document, "keydown", method(this, "keyDown"));
 441        addEventHandler(document, "keypress", method(this, "keyPress"));
 442        addEventHandler(document, "keyup", method(this, "keyUp"));
 443  
 444        function cursorActivity() {self.cursorActivity(false);}
 445        addEventHandler(document.body, "mouseup", cursorActivity);
 446        addEventHandler(document.body, "cut", cursorActivity);
 447  
 448        // workaround for a gecko bug [?] where going forward and then
 449        // back again breaks designmode (no more cursor)
 450        if (gecko)
 451          addEventHandler(window, "pagehide", function(){self.unloaded = true;});
 452  
 453        addEventHandler(document.body, "paste", function(event) {
 454          cursorActivity();
 455          var text = null;
 456          try {
 457            var clipboardData = event.clipboardData || window.clipboardData;
 458            if (clipboardData) text = clipboardData.getData('Text');
 459          }
 460          catch(e) {}
 461          if (text !== null) {
 462            event.stop();
 463            self.replaceSelection(text);
 464            select.scrollToCursor(self.container);
 465          }
 466        });
 467  
 468        if (this.options.autoMatchParens)
 469          addEventHandler(document.body, "click", method(this, "scheduleParenHighlight"));
 470      }
 471      else if (!options.textWrapping) {
 472        container.style.whiteSpace = "nowrap";
 473      }
 474    }
 475  
 476    function isSafeKey(code) {
 477      return (code >= 16 && code <= 18) || // shift, control, alt
 478             (code >= 33 && code <= 40); // arrows, home, end
 479    }
 480  
 481    Editor.prototype = {
 482      // Import a piece of code into the editor.
 483      importCode: function(code) {
 484        this.history.push(null, null, asEditorLines(code));
 485        this.history.reset();
 486      },
 487  
 488      // Extract the code from the editor.
 489      getCode: function() {
 490        if (!this.container.firstChild)
 491          return "";
 492  
 493        var accum = [];
 494        select.markSelection();
 495        forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
 496        webkitLastLineHack(this.container);
 497        select.selectMarked();
 498        // On webkit, don't count last (empty) line if the webkitLastLineHack BR is present
 499        if (webkit && this.container.lastChild.hackBR)
 500          accum.pop();
 501        return cleanText(accum.join(""));
 502      },
 503  
 504      checkLine: function(node) {
 505        if (node === false || !(node == null || node.parentNode == this.container))
 506          throw parent.CodeMirror.InvalidLineHandle;
 507      },
 508  
 509      cursorPosition: function(start) {
 510        if (start == null) start = true;
 511        var pos = select.cursorPos(this.container, start);
 512        if (pos) return {line: pos.node, character: pos.offset};
 513        else return {line: null, character: 0};
 514      },
 515  
 516      firstLine: function() {
 517        return null;
 518      },
 519  
 520      lastLine: function() {
 521        if (this.container.lastChild) return startOfLine(this.container.lastChild);
 522        else return null;
 523      },
 524  
 525      nextLine: function(line) {
 526        this.checkLine(line);
 527        var end = endOfLine(line, this.container);
 528        return end || false;
 529      },
 530  
 531      prevLine: function(line) {
 532        this.checkLine(line);
 533        if (line == null) return false;
 534        return startOfLine(line.previousSibling);
 535      },
 536  
 537      visibleLineCount: function() {
 538        var line = this.container.firstChild;
 539        while (line && isBR(line)) line = line.nextSibling; // BR heights are unreliable
 540        if (!line) return false;
 541        var innerHeight = (window.innerHeight
 542                           || document.documentElement.clientHeight
 543                           || document.body.clientHeight);
 544        return Math.floor(innerHeight / line.offsetHeight);
 545      },
 546  
 547      selectLines: function(startLine, startOffset, endLine, endOffset) {
 548        this.checkLine(startLine);
 549        var start = {node: startLine, offset: startOffset}, end = null;
 550        if (endOffset !== undefined) {
 551          this.checkLine(endLine);
 552          end = {node: endLine, offset: endOffset};
 553        }
 554        select.setCursorPos(this.container, start, end);
 555        select.scrollToCursor(this.container);
 556      },
 557  
 558      lineContent: function(line) {
 559        var accum = [];
 560        for (line = line ? line.nextSibling : this.container.firstChild;
 561             line && !isBR(line); line = line.nextSibling)
 562          accum.push(nodeText(line));
 563        return cleanText(accum.join(""));
 564      },
 565  
 566      setLineContent: function(line, content) {
 567        this.history.commit();
 568        this.replaceRange({node: line, offset: 0},
 569                          {node: line, offset: this.history.textAfter(line).length},
 570                          content);
 571        this.addDirtyNode(line);
 572        this.scheduleHighlight();
 573      },
 574  
 575      removeLine: function(line) {
 576        var node = line ? line.nextSibling : this.container.firstChild;
 577        while (node) {
 578          var next = node.nextSibling;
 579          removeElement(node);
 580          if (isBR(node)) break;
 581          node = next;
 582        }
 583        this.addDirtyNode(line);
 584        this.scheduleHighlight();
 585      },
 586  
 587      insertIntoLine: function(line, position, content) {
 588        var before = null;
 589        if (position == "end") {
 590          before = endOfLine(line, this.container);
 591        }
 592        else {
 593          for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
 594            if (position == 0) {
 595              before = cur;
 596              break;
 597            }
 598            var text = nodeText(cur);
 599            if (text.length > position) {
 600              before = cur.nextSibling;
 601              content = text.slice(0, position) + content + text.slice(position);
 602              removeElement(cur);
 603              break;
 604            }
 605            position -= text.length;
 606          }
 607        }
 608  
 609        var lines = asEditorLines(content);
 610        for (var i = 0; i < lines.length; i++) {
 611          if (i > 0) this.container.insertBefore(document.createElement("BR"), before);
 612          this.container.insertBefore(makePartSpan(lines[i]), before);
 613        }
 614        this.addDirtyNode(line);
 615        this.scheduleHighlight();
 616      },
 617  
 618      // Retrieve the selected text.
 619      selectedText: function() {
 620        var h = this.history;
 621        h.commit();
 622  
 623        var start = select.cursorPos(this.container, true),
 624            end = select.cursorPos(this.container, false);
 625        if (!start || !end) return "";
 626  
 627        if (start.node == end.node)
 628          return h.textAfter(start.node).slice(start.offset, end.offset);
 629  
 630        var text = [h.textAfter(start.node).slice(start.offset)];
 631        for (var pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos))
 632          text.push(h.textAfter(pos));
 633        text.push(h.textAfter(end.node).slice(0, end.offset));
 634        return cleanText(text.join("\n"));
 635      },
 636  
 637      // Replace the selection with another piece of text.
 638      replaceSelection: function(text) {
 639        this.history.commit();
 640  
 641        var start = select.cursorPos(this.container, true),
 642            end = select.cursorPos(this.container, false);
 643        if (!start || !end) return;
 644  
 645        end = this.replaceRange(start, end, text);
 646        select.setCursorPos(this.container, end);
 647        webkitLastLineHack(this.container);
 648      },
 649  
 650      cursorCoords: function(start) {
 651        var sel = select.cursorPos(this.container, start);
 652        if (!sel) return null;
 653        var off = sel.offset, node = sel.node, self = this;
 654        function measureFromNode(node, xOffset) {
 655          var y = -(document.body.scrollTop || document.documentElement.scrollTop || 0),
 656              x = -(document.body.scrollLeft || document.documentElement.scrollLeft || 0) + xOffset;
 657          forEach([node, window.frameElement], function(n) {
 658            while (n) {x += n.offsetLeft; y += n.offsetTop;n = n.offsetParent;}
 659          });
 660          return {x: x, y: y, yBot: y + node.offsetHeight};
 661        }
 662        function withTempNode(text, f) {
 663          var node = document.createElement("SPAN");
 664          node.appendChild(document.createTextNode(text));
 665          try {return f(node);}
 666          finally {if (node.parentNode) node.parentNode.removeChild(node);}
 667        }
 668  
 669        while (off) {
 670          node = node ? node.nextSibling : this.container.firstChild;
 671          var txt = nodeText(node);
 672          if (off < txt.length)
 673            return withTempNode(txt.substr(0, off), function(tmp) {
 674              tmp.style.position = "absolute"; tmp.style.visibility = "hidden";
 675              tmp.className = node.className;
 676              self.container.appendChild(tmp);
 677              return measureFromNode(node, tmp.offsetWidth);
 678            });
 679          off -= txt.length;
 680        }
 681        if (node && isSpan(node))
 682          return measureFromNode(node, node.offsetWidth);
 683        else if (node && node.nextSibling && isSpan(node.nextSibling))
 684          return measureFromNode(node.nextSibling, 0);
 685        else
 686          return withTempNode("\u200b", function(tmp) {
 687            if (node) node.parentNode.insertBefore(tmp, node.nextSibling);
 688            else self.container.insertBefore(tmp, self.container.firstChild);
 689            return measureFromNode(tmp, 0);
 690          });
 691      },
 692  
 693      reroutePasteEvent: function() {
 694        if (this.capturingPaste || window.opera) return;
 695        this.capturingPaste = true;
 696        var te = window.frameElement.CodeMirror.textareaHack;
 697        parent.focus();
 698        te.value = "";
 699        te.focus();
 700  
 701        var self = this;
 702        this.parent.setTimeout(function() {
 703          self.capturingPaste = false;
 704          window.focus();
 705          if (self.selectionSnapshot) // IE hack
 706            window.select.setBookmark(self.container, self.selectionSnapshot);
 707          var text = te.value;
 708          if (text) {
 709            self.replaceSelection(text);
 710            select.scrollToCursor(self.container);
 711          }
 712        }, 10);
 713      },
 714  
 715      replaceRange: function(from, to, text) {
 716        var lines = asEditorLines(text);
 717        lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
 718        var lastLine = lines[lines.length - 1];
 719        lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
 720        var end = this.history.nodeAfter(to.node);
 721        this.history.push(from.node, end, lines);
 722        return {node: this.history.nodeBefore(end),
 723                offset: lastLine.length};
 724      },
 725  
 726      getSearchCursor: function(string, fromCursor, caseFold) {
 727        return new SearchCursor(this, string, fromCursor, caseFold);
 728      },
 729  
 730      // Re-indent the whole buffer
 731      reindent: function() {
 732        if (this.container.firstChild)
 733          this.indentRegion(null, this.container.lastChild);
 734      },
 735  
 736      reindentSelection: function(direction) {
 737        if (!select.somethingSelected()) {
 738          this.indentAtCursor(direction);
 739        }
 740        else {
 741          var start = select.selectionTopNode(this.container, true),
 742              end = select.selectionTopNode(this.container, false);
 743          if (start === false || end === false) return;
 744          this.indentRegion(start, end, direction);
 745        }
 746      },
 747  
 748      grabKeys: function(eventHandler, filter) {
 749        this.frozen = eventHandler;
 750        this.keyFilter = filter;
 751      },
 752      ungrabKeys: function() {
 753        this.frozen = "leave";
 754      },
 755  
 756      setParser: function(name, parserConfig) {
 757        Editor.Parser = window[name];
 758        parserConfig = parserConfig || this.options.parserConfig;
 759        if (parserConfig && Editor.Parser.configure)
 760          Editor.Parser.configure(parserConfig);
 761  
 762        if (this.container.firstChild) {
 763          forEach(this.container.childNodes, function(n) {
 764            if (n.nodeType != 3) n.dirty = true;
 765          });
 766          this.addDirtyNode(this.firstChild);
 767          this.scheduleHighlight();
 768        }
 769      },
 770  
 771      // Intercept enter and tab, and assign their new functions.
 772      keyDown: function(event) {
 773        if (this.frozen == "leave") {this.frozen = null; this.keyFilter = null;}
 774        if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode, event))) {
 775          event.stop();
 776          this.frozen(event);
 777          return;
 778        }
 779  
 780        var code = event.keyCode;
 781        // Don't scan when the user is typing.
 782        this.delayScanning();
 783        // Schedule a paren-highlight event, if configured.
 784        if (this.options.autoMatchParens)
 785          this.scheduleParenHighlight();
 786  
 787        // The various checks for !altKey are there because AltGr sets both
 788        // ctrlKey and altKey to true, and should not be recognised as
 789        // Control.
 790        if (code == 13) { // enter
 791          if (event.ctrlKey && !event.altKey) {
 792            this.reparseBuffer();
 793          }
 794          else {
 795            select.insertNewlineAtCursor();
 796            var mode = this.options.enterMode;
 797            if (mode != "flat") this.indentAtCursor(mode == "keep" ? "keep" : undefined);
 798            select.scrollToCursor(this.container);
 799          }
 800          event.stop();
 801        }
 802        else if (code == 9 && this.options.tabMode != "default" && !event.ctrlKey) { // tab
 803          this.handleTab(!event.shiftKey);
 804          event.stop();
 805        }
 806        else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
 807          this.handleTab(true);
 808          event.stop();
 809        }
 810        else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home
 811          if (this.home()) event.stop();
 812        }
 813        else if (code == 35 && !event.shiftKey && !event.ctrlKey) { // end
 814          if (this.end()) event.stop();
 815        }
 816        // Only in Firefox is the default behavior for PgUp/PgDn correct.
 817        else if (code == 33 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgUp
 818          if (this.pageUp()) event.stop();
 819        }
 820        else if (code == 34 && !event.shiftKey && !event.ctrlKey && !gecko) {  // PgDn
 821          if (this.pageDown()) event.stop();
 822        }
 823        else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
 824          this.highlightParens(event.shiftKey, true);
 825          event.stop();
 826        }
 827        else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right
 828          var cursor = select.selectionTopNode(this.container);
 829          if (cursor === false || !this.container.firstChild) return;
 830  
 831          if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
 832          else {
 833            var end = endOfLine(cursor, this.container);
 834            select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
 835          }
 836          event.stop();
 837        }
 838        else if ((event.ctrlKey || event.metaKey) && !event.altKey) {
 839          if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y
 840            select.scrollToNode(this.history.redo());
 841            event.stop();
 842          }
 843          else if (code == 90 || (safari && code == 8)) { // Z, backspace
 844            select.scrollToNode(this.history.undo());
 845            event.stop();
 846          }
 847          else if (code == 83 && this.options.saveFunction) { // S
 848            this.options.saveFunction();
 849            event.stop();
 850          }
 851          else if (code == 86 && !mac) { // V
 852            this.reroutePasteEvent();
 853          }
 854        }
 855      },
 856  
 857      // Check for characters that should re-indent the current line,
 858      // and prevent Opera from handling enter and tab anyway.
 859      keyPress: function(event) {
 860        var electric = this.options.electricChars && Editor.Parser.electricChars, self = this;
 861        // Hack for Opera, and Firefox on OS X, in which stopping a
 862        // keydown event does not prevent the associated keypress event
 863        // from happening, so we have to cancel enter and tab again
 864        // here.
 865        if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode || event.code, event))) ||
 866            event.code == 13 || (event.code == 9 && this.options.tabMode != "default") ||
 867            (event.code == 32 && event.shiftKey && this.options.tabMode == "default"))
 868          event.stop();
 869        else if (mac && (event.ctrlKey || event.metaKey) && event.character == "v") {
 870          this.reroutePasteEvent();
 871        }
 872        else if (electric && electric.indexOf(event.character) != -1)
 873          this.parent.setTimeout(function(){self.indentAtCursor(null);}, 0);
 874        // Work around a bug where pressing backspace at the end of a
 875        // line, or delete at the start, often causes the cursor to jump
 876        // to the start of the line in Opera 10.60.
 877        else if (brokenOpera) {
 878          if (event.code == 8) { // backspace
 879            var sel = select.selectionTopNode(this.container), self = this,
 880                next = sel ? sel.nextSibling : this.container.firstChild;
 881            if (sel !== false && next && isBR(next))
 882              this.parent.setTimeout(function(){
 883                if (select.selectionTopNode(self.container) == next)
 884                  select.focusAfterNode(next.previousSibling, self.container);
 885              }, 20);
 886          }
 887          else if (event.code == 46) { // delete
 888            var sel = select.selectionTopNode(this.container), self = this;
 889            if (sel && isBR(sel)) {
 890              this.parent.setTimeout(function(){
 891                if (select.selectionTopNode(self.container) != sel)
 892                  select.focusAfterNode(sel, self.container);
 893              }, 20);
 894            }
 895          }
 896        }
 897        // In 533.* WebKit versions, when the document is big, typing
 898        // something at the end of a line causes the browser to do some
 899        // kind of stupid heavy operation, creating delays of several
 900        // seconds before the typed characters appear. This very crude
 901        // hack inserts a temporary zero-width space after the cursor to
 902        // make it not be at the end of the line.
 903        else if (slowWebkit) {
 904          var sel = select.selectionTopNode(this.container),
 905              next = sel ? sel.nextSibling : this.container.firstChild;
 906          // Doesn't work on empty lines, for some reason those always
 907          // trigger the delay.
 908          if (sel && next && isBR(next) && !isBR(sel)) {
 909            var cheat = document.createTextNode("\u200b");
 910            this.container.insertBefore(cheat, next);
 911            this.parent.setTimeout(function() {
 912              if (cheat.nodeValue == "\u200b") removeElement(cheat);
 913              else cheat.nodeValue = cheat.nodeValue.replace("\u200b", "");
 914            }, 20);
 915          }
 916        }
 917      },
 918  
 919      // Mark the node at the cursor dirty when a non-safe key is
 920      // released.
 921      keyUp: function(event) {
 922        this.cursorActivity(isSafeKey(event.keyCode));
 923      },
 924  
 925      // Indent the line following a given <br>, or null for the first
 926      // line. If given a <br> element, this must have been highlighted
 927      // so that it has an indentation method. Returns the whitespace
 928      // element that has been modified or created (if any).
 929      indentLineAfter: function(start, direction) {
 930        function whiteSpaceAfter(node) {
 931          var ws = node ? node.nextSibling : self.container.firstChild;
 932          if (!ws || !hasClass(ws, "whitespace")) return null;
 933          return ws;
 934        }
 935  
 936        // whiteSpace is the whitespace span at the start of the line,
 937        // or null if there is no such node.
 938        var self = this, whiteSpace = whiteSpaceAfter(start);
 939        var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
 940  
 941        if (direction == "keep") {
 942          if (start) {
 943            var prevWS = whiteSpaceAfter(startOfLine(start.previousSibling))
 944            if (prevWS) newIndent = prevWS.currentText.length;
 945          }
 946        }
 947        else {
 948          // Sometimes the start of the line can influence the correct
 949          // indentation, so we retrieve it.
 950          var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
 951          var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
 952  
 953          // Ask the lexical context for the correct indentation, and
 954          // compute how much this differs from the current indentation.
 955          if (direction != null && this.options.tabMode == "shift")
 956            newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
 957          else if (start)
 958            newIndent = start.indentation(nextChars, curIndent, direction);
 959          else if (Editor.Parser.firstIndentation)
 960            newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
 961        }
 962        
 963        var indentDiff = newIndent - curIndent;
 964  
 965        // If there is too much, this is just a matter of shrinking a span.
 966        if (indentDiff < 0) {
 967          if (newIndent == 0) {
 968            if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild || firstText, 0);
 969            removeElement(whiteSpace);
 970            whiteSpace = null;
 971          }
 972          else {
 973            select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
 974            whiteSpace.currentText = makeWhiteSpace(newIndent);
 975            whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
 976          }
 977        }
 978        // Not enough...
 979        else if (indentDiff > 0) {
 980          // If there is whitespace, we grow it.
 981          if (whiteSpace) {
 982            whiteSpace.currentText = makeWhiteSpace(newIndent);
 983            whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
 984            select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
 985          }
 986          // Otherwise, we have to add a new whitespace node.
 987          else {
 988            whiteSpace = makePartSpan(makeWhiteSpace(newIndent));
 989            whiteSpace.className = "whitespace";
 990            if (start) insertAfter(whiteSpace, start);
 991            else this.container.insertBefore(whiteSpace, this.container.firstChild);
 992            select.snapshotMove(firstText && (firstText.firstChild || firstText),
 993                                whiteSpace.firstChild, newIndent, false, true);
 994          }
 995        }
 996        // Make sure cursor ends up after the whitespace
 997        else if (whiteSpace) {
 998      select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, newIndent, false);
 999        }
1000        if (indentDiff != 0) this.addDirtyNode(start);
1001      },
1002  
1003      // Re-highlight the selected part of the document.
1004      highlightAtCursor: function() {
1005        var pos = select.selectionTopNode(this.container, true);
1006        var to = select.selectionTopNode(this.container, false);
1007        if (pos === false || to === false) return false;
1008  
1009        select.markSelection();
1010        if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
1011          return false;
1012        select.selectMarked();
1013        return true;
1014      },
1015  
1016      // When tab is pressed with text selected, the whole selection is
1017      // re-indented, when nothing is selected, the line with the cursor
1018      // is re-indented.
1019      handleTab: function(direction) {
1020        if (this.options.tabMode == "spaces")
1021          select.insertTabAtCursor();
1022        else
1023          this.reindentSelection(direction);
1024      },
1025  
1026      // Custom home behaviour that doesn't land the cursor in front of
1027      // leading whitespace unless pressed twice.
1028      home: function() {
1029        var cur = select.selectionTopNode(this.container, true), start = cur;
1030        if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild)
1031          return false;
1032  
1033        while (cur && !isBR(cur)) cur = cur.previousSibling;
1034        var next = cur ? cur.nextSibling : this.container.firstChild;
1035        if (next && next != start && next.isPart && hasClass(next, "whitespace"))
1036          select.focusAfterNode(next, this.container);
1037        else
1038          select.focusAfterNode(cur, this.container);
1039  
1040        select.scrollToCursor(this.container);
1041        return true;
1042      },
1043  
1044      // Some browsers (Opera) don't manage to handle the end key
1045      // properly in the face of vertical scrolling.
1046      end: function() {
1047        var cur = select.selectionTopNode(this.container, true);
1048        if (cur === false) return false;
1049        cur = endOfLine(cur, this.container);
1050        if (!cur) return false;
1051        select.focusAfterNode(cur.previousSibling, this.container);
1052        select.scrollToCursor(this.container);
1053        return true;
1054      },
1055  
1056      pageUp: function() {
1057        var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
1058        if (line === false || scrollAmount === false) return false;
1059        // Try to keep one line on the screen.
1060        scrollAmount -= 2;
1061        for (var i = 0; i < scrollAmount; i++) {
1062          line = this.prevLine(line);
1063          if (line === false) break;
1064        }
1065        if (i == 0) return false; // Already at first line
1066        select.setCursorPos(this.container, {node: line, offset: 0});
1067        select.scrollToCursor(this.container);
1068        return true;
1069      },
1070  
1071      pageDown: function() {
1072        var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
1073        if (line === false || scrollAmount === false) return false;
1074        // Try to move to the last line of the current page.
1075        scrollAmount -= 2;
1076        for (var i = 0; i < scrollAmount; i++) {
1077          var nextLine = this.nextLine(line);
1078          if (nextLine === false) break;
1079          line = nextLine;
1080        }
1081        if (i == 0) return false; // Already at last line
1082        select.setCursorPos(this.container, {node: line, offset: 0});
1083        select.scrollToCursor(this.container);
1084        return true;
1085      },
1086  
1087      // Delay (or initiate) the next paren highlight event.
1088      scheduleParenHighlight: function() {
1089        if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
1090        var self = this;
1091        this.parenEvent = this.parent.setTimeout(function(){self.highlightParens();}, 300);
1092      },
1093  
1094      // Take the token before the cursor. If it contains a character in
1095      // '()[]{}', search for the matching paren/brace/bracket, and
1096      // highlight them in green for a moment, or red if no proper match
1097      // was found.
1098      highlightParens: function(jump, fromKey) {
1099        var self = this;
1100        // give the relevant nodes a colour.
1101        function highlight(node, ok) {
1102          if (!node) return;
1103          if (self.options.markParen) {
1104            self.options.markParen(node, ok);
1105          }
1106          else {
1107            node.style.fontWeight = "bold";
1108            node.style.color = ok ? "#8F8" : "#F88";
1109          }
1110        }
1111        function unhighlight(node) {
1112          if (!node) return;
1113          if (self.options.unmarkParen) {
1114            self.options.unmarkParen(node);
1115          }
1116          else {
1117            node.style.fontWeight = "";
1118            node.style.color = "";
1119          }
1120        }
1121        if (!fromKey && self.highlighted) {
1122          unhighlight(self.highlighted[0]);
1123          unhighlight(self.highlighted[1]);
1124        }
1125  
1126        if (!window.parent || !window.select) return;
1127        // Clear the event property.
1128        if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
1129        this.parenEvent = null;
1130  
1131        // Extract a 'paren' from a piece of text.
1132        function paren(node) {
1133          if (node.currentText) {
1134            var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
1135            return match && match[1];
1136          }
1137        }
1138        // Determine the direction a paren is facing.
1139        function forward(ch) {
1140          return /[\(\[\{]/.test(ch);
1141        }
1142  
1143        var ch, cursor = select.selectionTopNode(this.container, true);
1144        if (!cursor || !this.highlightAtCursor()) return;
1145        cursor = select.selectionTopNode(this.container, true);
1146        if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor)))))
1147          return;
1148        // We only look for tokens with the same className.
1149        var className = cursor.className, dir = forward(ch), match = matching[ch];
1150  
1151        // Since parts of the document might not have been properly
1152        // highlighted, and it is hard to know in advance which part we
1153        // have to scan, we just try, and when we find dirty nodes we
1154        // abort, parse them, and re-try.
1155        function tryFindMatch() {
1156          var stack = [], ch, ok = true;
1157          for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
1158            if (runner.className == className && isSpan(runner) && (ch = paren(runner))) {
1159              if (forward(ch) == dir)
1160                stack.push(ch);
1161              else if (!stack.length)
1162                ok = false;
1163              else if (stack.pop() != matching[ch])
1164                ok = false;
1165              if (!stack.length) break;
1166            }
1167            else if (runner.dirty || !isSpan(runner) && !isBR(runner)) {
1168              return {node: runner, status: "dirty"};
1169            }
1170          }
1171          return {node: runner, status: runner && ok};
1172        }
1173  
1174        while (true) {
1175          var found = tryFindMatch();
1176          if (found.status == "dirty") {
1177            this.highlight(found.node, endOfLine(found.node));
1178            // Needed because in some corner cases a highlight does not
1179            // reach a node.
1180            found.node.dirty = false;
1181            continue;
1182          }
1183          else {
1184            highlight(cursor, found.status);
1185            highlight(found.node, found.status);
1186            if (fromKey)
1187              self.parent.setTimeout(function() {unhighlight(cursor); unhighlight(found.node);}, 500);
1188            else
1189              self.highlighted = [cursor, found.node];
1190            if (jump && found.node)
1191              select.focusAfterNode(found.node.previousSibling, this.container);
1192            break;
1193          }
1194        }
1195      },
1196  
1197      // Adjust the amount of whitespace at the start of the line that
1198      // the cursor is on so that it is indented properly.
1199      indentAtCursor: function(direction) {
1200        if (!this.container.firstChild) return;
1201        // The line has to have up-to-date lexical information, so we
1202        // highlight it first.
1203        if (!this.highlightAtCursor()) return;
1204        var cursor = select.selectionTopNode(this.container, false);
1205        // If we couldn't determine the place of the cursor,
1206        // there's nothing to indent.
1207        if (cursor === false)
1208          return;
1209        select.markSelection();
1210        this.indentLineAfter(startOfLine(cursor), direction);
1211        select.selectMarked();
1212      },
1213  
1214      // Indent all lines whose start falls inside of the current
1215      // selection.
1216      indentRegion: function(start, end, direction) {
1217        var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling);
1218        if (!isBR(end)) end = endOfLine(end, this.container);
1219        this.addDirtyNode(start);
1220  
1221        do {
1222          var next = endOfLine(current, this.container);
1223          if (current) this.highlight(before, next, true);
1224          this.indentLineAfter(current, direction);
1225          before = current;
1226          current = next;
1227        } while (current != end);
1228        select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
1229      },
1230  
1231      // Find the node that the cursor is in, mark it as dirty, and make
1232      // sure a highlight pass is scheduled.
1233      cursorActivity: function(safe) {
1234        // pagehide event hack above
1235        if (this.unloaded) {
1236          window.document.designMode = "off";
1237          window.document.designMode = "on";
1238          this.unloaded = false;
1239        }
1240  
1241        if (internetExplorer) {
1242          this.container.createTextRange().execCommand("unlink");
1243          this.selectionSnapshot = select.getBookmark(this.container);
1244        }
1245  
1246        var activity = this.options.cursorActivity;
1247        if (!safe || activity) {
1248          var cursor = select.selectionTopNode(this.container, false);
1249          if (cursor === false || !this.container.firstChild) return;
1250          cursor = cursor || this.container.firstChild;
1251          if (activity) activity(cursor);
1252          if (!safe) {
1253            this.scheduleHighlight();
1254            this.addDirtyNode(cursor);
1255          }
1256        }
1257      },
1258  
1259      reparseBuffer: function() {
1260        forEach(this.container.childNodes, function(node) {node.dirty = true;});
1261        if (this.container.firstChild)
1262          this.addDirtyNode(this.container.firstChild);
1263      },
1264  
1265      // Add a node to the set of dirty nodes, if it isn't already in
1266      // there.
1267      addDirtyNode: function(node) {
1268        node = node || this.container.firstChild;
1269        if (!node) return;
1270  
1271        for (var i = 0; i < this.dirty.length; i++)
1272          if (this.dirty[i] == node) return;
1273  
1274        if (node.nodeType != 3)
1275          node.dirty = true;
1276        this.dirty.push(node);
1277      },
1278  
1279      allClean: function() {
1280        return !this.dirty.length;
1281      },
1282  
1283      // Cause a highlight pass to happen in options.passDelay
1284      // milliseconds. Clear the existing timeout, if one exists. This
1285      // way, the passes do not happen while the user is typing, and
1286      // should as unobtrusive as possible.
1287      scheduleHighlight: function() {
1288        // Timeouts are routed through the parent window, because on
1289        // some browsers designMode windows do not fire timeouts.
1290        var self = this;
1291        this.parent.clearTimeout(this.highlightTimeout);
1292        this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
1293      },
1294  
1295      // Fetch one dirty node, and remove it from the dirty set.
1296      getDirtyNode: function() {
1297        while (this.dirty.length > 0) {
1298          var found = this.dirty.pop();
1299          // IE8 sometimes throws an unexplainable 'invalid argument'
1300          // exception for found.parentNode
1301          try {
1302            // If the node has been coloured in the meantime, or is no
1303            // longer in the document, it should not be returned.
1304            while (found && found.parentNode != this.container)
1305              found = found.parentNode;
1306            if (found && (found.dirty || found.nodeType == 3))
1307              return found;
1308          } catch (e) {}
1309        }
1310        return null;
1311      },
1312  
1313      // Pick dirty nodes, and highlight them, until options.passTime
1314      // milliseconds have gone by. The highlight method will continue
1315      // to next lines as long as it finds dirty nodes. It returns
1316      // information about the place where it stopped. If there are
1317      // dirty nodes left after this function has spent all its lines,
1318      // it shedules another highlight to finish the job.
1319      highlightDirty: function(force) {
1320        // Prevent FF from raising an error when it is firing timeouts
1321        // on a page that's no longer loaded.
1322        if (!window.parent || !window.select) return false;
1323  
1324        if (!this.options.readOnly) select.markSelection();
1325        var start, endTime = force ? null : time() + this.options.passTime;
1326        while ((time() < endTime || force) && (start = this.getDirtyNode())) {
1327          var result = this.highlight(start, endTime);
1328          if (result && result.node && result.dirty)
1329            this.addDirtyNode(result.node.nextSibling);
1330        }
1331        if (!this.options.readOnly) select.selectMarked();
1332        if (start) this.scheduleHighlight();
1333        return this.dirty.length == 0;
1334      },
1335  
1336      // Creates a function that, when called through a timeout, will
1337      // continuously re-parse the document.
1338      documentScanner: function(passTime) {
1339        var self = this, pos = null;
1340        return function() {
1341          // FF timeout weirdness workaround.
1342          if (!window.parent || !window.select) return;
1343          // If the current node is no longer in the document... oh
1344          // well, we start over.
1345          if (pos && pos.parentNode != self.container)
1346            pos = null;
1347          select.markSelection();
1348          var result = self.highlight(pos, time() + passTime, true);
1349          select.selectMarked();
1350          var newPos = result ? (result.node && result.node.nextSibling) : null;
1351          pos = (pos == newPos) ? null : newPos;
1352          self.delayScanning();
1353        };
1354      },
1355  
1356      // Starts the continuous scanning process for this document after
1357      // a given interval.
1358      delayScanning: function() {
1359        if (this.scanner) {
1360          this.parent.clearTimeout(this.documentScan);
1361          this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
1362        }
1363      },
1364  
1365      // The function that does the actual highlighting/colouring (with
1366      // help from the parser and the DOM normalizer). Its interface is
1367      // rather overcomplicated, because it is used in different
1368      // situations: ensuring that a certain line is highlighted, or
1369      // highlighting up to X milliseconds starting from a certain
1370      // point. The 'from' argument gives the node at which it should
1371      // start. If this is null, it will start at the beginning of the
1372      // document. When a timestamp is given with the 'target' argument,
1373      // it will stop highlighting at that time. If this argument holds
1374      // a DOM node, it will highlight until it reaches that node. If at
1375      // any time it comes across two 'clean' lines (no dirty nodes), it
1376      // will stop, except when 'cleanLines' is true. maxBacktrack is
1377      // the maximum number of lines to backtrack to find an existing
1378      // parser instance. This is used to give up in situations where a
1379      // highlight would take too long and freeze the browser interface.
1380      highlight: function(from, target, cleanLines, maxBacktrack){
1381        var container = this.container, self = this, active = this.options.activeTokens;
1382        var endTime = (typeof target == "number" ? target : null);
1383  
1384        if (!container.firstChild)
1385          return false;
1386        // Backtrack to the first node before from that has a partial
1387        // parse stored.
1388        while (from && (!from.parserFromHere || from.dirty)) {
1389          if (maxBacktrack != null && isBR(from) && (--maxBacktrack) < 0)
1390            return false;
1391          from = from.previousSibling;
1392        }
1393        // If we are at the end of the document, do nothing.
1394        if (from && !from.nextSibling)
1395          return false;
1396  
1397        // Check whether a part (<span> node) and the corresponding token
1398        // match.
1399        function correctPart(token, part){
1400          return !part.reduced && part.currentText == token.value && part.className == token.style;
1401        }
1402        // Shorten the text associated with a part by chopping off
1403        // characters from the front. Note that only the currentText
1404        // property gets changed. For efficiency reasons, we leave the
1405        // nodeValue alone -- we set the reduced flag to indicate that
1406        // this part must be replaced.
1407        function shortenPart(part, minus){
1408          part.currentText = part.currentText.substring(minus);
1409          part.reduced = true;
1410        }
1411        // Create a part corresponding to a given token.
1412        function tokenPart(token){
1413          var part = makePartSpan(token.value);     
1414          part.className = token.style;
1415          return part;
1416        }
1417  
1418        function maybeTouch(node) {
1419          if (node) {
1420            var old = node.oldNextSibling;
1421            if (lineDirty || old === undefined || node.nextSibling != old)
1422              self.history.touch(node);
1423            node.oldNextSibling = node.nextSibling;
1424          }
1425          else {
1426            var old = self.container.oldFirstChild;
1427            if (lineDirty || old === undefined || self.container.firstChild != old)
1428              self.history.touch(null);
1429            self.container.oldFirstChild = self.container.firstChild;
1430          }
1431        }
1432  
1433        // Get the token stream. If from is null, we start with a new
1434        // parser from the start of the frame, otherwise a partial parse
1435        // is resumed.
1436        var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
1437            stream = stringStream(traversal),
1438            parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
1439  
1440        function surroundedByBRs(node) {
1441          return (node.previousSibling == null || isBR(node.previousSibling)) &&
1442                 (node.nextSibling == null || isBR(node.nextSibling));
1443        }
1444  
1445        // parts is an interface to make it possible to 'delay' fetching
1446        // the next DOM node until we are completely done with the one
1447        // before it. This is necessary because often the next node is
1448        // not yet available when we want to proceed past the current
1449        // one.
1450        var parts = {
1451          current: null,
1452          // Fetch current node.
1453          get: function(){
1454            if (!this.current)
1455              this.current = traversal.nodes.shift();
1456            return this.current;
1457          },
1458          // Advance to the next part (do not fetch it yet).
1459          next: function(){
1460            this.current = null;
1461          },
1462          // Remove the current part from the DOM tree, and move to the
1463          // next.
1464          remove: function(){
1465            container.removeChild(this.get());
1466            this.current = null;
1467          },
1468          // Advance to the next part that is not empty, discarding empty
1469          // parts.
1470          getNonEmpty: function(){
1471            var part = this.get();
1472            // Allow empty nodes when they are alone on a line, needed
1473            // for the FF cursor bug workaround (see select.js,
1474            // insertNewlineAtCursor).
1475            while (part && isSpan(part) && part.currentText == "") {
1476              // Leave empty nodes that are alone on a line alone in
1477              // Opera, since that browsers doesn't deal well with
1478              // having 2 BRs in a row.
1479              if (window.opera && surroundedByBRs(part)) {
1480                this.next();
1481                part = this.get();
1482              }
1483              else {
1484                var old = part;
1485                this.remove();
1486                part = this.get();
1487                // Adjust selection information, if any. See select.js for details.
1488                select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
1489              }
1490            }
1491            
1492            return part;
1493          }
1494        };
1495  
1496        var lineDirty = false, prevLineDirty = true, lineNodes = 0;
1497  
1498        // This forEach loops over the tokens from the parsed stream, and
1499        // at the same time uses the parts object to proceed through the
1500        // corresponding DOM nodes.
1501        forEach(parsed, function(token){
1502          var part = parts.getNonEmpty();
1503  
1504          if (token.value == "\n"){
1505            // The idea of the two streams actually staying synchronized
1506            // is such a long shot that we explicitly check.
1507            if (!isBR(part))
1508              throw "Parser out of sync. Expected BR.";
1509  
1510            if (part.dirty || !part.indentation) lineDirty = true;
1511            maybeTouch(from);
1512            from = part;
1513  
1514            // Every <br> gets a copy of the parser state and a lexical
1515            // context assigned to it. The first is used to be able to
1516            // later resume parsing from this point, the second is used
1517            // for indentation.
1518            part.parserFromHere = parsed.copy();
1519            part.indentation = token.indentation;
1520            part.dirty = false;
1521  
1522            // If the target argument wasn't an integer, go at least
1523            // until that node.
1524            if (endTime == null && part == target) throw StopIteration;
1525  
1526            // A clean line with more than one node means we are done.
1527            // Throwing a StopIteration is the way to break out of a
1528            // MochiKit forEach loop.
1529            if ((endTime != null && time() >= endTime) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines))
1530              throw StopIteration;
1531            prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
1532            parts.next();
1533          }
1534          else {
1535            if (!isSpan(part))
1536              throw "Parser out of sync. Expected SPAN.";
1537            if (part.dirty)
1538              lineDirty = true;
1539            lineNodes++;
1540  
1541            // If the part matches the token, we can leave it alone.
1542            if (correctPart(token, part)){
1543              part.dirty = false;
1544              parts.next();
1545            }
1546            // Otherwise, we have to fix it.
1547            else {
1548              lineDirty = true;
1549              // Insert the correct part.
1550              var newPart = tokenPart(token);
1551              container.insertBefore(newPart, part);
1552              if (active) active(newPart, token, self);
1553              var tokensize = token.value.length;
1554              var offset = 0;
1555              // Eat up parts until the text for this token has been
1556              // removed, adjusting the stored selection info (see
1557              // select.js) in the process.
1558              while (tokensize > 0) {
1559                part = parts.get();
1560                var partsize = part.currentText.length;
1561                select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
1562                if (partsize > tokensize){
1563                  shortenPart(part, tokensize);
1564                  tokensize = 0;
1565                }
1566                else {
1567                  tokensize -= partsize;
1568                  offset += partsize;
1569                  parts.remove();
1570                }
1571              }
1572            }
1573          }
1574        });
1575        maybeTouch(from);
1576        webkitLastLineHack(this.container);
1577  
1578        // The function returns some status information that is used by
1579        // hightlightDirty to determine whether and where it has to
1580        // continue.
1581        return {node: parts.getNonEmpty(),
1582                dirty: lineDirty};
1583      }
1584    };
1585  
1586    return Editor;
1587  })();
1588  
1589  addEventHandler(window, "load", function() {
1590    var CodeMirror = window.frameElement.CodeMirror;
1591    var e = CodeMirror.editor = new Editor(CodeMirror.options);
1592    this.parent.setTimeout(method(CodeMirror, "init"), 0);
1593  });


Generated: Tue May 19 15:55:14 2020 Cross-referenced by PHPXref 0.7.1