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
 
   7 // Make sure a string does not contain two consecutive 'collapseable'
 
   8 // whitespace characters.
 
   9 function makeWhiteSpace(n) {
 
  10   var buffer = [], nb = true;
 
  12     buffer.push((nb || n == 1) ? nbsp : " ");
 
  15   return buffer.join("");
 
  18 // Create a set of white-space characters that will not be collapsed
 
  19 // by the browser, but will not break text-wrapping either.
 
  20 function fixSpaces(string) {
 
  21   if (string.charAt(0) == " ") string = nbsp + string.slice(1);
 
  22   return string.replace(/\t/g, function(){return makeWhiteSpace(indentUnit);})
 
  23     .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
 
  26 function cleanText(text) {
 
  27   return text.replace(/\u00a0/g, " ").replace(/\u200b/g, "");
 
  30 // Create a SPAN node with the expected properties for document part
 
  32 function makePartSpan(value, doc) {
 
  34   if (value.nodeType == 3) text = value.nodeValue;
 
  35   else value = doc.createTextNode(text);
 
  37   var span = doc.createElement("SPAN");
 
  39   span.appendChild(value);
 
  40   span.currentText = text;
 
  44 // On webkit, when the last BR of the document does not have text
 
  45 // behind it, the cursor can not be put on the line after it. This
 
  46 // makes pressing enter at the end of the document occasionally do
 
  47 // nothing (or at least seem to do nothing). To work around it, this
 
  48 // function makes sure the document ends with a span containing a
 
  49 // zero-width space character. The traverseDOM iterator filters such
 
  50 // character out again, so that the parsers won't see them. This
 
  51 // function is called from a few strategic places to make sure the
 
  52 // zwsp is restored after the highlighting process eats it.
 
  53 var webkitLastLineHack = webkit ?
 
  55     var last = container.lastChild;
 
  56     if (!last || !last.isPart || last.textContent != "\u200b")
 
  57       container.appendChild(makePartSpan("\u200b", container.ownerDocument));
 
  60 var Editor = (function(){
 
  61   // The HTML elements whose content should be suffixed by a newline
 
  62   // when converting them to flat text.
 
  63   var newlineElements = {"P": true, "DIV": true, "LI": true};
 
  65   function asEditorLines(string) {
 
  66     var tab = makeWhiteSpace(indentUnit);
 
  67     return map(string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n").split("\n"), fixSpaces);
 
  70   // Helper function for traverseDOM. Flattens an arbitrary DOM node
 
  71   // into an array of textnodes and <br> tags.
 
  72   function simplifyDOM(root, atEnd) {
 
  73     var doc = root.ownerDocument;
 
  77     function simplifyNode(node, top) {
 
  78       if (node.nodeType == 3) {
 
  79         var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " "));
 
  80         if (text.length) leaving = false;
 
  83       else if (node.nodeName == "BR" && node.childNodes.length == 0) {
 
  88         forEach(node.childNodes, simplifyNode);
 
  89         if (!leaving && newlineElements.hasOwnProperty(node.nodeName)) {
 
  92             result.push(doc.createElement("BR"));
 
  97     simplifyNode(root, true);
 
 101   // Creates a MochiKit-style iterator that goes over a series of DOM
 
 102   // nodes. The values it yields are strings, the textual content of
 
 103   // the nodes. It makes sure that all nodes up to and including the
 
 104   // one whose text is being yielded have been 'normalized' to be just
 
 105   // <span> and <br> elements.
 
 106   // See the story.html file for some short remarks about the use of
 
 107   // continuation-passing style in this iterator.
 
 108   function traverseDOM(start){
 
 109     function yield(value, c){cc = c; return value;}
 
 110     function push(fun, arg, c){return function(){return fun(arg, c);};}
 
 111     function stop(){cc = stop; throw StopIteration;};
 
 112     var cc = push(scanNode, start, stop);
 
 113     var owner = start.ownerDocument;
 
 116     // Create a function that can be used to insert nodes after the
 
 117     // one given as argument.
 
 118     function pointAt(node){
 
 119       var parent = node.parentNode;
 
 120       var next = node.nextSibling;
 
 121       return function(newnode) {
 
 122         parent.insertBefore(newnode, next);
 
 127     // Insert a normalized node at the current point. If it is a text
 
 128     // node, wrap it in a <span>, and give that span a currentText
 
 129     // property -- this is used to cache the nodeValue, because
 
 130     // directly accessing nodeValue is horribly slow on some browsers.
 
 131     // The dirty property is used by the highlighter to determine
 
 132     // which parts of the document have to be re-highlighted.
 
 133     function insertPart(part){
 
 135       if (part.nodeType == 3) {
 
 136         select.snapshotChanged();
 
 137         part = makePartSpan(part, owner);
 
 138         text = part.currentText;
 
 141       nodeQueue.push(part);
 
 146     // Extract the text and newlines from a DOM node, insert them into
 
 147     // the document, and yield the textual content. Used to replace
 
 148     // non-normalized nodes.
 
 149     function writeNode(node, c, end) {
 
 151       forEach(simplifyDOM(node, end), function(part) {
 
 152         toYield.push(insertPart(part));
 
 154       return yield(toYield.join(""), c);
 
 157     // Check whether a node is a normalized <span> element.
 
 158     function partNode(node){
 
 159       if (node.isPart && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
 
 160         node.currentText = node.firstChild.nodeValue;
 
 161         return !/[\n\t\r]/.test(node.currentText);
 
 166     // Handle a node. Add its successor to the continuation if there
 
 167     // is one, find out whether the node is normalized. If it is,
 
 168     // yield its content, otherwise, normalize it (writeNode will take
 
 169     // care of yielding).
 
 170     function scanNode(node, c){
 
 171       if (node.nextSibling)
 
 172         c = push(scanNode, node.nextSibling, c);
 
 175         nodeQueue.push(node);
 
 176         return yield(node.currentText, c);
 
 178       else if (node.nodeName == "BR") {
 
 179         nodeQueue.push(node);
 
 180         return yield("\n", c);
 
 183         var end = !node.nextSibling;
 
 184         point = pointAt(node);
 
 186         return writeNode(node, c, end);
 
 190     // MochiKit iterators are objects with a next function that
 
 191     // returns the next value or throws StopIteration when there are
 
 193     return {next: function(){return cc();}, nodes: nodeQueue};
 
 196   // Determine the text size of a processed node.
 
 197   function nodeSize(node) {
 
 198     if (node.nodeName == "BR")
 
 201       return node.currentText.length;
 
 204   // Search backwards through the top-level nodes until the next BR or
 
 205   // the start of the frame.
 
 206   function startOfLine(node) {
 
 207     while (node && node.nodeName != "BR") node = node.previousSibling;
 
 210   function endOfLine(node, container) {
 
 211     if (!node) node = container.firstChild;
 
 212     else if (node.nodeName == "BR") node = node.nextSibling;
 
 214     while (node && node.nodeName != "BR") node = node.nextSibling;
 
 218   function time() {return new Date().getTime();}
 
 220   // Client interface for searching the content of the editor. Create
 
 221   // these by calling CodeMirror.getSearchCursor. To use, call
 
 222   // findNext on the resulting object -- this returns a boolean
 
 223   // indicating whether anything was found, and can be called again to
 
 224   // skip to the next find. Use the select and replace methods to
 
 225   // actually do something with the found locations.
 
 226   function SearchCursor(editor, string, fromCursor) {
 
 227     this.editor = editor;
 
 228     this.history = editor.history;
 
 229     this.history.commit();
 
 231     // Are we currently at an occurrence of the search string?
 
 232     this.atOccurrence = false;
 
 233     // The object stores a set of nodes coming after its current
 
 234     // position, so that when the current point is taken out of the
 
 235     // DOM tree, we can still try to continue.
 
 236     this.fallbackSize = 15;
 
 238     // Start from the cursor when specified and a cursor can be found.
 
 239     if (fromCursor && (cursor = select.cursorPos(this.editor.container))) {
 
 240       this.line = cursor.node;
 
 241       this.offset = cursor.offset;
 
 247     this.valid = !!string;
 
 249     // Create a matcher function based on the kind of string we have.
 
 250     var target = string.split("\n"), self = this;
 
 251     this.matches = (target.length == 1) ?
 
 252       // For one-line strings, searching can be done simply by calling
 
 253       // indexOf on the current line.
 
 255         var match = cleanText(self.history.textAfter(self.line).slice(self.offset)).indexOf(string);
 
 257           return {from: {node: self.line, offset: self.offset + match},
 
 258                   to: {node: self.line, offset: self.offset + match + string.length}};
 
 260       // Multi-line strings require internal iteration over lines, and
 
 261       // some clunky checks to make sure the first match ends at the
 
 262       // end of the line and the last match starts at the start.
 
 264         var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
 
 265         var match = firstLine.lastIndexOf(target[0]);
 
 266         if (match == -1 || match != firstLine.length - target[0].length)
 
 268         var startOffset = self.offset + match;
 
 270         var line = self.history.nodeAfter(self.line);
 
 271         for (var i = 1; i < target.length - 1; i++) {
 
 272           if (cleanText(self.history.textAfter(line)) != target[i])
 
 274           line = self.history.nodeAfter(line);
 
 277         if (cleanText(self.history.textAfter(line)).indexOf(target[target.length - 1]) != 0)
 
 280         return {from: {node: self.line, offset: startOffset},
 
 281                 to: {node: line, offset: target[target.length - 1].length}};
 
 285   SearchCursor.prototype = {
 
 286     findNext: function() {
 
 287       if (!this.valid) return false;
 
 288       this.atOccurrence = false;
 
 291       // Go back to the start of the document if the current line is
 
 292       // no longer in the DOM tree.
 
 293       if (this.line && !this.line.parentNode) {
 
 298       // Set the cursor's position one character after the given
 
 300       function saveAfter(pos) {
 
 301         if (self.history.textAfter(pos.node).length > pos.offset) {
 
 302           self.line = pos.node;
 
 303           self.offset = pos.offset + 1;
 
 306           self.line = self.history.nodeAfter(pos.node);
 
 312         var match = this.matches();
 
 313         // Found the search string.
 
 315           this.atOccurrence = match;
 
 316           saveAfter(match.from);
 
 319         this.line = this.history.nodeAfter(this.line);
 
 330       if (this.atOccurrence) {
 
 331         select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
 
 332         select.scrollToCursor(this.editor.container);
 
 336     replace: function(string) {
 
 337       if (this.atOccurrence) {
 
 338         var end = this.editor.replaceRange(this.atOccurrence.from, this.atOccurrence.to, string);
 
 339         this.line = end.node;
 
 340         this.offset = end.offset;
 
 341         this.atOccurrence = false;
 
 346   // The Editor object is the main inside-the-iframe interface.
 
 347   function Editor(options) {
 
 348     this.options = options;
 
 349     window.indentUnit = options.indentUnit;
 
 350     this.parent = parent;
 
 352     var container = this.container = this.doc.body;
 
 354     this.history = new History(container, options.undoDepth, options.undoDelay,
 
 355                                this, options.onChange);
 
 359       throw "No parser loaded.";
 
 360     if (options.parserConfig && Editor.Parser.configure)
 
 361       Editor.Parser.configure(options.parserConfig);
 
 363     if (!options.readOnly)
 
 364       select.setCursorPos(container, {node: null, offset: 0});
 
 368       this.importCode(options.content);
 
 369     else // FF acts weird when the editable document is completely empty
 
 370       container.appendChild(this.doc.createElement("BR"));
 
 372     if (!options.readOnly) {
 
 373       if (options.continuousScanning !== false) {
 
 374         this.scanner = this.documentScanner(options.passTime);
 
 375         this.delayScanning();
 
 378       function setEditable() {
 
 379         // In IE, designMode frames can not run any scripts, so we use
 
 380         // contentEditable instead.
 
 381         if (document.body.contentEditable != undefined && internetExplorer)
 
 382           document.body.contentEditable = "true";
 
 384           document.designMode = "on";
 
 386         document.documentElement.style.borderWidth = "0";
 
 387         if (!options.textWrapping)
 
 388           container.style.whiteSpace = "nowrap";
 
 391       // If setting the frame editable fails, try again when the user
 
 392       // focus it (happens when the frame is not visible on
 
 393       // initialisation, in Firefox).
 
 398         var focusEvent = addEventHandler(document, "focus", function() {
 
 404       addEventHandler(document, "keydown", method(this, "keyDown"));
 
 405       addEventHandler(document, "keypress", method(this, "keyPress"));
 
 406       addEventHandler(document, "keyup", method(this, "keyUp"));
 
 408       function cursorActivity() {self.cursorActivity(false);}
 
 409       addEventHandler(document.body, "mouseup", cursorActivity);
 
 410       addEventHandler(document.body, "cut", cursorActivity);
 
 412       addEventHandler(document.body, "paste", function(event) {
 
 416           var clipboardData = event.clipboardData || window.clipboardData;
 
 417           if (clipboardData) text = clipboardData.getData('Text');
 
 421           self.replaceSelection(text);
 
 426       addEventHandler(document.body, "beforepaste", method(this, "reroutePasteEvent"));
 
 428       if (this.options.autoMatchParens)
 
 429         addEventHandler(document.body, "click", method(this, "scheduleParenBlink"));
 
 431     else if (!options.textWrapping) {
 
 432       container.style.whiteSpace = "nowrap";
 
 436   function isSafeKey(code) {
 
 437     return (code >= 16 && code <= 18) || // shift, control, alt
 
 438            (code >= 33 && code <= 40); // arrows, home, end
 
 442     // Import a piece of code into the editor.
 
 443     importCode: function(code) {
 
 444       this.history.push(null, null, asEditorLines(code));
 
 445       this.history.reset();
 
 448     // Extract the code from the editor.
 
 449     getCode: function() {
 
 450       if (!this.container.firstChild)
 
 454       select.markSelection(this.win);
 
 455       forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
 
 456       webkitLastLineHack(this.container);
 
 457       select.selectMarked();
 
 458       return cleanText(accum.join(""));
 
 461     checkLine: function(node) {
 
 462       if (node === false || !(node == null || node.parentNode == this.container))
 
 463         throw parent.CodeMirror.InvalidLineHandle;
 
 466     cursorPosition: function(start) {
 
 467       if (start == null) start = true;
 
 468       var pos = select.cursorPos(this.container, start);
 
 469       if (pos) return {line: pos.node, character: pos.offset};
 
 470       else return {line: null, character: 0};
 
 473     firstLine: function() {
 
 477     lastLine: function() {
 
 478       if (this.container.lastChild) return startOfLine(this.container.lastChild);
 
 482     nextLine: function(line) {
 
 483       this.checkLine(line);
 
 484       var end = endOfLine(line, this.container);
 
 488     prevLine: function(line) {
 
 489       this.checkLine(line);
 
 490       if (line == null) return false;
 
 491       return startOfLine(line.previousSibling);
 
 494     selectLines: function(startLine, startOffset, endLine, endOffset) {
 
 495       this.checkLine(startLine);
 
 496       var start = {node: startLine, offset: startOffset}, end = null;
 
 497       if (endOffset !== undefined) {
 
 498         this.checkLine(endLine);
 
 499         end = {node: endLine, offset: endOffset};
 
 501       select.setCursorPos(this.container, start, end);
 
 502       select.scrollToCursor(this.container);
 
 505     lineContent: function(line) {
 
 506       this.checkLine(line);
 
 508       for (line = line ? line.nextSibling : this.container.firstChild;
 
 509            line && line.nodeName != "BR"; line = line.nextSibling)
 
 510         accum.push(nodeText(line));
 
 511       return cleanText(accum.join(""));
 
 514     setLineContent: function(line, content) {
 
 515       this.history.commit();
 
 516       this.replaceRange({node: line, offset: 0},
 
 517                         {node: line, offset: this.history.textAfter(line).length},
 
 519       this.addDirtyNode(line);
 
 520       this.scheduleHighlight();
 
 523     insertIntoLine: function(line, position, content) {
 
 525       if (position == "end") {
 
 526         before = endOfLine(line, this.container);
 
 529         for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
 
 534           var text = (cur.innerText || cur.textContent || cur.nodeValue || "");
 
 535           if (text.length > position) {
 
 536             before = cur.nextSibling;
 
 537             content = text.slice(0, position) + content + text.slice(position);
 
 541           position -= text.length;
 
 545       var lines = asEditorLines(content), doc = this.container.ownerDocument;
 
 546       for (var i = 0; i < lines.length; i++) {
 
 547         if (i > 0) this.container.insertBefore(doc.createElement("BR"), before);
 
 548         this.container.insertBefore(makePartSpan(lines[i], doc), before);
 
 550       this.addDirtyNode(line);
 
 551       this.scheduleHighlight();
 
 554     // Retrieve the selected text.
 
 555     selectedText: function() {
 
 556       var h = this.history;
 
 559       var start = select.cursorPos(this.container, true),
 
 560           end = select.cursorPos(this.container, false);
 
 561       if (!start || !end) return "";
 
 563       if (start.node == end.node)
 
 564         return h.textAfter(start.node).slice(start.offset, end.offset);
 
 566       var text = [h.textAfter(start.node).slice(start.offset)];
 
 567       for (var pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos))
 
 568         text.push(h.textAfter(pos));
 
 569       text.push(h.textAfter(end.node).slice(0, end.offset));
 
 570       return cleanText(text.join("\n"));
 
 573     // Replace the selection with another p
 
 575     replaceSelection: function(text) {
 
 576       this.history.commit();
 
 578       var start = select.cursorPos(this.container, true),
 
 579           end = select.cursorPos(this.container, false);
 
 580       if (!start || !end) return;
 
 582       end = this.replaceRange(start, end, text);
 
 583       // select.setCursorPos(this.container, end);
 
 584       webkitLastLineHack(this.container);
 
 587     reroutePasteEvent: function() {
 
 588       if (this.capturingPaste || window.opera) return;
 
 589       this.capturingPaste = true;
 
 590       var te = parent.document.createElement("TEXTAREA");
 
 591       te.style.position = "absolute";
 
 592       te.style.left = "-500px";
 
 593       te.style.width = "10px";
 
 594       te.style.top = nodeTop(frameElement) + "px";
 
 595       parent.document.body.appendChild(te);
 
 600       this.parent.setTimeout(function() {
 
 601         self.capturingPaste = false;
 
 603         if (self.selectionSnapshot) // IE hack
 
 604           self.win.select.selectCoords(self.win, self.selectionSnapshot);
 
 606         if (text) self.replaceSelection(text);
 
 611     replaceRange: function(from, to, text) {
 
 612       var lines = asEditorLines(text);
 
 613       lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
 
 614       var lastLine = lines[lines.length - 1];
 
 615       lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
 
 616       var end = this.history.nodeAfter(to.node);
 
 617       this.history.push(from.node, end, lines);
 
 618       return {node: this.history.nodeBefore(end),
 
 619               offset: lastLine.length};
 
 622     getSearchCursor: function(string, fromCursor) {
 
 623       return new SearchCursor(this, string, fromCursor);
 
 626     // Re-indent the whole buffer
 
 627     reindent: function() {
 
 628       if (this.container.firstChild)
 
 629         this.indentRegion(null, this.container.lastChild);
 
 632     reindentSelection: function(direction) {
 
 633       if (!select.somethingSelected(this.win)) {
 
 634         this.indentAtCursor(direction);
 
 637         var start = select.selectionTopNode(this.container, true),
 
 638             end = select.selectionTopNode(this.container, false);
 
 639         if (start === false || end === false) return;
 
 640         this.indentRegion(start, end, direction);
 
 644     grabKeys: function(eventHandler, filter) {
 
 645       this.frozen = eventHandler;
 
 646       this.keyFilter = filter;
 
 648     ungrabKeys: function() {
 
 649       this.frozen = "leave";
 
 650       this.keyFilter = null;
 
 653     setParser: function(name) {
 
 654       Editor.Parser = window[name];
 
 655       if (this.container.firstChild) {
 
 656         forEach(this.container.childNodes, function(n) {
 
 657           if (n.nodeType != 3) n.dirty = true;
 
 659         this.addDirtyNode(this.firstChild);
 
 660         this.scheduleHighlight();
 
 664     // Intercept enter and tab, and assign their new functions.
 
 665     keyDown: function(event) {
 
 666       if (this.frozen == "leave") this.frozen = null;
 
 667       if (this.frozen && (!this.keyFilter || this.keyFilter(event))) {
 
 673       var code = event.keyCode;
 
 674       // Don't scan when the user is typing.
 
 675       this.delayScanning();
 
 676       // Schedule a paren-highlight event, if configured.
 
 677       if (this.options.autoMatchParens)
 
 678         this.scheduleParenBlink();
 
 680       // The various checks for !altKey are there because AltGr sets both
 
 681       // ctrlKey and altKey to true, and should not be recognised as
 
 683       if (code == 13) { // enter
 
 684         if (event.ctrlKey && !event.altKey) {
 
 685           this.reparseBuffer();
 
 688           select.insertNewlineAtCursor(this.win);
 
 689           this.indentAtCursor();
 
 690           select.scrollToCursor(this.container);
 
 694       else if (code == 9 && this.options.tabMode != "default") { // tab
 
 695         this.handleTab(!event.ctrlKey && !event.shiftKey);
 
 698       else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
 
 699         this.handleTab(true);
 
 702       else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home
 
 706       else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
 
 707         this.blinkParens(event.shiftKey);
 
 710       else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right
 
 711         var cursor = select.selectionTopNode(this.container);
 
 712         if (cursor === false || !this.container.firstChild) return;
 
 714         if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
 
 716           var end = endOfLine(cursor, this.container);
 
 717           select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
 
 721       else if ((event.ctrlKey || event.metaKey) && !event.altKey) {
 
 722         if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y
 
 723           select.scrollToNode(this.history.redo());
 
 726         else if (code == 90 || (safari && code == 8)) { // Z, backspace
 
 727           select.scrollToNode(this.history.undo());
 
 730         else if (code == 83 && this.options.saveFunction) { // S
 
 731           this.options.saveFunction();
 
 737     // Check for characters that should re-indent the current line,
 
 738     // and prevent Opera from handling enter and tab anyway.
 
 739     keyPress: function(event) {
 
 740       var electric = Editor.Parser.electricChars, self = this;
 
 741       // Hack for Opera, and Firefox on OS X, in which stopping a
 
 742       // keydown event does not prevent the associated keypress event
 
 743       // from happening, so we have to cancel enter and tab again
 
 745       if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode))) ||
 
 746           event.code == 13 || (event.code == 9 && this.options.tabMode != "default") ||
 
 747           (event.keyCode == 32 && event.shiftKey && this.options.tabMode == "default"))
 
 749       else if (electric && electric.indexOf(event.character) != -1)
 
 750         this.parent.setTimeout(function(){self.indentAtCursor(null);}, 0);
 
 751       else if ((event.character == "v" || event.character == "V")
 
 752                && (event.ctrlKey || event.metaKey) && !event.altKey) // ctrl-V
 
 753         this.reroutePasteEvent();
 
 756     // Mark the node at the cursor dirty when a non-safe key is
 
 758     keyUp: function(event) {
 
 759       this.cursorActivity(isSafeKey(event.keyCode));
 
 762     // Indent the line following a given <br>, or null for the first
 
 763     // line. If given a <br> element, this must have been highlighted
 
 764     // so that it has an indentation method. Returns the whitespace
 
 765     // element that has been modified or created (if any).
 
 766     indentLineAfter: function(start, direction) {
 
 767       // whiteSpace is the whitespace span at the start of the line,
 
 768       // or null if there is no such node.
 
 769       var whiteSpace = start ? start.nextSibling : this.container.firstChild;
 
 770       if (whiteSpace && !hasClass(whiteSpace, "whitespace"))
 
 773       // Sometimes the start of the line can influence the correct
 
 774       // indentation, so we retrieve it.
 
 775       var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
 
 776       var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
 
 778       // Ask the lexical context for the correct indentation, and
 
 779       // compute how much this differs from the current indentation.
 
 780       var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
 
 781       if (direction != null && this.options.tabMode == "shift")
 
 782         newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
 
 784         newIndent = start.indentation(nextChars, curIndent, direction);
 
 785       else if (Editor.Parser.firstIndentation)
 
 786         newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
 
 787       var indentDiff = newIndent - curIndent;
 
 789       // If there is too much, this is just a matter of shrinking a span.
 
 790       if (indentDiff < 0) {
 
 791         if (newIndent == 0) {
 
 792           if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0);
 
 793           removeElement(whiteSpace);
 
 797           select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
 
 798           whiteSpace.currentText = makeWhiteSpace(newIndent);
 
 799           whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
 
 803       else if (indentDiff > 0) {
 
 804         // If there is whitespace, we grow it.
 
 806           whiteSpace.currentText = makeWhiteSpace(newIndent);
 
 807           whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
 
 809         // Otherwise, we have to add a new whitespace node.
 
 811           whiteSpace = makePartSpan(makeWhiteSpace(newIndent), this.doc);
 
 812           whiteSpace.className = "whitespace";
 
 813           if (start) insertAfter(whiteSpace, start);
 
 814           else this.container.insertBefore(whiteSpace, this.container.firstChild);
 
 816         if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
 
 818       if (indentDiff != 0) this.addDirtyNode(start);
 
 822     // Re-highlight the selected part of the document.
 
 823     highlightAtCursor: function() {
 
 824       var pos = select.selectionTopNode(this.container, true);
 
 825       var to = select.selectionTopNode(this.container, false);
 
 826       if (pos === false || to === false) return;
 
 828       select.markSelection(this.win);
 
 829       if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
 
 831       select.selectMarked();
 
 835     // When tab is pressed with text selected, the whole selection is
 
 836     // re-indented, when nothing is selected, the line with the cursor
 
 838     handleTab: function(direction) {
 
 839       if (this.options.tabMode == "spaces")
 
 840         select.insertTabAtCursor(this.win);
 
 842         this.reindentSelection(direction);
 
 846       var cur = select.selectionTopNode(this.container, true), start = cur;
 
 847       if (cur === false || !(!cur || cur.isPart || cur.nodeName == "BR") || !this.container.firstChild)
 
 850       while (cur && cur.nodeName != "BR") cur = cur.previousSibling;
 
 851       var next = cur ? cur.nextSibling : this.container.firstChild;
 
 852       if (next && next != start && next.isPart && hasClass(next, "whitespace"))
 
 853         select.focusAfterNode(next, this.container);
 
 855         select.focusAfterNode(cur, this.container);
 
 857       select.scrollToCursor(this.container);
 
 861     // Delay (or initiate) the next paren blink event.
 
 862     scheduleParenBlink: function() {
 
 863       if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
 
 865       this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300);
 
 868     // Take the token before the cursor. If it contains a character in
 
 869     // '()[]{}', search for the matching paren/brace/bracket, and
 
 870     // highlight them in green for a moment, or red if no proper match
 
 872     blinkParens: function(jump) {
 
 873       if (!window.select) return;
 
 874       // Clear the event property.
 
 875       if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
 
 876       this.parenEvent = null;
 
 878       // Extract a 'paren' from a piece of text.
 
 879       function paren(node) {
 
 880         if (node.currentText) {
 
 881           var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
 
 882           return match && match[1];
 
 885       // Determine the direction a paren is facing.
 
 886       function forward(ch) {
 
 887         return /[\(\[\{]/.test(ch);
 
 890       var ch, self = this, cursor = select.selectionTopNode(this.container, true);
 
 891       if (!cursor || !this.highlightAtCursor()) return;
 
 892       cursor = select.selectionTopNode(this.container, true);
 
 893       if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor)))))
 
 895       // We only look for tokens with the same className.
 
 896       var className = cursor.className, dir = forward(ch), match = matching[ch];
 
 898       // Since parts of the document might not have been properly
 
 899       // highlighted, and it is hard to know in advance which part we
 
 900       // have to scan, we just try, and when we find dirty nodes we
 
 901       // abort, parse them, and re-try.
 
 902       function tryFindMatch() {
 
 903         var stack = [], ch, ok = true;;
 
 904         for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
 
 905           if (runner.className == className && runner.nodeName == "SPAN" && (ch = paren(runner))) {
 
 906             if (forward(ch) == dir)
 
 908             else if (!stack.length)
 
 910             else if (stack.pop() != matching[ch])
 
 912             if (!stack.length) break;
 
 914           else if (runner.dirty || runner.nodeName != "SPAN" && runner.nodeName != "BR") {
 
 915             return {node: runner, status: "dirty"};
 
 918         return {node: runner, status: runner && ok};
 
 920       // Temporarily give the relevant nodes a colour.
 
 921       function blink(node, ok) {
 
 922         node.style.fontWeight = "bold";
 
 923         node.style.color = ok ? "#8F8" : "#F88";
 
 924         self.parent.setTimeout(function() {node.style.fontWeight = ""; node.style.color = "";}, 500);
 
 928         var found = tryFindMatch();
 
 929         if (found.status == "dirty") {
 
 930           this.highlight(found.node, endOfLine(found.node));
 
 931           // Needed because in some corner cases a highlight does not
 
 933           found.node.dirty = false;
 
 937           blink(cursor, found.status);
 
 939             blink(found.node, found.status);
 
 940             if (jump) select.focusAfterNode(found.node.previousSibling, this.container);
 
 947     // Adjust the amount of whitespace at the start of the line that
 
 948     // the cursor is on so that it is indented properly.
 
 949     indentAtCursor: function(direction) {
 
 950       if (!this.container.firstChild) return;
 
 951       // The line has to have up-to-date lexical information, so we
 
 952       // highlight it first.
 
 953       if (!this.highlightAtCursor()) return;
 
 954       var cursor = select.selectionTopNode(this.container, false);
 
 955       // If we couldn't determine the place of the cursor,
 
 956       // there's nothing to indent.
 
 957       if (cursor === false)
 
 959       var lineStart = startOfLine(cursor);
 
 960       var whiteSpace = this.indentLineAfter(lineStart, direction);
 
 961       if (cursor == lineStart && whiteSpace)
 
 963       // This means the indentation has probably messed up the cursor.
 
 964       if (cursor == whiteSpace)
 
 965         select.focusAfterNode(cursor, this.container);
 
 968     // Indent all lines whose start falls inside of the current
 
 970     indentRegion: function(start, end, direction) {
 
 971       var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling);
 
 972       if (end.nodeName != "BR") end = endOfLine(end, this.container);
 
 975         var next = endOfLine(current, this.container);
 
 976         if (current) this.highlight(before, next, true);
 
 977         this.indentLineAfter(current, direction);
 
 980       } while (current != end);
 
 981       select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
 
 984     // Find the node that the cursor is in, mark it as dirty, and make
 
 985     // sure a highlight pass is scheduled.
 
 986     cursorActivity: function(safe) {
 
 987       if (internetExplorer) {
 
 988         this.container.createTextRange().execCommand("unlink");
 
 989         this.selectionSnapshot = select.selectionCoords(this.win);
 
 992       var activity = this.options.cursorActivity;
 
 993       if (!safe || activity) {
 
 994         var cursor = select.selectionTopNode(this.container, false);
 
 995         if (cursor === false || !this.container.firstChild) return;
 
 996         cursor = cursor || this.container.firstChild;
 
 997         if (activity) activity(cursor);
 
 999           this.scheduleHighlight();
 
1000           this.addDirtyNode(cursor);
 
1005     reparseBuffer: function() {
 
1006       forEach(this.container.childNodes, function(node) {node.dirty = true;});
 
1007       if (this.container.firstChild)
 
1008         this.addDirtyNode(this.container.firstChild);
 
1011     // Add a node to the set of dirty nodes, if it isn't already in
 
1013     addDirtyNode: function(node) {
 
1014       node = node || this.container.firstChild;
 
1017       for (var i = 0; i < this.dirty.length; i++)
 
1018         if (this.dirty[i] == node) return;
 
1020       if (node.nodeType != 3)
 
1022       this.dirty.push(node);
 
1025     // Cause a highlight pass to happen in options.passDelay
 
1026     // milliseconds. Clear the existing timeout, if one exists. This
 
1027     // way, the passes do not happen while the user is typing, and
 
1028     // should as unobtrusive as possible.
 
1029     scheduleHighlight: function() {
 
1030       // Timeouts are routed through the parent window, because on
 
1031       // some browsers designMode windows do not fire timeouts.
 
1033       this.parent.clearTimeout(this.highlightTimeout);
 
1034       this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
 
1037     // Fetch one dirty node, and remove it from the dirty set.
 
1038     getDirtyNode: function() {
 
1039       while (this.dirty.length > 0) {
 
1040         var found = this.dirty.pop();
 
1041         // IE8 sometimes throws an unexplainable 'invalid argument'
 
1042         // exception for found.parentNode
 
1044           // If the node has been coloured in the meantime, or is no
 
1045           // longer in the document, it should not be returned.
 
1046           while (found && found.parentNode != this.container)
 
1047             found = found.parentNode
 
1048           if (found && (found.dirty || found.nodeType == 3))
 
1055     // Pick dirty nodes, and highlight them, until options.passTime
 
1056     // milliseconds have gone by. The highlight method will continue
 
1057     // to next lines as long as it finds dirty nodes. It returns
 
1058     // information about the place where it stopped. If there are
 
1059     // dirty nodes left after this function has spent all its lines,
 
1060     // it shedules another highlight to finish the job.
 
1061     highlightDirty: function(force) {
 
1062       // Prevent FF from raising an error when it is firing timeouts
 
1063       // on a page that's no longer loaded.
 
1064       if (!window.select) return;
 
1066       if (!this.options.readOnly) select.markSelection(this.win);
 
1067       var start, endTime = force ? null : time() + this.options.passTime;
 
1068       while ((time() < endTime || force) && (start = this.getDirtyNode())) {
 
1069         var result = this.highlight(start, endTime);
 
1070         if (result && result.node && result.dirty)
 
1071           this.addDirtyNode(result.node);
 
1073       if (!this.options.readOnly) select.selectMarked();
 
1074       if (start) this.scheduleHighlight();
 
1075       return this.dirty.length == 0;
 
1078     // Creates a function that, when called through a timeout, will
 
1079     // continuously re-parse the document.
 
1080     documentScanner: function(passTime) {
 
1081       var self = this, pos = null;
 
1083         // FF timeout weirdness workaround.
 
1084         if (!window.select) return;
 
1085         // If the current node is no longer in the document... oh
 
1086         // well, we start over.
 
1087         if (pos && pos.parentNode != self.container)
 
1089         select.markSelection(self.win);
 
1090         var result = self.highlight(pos, time() + passTime, true);
 
1091         select.selectMarked();
 
1092         var newPos = result ? (result.node && result.node.nextSibling) : null;
 
1093         pos = (pos == newPos) ? null : newPos;
 
1094         self.delayScanning();
 
1098     // Starts the continuous scanning process for this document after
 
1099     // a given interval.
 
1100     delayScanning: function() {
 
1102         this.parent.clearTimeout(this.documentScan);
 
1103         this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
 
1107     // The function that does the actual highlighting/colouring (with
 
1108     // help from the parser and the DOM normalizer). Its interface is
 
1109     // rather overcomplicated, because it is used in different
 
1110     // situations: ensuring that a certain line is highlighted, or
 
1111     // highlighting up to X milliseconds starting from a certain
 
1112     // point. The 'from' argument gives the node at which it should
 
1113     // start. If this is null, it will start at the beginning of the
 
1114     // document. When a timestamp is given with the 'target' argument,
 
1115     // it will stop highlighting at that time. If this argument holds
 
1116     // a DOM node, it will highlight until it reaches that node. If at
 
1117     // any time it comes across two 'clean' lines (no dirty nodes), it
 
1118     // will stop, except when 'cleanLines' is true. maxBacktrack is
 
1119     // the maximum number of lines to backtrack to find an existing
 
1120     // parser instance. This is used to give up in situations where a
 
1121     // highlight would take too long and freeze the browser interface.
 
1122     highlight: function(from, target, cleanLines, maxBacktrack){
 
1123       var container = this.container, self = this, active = this.options.activeTokens;
 
1124       var endTime = (typeof target == "number" ? target : null);
 
1126       if (!container.firstChild)
 
1128       // Backtrack to the first node before from that has a partial
 
1130       while (from && (!from.parserFromHere || from.dirty)) {
 
1131         if (maxBacktrack != null && from.nodeName == "BR" && (--maxBacktrack) < 0)
 
1133         from = from.previousSibling;
 
1135       // If we are at the end of the document, do nothing.
 
1136       if (from && !from.nextSibling)
 
1139       // Check whether a part (<span> node) and the corresponding token
 
1141       function correctPart(token, part){
 
1142         return !part.reduced && part.currentText == token.value && part.className == token.style;
 
1144       // Shorten the text associated with a part by chopping off
 
1145       // characters from the front. Note that only the currentText
 
1146       // property gets changed. For efficiency reasons, we leave the
 
1147       // nodeValue alone -- we set the reduced flag to indicate that
 
1148       // this part must be replaced.
 
1149       function shortenPart(part, minus){
 
1150         part.currentText = part.currentText.substring(minus);
 
1151         part.reduced = true;
 
1153       // Create a part corresponding to a given token.
 
1154       function tokenPart(token){
 
1155         var part = makePartSpan(token.value, self.doc);
 
1156         part.className = token.style;
 
1160       function maybeTouch(node) {
 
1162           var old = node.oldNextSibling;
 
1163           if (lineDirty || old === undefined || node.nextSibling != old)
 
1164             self.history.touch(node);
 
1165           node.oldNextSibling = node.nextSibling;
 
1168           var old = self.container.oldFirstChild;
 
1169           if (lineDirty || old === undefined || self.container.firstChild != old)
 
1170             self.history.touch(null);
 
1171           self.container.oldFirstChild = self.container.firstChild;
 
1175       // Get the token stream. If from is null, we start with a new
 
1176       // parser from the start of the frame, otherwise a partial parse
 
1178       var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
 
1179           stream = stringStream(traversal),
 
1180           parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
 
1182       // parts is an interface to make it possible to 'delay' fetching
 
1183       // the next DOM node until we are completely done with the one
 
1184       // before it. This is necessary because often the next node is
 
1185       // not yet available when we want to proceed past the current
 
1189         // Fetch current node.
 
1192             this.current = traversal.nodes.shift();
 
1193           return this.current;
 
1195         // Advance to the next part (do not fetch it yet).
 
1197           this.current = null;
 
1199         // Remove the current part from the DOM tree, and move to the
 
1202           container.removeChild(this.get());
 
1203           this.current = null;
 
1205         // Advance to the next part that is not empty, discarding empty
 
1207         getNonEmpty: function(){
 
1208           var part = this.get();
 
1209           // Allow empty nodes when they are alone on a line, needed
 
1210           // for the FF cursor bug workaround (see select.js,
 
1211           // insertNewlineAtCursor).
 
1212           while (part && part.nodeName == "SPAN" && part.currentText == "") {
 
1216             // Adjust selection information, if any. See select.js for details.
 
1217             select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
 
1223       var lineDirty = false, prevLineDirty = true, lineNodes = 0;
 
1225       // This forEach loops over the tokens from the parsed stream, and
 
1226       // at the same time uses the parts object to proceed through the
 
1227       // corresponding DOM nodes.
 
1228       forEach(parsed, function(token){
 
1229         var part = parts.getNonEmpty();
 
1231         if (token.value == "\n"){
 
1232           // The idea of the two streams actually staying synchronized
 
1233           // is such a long shot that we explicitly check.
 
1234           if (part.nodeName != "BR")
 
1235             throw "Parser out of sync. Expected BR.";
 
1237           if (part.dirty || !part.indentation) lineDirty = true;
 
1241           // Every <br> gets a copy of the parser state and a lexical
 
1242           // context assigned to it. The first is used to be able to
 
1243           // later resume parsing from this point, the second is used
 
1245           part.parserFromHere = parsed.copy();
 
1246           part.indentation = token.indentation;
 
1249           // If the target argument wasn't an integer, go at least
 
1251           if (endTime == null && part == target) throw StopIteration;
 
1253           // A clean line with more than one node means we are done.
 
1254           // Throwing a StopIteration is the way to break out of a
 
1255           // MochiKit forEach loop.
 
1256           if ((endTime != null && time() >= endTime) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines))
 
1257             throw StopIteration;
 
1258           prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
 
1262           if (part.nodeName != "SPAN")
 
1263             throw "Parser out of sync. Expected SPAN.";
 
1268           // If the part matches the token, we can leave it alone.
 
1269           if (correctPart(token, part)){
 
1273           // Otherwise, we have to fix it.
 
1276             // Insert the correct part.
 
1277             var newPart = tokenPart(token);
 
1278             container.insertBefore(newPart, part);
 
1279             if (active) active(newPart, token, self);
 
1280             var tokensize = token.value.length;
 
1282             // Eat up parts until the text for this token has been
 
1283             // removed, adjusting the stored selection info (see
 
1284             // select.js) in the process.
 
1285             while (tokensize > 0) {
 
1287               var partsize = part.currentText.length;
 
1288               select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
 
1289               if (partsize > tokensize){
 
1290                 shortenPart(part, tokensize);
 
1294                 tokensize -= partsize;
 
1303       webkitLastLineHack(this.container);
 
1305       // The function returns some status information that is used by
 
1306       // hightlightDirty to determine whether and where it has to
 
1308       return {node: parts.getNonEmpty(),
 
1316 addEventHandler(window, "load", function() {
 
1317   var CodeMirror = window.frameElement.CodeMirror;
 
1318   CodeMirror.editor = new Editor(CodeMirror.options);
 
1319   this.parent.setTimeout(method(CodeMirror, "init"), 0);