Wydzielenie funkcji guess_mime z VersionedStorage.page_mime, żeby można było ją łatwo...
[redakcja.git] / platforma / 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 p
574     // iece of text.
575     replaceSelection: function(text) {
576       this.history.commit();
577
578       var start = select.cursorPos(this.container, true),
579           end = select.cursorPos(this.container, false);
580       if (!start || !end) return;
581
582       end = this.replaceRange(start, end, text);
583       // select.setCursorPos(this.container, end);
584       webkitLastLineHack(this.container);
585     },
586
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);
596       parent.focus();
597       te.focus();
598
599       var self = this;
600       this.parent.setTimeout(function() {
601         self.capturingPaste = false;
602         self.win.focus();
603         if (self.selectionSnapshot) // IE hack
604           self.win.select.selectCoords(self.win, self.selectionSnapshot);
605         var text = te.value;
606         if (text) self.replaceSelection(text);
607         removeElement(te);
608       }, 10);
609     },
610
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};
620     },
621
622     getSearchCursor: function(string, fromCursor) {
623       return new SearchCursor(this, string, fromCursor);
624     },
625
626     // Re-indent the whole buffer
627     reindent: function() {
628       if (this.container.firstChild)
629         this.indentRegion(null, this.container.lastChild);
630     },
631
632     reindentSelection: function(direction) {
633       if (!select.somethingSelected(this.win)) {
634         this.indentAtCursor(direction);
635       }
636       else {
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);
641       }
642     },
643
644     grabKeys: function(eventHandler, filter) {
645       this.frozen = eventHandler;
646       this.keyFilter = filter;
647     },
648     ungrabKeys: function() {
649       this.frozen = "leave";
650       this.keyFilter = null;
651     },
652
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;
658         });
659         this.addDirtyNode(this.firstChild);
660         this.scheduleHighlight();
661       }
662     },
663
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))) {
668         event.stop();
669         this.frozen(event);
670         return;
671       }
672
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();
679
680       // The various checks for !altKey are there because AltGr sets both
681       // ctrlKey and altKey to true, and should not be recognised as
682       // Control.
683       if (code == 13) { // enter
684         if (event.ctrlKey && !event.altKey) {
685           this.reparseBuffer();
686         }
687         else {
688           select.insertNewlineAtCursor(this.win);
689           this.indentAtCursor();
690           select.scrollToCursor(this.container);
691         }
692         event.stop();
693       }
694       else if (code == 9 && this.options.tabMode != "default") { // tab
695         this.handleTab(!event.ctrlKey && !event.shiftKey);
696         event.stop();
697       }
698       else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
699         this.handleTab(true);
700         event.stop();
701       }
702       else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home
703         if (this.home())
704           event.stop();
705       }
706       else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
707         this.blinkParens(event.shiftKey);
708         event.stop();
709       }
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;
713
714         if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
715         else {
716           var end = endOfLine(cursor, this.container);
717           select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
718         }
719         event.stop();
720       }
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());
724           event.stop();
725         }
726         else if (code == 90 || (safari && code == 8)) { // Z, backspace
727           select.scrollToNode(this.history.undo());
728           event.stop();
729         }
730         else if (code == 83 && this.options.saveFunction) { // S
731           this.options.saveFunction();
732           event.stop();
733         }
734       }
735     },
736
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
744       // here.
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"))
748         event.stop();
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();
754     },
755
756     // Mark the node at the cursor dirty when a non-safe key is
757     // released.
758     keyUp: function(event) {
759       this.cursorActivity(isSafeKey(event.keyCode));
760     },
761
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"))
771         whiteSpace = null;
772
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 : "";
777
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)
783       else if (start)
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;
788
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);
794           whiteSpace = null;
795         }
796         else {
797           select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
798           whiteSpace.currentText = makeWhiteSpace(newIndent);
799           whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
800         }
801       }
802       // Not enough...
803       else if (indentDiff > 0) {
804         // If there is whitespace, we grow it.
805         if (whiteSpace) {
806           whiteSpace.currentText = makeWhiteSpace(newIndent);
807           whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
808         }
809         // Otherwise, we have to add a new whitespace node.
810         else {
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);
815         }
816         if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
817       }
818       if (indentDiff != 0) this.addDirtyNode(start);
819       return whiteSpace;
820     },
821
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;
827
828       select.markSelection(this.win);
829       if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
830         return false;
831       select.selectMarked();
832       return true;
833     },
834
835     // When tab is pressed with text selected, the whole selection is
836     // re-indented, when nothing is selected, the line with the cursor
837     // is re-indented.
838     handleTab: function(direction) {
839       if (this.options.tabMode == "spaces")
840         select.insertTabAtCursor(this.win);
841       else
842         this.reindentSelection(direction);
843     },
844
845     home: function() {
846       var cur = select.selectionTopNode(this.container, true), start = cur;
847       if (cur === false || !(!cur || cur.isPart || cur.nodeName == "BR") || !this.container.firstChild)
848         return false;
849
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);
854       else
855         select.focusAfterNode(cur, this.container);
856
857       select.scrollToCursor(this.container);
858       return true;
859     },
860
861     // Delay (or initiate) the next paren blink event.
862     scheduleParenBlink: function() {
863       if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
864       var self = this;
865       this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300);
866     },
867
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
871     // was found.
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;
877
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];
883         }
884       }
885       // Determine the direction a paren is facing.
886       function forward(ch) {
887         return /[\(\[\{]/.test(ch);
888       }
889
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)))))
894         return;
895       // We only look for tokens with the same className.
896       var className = cursor.className, dir = forward(ch), match = matching[ch];
897
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)
907               stack.push(ch);
908             else if (!stack.length)
909               ok = false;
910             else if (stack.pop() != matching[ch])
911               ok = false;
912             if (!stack.length) break;
913           }
914           else if (runner.dirty || runner.nodeName != "SPAN" && runner.nodeName != "BR") {
915             return {node: runner, status: "dirty"};
916           }
917         }
918         return {node: runner, status: runner && ok};
919       }
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);
925       }
926
927       while (true) {
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
932           // reach a node.
933           found.node.dirty = false;
934           continue;
935         }
936         else {
937           blink(cursor, found.status);
938           if (found.node) {
939             blink(found.node, found.status);
940             if (jump) select.focusAfterNode(found.node.previousSibling, this.container);
941           }
942           break;
943         }
944       }
945     },
946
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)
958         return;
959       var lineStart = startOfLine(cursor);
960       var whiteSpace = this.indentLineAfter(lineStart, direction);
961       if (cursor == lineStart && whiteSpace)
962           cursor = whiteSpace;
963       // This means the indentation has probably messed up the cursor.
964       if (cursor == whiteSpace)
965         select.focusAfterNode(cursor, this.container);
966     },
967
968     // Indent all lines whose start falls inside of the current
969     // selection.
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);
973
974       do {
975         var next = endOfLine(current, this.container);
976         if (current) this.highlight(before, next, true);
977         this.indentLineAfter(current, direction);
978         before = current;
979         current = next;
980       } while (current != end);
981       select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
982     },
983
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);
990       }
991
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);
998         if (!safe) {
999           this.scheduleHighlight();
1000           this.addDirtyNode(cursor);
1001         }
1002       }
1003     },
1004
1005     reparseBuffer: function() {
1006       forEach(this.container.childNodes, function(node) {node.dirty = true;});
1007       if (this.container.firstChild)
1008         this.addDirtyNode(this.container.firstChild);
1009     },
1010
1011     // Add a node to the set of dirty nodes, if it isn't already in
1012     // there.
1013     addDirtyNode: function(node) {
1014       node = node || this.container.firstChild;
1015       if (!node) return;
1016
1017       for (var i = 0; i < this.dirty.length; i++)
1018         if (this.dirty[i] == node) return;
1019
1020       if (node.nodeType != 3)
1021         node.dirty = true;
1022       this.dirty.push(node);
1023     },
1024
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.
1032       var self = this;
1033       this.parent.clearTimeout(this.highlightTimeout);
1034       this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
1035     },
1036
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
1043         try {
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))
1049             return found;
1050         } catch (e) {}
1051       }
1052       return null;
1053     },
1054
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;
1065
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);
1072       }
1073       if (!this.options.readOnly) select.selectMarked();
1074       if (start) this.scheduleHighlight();
1075       return this.dirty.length == 0;
1076     },
1077
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;
1082       return function() {
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)
1088           pos = null;
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();
1095       };
1096     },
1097
1098     // Starts the continuous scanning process for this document after
1099     // a given interval.
1100     delayScanning: function() {
1101       if (this.scanner) {
1102         this.parent.clearTimeout(this.documentScan);
1103         this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
1104       }
1105     },
1106
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);
1125
1126       if (!container.firstChild)
1127         return;
1128       // Backtrack to the first node before from that has a partial
1129       // parse stored.
1130       while (from && (!from.parserFromHere || from.dirty)) {
1131         if (maxBacktrack != null && from.nodeName == "BR" && (--maxBacktrack) < 0)
1132           return false;
1133         from = from.previousSibling;
1134       }
1135       // If we are at the end of the document, do nothing.
1136       if (from && !from.nextSibling)
1137         return;
1138
1139       // Check whether a part (<span> node) and the corresponding token
1140       // match.
1141       function correctPart(token, part){
1142         return !part.reduced && part.currentText == token.value && part.className == token.style;
1143       }
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;
1152       }
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;
1157         return part;
1158       }
1159
1160       function maybeTouch(node) {
1161         if (node) {
1162           var old = node.oldNextSibling;
1163           if (lineDirty || old === undefined || node.nextSibling != old)
1164             self.history.touch(node);
1165           node.oldNextSibling = node.nextSibling;
1166         }
1167         else {
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;
1172         }
1173       }
1174
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
1177       // is resumed.
1178       var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
1179           stream = stringStream(traversal),
1180           parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
1181
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
1186       // one.
1187       var parts = {
1188         current: null,
1189         // Fetch current node.
1190         get: function(){
1191           if (!this.current)
1192             this.current = traversal.nodes.shift();
1193           return this.current;
1194         },
1195         // Advance to the next part (do not fetch it yet).
1196         next: function(){
1197           this.current = null;
1198         },
1199         // Remove the current part from the DOM tree, and move to the
1200         // next.
1201         remove: function(){
1202           container.removeChild(this.get());
1203           this.current = null;
1204         },
1205         // Advance to the next part that is not empty, discarding empty
1206         // parts.
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 == "") {
1213             var old = part;
1214             this.remove();
1215             part = this.get();
1216             // Adjust selection information, if any. See select.js for details.
1217             select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
1218           }
1219           return part;
1220         }
1221       };
1222
1223       var lineDirty = false, prevLineDirty = true, lineNodes = 0;
1224
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();
1230
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.";
1236
1237           if (part.dirty || !part.indentation) lineDirty = true;
1238           maybeTouch(from);
1239           from = part;
1240
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
1244           // for indentation.
1245           part.parserFromHere = parsed.copy();
1246           part.indentation = token.indentation;
1247           part.dirty = false;
1248
1249           // If the target argument wasn't an integer, go at least
1250           // until that node.
1251           if (endTime == null && part == target) throw StopIteration;
1252
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;
1259           parts.next();
1260         }
1261         else {
1262           if (part.nodeName != "SPAN")
1263             throw "Parser out of sync. Expected SPAN.";
1264           if (part.dirty)
1265             lineDirty = true;
1266           lineNodes++;
1267
1268           // If the part matches the token, we can leave it alone.
1269           if (correctPart(token, part)){
1270             part.dirty = false;
1271             parts.next();
1272           }
1273           // Otherwise, we have to fix it.
1274           else {
1275             lineDirty = true;
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;
1281             var offset = 0;
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) {
1286               part = parts.get();
1287               var partsize = part.currentText.length;
1288               select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
1289               if (partsize > tokensize){
1290                 shortenPart(part, tokensize);
1291                 tokensize = 0;
1292               }
1293               else {
1294                 tokensize -= partsize;
1295                 offset += partsize;
1296                 parts.remove();
1297               }
1298             }
1299           }
1300         }
1301       });
1302       maybeTouch(from);
1303       webkitLastLineHack(this.container);
1304
1305       // The function returns some status information that is used by
1306       // hightlightDirty to determine whether and where it has to
1307       // continue.
1308       return {node: parts.getNonEmpty(),
1309               dirty: lineDirty};
1310     }
1311   };
1312
1313   return Editor;
1314 })();
1315
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);
1320 });