0ed59012e916fe9741505910017c0872a5268fa4
[Packages/TYPO3.CMS.git] / typo3 / sysext / t3editor / jslib / 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 var Editor = (function(){
8 // The HTML elements whose content should be suffixed by a newline
9 // when converting them to flat text.
10 var newlineElements = {"P": true, "DIV": true, "LI": true};
11
12 // Create a set of white-space characters that will not be collapsed
13 // by the browser, but will not break text-wrapping either.
14 function safeWhiteSpace(n) {
15 var buffer = [], nb = true;
16 for (; n > 0; n--) {
17 buffer.push((nb || n == 1) ? nbsp : " ");
18 nb = !nb;
19 }
20 return buffer.join("");
21 }
22
23 function splitSpaces(string) {
24 return string.replace(/[\t \u00a0]{2,}/g, function(s) {return safeWhiteSpace(s.length);});
25 }
26 function asEditorLines(string) {
27 return splitSpaces(string.replace(/\u00a0/g, " ")).replace(/\r\n?/g, "\n").split("\n");
28 }
29
30 // Helper function for traverseDOM. Flattens an arbitrary DOM node
31 // into an array of textnodes and <br> tags.
32 function simplifyDOM(root) {
33 var doc = root.ownerDocument;
34 var result = [];
35 var leaving = false;
36
37 function simplifyNode(node) {
38 if (node.nodeType == 3) {
39 var text = node.nodeValue = splitSpaces(node.nodeValue.replace(/[\n\r]/g, ""));
40 if (text.length) leaving = false;
41 result.push(node);
42 }
43 else if (node.nodeName == "BR" && node.childNodes.length == 0) {
44 leaving = false;
45 result.push(node);
46 }
47 else {
48 forEach(node.childNodes, simplifyNode);
49 if (!leaving && newlineElements.hasOwnProperty(node.nodeName)) {
50 leaving = true;
51 result.push(doc.createElement("BR"));
52 }
53 }
54 }
55
56 simplifyNode(root);
57 return result;
58 }
59
60 // Creates a MochiKit-style iterator that goes over a series of DOM
61 // nodes. The values it yields are strings, the textual content of
62 // the nodes. It makes sure that all nodes up to and including the
63 // one whose text is being yielded have been 'normalized' to be just
64 // <span> and <br> elements.
65 // See the story.html file for some short remarks about the use of
66 // continuation-passing style in this iterator.
67 function traverseDOM(start){
68 function yield(value, c){cc = c; return value;}
69 function push(fun, arg, c){return function(){return fun(arg, c);};}
70 function stop(){cc = stop; throw StopIteration;};
71 var cc = push(scanNode, start, stop);
72 var owner = start.ownerDocument;
73 var nodeQueue = [];
74
75 // Create a function that can be used to insert nodes after the
76 // one given as argument.
77 function pointAt(node){
78 var parent = node.parentNode;
79 var next = node.nextSibling;
80 if (next)
81 return function(newnode){parent.insertBefore(newnode, next);};
82 else
83 return function(newnode){parent.appendChild(newnode);};
84 }
85 var point = null;
86
87 // Insert a normalized node at the current point. If it is a text
88 // node, wrap it in a <span>, and give that span a currentText
89 // property -- this is used to cache the nodeValue, because
90 // directly accessing nodeValue is horribly slow on some browsers.
91 // The dirty property is used by the highlighter to determine
92 // which parts of the document have to be re-highlighted.
93 function insertPart(part){
94 var text = "\n";
95 if (part.nodeType == 3) {
96 text = part.nodeValue;
97 var span = owner.createElement("SPAN");
98 span.className = "part";
99 span.appendChild(part);
100 part = span;
101 part.currentText = text;
102 }
103 part.dirty = true;
104 nodeQueue.push(part);
105 point(part);
106 return text;
107 }
108
109 // Extract the text and newlines from a DOM node, insert them into
110 // the document, and yield the textual content. Used to replace
111 // non-normalized nodes.
112 function writeNode(node, c){
113 var toYield = [];
114 forEach(simplifyDOM(node), function(part) {
115 toYield.push(insertPart(part));
116 });
117 return yield(toYield.join(""), c);
118 }
119
120 // Check whether a node is a normalized <span> element.
121 function partNode(node){
122 if (node.nodeName == "SPAN" && node.childNodes.length == 1 && node.firstChild.nodeType == 3){
123 node.currentText = node.firstChild.nodeValue;
124 return true;
125 }
126 return false;
127 }
128
129 // Handle a node. Add its successor to the continuation if there
130 // is one, find out whether the node is normalized. If it is,
131 // yield its content, otherwise, normalize it (writeNode will take
132 // care of yielding).
133 function scanNode(node, c){
134 if (node.nextSibling)
135 c = push(scanNode, node.nextSibling, c);
136
137 if (partNode(node)){
138 nodeQueue.push(node);
139 return yield(node.currentText, c);
140 }
141 else if (node.nodeName == "BR") {
142 nodeQueue.push(node);
143 return yield("\n", c);
144 }
145 else {
146 point = pointAt(node);
147 removeElement(node);
148 return writeNode(node, c);
149 }
150 }
151
152 // MochiKit iterators are objects with a next function that
153 // returns the next value or throws StopIteration when there are
154 // no more values.
155 return {next: function(){return cc();}, nodes: nodeQueue};
156 }
157
158 // Determine the text size of a processed node.
159 function nodeSize(node) {
160 if (node.nodeName == "BR")
161 return 1;
162 else
163 return node.currentText.length;
164 }
165
166 // Search backwards through the top-level nodes until the next BR or
167 // the start of the frame.
168 function startOfLine(node) {
169 while (node && node.nodeName != "BR")
170 node = node.previousSibling;
171 return node;
172 }
173
174 function cleanText(text) {
175 return text.replace(/\u00a0/g, " ");
176 }
177
178 // Client interface for searching the content of the editor. Create
179 // these by calling CodeMirror.getSearchCursor. To use, call
180 // findNext on the resulting object -- this returns a boolean
181 // indicating whether anything was found, and can be called again to
182 // skip to the next find. Use the select and replace methods to
183 // actually do something with the found locations.
184 function SearchCursor(editor, string, fromCursor) {
185 this.editor = editor;
186 this.history = editor.history;
187 this.history.commit();
188
189 // Are we currently at an occurrence of the search string?
190 this.atOccurrence = false;
191 // The object stores a set of nodes coming after its current
192 // position, so that when the current point is taken out of the
193 // DOM tree, we can still try to continue.
194 this.fallbackSize = 15;
195 var cursor;
196 // Start from the cursor when specified and a cursor can be found.
197 if (fromCursor && (cursor = select.cursorPos(this.editor.container))) {
198 this.line = cursor.node;
199 this.offset = cursor.offset;
200 }
201 else {
202 this.line = null;
203 this.offset = 0;
204 }
205 this.valid = !!string;
206
207 // Create a matcher function based on the kind of string we have.
208 var target = string.split("\n"), self = this;;
209 this.matches = (target.length == 1) ?
210 // For one-line strings, searching can be done simply by calling
211 // indexOf on the current line.
212 function() {
213 var match = cleanText(self.history.textAfter(self.line).slice(self.offset)).indexOf(string);
214 if (match > -1)
215 return {from: {node: self.line, offset: self.offset + match},
216 to: {node: self.line, offset: self.offset + match + string.length}};
217 } :
218 // Multi-line strings require internal iteration over lines, and
219 // some clunky checks to make sure the first match ends at the
220 // end of the line and the last match starts at the start.
221 function() {
222 var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
223 var match = firstLine.lastIndexOf(target[0]);
224 if (match == -1 || match != firstLine.length - target[0].length)
225 return false;
226 var startOffset = self.offset + match;
227
228 var line = self.history.nodeAfter(self.line);
229 for (var i = 1; i < target.length - 1; i++) {
230 if (cleanText(self.history.textAfter(line)) != target[i])
231 return false;
232 line = self.history.nodeAfter(line);
233 }
234
235 if (cleanText(self.history.textAfter(line)).indexOf(target[target.length - 1]) != 0)
236 return false;
237
238 return {from: {node: self.line, offset: startOffset},
239 to: {node: line, offset: target[target.length - 1].length}};
240 };
241 }
242
243 SearchCursor.prototype = {
244 findNext: function() {
245 if (!this.valid) return false;
246 this.atOccurrence = false;
247 var self = this;
248
249 // Go back to the start of the document if the current line is
250 // no longer in the DOM tree.
251 if (this.line && !this.line.parentNode) {
252 this.line = null;
253 this.offset = 0;
254 }
255
256 // Set the cursor's position one character after the given
257 // position.
258 function saveAfter(pos) {
259 if (self.history.textAfter(pos.node).length < pos.offset) {
260 self.line = pos.node;
261 self.offset = pos.offset + 1;
262 }
263 else {
264 self.line = self.history.nodeAfter(pos.node);
265 self.offset = 0;
266 }
267 }
268
269 while (true) {
270 var match = this.matches();
271 // Found the search string.
272 if (match) {
273 this.atOccurrence = match;
274 saveAfter(match.from);
275 return true;
276 }
277 this.line = this.history.nodeAfter(this.line);
278 this.offset = 0;
279 // End of document.
280 if (!this.line) {
281 this.valid = false;
282 return false;
283 }
284 }
285 },
286
287 select: function() {
288 if (this.atOccurrence) {
289 select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
290 select.scrollToCursor(this.editor.container);
291 }
292 },
293
294 replace: function(string) {
295 if (this.atOccurrence) {
296 this.editor.replaceRange(this.atOccurrence.from, this.atOccurrence.to, string);
297 this.line = this.atOccurrence.from.node;
298 this.offset = this.atOccurrence.from.offset;
299 this.atOccurrence = false;
300 }
301 }
302 };
303
304 // The Editor object is the main inside-the-iframe interface.
305 function Editor(options) {
306 this.options = options;
307 this.parent = parent;
308 this.doc = document;
309 this.container = this.doc.body;
310 this.win = window;
311 this.history = new History(this.container, this.options.undoDepth, this.options.undoDelay, this);
312
313 if (!Editor.Parser)
314 throw "No parser loaded.";
315 if (options.parserConfig && Editor.Parser.configure)
316 Editor.Parser.configure(options.parserConfig);
317
318 if (!options.textWrapping)
319 this.doc.body.style.whiteSpace = "pre";
320
321 this.dirty = [];
322 if (options.content)
323 this.importCode(options.content);
324 else // FF acts weird when the editable document is completely empty
325 this.container.appendChild(this.doc.createElement("SPAN"));
326
327 if (!options.readOnly) {
328 if (options.continuousScanning !== false) {
329 this.scanner = this.documentScanner(options.linesPerPass);
330 this.delayScanning();
331 }
332
333 // In IE, designMode frames can not run any scripts, so we use
334 // contentEditable instead. Random ActiveX check is there because
335 // Opera apparently also supports some kind of perverted form of
336 // contentEditable.
337 if (document.body.contentEditable != undefined && window.ActiveXObject)
338 document.body.contentEditable = "true";
339 else
340 document.designMode = "on";
341
342 addEventHandler(document, "keydown", method(this, "keyDown"));
343 addEventHandler(document, "keypress", method(this, "keyPress"));
344 addEventHandler(document, "keyup", method(this, "keyUp"));
345 addEventHandler(document.body, "paste", method(this, "markCursorDirty"));
346 if (options.outerEditor && options.outerEditor.scroll) {
347 addEventHandler(document, "scroll", method(options.outerEditor, "scroll"));
348 // addEventHandler(window, "scroll", method(options.outerEditor, "scroll"));
349 }
350 if (options.outerEditor && options.outerEditor.click) {
351 addEventHandler(document, "click", method(options.outerEditor, "click"));
352 }
353 }
354 }
355
356 function isSafeKey(code) {
357 return (code >= 16 && code <= 18) || // shift, control, alt
358 (code >= 33 && code <= 40); // arrows, home, end
359 }
360
361 Editor.prototype = {
362 // Import a piece of code into the editor.
363 importCode: function(code) {
364 this.history.push(null, null, asEditorLines(code));
365 this.history.reset();
366 if (this.options.outerEditor && this.options.outerEditor.updateLinenum) {
367 this.options.outerEditor.updateLinenum(code);
368 }
369 },
370
371 // Extract the code from the editor.
372 getCode: function() {
373 if (!this.container.firstChild)
374 return "";
375
376 var accum = [];
377 forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
378 return cleanText(accum.join(""));
379 },
380
381 // Move the cursor to the start of a specific line (counting from 1).
382 jumpToLine: function(line) {
383 if (line <= 1 || !this.container.firstChild) {
384 select.focusAfterNode(null, this.container);
385 }
386 else {
387 var pos = this.container.firstChild;
388 while (true) {
389 if (pos.nodeName == "BR") line--;
390 if (line <= 1 || !pos.nextSibling) break;
391 pos = pos.nextSibling;
392 }
393 select.focusAfterNode(pos, this.container);
394 }
395 select.scrollToCursor(this.container);
396 },
397
398 // Find the line that the cursor is currently on.
399 currentLine: function() {
400 var pos = select.cursorPos(this.container, true), line = 1;
401 if (!pos) return 1;
402 for (cursor = pos.node; cursor; cursor = cursor.previousSibling)
403 if (cursor.nodeName == "BR") line++;
404 return line;
405 },
406
407 // Retrieve the selected text.
408 selectedText: function() {
409 var h = this.history;
410 h.commit();
411
412 var start = select.cursorPos(this.container, true),
413 end = select.cursorPos(this.container, false);
414 if (!start || !end) return "";
415
416 if (start.node == end.node)
417 return h.textAfter(start.node).slice(start.offset, end.offset);
418
419 var text = [h.textAfter(start.node).slice(start.offset)];
420 for (pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos))
421 text.push(h.textAfter(pos));
422 text.push(h.textAfter(end.node).slice(0, end.offset));
423 return cleanText(text.join("\n"));
424 },
425
426 // Replace the selection with another piece of text.
427 replaceSelection: function(text) {
428 this.history.commit();
429 var start = select.cursorPos(this.container, true),
430 end = select.cursorPos(this.container, false);
431 if (!start || !end) return;
432
433 end = this.replaceRange(start, end, text);
434 select.setCursorPos(this.container, start, end);
435 },
436
437 replaceRange: function(from, to, text) {
438 var lines = asEditorLines(text);
439 lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
440 var lastLine = lines[lines.length - 1];
441 lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
442 var end = this.history.nodeAfter(to.node)
443 this.history.push(from.node, end, lines);
444 return {node: this.history.nodeBefore(end),
445 offset: lastLine.length};
446 },
447
448 getSearchCursor: function(string, fromCursor) {
449 return new SearchCursor(this, string, fromCursor);
450 },
451
452 // Re-indent the whole buffer
453 reindent: function() {
454 if (this.container.firstChild)
455 this.indentRegion(null, this.container.lastChild);
456 },
457
458 // Intercept enter and tab, and assign their new functions.
459 keyDown: function(event) {
460
461 // Don't scan when the user is typing.
462 this.delayScanning();
463
464 if (
465 this.options.outerEditor
466 && this.options.outerEditor.checkTextModified
467 && !isSafeKey(event.keyCode)
468 && !event.ctrlKey ) {
469 this.options.outerEditor.checkTextModified();
470 }
471
472 if (event.keyCode == 13) { // enter
473 if (event.ctrlKey) {
474 this.reparseBuffer();
475 }
476 else {
477 select.insertNewlineAtCursor(this.win);
478 this.indentAtCursor();
479 select.scrollToCursor(this.container);
480 if (this.options.outerEditor && this.options.outerEditor.updateLinenum) {
481 this.options.outerEditor.updateLinenum();
482 }
483 }
484 event.stop();
485 }
486 else if (event.keyCode == 9) { // tab
487 this.handleTab();
488 event.stop();
489 }
490 else if (event.ctrlKey) {
491 if (event.keyCode == 90 || event.keyCode == 8) { // Z, backspace
492 this.history.undo();
493 event.stop();
494 }
495 else if (event.keyCode == 89) { // Y
496 this.history.redo();
497 event.stop();
498 }
499 else if (event.keyCode == 83 && this.options.saveFunction) { // S
500 this.options.saveFunction();
501 event.stop();
502 }
503 else if (event.keyCode == 122 ) { // F11 toogle fullscreen mode
504 if (this.options.outerEditor && this.options.outerEditor.toggleFullscreen) {
505 this.options.outerEditor.toggleFullscreen();
506 event.stop();
507 }
508 }
509 }
510 },
511
512 // Check for characters that should re-indent the current line,
513 // and prevent Opera from handling enter and tab anyway.
514 keyPress: function(event) {
515 var electric = Editor.Parser.electricChars;
516 // Hack for Opera, and Firefox on OS X, in which stopping a
517 // keydown event does not prevent the associated keypress event
518 // from happening, so we have to cancel enter and tab again
519 // here.
520 if (event.code == 13 || event.code == 9)
521 event.stop();
522 else if (electric && electric.indexOf(event.character) != -1) {
523 this.parent.setTimeout(method(this, "indentAtCursor"), 0);
524 }
525
526 },
527
528 // Mark the node at the cursor dirty when a non-safe key is
529 // released.
530 keyUp: function(event) {
531 if (!isSafeKey(event.keyCode))
532 this.markCursorDirty();
533
534 if (event.keyCode == 46 // Delete
535 || event.keyCode == 13 // Return
536 || event.keyCode == 8) { // Backspace
537 if (this.options.outerEditor && this.options.outerEditor.updateLinenum) {
538 this.options.outerEditor.updateLinenum();
539 }
540 }
541 if (this.options.outerEditor && this.options.outerEditor.checkBracketAtCursor) {
542 this.options.outerEditor.checkBracketAtCursor();
543 }
544
545 },
546
547 // Indent the line following a given <br>, or null for the first
548 // line. If given a <br> element, this must have been highlighted
549 // so that it has an indentation method. Returns the whitespace
550 // element that has been modified or created (if any).
551 indentLineAfter: function(start) {
552 // whiteSpace is the whitespace span at the start of the line,
553 // or null if there is no such node.
554 var whiteSpace = start ? start.nextSibling : this.container.firstChild;
555 if (whiteSpace && !hasClass(whiteSpace, "whitespace"))
556 whiteSpace = null;
557
558 // Sometimes the start of the line can influence the correct
559 // indentation, so we retrieve it.
560 var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
561 var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
562
563 // Ask the lexical context for the correct indentation, and
564 // compute how much this differs from the current indentation.
565 var indent = start ? start.indentation(nextChars) : 0;
566 var indentDiff = indent - (whiteSpace ? whiteSpace.currentText.length : 0);
567
568 // If there is too much, this is just a matter of shrinking a span.
569 if (indentDiff < 0) {
570 if (indent == 0) {
571 removeElement(whiteSpace);
572 whiteSpace = null;
573 }
574 else {
575 whiteSpace.currentText = safeWhiteSpace(indent);
576 whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
577 }
578 }
579 // Not enough...
580 else if (indentDiff > 0) {
581 // If there is whitespace, we grow it.
582 if (whiteSpace) {
583 whiteSpace.currentText = safeWhiteSpace(indent);
584 whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
585 }
586 // Otherwise, we have to add a new whitespace node.
587 else {
588 whiteSpace = this.doc.createElement("SPAN");
589 whiteSpace.className = "part whitespace";
590 whiteSpace.appendChild(this.doc.createTextNode(safeWhiteSpace(indent)));
591 if (start)
592 insertAfter(whiteSpace, start);
593 else
594 insertAtStart(whiteSpace, this.containter);
595 }
596 }
597 return whiteSpace;
598 },
599
600 // Re-highlight the selected part of the document.
601 highlightAtCursor: function() {
602 var pos = select.selectionTopNode(this.container, true);
603 var to = select.selectionTopNode(this.container, false);
604 if (pos === false || !to) return;
605 // Skip one node ahead to make sure the cursor itself is
606 // *inside* a highlighted line.
607 if (to.nextSibling) to = to.nextSibling;
608
609 var sel = select.markSelection(this.win);
610 var toIsText = to.nodeType == 3;
611 if (!toIsText) to.dirty = true;
612
613 // Highlight lines as long as to is in the document and dirty.
614 while (to.parentNode == this.container && (toIsText || to.dirty)) {
615 var result = this.highlight(pos, 1, true);
616 if (result) pos = result.node;
617 if (!result || result.left) break;
618 }
619 select.selectMarked(sel);
620 },
621
622 // When tab is pressed with text selected, the whole selection is
623 // re-indented, when nothing is selected, the line with the cursor
624 // is re-indented.
625 handleTab: function() {
626 var start = select.selectionTopNode(this.container, true),
627 end = select.selectionTopNode(this.container, false);
628 if (start === false || end === false) return;
629
630 if (start == end)
631 this.indentAtCursor();
632 else
633 this.indentRegion(start, end);
634 },
635
636 // Adjust the amount of whitespace at the start of the line that
637 // the cursor is on so that it is indented properly.
638 indentAtCursor: function() {
639 if (!this.container.firstChild) return;
640 // The line has to have up-to-date lexical information, so we
641 // highlight it first.
642 this.highlightAtCursor();
643 var cursor = select.selectionTopNode(this.container, false);
644 // If we couldn't determine the place of the cursor,
645 // there's nothing to indent.
646 if (cursor === false)
647 return;
648 var lineStart = startOfLine(cursor);
649
650 if (this.options.outerEditor && this.options.outerEditor.autoCloseBracket) {
651 this.options.outerEditor.autoCloseBracket(lineStart.previousSibling);
652 }
653
654 var whiteSpace = this.indentLineAfter(lineStart);
655 if (cursor == lineStart && whiteSpace)
656 cursor = whiteSpace;
657 // This means the indentation has probably messed up the cursor.
658 if (cursor == whiteSpace)
659 select.focusAfterNode(cursor, this.container);
660 },
661
662 // Indent all lines whose start falls inside of the current
663 // selection.
664 indentRegion: function(current, end) {
665 var sel = select.markSelection(this.win);
666 if (!current)
667 this.indentLineAfter(current);
668 else
669 current = startOfLine(current.previousSibling);
670 end = startOfLine(end);
671
672 while (true) {
673 var result = this.highlight(current, 1);
674 var next = result ? result.node : null;
675
676 while (current != next)
677 current = current ? current.nextSibling : this.container.firstChild;
678 if (next)
679 this.indentLineAfter(next);
680 if (current == end)
681 break;
682 }
683 select.selectMarked(sel);
684 },
685
686 // Find the node that the cursor is in, mark it as dirty, and make
687 // sure a highlight pass is scheduled.
688 markCursorDirty: function() {
689 var cursor = select.selectionTopNode(this.container, false);
690 if (cursor !== false && this.container.firstChild) {
691 this.scheduleHighlight();
692 this.addDirtyNode(cursor || this.container.firstChild);
693 }
694 },
695
696 reparseBuffer: function() {
697 forEach(this.container.childNodes, function(node) {node.dirty = true;});
698 if (this.container.firstChild)
699 this.addDirtyNode(this.container.firstChild);
700 },
701
702 // Add a node to the set of dirty nodes, if it isn't already in
703 // there.
704 addDirtyNode: function(node) {
705 node = node || this.container.firstChild;
706 if (!node) return;
707
708 for (var i = 0; i < this.dirty.length; i++)
709 if (this.dirty[i] == node) return;
710
711 if (node.nodeType != 3)
712 node.dirty = true;
713 this.dirty.push(node);
714 },
715
716 // Cause a highlight pass to happen in options.passDelay
717 // milliseconds. Clear the existing timeout, if one exists. This
718 // way, the passes do not happen while the user is typing, and
719 // should as unobtrusive as possible.
720 scheduleHighlight: function() {
721 // Timeouts are routed through the parent window, because on
722 // some browsers designMode windows do not fire timeouts.
723 this.parent.clearTimeout(this.highlightTimeout);
724 this.highlightTimeout = this.parent.setTimeout(method(this, "highlightDirty"), this.options.passDelay);
725 },
726
727 // Fetch one dirty node, and remove it from the dirty set.
728 getDirtyNode: function() {
729 while (this.dirty.length > 0) {
730 var found = this.dirty.pop();
731 // If the node has been coloured in the meantime, or is no
732 // longer in the document, it should not be returned.
733 if ((found.dirty || found.nodeType == 3) && found.parentNode)
734 return found;
735 }
736 return null;
737 },
738
739 // Pick dirty nodes, and highlight them, until
740 // options.linesPerPass lines have been highlighted. The highlight
741 // method will continue to next lines as long as it finds dirty
742 // nodes. It returns an object indicating the amount of lines
743 // left, and information about the place where it stopped. If
744 // there are dirty nodes left after this function has spent all
745 // its lines, it shedules another highlight to finish the job.
746 highlightDirty: function(all) {
747 var lines = all ? Infinity : this.options.linesPerPass;
748 var sel = select.markSelection(this.win);
749 var start;
750 while (lines > 0 && (start = this.getDirtyNode())){
751 var result = this.highlight(start, lines);
752 if (result) {
753 lines = result.left;
754 if (result.node && result.dirty)
755 this.addDirtyNode(result.node);
756 }
757 }
758 select.selectMarked(sel);
759 if (start)
760 this.scheduleHighlight();
761 },
762
763 // Creates a function that, when called through a timeout, will
764 // continuously re-parse the document.
765 documentScanner: function(linesPer) {
766 var self = this, pos = null;
767 return function() {
768 // If the current node is no longer in the document... oh
769 // well, we start over.
770 if (pos && pos.parentNode != self.container)
771 pos = null;
772 var sel = select.markSelection(self.win);
773 var result = self.highlight(pos, linesPer, true);
774 select.selectMarked(sel);
775 pos = result ? result.node : null;
776 self.delayScanning();
777 }
778 },
779
780 // Starts the continuous scanning process for this document after
781 // a given interval.
782 delayScanning: function() {
783 if (this.scanner) {
784 this.parent.clearTimeout(this.documentScan);
785 this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
786 }
787 },
788
789 // The function that does the actual highlighting/colouring (with
790 // help from the parser and the DOM normalizer). Its interface is
791 // rather overcomplicated, because it is used in different
792 // situations: ensuring that a certain line is highlighted, or
793 // highlighting up to X lines starting from a certain point. The
794 // 'from' argument gives the node at which it should start. If
795 // this is null, it will start at the beginning of the frame. When
796 // a number of lines is given with the 'lines' argument, it will
797 // colour no more than that amount. If at any time it comes across
798 // a 'clean' line (no dirty nodes), it will stop, except when
799 // 'cleanLines' is true.
800 highlight: function(from, lines, cleanLines){
801 var container = this.container, self = this;
802
803 if (!container.firstChild)
804 return;
805 // Backtrack to the first node before from that has a partial
806 // parse stored.
807 while (from && (!from.parserFromHere || from.dirty))
808 from = from.previousSibling;
809 // If we are at the end of the document, do nothing.
810 if (from && !from.nextSibling)
811 return;
812
813 // Check whether a part (<span> node) and the corresponding token
814 // match.
815 function correctPart(token, part){
816 return !part.reduced && part.currentText == token.value && hasClass(part, token.style);
817 }
818 // Shorten the text associated with a part by chopping off
819 // characters from the front. Note that only the currentText
820 // property gets changed. For efficiency reasons, we leave the
821 // nodeValue alone -- we set the reduced flag to indicate that
822 // this part must be replaced.
823 function shortenPart(part, minus){
824 part.currentText = part.currentText.substring(minus);
825 part.reduced = true;
826 }
827 // Create a part corresponding to a given token.
828 function tokenPart(token){
829 var part = self.doc.createElement("SPAN");
830 part.className = "part " + token.style;
831 part.appendChild(self.doc.createTextNode(token.value));
832 part.currentText = token.value;
833 return part;
834 }
835
836 // Get the token stream. If from is null, we start with a new
837 // parser from the start of the frame, otherwise a partial parse
838 // is resumed.
839 var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
840 stream = multiStringStream(traversal),
841 parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
842
843 // parts is an interface to make it possible to 'delay' fetching
844 // the next DOM node until we are completely done with the one
845 // before it. This is necessary because often the next node is
846 // not yet available when we want to proceed past the current
847 // one.
848 var parts = {
849 current: null,
850 // Fetch current node.
851 get: function(){
852 if (!this.current)
853 this.current = traversal.nodes.shift();
854 return this.current;
855 },
856 // Advance to the next part (do not fetch it yet).
857 next: function(){
858 this.current = null;
859 },
860 // Remove the current part from the DOM tree, and move to the
861 // next.
862 remove: function(){
863 container.removeChild(this.get());
864 this.current = null;
865 },
866 // Advance to the next part that is not empty, discarding empty
867 // parts.
868 getNonEmpty: function(){
869 var part = this.get();
870 while (part.nodeName == "SPAN" && part.currentText == ""){
871 var old = part;
872 this.remove();
873 part = this.get();
874 // Adjust selection information, if any. See select.js for
875 // details.
876 select.replaceSelection(old.firstChild, part.firstChild || part, 0, 0);
877 }
878 return part;
879 }
880 };
881
882 var lineDirty = false, lineHasNodes = false;;
883 this.history.touch(from);
884
885 // This forEach loops over the tokens from the parsed stream, and
886 // at the same time uses the parts object to proceed through the
887 // corresponding DOM nodes.
888 forEach(parsed, function(token){
889 var part = parts.getNonEmpty();
890
891 if (token.value == "\n"){
892 // The idea of the two streams actually staying synchronized
893 // is such a long shot that we explicitly check.
894 if (part.nodeName != "BR")
895 throw "Parser out of sync. Expected BR.";
896
897 if (part.dirty || !part.indentation)
898 lineDirty = true;
899 self.history.touch(part);
900
901 // Every <br> gets a copy of the parser state and a lexical
902 // context assigned to it. The first is used to be able to
903 // later resume parsing from this point, the second is used
904 // for indentation.
905 part.parserFromHere = parsed.copy();
906 part.indentation = token.indentation;
907 part.dirty = false;
908 // A clean line means we are done. Throwing a StopIteration is
909 // the way to break out of a MochiKit forEach loop.
910 if ((lines !== undefined && --lines <= 0) || (!lineDirty && lineHasNodes && !cleanLines))
911 throw StopIteration;
912 lineDirty = false; lineHasNodes = false;
913 parts.next();
914 }
915 else {
916 if (part.nodeName != "SPAN")
917 throw "Parser out of sync. Expected SPAN.";
918 if (part.dirty)
919 lineDirty = true;
920 lineHasNodes = true;
921
922 // If the part matches the token, we can leave it alone.
923 if (correctPart(token, part)){
924 part.dirty = false;
925 parts.next();
926 }
927 // Otherwise, we have to fix it.
928 else {
929 lineDirty = true;
930 // Insert the correct part.
931 var newPart = tokenPart(token);
932 container.insertBefore(newPart, part);
933 var tokensize = token.value.length;
934 var offset = 0;
935 // Eat up parts until the text for this token has been
936 // removed, adjusting the stored selection info (see
937 // select.js) in the process.
938 while (tokensize > 0) {
939 part = parts.get();
940 var partsize = part.currentText.length;
941 select.replaceSelection(part.firstChild, newPart.firstChild, tokensize, offset);
942 if (partsize > tokensize){
943 shortenPart(part, tokensize);
944 tokensize = 0;
945 }
946 else {
947 tokensize -= partsize;
948 offset += partsize;
949 parts.remove();
950 }
951 }
952 }
953 }
954 });
955
956 // The function returns some status information that is used by
957 // hightlightDirty to determine whether and where it has to
958 // continue.
959 return {left: lines,
960 node: parts.get(),
961 dirty: lineDirty};
962 }
963 };
964
965 return Editor;
966 })();
967
968 addEventHandler(window, "load", function() {
969 var CodeMirror = window.frameElement.CodeMirror;
970 CodeMirror.editor = new Editor(CodeMirror.options);
971 if (CodeMirror.options.initCallback) {
972 this.parent.setTimeout(function(){
973 CodeMirror.options.initCallback(CodeMirror);
974 }, 0);
975 }
976 });