comparison src/nabble/view/web/util/codemirror/js/editor.js @ 0:7ecd1a4ef557

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