0f7a8af03099323daf4154919aacd4ddcea5fc15
[redakcja.git] / project / static / js / lib / codemirror / editor.js
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 // Make sure a string does not contain two consecutive 'collapseable'
8 // whitespace characters.
9 function makeWhiteSpace(n) {
10   var buffer = [], nb = true;
11   for (; n > 0; n--) {
12     buffer.push((nb || n == 1) ? nbsp : " ");
13     nb = !nb;
14   }
15   return buffer.join("");
16 }
17
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);});
24 }
25
26 function cleanText(text) {
27   return text.replace(/\u00a0/g, " ").replace(/\u200b/g, "");
28 }
29
30 // Create a SPAN node with the expected properties for document part
31 // spans.
32 function makePartSpan(value, doc) {
33   var text = value;
34   if (value.nodeType == 3) text = value.nodeValue;
35   else value = doc.createTextNode(text);
36
37   var span = doc.createElement("SPAN");
38   span.isPart = true;
39   span.appendChild(value);
40   span.currentText = text;
41   return span;
42 }
43
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 ?
54   function(container) {
55     var last = container.lastChild;
56     if (!last || !last.isPart || last.textContent != "\u200b")
57       container.appendChild(makePartSpan("\u200b", container.ownerDocument));
58   } : function() {};
59
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};
64
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);
68   }
69
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;
74     var result = [];
75     var leaving = true;
76
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;
81         result.push(node);
82       }
83       else if (node.nodeName == "BR" && node.childNodes.length == 0) {
84         leaving = true;
85         result.push(node);
86       }
87       else {
88         forEach(node.childNodes, simplifyNode);
89         if (!leaving && newlineElements.hasOwnProperty(node.nodeName)) {
90           leaving = true;
91           if (!atEnd || !top)
92             result.push(doc.createElement("BR"));
93         }
94       }
95     }
96
97     simplifyNode(root, true);
98     return result;
99   }
100
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;
114     var nodeQueue = [];
115
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);
123       };
124     }
125     var point = null;
126
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){
134       var text = "\n";
135       if (part.nodeType == 3) {
136         select.snapshotChanged();
137         part = makePartSpan(part, owner);
138         text = part.currentText;
139       }
140       part.dirty = true;
141       nodeQueue.push(part);
142       point(part);
143       return text;
144     }
145
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) {
150       var toYield = [];
151       forEach(simplifyDOM(node, end), function(part) {
152         toYield.push(insertPart(part));
153       });
154       return yield(toYield.join(""), c);
155     }
156
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);
162       }
163       return false;
164     }
165
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);
173
174       if (partNode(node)){
175         nodeQueue.push(node);
176         return yield(node.currentText, c);
177       }
178       else if (node.nodeName == "BR") {
179         nodeQueue.push(node);
180         return yield("\n", c);
181       }
182       else {
183         var end = !node.nextSibling;
184         point = pointAt(node);
185         removeElement(node);
186         return writeNode(node, c, end);
187       }
188     }
189
190     // MochiKit iterators are objects with a next function that
191     // returns the next value or throws StopIteration when there are
192     // no more values.
193     return {next: function(){return cc();}, nodes: nodeQueue};
194   }
195
196   // Determine the text size of a processed node.
197   function nodeSize(node) {
198     if (node.nodeName == "BR")
199       return 1;
200     else
201       return node.currentText.length;
202   }
203
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;
208     return node;
209   }
210   function endOfLine(node, container) {
211     if (!node) node = container.firstChild;
212     else if (node.nodeName == "BR") node = node.nextSibling;
213
214     while (node && node.nodeName != "BR") node = node.nextSibling;
215     return node;
216   }
217
218   function time() {return new Date().getTime();}
219
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();
230
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;
237     var cursor;
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;
242     }
243     else {
244       this.line = null;
245       this.offset = 0;
246     }
247     this.valid = !!string;
248
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.
254       function() {
255         var match = cleanText(self.history.textAfter(self.line).slice(self.offset)).indexOf(string);
256         if (match > -1)
257           return {from: {node: self.line, offset: self.offset + match},
258                   to: {node: self.line, offset: self.offset + match + string.length}};
259       } :
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.
263       function() {
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)
267           return false;
268         var startOffset = self.offset + match;
269
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])
273             return false;
274           line = self.history.nodeAfter(line);
275         }
276
277         if (cleanText(self.history.textAfter(line)).indexOf(target[target.length - 1]) != 0)
278           return false;
279
280         return {from: {node: self.line, offset: startOffset},
281                 to: {node: line, offset: target[target.length - 1].length}};
282       };
283   }
284
285   SearchCursor.prototype = {
286     findNext: function() {
287       if (!this.valid) return false;
288       this.atOccurrence = false;
289       var self = this;
290
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) {
294         this.line = null;
295         this.offset = 0;
296       }
297
298       // Set the cursor's position one character after the given
299       // position.
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;
304         }
305         else {
306           self.line = self.history.nodeAfter(pos.node);
307           self.offset = 0;
308         }
309       }
310
311       while (true) {
312         var match = this.matches();
313         // Found the search string.
314         if (match) {
315           this.atOccurrence = match;
316           saveAfter(match.from);
317           return true;
318         }
319         this.line = this.history.nodeAfter(this.line);
320         this.offset = 0;
321         // End of document.
322         if (!this.line) {
323           this.valid = false;
324           return false;
325         }
326       }
327     },
328
329     select: function() {
330       if (this.atOccurrence) {
331         select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
332         select.scrollToCursor(this.editor.container);
333       }
334     },
335
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;
342       }
343     }
344   };
345
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;
351     this.doc = document;
352     var container = this.container = this.doc.body;
353     this.win = window;
354     this.history = new History(container, options.undoDepth, options.undoDelay,
355                                this, options.onChange);
356     var self = this;
357
358     if (!Editor.Parser)
359       throw "No parser loaded.";
360     if (options.parserConfig && Editor.Parser.configure)
361       Editor.Parser.configure(options.parserConfig);
362
363     if (!options.readOnly)
364       select.setCursorPos(container, {node: null, offset: 0});
365
366     this.dirty = [];
367     if (options.content)
368       this.importCode(options.content);
369     else // FF acts weird when the editable document is completely empty
370       container.appendChild(this.doc.createElement("BR"));
371
372     if (!options.readOnly) {
373       if (options.continuousScanning !== false) {
374         this.scanner = this.documentScanner(options.passTime);
375         this.delayScanning();
376       }
377
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";
383         else
384           document.designMode = "on";
385
386         document.documentElement.style.borderWidth = "0";
387         if (!options.textWrapping)
388           container.style.whiteSpace = "nowrap";
389       }
390
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).
394       try {
395         setEditable();
396       }
397       catch(e) {
398         var focusEvent = addEventHandler(document, "focus", function() {
399           focusEvent();
400           setEditable();
401         }, true);
402       }
403
404       addEventHandler(document, "keydown", method(this, "keyDown"));
405       addEventHandler(document, "keypress", method(this, "keyPress"));
406       addEventHandler(document, "keyup", method(this, "keyUp"));
407
408       function cursorActivity() {self.cursorActivity(false);}
409       addEventHandler(document.body, "mouseup", cursorActivity);
410       addEventHandler(document.body, "cut", cursorActivity);
411
412       addEventHandler(document.body, "paste", function(event) {
413         cursorActivity();
414         var text = null;
415         try {
416           var clipboardData = event.clipboardData || window.clipboardData;
417           if (clipboardData) text = clipboardData.getData('Text');
418         }
419         catch(e) {}
420         if (text !== null) {
421           self.replaceSelection(text);
422           event.stop();
423         }
424       });
425
426       addEventHandler(document.body, "beforepaste", method(this, "reroutePasteEvent"));
427
428       if (this.options.autoMatchParens)
429         addEventHandler(document.body, "click", method(this, "scheduleParenBlink"));
430     }
431     else if (!options.textWrapping) {
432       container.style.whiteSpace = "nowrap";
433     }
434   }
435
436   function isSafeKey(code) {
437     return (code >= 16 && code <= 18) || // shift, control, alt
438            (code >= 33 && code <= 40); // arrows, home, end
439   }
440
441   Editor.prototype = {
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();
446     },
447
448     // Extract the code from the editor.
449     getCode: function() {
450       if (!this.container.firstChild)
451         return "";
452
453       var accum = [];
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(""));
459     },
460
461     checkLine: function(node) {
462       if (node === false || !(node == null || node.parentNode == this.container))
463         throw parent.CodeMirror.InvalidLineHandle;
464     },
465
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};
471     },
472
473     firstLine: function() {
474       return null;
475     },
476
477     lastLine: function() {
478       if (this.container.lastChild) return startOfLine(this.container.lastChild);
479       else return null;
480     },
481
482     nextLine: function(line) {
483       this.checkLine(line);
484       var end = endOfLine(line, this.container);
485       return end || false;
486     },
487
488     prevLine: function(line) {
489       this.checkLine(line);
490       if (line == null) return false;
491       return startOfLine(line.previousSibling);
492     },
493
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};
500       }
501       select.setCursorPos(this.container, start, end);
502       select.scrollToCursor(this.container);
503     },
504
505     lineContent: function(line) {
506       this.checkLine(line);
507       var accum = [];
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(""));
512     },
513
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},
518                         content);
519       this.addDirtyNode(line);
520       this.scheduleHighlight();
521     },
522
523     insertIntoLine: function(line, position, content) {
524       var before = null;
525       if (position == "end") {
526         before = endOfLine(line, this.container);
527       }
528       else {
529         for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
530           if (position == 0) {
531             before = cur;
532             break;
533           }
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);
538             removeElement(cur);
539             break;
540           }
541           position -= text.length;
542         }
543       }
544
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);
549       }
550       this.addDirtyNode(line);
551       this.scheduleHighlight();
552     },
553
554     // Retrieve the selected text.
555     selectedText: function() {
556       var h = this.history;
557       h.commit();
558
559       var start = select.cursorPos(this.container, true),
560           end = select.cursorPos(this.container, false);
561       if (!start || !end) return "";
562
563       if (start.node == end.node)
564         return h.textAfter(start.node).slice(start.offset, end.offset);
565
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"));
571     },
572
573     // Replace the selection with another piece of text.
574     replaceSelection: function(text) {
575       this.history.commit();
576
577       var start = select.cursorPos(this.container, true),
578           end = select.cursorPos(this.container, false);
579       if (!start || !end) return;
580
581       end = this.replaceRange(start, end, text);
582       select.setCursorPos(this.container, end);
583       webkitLastLineHack(this.container);
584     },
585
586     reroutePasteEvent: function() {
587       if (this.capturingPaste || window.opera) return;
588       this.capturingPaste = true;
589       var te = parent.document.createElement("TEXTAREA");
590       te.style.position = "absolute";
591       te.style.left = "-500px";
592       te.style.width = "10px";
593       te.style.top = nodeTop(frameElement) + "px";
594       parent.document.body.appendChild(te);
595       parent.focus();
596       te.focus();
597
598       var self = this;
599       this.parent.setTimeout(function() {
600         self.capturingPaste = false;
601         self.win.focus();
602         if (self.selectionSnapshot) // IE hack
603           self.win.select.selectCoords(self.win, self.selectionSnapshot);
604         var text = te.value;
605         if (text) self.replaceSelection(text);
606         removeElement(te);
607       }, 10);
608     },
609
610     replaceRange: function(from, to, text) {
611       var lines = asEditorLines(text);
612       lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
613       var lastLine = lines[lines.length - 1];
614       lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
615       var end = this.history.nodeAfter(to.node);
616       this.history.push(from.node, end, lines);
617       return {node: this.history.nodeBefore(end),
618               offset: lastLine.length};
619     },
620
621     getSearchCursor: function(string, fromCursor) {
622       return new SearchCursor(this, string, fromCursor);
623     },
624
625     // Re-indent the whole buffer
626     reindent: function() {
627       if (this.container.firstChild)
628         this.indentRegion(null, this.container.lastChild);
629     },
630
631     reindentSelection: function(direction) {
632       if (!select.somethingSelected(this.win)) {
633         this.indentAtCursor(direction);
634       }
635       else {
636         var start = select.selectionTopNode(this.container, true),
637             end = select.selectionTopNode(this.container, false);
638         if (start === false || end === false) return;
639         this.indentRegion(start, end, direction);
640       }
641     },
642
643     grabKeys: function(eventHandler, filter) {
644       this.frozen = eventHandler;
645       this.keyFilter = filter;
646     },
647     ungrabKeys: function() {
648       this.frozen = "leave";
649       this.keyFilter = null;
650     },
651
652     setParser: function(name) {
653       Editor.Parser = window[name];
654       if (this.container.firstChild) {
655         forEach(this.container.childNodes, function(n) {
656           if (n.nodeType != 3) n.dirty = true;
657         });
658         this.addDirtyNode(this.firstChild);
659         this.scheduleHighlight();
660       }
661     },
662
663     // Intercept enter and tab, and assign their new functions.
664     keyDown: function(event) {
665       if (this.frozen == "leave") this.frozen = null;
666       if (this.frozen && (!this.keyFilter || this.keyFilter(event)) ) {
667         event.stop();
668         this.frozen(event);
669         return;
670       }
671
672       var code = event.keyCode;
673       // Don't scan when the user is typing.
674       this.delayScanning();
675       // Schedule a paren-highlight event, if configured.
676       if (this.options.autoMatchParens)
677         this.scheduleParenBlink();
678
679       // The various checks for !altKey are there because AltGr sets both
680       // ctrlKey and altKey to true, and should not be recognised as
681       // Control.
682       if (code == 13) { // enter
683         if (event.ctrlKey && !event.altKey) {
684           this.reparseBuffer();
685         }
686         else {
687           select.insertNewlineAtCursor(this.win);
688           this.indentAtCursor();
689           select.scrollToCursor(this.container);
690         }
691         event.stop();
692       }
693       else if (code == 9 && this.options.tabMode != "default") { // tab
694         this.handleTab(!event.ctrlKey && !event.shiftKey);
695         event.stop();
696       }
697       else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
698         this.handleTab(true);
699         event.stop();
700       }
701       else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home
702         if (this.home())
703           event.stop();
704       }
705       else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
706         this.blinkParens(event.shiftKey);
707         event.stop();
708       }
709       else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right
710         var cursor = select.selectionTopNode(this.container);
711         if (cursor === false || !this.container.firstChild) return;
712
713         if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
714         else {
715           var end = endOfLine(cursor, this.container);
716           select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
717         }
718         event.stop();
719       }
720       else if ((event.ctrlKey || event.metaKey) && !event.altKey) {
721         if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y
722           select.scrollToNode(this.history.redo());
723           event.stop();
724         }
725         else if (code == 90 || (safari && code == 8)) { // Z, backspace
726           select.scrollToNode(this.history.undo());
727           event.stop();
728         }
729         else if (code == 83 && this.options.saveFunction) { // S
730           this.options.saveFunction();
731           event.stop();
732         }
733       }
734     },
735
736     // Check for characters that should re-indent the current line,
737     // and prevent Opera from handling enter and tab anyway.
738     keyPress: function(event) {
739       var electric = Editor.Parser.electricChars, self = this;
740       // Hack for Opera, and Firefox on OS X, in which stopping a
741       // keydown event does not prevent the associated keypress event
742       // from happening, so we have to cancel enter and tab again
743       // here.
744       if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode))) ||
745           event.code == 13 || (event.code == 9 && this.options.tabMode != "default") ||
746           (event.keyCode == 32 && event.shiftKey && this.options.tabMode == "default"))
747         event.stop();
748       else if (electric && electric.indexOf(event.character) != -1)
749         this.parent.setTimeout(function(){self.indentAtCursor(null);}, 0);
750       else if ((event.character == "v" || event.character == "V")
751                && (event.ctrlKey || event.metaKey) && !event.altKey) // ctrl-V
752         this.reroutePasteEvent();
753     },
754
755     // Mark the node at the cursor dirty when a non-safe key is
756     // released.
757     keyUp: function(event) {
758       this.cursorActivity(isSafeKey(event.keyCode));
759     },
760
761     // Indent the line following a given <br>, or null for the first
762     // line. If given a <br> element, this must have been highlighted
763     // so that it has an indentation method. Returns the whitespace
764     // element that has been modified or created (if any).
765     indentLineAfter: function(start, direction) {
766       // whiteSpace is the whitespace span at the start of the line,
767       // or null if there is no such node.
768       var whiteSpace = start ? start.nextSibling : this.container.firstChild;
769       if (whiteSpace && !hasClass(whiteSpace, "whitespace"))
770         whiteSpace = null;
771
772       // Sometimes the start of the line can influence the correct
773       // indentation, so we retrieve it.
774       var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
775       var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
776
777       // Ask the lexical context for the correct indentation, and
778       // compute how much this differs from the current indentation.
779       var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
780       if (direction != null && this.options.tabMode == "shift")
781         newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
782       else if (start)
783         newIndent = start.indentation(nextChars, curIndent, direction);
784       else if (Editor.Parser.firstIndentation)
785         newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
786       var indentDiff = newIndent - curIndent;
787
788       // If there is too much, this is just a matter of shrinking a span.
789       if (indentDiff < 0) {
790         if (newIndent == 0) {
791           if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0);
792           removeElement(whiteSpace);
793           whiteSpace = null;
794         }
795         else {
796           select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
797           whiteSpace.currentText = makeWhiteSpace(newIndent);
798           whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
799         }
800       }
801       // Not enough...
802       else if (indentDiff > 0) {
803         // If there is whitespace, we grow it.
804         if (whiteSpace) {
805           whiteSpace.currentText = makeWhiteSpace(newIndent);
806           whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
807         }
808         // Otherwise, we have to add a new whitespace node.
809         else {
810           whiteSpace = makePartSpan(makeWhiteSpace(newIndent), this.doc);
811           whiteSpace.className = "whitespace";
812           if (start) insertAfter(whiteSpace, start);
813           else this.container.insertBefore(whiteSpace, this.container.firstChild);
814         }
815         if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
816       }
817       if (indentDiff != 0) this.addDirtyNode(start);
818       return whiteSpace;
819     },
820
821     // Re-highlight the selected part of the document.
822     highlightAtCursor: function() {
823       var pos = select.selectionTopNode(this.container, true);
824       var to = select.selectionTopNode(this.container, false);
825       if (pos === false || to === false) return;
826
827       select.markSelection(this.win);
828       if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
829         return false;
830       select.selectMarked();
831       return true;
832     },
833
834     // When tab is pressed with text selected, the whole selection is
835     // re-indented, when nothing is selected, the line with the cursor
836     // is re-indented.
837     handleTab: function(direction) {
838       if (this.options.tabMode == "spaces")
839         select.insertTabAtCursor(this.win);
840       else
841         this.reindentSelection(direction);
842     },
843
844     home: function() {
845       var cur = select.selectionTopNode(this.container, true), start = cur;
846       if (cur === false || !(!cur || cur.isPart || cur.nodeName == "BR") || !this.container.firstChild)
847         return false;
848
849       while (cur && cur.nodeName != "BR") cur = cur.previousSibling;
850       var next = cur ? cur.nextSibling : this.container.firstChild;
851       if (next && next != start && next.isPart && hasClass(next, "whitespace"))
852         select.focusAfterNode(next, this.container);
853       else
854         select.focusAfterNode(cur, this.container);
855
856       select.scrollToCursor(this.container);
857       return true;
858     },
859
860     // Delay (or initiate) the next paren blink event.
861     scheduleParenBlink: function() {
862       if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
863       var self = this;
864       this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300);
865     },
866
867     // Take the token before the cursor. If it contains a character in
868     // '()[]{}', search for the matching paren/brace/bracket, and
869     // highlight them in green for a moment, or red if no proper match
870     // was found.
871     blinkParens: function(jump) {
872       if (!window.select) return;
873       // Clear the event property.
874       if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
875       this.parenEvent = null;
876
877       // Extract a 'paren' from a piece of text.
878       function paren(node) {
879         if (node.currentText) {
880           var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
881           return match && match[1];
882         }
883       }
884       // Determine the direction a paren is facing.
885       function forward(ch) {
886         return /[\(\[\{]/.test(ch);
887       }
888
889       var ch, self = this, cursor = select.selectionTopNode(this.container, true);
890       if (!cursor || !this.highlightAtCursor()) return;
891       cursor = select.selectionTopNode(this.container, true);
892       if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor)))))
893         return;
894       // We only look for tokens with the same className.
895       var className = cursor.className, dir = forward(ch), match = matching[ch];
896
897       // Since parts of the document might not have been properly
898       // highlighted, and it is hard to know in advance which part we
899       // have to scan, we just try, and when we find dirty nodes we
900       // abort, parse them, and re-try.
901       function tryFindMatch() {
902         var stack = [], ch, ok = true;;
903         for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
904           if (runner.className == className && runner.nodeName == "SPAN" && (ch = paren(runner))) {
905             if (forward(ch) == dir)
906               stack.push(ch);
907             else if (!stack.length)
908               ok = false;
909             else if (stack.pop() != matching[ch])
910               ok = false;
911             if (!stack.length) break;
912           }
913           else if (runner.dirty || runner.nodeName != "SPAN" && runner.nodeName != "BR") {
914             return {node: runner, status: "dirty"};
915           }
916         }
917         return {node: runner, status: runner && ok};
918       }
919       // Temporarily give the relevant nodes a colour.
920       function blink(node, ok) {
921         node.style.fontWeight = "bold";
922         node.style.color = ok ? "#8F8" : "#F88";
923         self.parent.setTimeout(function() {node.style.fontWeight = ""; node.style.color = "";}, 500);
924       }
925
926       while (true) {
927         var found = tryFindMatch();
928         if (found.status == "dirty") {
929           this.highlight(found.node, endOfLine(found.node));
930           // Needed because in some corner cases a highlight does not
931           // reach a node.
932           found.node.dirty = false;
933           continue;
934         }
935         else {
936           blink(cursor, found.status);
937           if (found.node) {
938             blink(found.node, found.status);
939             if (jump) select.focusAfterNode(found.node.previousSibling, this.container);
940           }
941           break;
942         }
943       }
944     },
945
946     // Adjust the amount of whitespace at the start of the line that
947     // the cursor is on so that it is indented properly.
948     indentAtCursor: function(direction) {
949       if (!this.container.firstChild) return;
950       // The line has to have up-to-date lexical information, so we
951       // highlight it first.
952       if (!this.highlightAtCursor()) return;
953       var cursor = select.selectionTopNode(this.container, false);
954       // If we couldn't determine the place of the cursor,
955       // there's nothing to indent.
956       if (cursor === false)
957         return;
958       var lineStart = startOfLine(cursor);
959       var whiteSpace = this.indentLineAfter(lineStart, direction);
960       if (cursor == lineStart && whiteSpace)
961           cursor = whiteSpace;
962       // This means the indentation has probably messed up the cursor.
963       if (cursor == whiteSpace)
964         select.focusAfterNode(cursor, this.container);
965     },
966
967     // Indent all lines whose start falls inside of the current
968     // selection.
969     indentRegion: function(start, end, direction) {
970       var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling);
971       if (end.nodeName != "BR") end = endOfLine(end, this.container);
972
973       do {
974         var next = endOfLine(current, this.container);
975         if (current) this.highlight(before, next, true);
976         this.indentLineAfter(current, direction);
977         before = current;
978         current = next;
979       } while (current != end);
980       select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
981     },
982
983     // Find the node that the cursor is in, mark it as dirty, and make
984     // sure a highlight pass is scheduled.
985     cursorActivity: function(safe) {
986       if (internetExplorer) {
987         this.container.createTextRange().execCommand("unlink");
988         this.selectionSnapshot = select.selectionCoords(this.win);
989       }
990
991       var activity = this.options.cursorActivity;
992       if (!safe || activity) {
993         var cursor = select.selectionTopNode(this.container, false);
994         if (cursor === false || !this.container.firstChild) return;
995         cursor = cursor || this.container.firstChild;
996         if (activity) activity(cursor);
997         if (!safe) {
998           this.scheduleHighlight();
999           this.addDirtyNode(cursor);
1000         }
1001       }
1002     },
1003
1004     reparseBuffer: function() {
1005       forEach(this.container.childNodes, function(node) {node.dirty = true;});
1006       if (this.container.firstChild)
1007         this.addDirtyNode(this.container.firstChild);
1008     },
1009
1010     // Add a node to the set of dirty nodes, if it isn't already in
1011     // there.
1012     addDirtyNode: function(node) {
1013       node = node || this.container.firstChild;
1014       if (!node) return;
1015
1016       for (var i = 0; i < this.dirty.length; i++)
1017         if (this.dirty[i] == node) return;
1018
1019       if (node.nodeType != 3)
1020         node.dirty = true;
1021       this.dirty.push(node);
1022     },
1023
1024     // Cause a highlight pass to happen in options.passDelay
1025     // milliseconds. Clear the existing timeout, if one exists. This
1026     // way, the passes do not happen while the user is typing, and
1027     // should as unobtrusive as possible.
1028     scheduleHighlight: function() {
1029       // Timeouts are routed through the parent window, because on
1030       // some browsers designMode windows do not fire timeouts.
1031       var self = this;
1032       this.parent.clearTimeout(this.highlightTimeout);
1033       this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
1034     },
1035
1036     // Fetch one dirty node, and remove it from the dirty set.
1037     getDirtyNode: function() {
1038       while (this.dirty.length > 0) {
1039         var found = this.dirty.pop();
1040         // IE8 sometimes throws an unexplainable 'invalid argument'
1041         // exception for found.parentNode
1042         try {
1043           // If the node has been coloured in the meantime, or is no
1044           // longer in the document, it should not be returned.
1045           while (found && found.parentNode != this.container)
1046             found = found.parentNode
1047           if (found && (found.dirty || found.nodeType == 3))
1048             return found;
1049         } catch (e) {}
1050       }
1051       return null;
1052     },
1053
1054     // Pick dirty nodes, and highlight them, until options.passTime
1055     // milliseconds have gone by. The highlight method will continue
1056     // to next lines as long as it finds dirty nodes. It returns
1057     // information about the place where it stopped. If there are
1058     // dirty nodes left after this function has spent all its lines,
1059     // it shedules another highlight to finish the job.
1060     highlightDirty: function(force) {
1061       // Prevent FF from raising an error when it is firing timeouts
1062       // on a page that's no longer loaded.
1063       if (!window.select) return;
1064
1065       if (!this.options.readOnly) select.markSelection(this.win);
1066       var start, endTime = force ? null : time() + this.options.passTime;
1067       while ((time() < endTime || force) && (start = this.getDirtyNode())) {
1068         var result = this.highlight(start, endTime);
1069         if (result && result.node && result.dirty)
1070           this.addDirtyNode(result.node);
1071       }
1072       if (!this.options.readOnly) select.selectMarked();
1073       if (start) this.scheduleHighlight();
1074       return this.dirty.length == 0;
1075     },
1076
1077     // Creates a function that, when called through a timeout, will
1078     // continuously re-parse the document.
1079     documentScanner: function(passTime) {
1080       var self = this, pos = null;
1081       return function() {
1082         // FF timeout weirdness workaround.
1083         if (!window.select) return;
1084         // If the current node is no longer in the document... oh
1085         // well, we start over.
1086         if (pos && pos.parentNode != self.container)
1087           pos = null;
1088         select.markSelection(self.win);
1089         var result = self.highlight(pos, time() + passTime, true);
1090         select.selectMarked();
1091         var newPos = result ? (result.node && result.node.nextSibling) : null;
1092         pos = (pos == newPos) ? null : newPos;
1093         self.delayScanning();
1094       };
1095     },
1096
1097     // Starts the continuous scanning process for this document after
1098     // a given interval.
1099     delayScanning: function() {
1100       if (this.scanner) {
1101         this.parent.clearTimeout(this.documentScan);
1102         this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
1103       }
1104     },
1105
1106     // The function that does the actual highlighting/colouring (with
1107     // help from the parser and the DOM normalizer). Its interface is
1108     // rather overcomplicated, because it is used in different
1109     // situations: ensuring that a certain line is highlighted, or
1110     // highlighting up to X milliseconds starting from a certain
1111     // point. The 'from' argument gives the node at which it should
1112     // start. If this is null, it will start at the beginning of the
1113     // document. When a timestamp is given with the 'target' argument,
1114     // it will stop highlighting at that time. If this argument holds
1115     // a DOM node, it will highlight until it reaches that node. If at
1116     // any time it comes across two 'clean' lines (no dirty nodes), it
1117     // will stop, except when 'cleanLines' is true. maxBacktrack is
1118     // the maximum number of lines to backtrack to find an existing
1119     // parser instance. This is used to give up in situations where a
1120     // highlight would take too long and freeze the browser interface.
1121     highlight: function(from, target, cleanLines, maxBacktrack){
1122       var container = this.container, self = this, active = this.options.activeTokens;
1123       var endTime = (typeof target == "number" ? target : null);
1124
1125       if (!container.firstChild)
1126         return;
1127       // Backtrack to the first node before from that has a partial
1128       // parse stored.
1129       while (from && (!from.parserFromHere || from.dirty)) {
1130         if (maxBacktrack != null && from.nodeName == "BR" && (--maxBacktrack) < 0)
1131           return false;
1132         from = from.previousSibling;
1133       }
1134       // If we are at the end of the document, do nothing.
1135       if (from && !from.nextSibling)
1136         return;
1137
1138       // Check whether a part (<span> node) and the corresponding token
1139       // match.
1140       function correctPart(token, part){
1141         return !part.reduced && part.currentText == token.value && part.className == token.style;
1142       }
1143       // Shorten the text associated with a part by chopping off
1144       // characters from the front. Note that only the currentText
1145       // property gets changed. For efficiency reasons, we leave the
1146       // nodeValue alone -- we set the reduced flag to indicate that
1147       // this part must be replaced.
1148       function shortenPart(part, minus){
1149         part.currentText = part.currentText.substring(minus);
1150         part.reduced = true;
1151       }
1152       // Create a part corresponding to a given token.
1153       function tokenPart(token){
1154         var part = makePartSpan(token.value, self.doc);     
1155         part.className = token.style;
1156         return part;
1157       }
1158
1159       function maybeTouch(node) {
1160         if (node) {
1161           var old = node.oldNextSibling;
1162           if (lineDirty || old === undefined || node.nextSibling != old)
1163             self.history.touch(node);
1164           node.oldNextSibling = node.nextSibling;
1165         }
1166         else {
1167           var old = self.container.oldFirstChild;
1168           if (lineDirty || old === undefined || self.container.firstChild != old)
1169             self.history.touch(null);
1170           self.container.oldFirstChild = self.container.firstChild;
1171         }
1172       }
1173
1174       // Get the token stream. If from is null, we start with a new
1175       // parser from the start of the frame, otherwise a partial parse
1176       // is resumed.
1177       var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
1178           stream = stringStream(traversal),
1179           parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
1180
1181       // parts is an interface to make it possible to 'delay' fetching
1182       // the next DOM node until we are completely done with the one
1183       // before it. This is necessary because often the next node is
1184       // not yet available when we want to proceed past the current
1185       // one.
1186       var parts = {
1187         current: null,
1188         // Fetch current node.
1189         get: function(){
1190           if (!this.current)
1191             this.current = traversal.nodes.shift();
1192           return this.current;
1193         },
1194         // Advance to the next part (do not fetch it yet).
1195         next: function(){
1196           this.current = null;
1197         },
1198         // Remove the current part from the DOM tree, and move to the
1199         // next.
1200         remove: function(){
1201           container.removeChild(this.get());
1202           this.current = null;
1203         },
1204         // Advance to the next part that is not empty, discarding empty
1205         // parts.
1206         getNonEmpty: function(){
1207           var part = this.get();
1208           // Allow empty nodes when they are alone on a line, needed
1209           // for the FF cursor bug workaround (see select.js,
1210           // insertNewlineAtCursor).
1211           while (part && part.nodeName == "SPAN" && part.currentText == "") {
1212             var old = part;
1213             this.remove();
1214             part = this.get();
1215             // Adjust selection information, if any. See select.js for details.
1216             select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
1217           }
1218           return part;
1219         }
1220       };
1221
1222       var lineDirty = false, prevLineDirty = true, lineNodes = 0;
1223
1224       // This forEach loops over the tokens from the parsed stream, and
1225       // at the same time uses the parts object to proceed through the
1226       // corresponding DOM nodes.
1227       forEach(parsed, function(token){
1228         var part = parts.getNonEmpty();
1229
1230         if (token.value == "\n"){
1231           // The idea of the two streams actually staying synchronized
1232           // is such a long shot that we explicitly check.
1233           if (part.nodeName != "BR")
1234             throw "Parser out of sync. Expected BR.";
1235
1236           if (part.dirty || !part.indentation) lineDirty = true;
1237           maybeTouch(from);
1238           from = part;
1239
1240           // Every <br> gets a copy of the parser state and a lexical
1241           // context assigned to it. The first is used to be able to
1242           // later resume parsing from this point, the second is used
1243           // for indentation.
1244           part.parserFromHere = parsed.copy();
1245           part.indentation = token.indentation;
1246           part.dirty = false;
1247
1248           // If the target argument wasn't an integer, go at least
1249           // until that node.
1250           if (endTime == null && part == target) throw StopIteration;
1251
1252           // A clean line with more than one node means we are done.
1253           // Throwing a StopIteration is the way to break out of a
1254           // MochiKit forEach loop.
1255           if ((endTime != null && time() >= endTime) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines))
1256             throw StopIteration;
1257           prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
1258           parts.next();
1259         }
1260         else {
1261           if (part.nodeName != "SPAN")
1262             throw "Parser out of sync. Expected SPAN.";
1263           if (part.dirty)
1264             lineDirty = true;
1265           lineNodes++;
1266
1267           // If the part matches the token, we can leave it alone.
1268           if (correctPart(token, part)){
1269             part.dirty = false;
1270             parts.next();
1271           }
1272           // Otherwise, we have to fix it.
1273           else {
1274             lineDirty = true;
1275             // Insert the correct part.
1276             var newPart = tokenPart(token);
1277             container.insertBefore(newPart, part);
1278             if (active) active(newPart, token, self);
1279             var tokensize = token.value.length;
1280             var offset = 0;
1281             // Eat up parts until the text for this token has been
1282             // removed, adjusting the stored selection info (see
1283             // select.js) in the process.
1284             while (tokensize > 0) {
1285               part = parts.get();
1286               var partsize = part.currentText.length;
1287               select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
1288               if (partsize > tokensize){
1289                 shortenPart(part, tokensize);
1290                 tokensize = 0;
1291               }
1292               else {
1293                 tokensize -= partsize;
1294                 offset += partsize;
1295                 parts.remove();
1296               }
1297             }
1298           }
1299         }
1300       });
1301       maybeTouch(from);
1302       webkitLastLineHack(this.container);
1303
1304       // The function returns some status information that is used by
1305       // hightlightDirty to determine whether and where it has to
1306       // continue.
1307       return {node: parts.getNonEmpty(),
1308               dirty: lineDirty};
1309     }
1310   };
1311
1312   return Editor;
1313 })();
1314
1315 addEventHandler(window, "load", function() {
1316   var CodeMirror = window.frameElement.CodeMirror;
1317   CodeMirror.editor = new Editor(CodeMirror.options);
1318   this.parent.setTimeout(method(CodeMirror, "init"), 0);
1319 });