1
<html xmlns="http://www.w3.org/1999/xhtml">
3
<script src="js/codemirror.js" type="text/javascript"></script>
4
<title>CodeMirror: JavaScript demonstration</title>
5
<link rel="stylesheet" type="text/css" href="css/docs.css"/>
7
<body style="padding: 20px;">
9
<p>This page demonstrates <a href="index.html">CodeMirror</a>'s
10
JavaScript parser. Note that the ugly buttons at the top are not are
11
not part of CodeMirror proper -- they demonstrate the way it can be
12
embedded in a web-application.</p>
15
<textarea id="code" cols="120" rows="30">
16
/* The Editor object manages the content of the editable frame. It
17
* catches events, colours nodes, and indents lines. This file also
18
* holds some functions for transforming arbitrary DOM structures into
19
* plain sequences of <span> and <br> elements
22
// Make sure a string does not contain two consecutive 'collapseable'
23
// whitespace characters.
24
function makeWhiteSpace(n) {
25
var buffer = [], nb = true;
27
buffer.push((nb || n == 1) ? nbsp : " ");
30
return buffer.join("");
33
// Create a set of white-space characters that will not be collapsed
34
// by the browser, but will not break text-wrapping either.
35
function fixSpaces(string) {
36
if (string.charAt(0) == " ") string = nbsp + string.slice(1);
37
return string.replace(/[\t \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
40
function cleanText(text) {
41
return text.replace(/\u00a0/g, " ").replace(/\u200b/g, "");
44
// Create a SPAN node with the expected properties for document part
46
function makePartSpan(value, doc) {
48
if (value.nodeType == 3) text = value.nodeValue;
49
else value = doc.createTextNode(text);
51
var span = doc.createElement("SPAN");
53
span.appendChild(value);
54
span.currentText = text;
58
// On webkit, when the last BR of the document does not have text
59
// behind it, the cursor can not be put on the line after it. This
60
// makes pressing enter at the end of the document occasionally do
61
// nothing (or at least seem to do nothing). To work around it, this
62
// function makes sure the document ends with a span containing a
63
// zero-width space character. The traverseDOM iterator filters such
64
// character out again, so that the parsers won't see them. This
65
// function is called from a few strategic places to make sure the
66
// zwsp is restored after the highlighting process eats it.
67
var webkitLastLineHack = webkit ?
69
var last = container.lastChild;
70
if (!last || !last.isPart || last.textContent != "\u200b")
71
container.appendChild(makePartSpan("\u200b", container.ownerDocument));
74
var Editor = (function(){
75
// The HTML elements whose content should be suffixed by a newline
76
// when converting them to flat text.
77
var newlineElements = {"P": true, "DIV": true, "LI": true};
79
function asEditorLines(string) {
80
return fixSpaces(string.replace(/\t/g, " ").replace(/\u00a0/g, " ")).replace(/\r\n?/g, "\n").split("\n");
83
// Helper function for traverseDOM. Flattens an arbitrary DOM node
84
// into an array of textnodes and <br> tags.
85
function simplifyDOM(root) {
86
var doc = root.ownerDocument;
90
function simplifyNode(node) {
91
if (node.nodeType == 3) {
92
var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " "));
93
if (text.length) leaving = false;
96
else if (node.nodeName == "BR" && node.childNodes.length == 0) {
101
forEach(node.childNodes, simplifyNode);
102
if (!leaving && newlineElements.hasOwnProperty(node.nodeName)) {
104
result.push(doc.createElement("BR"));
113
// Creates a MochiKit-style iterator that goes over a series of DOM
114
// nodes. The values it yields are strings, the textual content of
115
// the nodes. It makes sure that all nodes up to and including the
116
// one whose text is being yielded have been 'normalized' to be just
117
// <span> and <br> elements.
118
// See the story.html file for some short remarks about the use of
119
// continuation-passing style in this iterator.
120
function traverseDOM(start){
121
function yield(value, c){cc = c; return value;}
122
function push(fun, arg, c){return function(){return fun(arg, c);};}
123
function stop(){cc = stop; throw StopIteration;};
124
var cc = push(scanNode, start, stop);
125
var owner = start.ownerDocument;
128
// Create a function that can be used to insert nodes after the
129
// one given as argument.
130
function pointAt(node){
131
var parent = node.parentNode;
132
var next = node.nextSibling;
133
return function(newnode) {
134
parent.insertBefore(newnode, next);
139
// Insert a normalized node at the current point. If it is a text
140
// node, wrap it in a <span>, and give that span a currentText
141
// property -- this is used to cache the nodeValue, because
142
// directly accessing nodeValue is horribly slow on some browsers.
143
// The dirty property is used by the highlighter to determine
144
// which parts of the document have to be re-highlighted.
145
function insertPart(part){
147
if (part.nodeType == 3) {
148
select.snapshotChanged();
149
part = makePartSpan(part, owner);
150
text = part.currentText;
153
nodeQueue.push(part);
158
// Extract the text and newlines from a DOM node, insert them into
159
// the document, and yield the textual content. Used to replace
160
// non-normalized nodes.
161
function writeNode(node, c){
163
forEach(simplifyDOM(node), function(part) {
164
toYield.push(insertPart(part));
166
return yield(toYield.join(""), c);
169
// Check whether a node is a normalized <span> element.
170
function partNode(node){
171
if (node.isPart && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
172
node.currentText = node.firstChild.nodeValue;
173
return !/[\n\t\r]/.test(node.currentText);
178
// Handle a node. Add its successor to the continuation if there
179
// is one, find out whether the node is normalized. If it is,
180
// yield its content, otherwise, normalize it (writeNode will take
181
// care of yielding).
182
function scanNode(node, c){
183
if (node.nextSibling)
184
c = push(scanNode, node.nextSibling, c);
187
nodeQueue.push(node);
188
return yield(node.currentText, c);
190
else if (node.nodeName == "BR") {
191
nodeQueue.push(node);
192
return yield("\n", c);
195
point = pointAt(node);
197
return writeNode(node, c);
201
// MochiKit iterators are objects with a next function that
202
// returns the next value or throws StopIteration when there are
204
return {next: function(){return cc();}, nodes: nodeQueue};
207
// Determine the text size of a processed node.
208
function nodeSize(node) {
209
if (node.nodeName == "BR")
212
return node.currentText.length;
215
// Search backwards through the top-level nodes until the next BR or
216
// the start of the frame.
217
function startOfLine(node) {
218
while (node && node.nodeName != "BR") node = node.previousSibling;
221
function endOfLine(node, container) {
222
if (!node) node = container.firstChild;
223
else if (node.nodeName == "BR") node = node.nextSibling;
225
while (node && node.nodeName != "BR") node = node.nextSibling;
229
// Replace all DOM nodes in the current selection with new ones.
230
// Needed to prevent issues in IE where the old DOM nodes can be
231
// pasted back into the document, still holding their old undo
233
function scrubPasted(container, start, start2) {
234
var end = select.selectionTopNode(container, true),
235
doc = container.ownerDocument;
236
if (start != null && start.parentNode != container) start = start2;
237
if (start === false) start = null;
238
if (start == end || !end || !container.firstChild) return;
240
var clear = traverseDOM(start ? start.nextSibling : container.firstChild);
241
while (end.parentNode == container) try{clear.next();}catch(e){break;}
242
forEach(clear.nodes, function(node) {
243
var newNode = node.nodeName == "BR" ? doc.createElement("BR") : makePartSpan(node.currentText, doc);
244
container.replaceChild(newNode, node);
248
// Client interface for searching the content of the editor. Create
249
// these by calling CodeMirror.getSearchCursor. To use, call
250
// findNext on the resulting object -- this returns a boolean
251
// indicating whether anything was found, and can be called again to
252
// skip to the next find. Use the select and replace methods to
253
// actually do something with the found locations.
254
function SearchCursor(editor, string, fromCursor) {
255
this.editor = editor;
256
this.history = editor.history;
257
this.history.commit();
259
// Are we currently at an occurrence of the search string?
260
this.atOccurrence = false;
261
// The object stores a set of nodes coming after its current
262
// position, so that when the current point is taken out of the
263
// DOM tree, we can still try to continue.
264
this.fallbackSize = 15;
266
// Start from the cursor when specified and a cursor can be found.
267
if (fromCursor && (cursor = select.cursorPos(this.editor.container))) {
268
this.line = cursor.node;
269
this.offset = cursor.offset;
275
this.valid = !!string;
277
// Create a matcher function based on the kind of string we have.
278
var target = string.split("\n"), self = this;;
279
this.matches = (target.length == 1) ?
280
// For one-line strings, searching can be done simply by calling
281
// indexOf on the current line.
283
var match = cleanText(self.history.textAfter(self.line).slice(self.offset)).indexOf(string);
285
return {from: {node: self.line, offset: self.offset + match},
286
to: {node: self.line, offset: self.offset + match + string.length}};
288
// Multi-line strings require internal iteration over lines, and
289
// some clunky checks to make sure the first match ends at the
290
// end of the line and the last match starts at the start.
292
var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
293
var match = firstLine.lastIndexOf(target[0]);
294
if (match == -1 || match != firstLine.length - target[0].length)
296
var startOffset = self.offset + match;
298
var line = self.history.nodeAfter(self.line);
299
for (var i = 1; i < target.length - 1; i++) {
300
if (cleanText(self.history.textAfter(line)) != target[i])
302
line = self.history.nodeAfter(line);
305
if (cleanText(self.history.textAfter(line)).indexOf(target[target.length - 1]) != 0)
308
return {from: {node: self.line, offset: startOffset},
309
to: {node: line, offset: target[target.length - 1].length}};
313
SearchCursor.prototype = {
314
findNext: function() {
315
if (!this.valid) return false;
316
this.atOccurrence = false;
319
// Go back to the start of the document if the current line is
320
// no longer in the DOM tree.
321
if (this.line && !this.line.parentNode) {
326
// Set the cursor's position one character after the given
328
function saveAfter(pos) {
329
if (self.history.textAfter(pos.node).length < pos.offset) {
330
self.line = pos.node;
331
self.offset = pos.offset + 1;
334
self.line = self.history.nodeAfter(pos.node);
340
var match = this.matches();
341
// Found the search string.
343
this.atOccurrence = match;
344
saveAfter(match.from);
347
this.line = this.history.nodeAfter(this.line);
358
if (this.atOccurrence) {
359
select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
360
select.scrollToCursor(this.editor.container);
364
replace: function(string) {
365
if (this.atOccurrence) {
366
var end = this.editor.replaceRange(this.atOccurrence.from, this.atOccurrence.to, string);
367
this.line = end.node;
368
this.offset = end.offset;
369
this.atOccurrence = false;
374
// The Editor object is the main inside-the-iframe interface.
375
function Editor(options) {
376
this.options = options;
377
window.indentUnit = options.indentUnit;
378
this.parent = parent;
380
var container = this.container = this.doc.body;
382
this.history = new History(container, options.undoDepth, options.undoDelay,
383
this, options.onChange);
387
throw "No parser loaded.";
388
if (options.parserConfig && Editor.Parser.configure)
389
Editor.Parser.configure(options.parserConfig);
391
if (!options.readOnly)
392
select.setCursorPos(container, {node: null, offset: 0});
396
this.importCode(options.content);
397
else // FF acts weird when the editable document is completely empty
398
container.appendChild(this.doc.createElement("BR"));
400
if (!options.readOnly) {
401
if (options.continuousScanning !== false) {
402
this.scanner = this.documentScanner(options.linesPerPass);
403
this.delayScanning();
406
function setEditable() {
407
// In IE, designMode frames can not run any scripts, so we use
408
// contentEditable instead.
409
if (document.body.contentEditable != undefined && internetExplorer)
410
document.body.contentEditable = "true";
412
document.designMode = "on";
414
document.documentElement.style.borderWidth = "0";
415
if (!options.textWrapping)
416
container.style.whiteSpace = "nowrap";
419
// If setting the frame editable fails, try again when the user
420
// focus it (happens when the frame is not visible on
421
// initialisation, in Firefox).
426
var focusEvent = addEventHandler(document, "focus", function() {
432
addEventHandler(document, "keydown", method(this, "keyDown"));
433
addEventHandler(document, "keypress", method(this, "keyPress"));
434
addEventHandler(document, "keyup", method(this, "keyUp"));
436
function cursorActivity() {self.cursorActivity(false);}
437
addEventHandler(document.body, "mouseup", cursorActivity);
438
addEventHandler(document.body, "paste", function(event) {
440
if (internetExplorer) {
442
try {text = window.clipboardData.getData("Text");}catch(e){}
444
self.replaceSelection(text);
448
var start = select.selectionTopNode(self.container, true),
449
start2 = start && start.previousSibling;
450
setTimeout(function(){scrubPasted(self.container, start, start2);}, 0);
454
addEventHandler(document.body, "cut", cursorActivity);
456
if (this.options.autoMatchParens)
457
addEventHandler(document.body, "click", method(this, "scheduleParenBlink"));
461
function isSafeKey(code) {
462
return (code >= 16 && code <= 18) || // shift, control, alt
463
(code >= 33 && code <= 40); // arrows, home, end
467
// Import a piece of code into the editor.
468
importCode: function(code) {
469
this.history.push(null, null, asEditorLines(code));
470
this.history.reset();
473
// Extract the code from the editor.
474
getCode: function() {
475
if (!this.container.firstChild)
479
select.markSelection(this.win);
480
forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
481
webkitLastLineHack(this.container);
482
select.selectMarked();
483
return cleanText(accum.join(""));
486
checkLine: function(node) {
487
if (node === false || !(node == null || node.parentNode == this.container))
488
throw parent.CodeMirror.InvalidLineHandle;
491
cursorPosition: function(start) {
492
if (start == null) start = true;
493
var pos = select.cursorPos(this.container, start);
494
if (pos) return {line: pos.node, character: pos.offset};
495
else return {line: null, character: 0};
498
firstLine: function() {
502
lastLine: function() {
503
if (this.container.lastChild) return startOfLine(this.container.lastChild);
507
nextLine: function(line) {
508
this.checkLine(line);
509
var end = endOfLine(line, this.container);
513
prevLine: function(line) {
514
this.checkLine(line);
515
if (line == null) return false;
516
return startOfLine(line.previousSibling);
519
selectLines: function(startLine, startOffset, endLine, endOffset) {
520
this.checkLine(startLine);
521
var start = {node: startLine, offset: startOffset}, end = null;
522
if (endOffset !== undefined) {
523
this.checkLine(endLine);
524
end = {node: endLine, offset: endOffset};
526
select.setCursorPos(this.container, start, end);
527
select.scrollToCursor(this.container);
530
lineContent: function(line) {
531
this.checkLine(line);
533
for (line = line ? line.nextSibling : this.container.firstChild;
534
line && line.nodeName != "BR"; line = line.nextSibling)
535
accum.push(nodeText(line));
536
return cleanText(accum.join(""));
539
setLineContent: function(line, content) {
540
this.history.commit();
541
this.replaceRange({node: line, offset: 0},
542
{node: line, offset: this.history.textAfter(line).length},
544
this.addDirtyNode(line);
545
this.scheduleHighlight();
548
insertIntoLine: function(line, position, content) {
550
if (position == "end") {
551
before = endOfLine(line, this.container);
554
for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
559
var text = (cur.innerText || cur.textContent || cur.nodeValue || "");
560
if (text.length > position) {
561
before = cur.nextSibling;
562
content = text.slice(0, position) + content + text.slice(position);
566
position -= text.length;
570
var lines = asEditorLines(content), doc = this.container.ownerDocument;
571
for (var i = 0; i < lines.length; i++) {
572
if (i > 0) this.container.insertBefore(doc.createElement("BR"), before);
573
this.container.insertBefore(makePartSpan(lines[i], doc), before);
575
this.addDirtyNode(line);
576
this.scheduleHighlight();
579
// Retrieve the selected text.
580
selectedText: function() {
581
var h = this.history;
584
var start = select.cursorPos(this.container, true),
585
end = select.cursorPos(this.container, false);
586
if (!start || !end) return "";
588
if (start.node == end.node)
589
return h.textAfter(start.node).slice(start.offset, end.offset);
591
var text = [h.textAfter(start.node).slice(start.offset)];
592
for (pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos))
593
text.push(h.textAfter(pos));
594
text.push(h.textAfter(end.node).slice(0, end.offset));
595
return cleanText(text.join("\n"));
598
// Replace the selection with another piece of text.
599
replaceSelection: function(text) {
600
this.history.commit();
601
var start = select.cursorPos(this.container, true),
602
end = select.cursorPos(this.container, false);
603
if (!start || !end) return;
605
end = this.replaceRange(start, end, text);
606
select.setCursorPos(this.container, start, end);
609
replaceRange: function(from, to, text) {
610
var lines = asEditorLines(text);
611
lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
612
var lastLine = lines[lines.length - 1];
613
lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
614
var end = this.history.nodeAfter(to.node);
615
this.history.push(from.node, end, lines);
616
return {node: this.history.nodeBefore(end),
617
offset: lastLine.length};
620
getSearchCursor: function(string, fromCursor) {
621
return new SearchCursor(this, string, fromCursor);
624
// Re-indent the whole buffer
625
reindent: function() {
626
if (this.container.firstChild)
627
this.indentRegion(null, this.container.lastChild);
630
grabKeys: function(eventHandler, filter) {
631
this.frozen = eventHandler;
632
this.keyFilter = filter;
634
ungrabKeys: function() {
635
this.frozen = "leave";
636
this.keyFilter = null;
639
// Intercept enter and tab, and assign their new functions.
640
keyDown: function(event) {
641
if (this.frozen == "leave") this.frozen = null;
642
if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode))) {
648
var code = event.keyCode;
649
// Don't scan when the user is typing.
650
this.delayScanning();
651
// Schedule a paren-highlight event, if configured.
652
if (this.options.autoMatchParens)
653
this.scheduleParenBlink();
655
if (code == 13) { // enter
657
this.reparseBuffer();
660
select.insertNewlineAtCursor(this.win);
661
this.indentAtCursor();
662
select.scrollToCursor(this.container);
666
else if (code == 9 && this.options.tabMode != "default") { // tab
667
this.handleTab(!event.ctrlKey && !event.shiftKey);
670
else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
671
this.handleTab(true);
674
else if ((code == 219 || code == 221) && event.ctrlKey) {
675
this.blinkParens(event.shiftKey);
678
else if (event.metaKey && (code == 37 || code == 39)) { // Meta-left/right
679
var cursor = select.selectionTopNode(this.container);
680
if (cursor === false || !this.container.firstChild) return;
682
if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
684
end = endOfLine(cursor, this.container);
685
select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
689
else if (event.ctrlKey || event.metaKey) {
690
if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y
691
select.scrollToNode(this.history.redo());
694
else if (code == 90 || code == 8) { // Z, backspace
695
select.scrollToNode(this.history.undo());
698
else if (code == 83 && this.options.saveFunction) { // S
699
this.options.saveFunction();
705
// Check for characters that should re-indent the current line,
706
// and prevent Opera from handling enter and tab anyway.
707
keyPress: function(event) {
708
var electric = /indent|default/.test(this.options.tabMode) && Editor.Parser.electricChars;
709
// Hack for Opera, and Firefox on OS X, in which stopping a
710
// keydown event does not prevent the associated keypress event
711
// from happening, so we have to cancel enter and tab again
713
if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode))) ||
714
event.code == 13 || (event.code == 9 && this.options.tabMode != "default") ||
715
(event.keyCode == 32 && event.shiftKey && this.options.tabMode == "default"))
717
else if (electric && electric.indexOf(event.character) != -1)
718
this.parent.setTimeout(method(this, "indentAtCursor"), 0);
721
// Mark the node at the cursor dirty when a non-safe key is
723
keyUp: function(event) {
724
this.cursorActivity(isSafeKey(event.keyCode));
727
// Indent the line following a given <br>, or null for the first
728
// line. If given a <br> element, this must have been highlighted
729
// so that it has an indentation method. Returns the whitespace
730
// element that has been modified or created (if any).
731
indentLineAfter: function(start, direction) {
732
// whiteSpace is the whitespace span at the start of the line,
733
// or null if there is no such node.
734
var whiteSpace = start ? start.nextSibling : this.container.firstChild;
735
if (whiteSpace && !hasClass(whiteSpace, "whitespace"))
738
// Sometimes the start of the line can influence the correct
739
// indentation, so we retrieve it.
740
var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
741
var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
743
// Ask the lexical context for the correct indentation, and
744
// compute how much this differs from the current indentation.
745
var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
746
if (direction != null && this.options.tabMode == "shift")
747
newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
749
newIndent = start.indentation(nextChars, curIndent, direction);
750
else if (Editor.Parser.firstIndentation)
751
newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
752
var indentDiff = newIndent - curIndent;
754
// If there is too much, this is just a matter of shrinking a span.
755
if (indentDiff < 0) {
756
if (newIndent == 0) {
757
if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0);
758
removeElement(whiteSpace);
762
select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
763
whiteSpace.currentText = makeWhiteSpace(newIndent);
764
whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
768
else if (indentDiff > 0) {
769
// If there is whitespace, we grow it.
771
whiteSpace.currentText = makeWhiteSpace(newIndent);
772
whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
774
// Otherwise, we have to add a new whitespace node.
776
whiteSpace = makePartSpan(makeWhiteSpace(newIndent), this.doc);
777
whiteSpace.className = "whitespace";
778
if (start) insertAfter(whiteSpace, start);
779
else this.container.insertBefore(whiteSpace, this.container.firstChild);
781
if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
783
if (indentDiff != 0) this.addDirtyNode(start);
787
// Re-highlight the selected part of the document.
788
highlightAtCursor: function() {
789
var pos = select.selectionTopNode(this.container, true);
790
var to = select.selectionTopNode(this.container, false);
791
if (pos === false || to === false) return;
793
select.markSelection(this.win);
794
if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
796
select.selectMarked();
800
// When tab is pressed with text selected, the whole selection is
801
// re-indented, when nothing is selected, the line with the cursor
803
handleTab: function(direction) {
804
if (this.options.tabMode == "spaces") {
805
select.insertTabAtCursor(this.win);
807
else if (!select.somethingSelected(this.win)) {
808
this.indentAtCursor(direction);
811
var start = select.selectionTopNode(this.container, true),
812
end = select.selectionTopNode(this.container, false);
813
if (start === false || end === false) return;
814
this.indentRegion(start, end, direction);
818
// Delay (or initiate) the next paren blink event.
819
scheduleParenBlink: function() {
820
if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
822
this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300);
825
// Take the token before the cursor. If it contains a character in
826
// '()[]{}', search for the matching paren/brace/bracket, and
827
// highlight them in green for a moment, or red if no proper match
829
blinkParens: function(jump) {
830
// Clear the event property.
831
if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
832
this.parenEvent = null;
834
// Extract a 'paren' from a piece of text.
835
function paren(node) {
836
if (node.currentText) {
837
var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
838
return match && match[1];
841
// Determine the direction a paren is facing.
842
function forward(ch) {
843
return /[\(\[\{]/.test(ch);
846
var ch, self = this, cursor = select.selectionTopNode(this.container, true);
847
if (!cursor || !this.highlightAtCursor()) return;
848
cursor = select.selectionTopNode(this.container, true);
849
if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor)))))
851
// We only look for tokens with the same className.
852
var className = cursor.className, dir = forward(ch), match = matching[ch];
854
// Since parts of the document might not have been properly
855
// highlighted, and it is hard to know in advance which part we
856
// have to scan, we just try, and when we find dirty nodes we
857
// abort, parse them, and re-try.
858
function tryFindMatch() {
859
var stack = [], ch, ok = true;;
860
for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
861
if (runner.className == className && runner.nodeName == "SPAN" && (ch = paren(runner))) {
862
if (forward(ch) == dir)
864
else if (!stack.length)
866
else if (stack.pop() != matching[ch])
868
if (!stack.length) break;
870
else if (runner.dirty || runner.nodeName != "SPAN" && runner.nodeName != "BR") {
871
return {node: runner, status: "dirty"};
874
return {node: runner, status: runner && ok};
876
// Temporarily give the relevant nodes a colour.
877
function blink(node, ok) {
878
node.style.fontWeight = "bold";
879
node.style.color = ok ? "#8F8" : "#F88";
880
self.parent.setTimeout(function() {node.style.fontWeight = ""; node.style.color = "";}, 500);
884
var found = tryFindMatch();
885
if (found.status == "dirty") {
886
this.highlight(found.node, 1);
887
// Needed because in some corner cases a highlight does not
889
found.node.dirty = false;
893
blink(cursor, found.status);
895
blink(found.node, found.status);
896
if (jump) select.focusAfterNode(found.node.previousSibling, this.container);
903
// Adjust the amount of whitespace at the start of the line that
904
// the cursor is on so that it is indented properly.
905
indentAtCursor: function(direction) {
906
if (!this.container.firstChild) return;
907
// The line has to have up-to-date lexical information, so we
908
// highlight it first.
909
if (!this.highlightAtCursor()) return;
910
var cursor = select.selectionTopNode(this.container, false);
911
// If we couldn't determine the place of the cursor,
912
// there's nothing to indent.
913
if (cursor === false)
915
var lineStart = startOfLine(cursor);
916
var whiteSpace = this.indentLineAfter(lineStart, direction);
917
if (cursor == lineStart && whiteSpace)
919
// This means the indentation has probably messed up the cursor.
920
if (cursor == whiteSpace)
921
select.focusAfterNode(cursor, this.container);
924
// Indent all lines whose start falls inside of the current
926
indentRegion: function(start, end, direction) {
927
var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling);
928
if (end.nodeName != "BR") end = endOfLine(end, this.container);
931
if (current) this.highlight(before, current, true);
932
this.indentLineAfter(current, direction);
934
current = endOfLine(current, this.container);
935
} while (current != end);
936
select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
939
// Find the node that the cursor is in, mark it as dirty, and make
940
// sure a highlight pass is scheduled.
941
cursorActivity: function(safe) {
942
if (internetExplorer) {
943
this.container.createTextRange().execCommand("unlink");
944
this.selectionSnapshot = select.selectionCoords(this.win);
947
var activity = this.options.cursorActivity;
948
if (!safe || activity) {
949
var cursor = select.selectionTopNode(this.container, false);
950
if (cursor === false || !this.container.firstChild) return;
951
cursor = cursor || this.container.firstChild;
952
if (activity) activity(cursor);
954
this.scheduleHighlight();
955
this.addDirtyNode(cursor);
960
reparseBuffer: function() {
961
forEach(this.container.childNodes, function(node) {node.dirty = true;});
962
if (this.container.firstChild)
963
this.addDirtyNode(this.container.firstChild);
966
// Add a node to the set of dirty nodes, if it isn't already in
968
addDirtyNode: function(node) {
969
node = node || this.container.firstChild;
972
for (var i = 0; i < this.dirty.length; i++)
973
if (this.dirty[i] == node) return;
975
if (node.nodeType != 3)
977
this.dirty.push(node);
980
// Cause a highlight pass to happen in options.passDelay
981
// milliseconds. Clear the existing timeout, if one exists. This
982
// way, the passes do not happen while the user is typing, and
983
// should as unobtrusive as possible.
984
scheduleHighlight: function() {
985
// Timeouts are routed through the parent window, because on
986
// some browsers designMode windows do not fire timeouts.
988
this.parent.clearTimeout(this.highlightTimeout);
989
this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
992
// Fetch one dirty node, and remove it from the dirty set.
993
getDirtyNode: function() {
994
while (this.dirty.length > 0) {
995
var found = this.dirty.pop();
996
// IE8 sometimes throws an unexplainable 'invalid argument'
997
// exception for found.parentNode
999
// If the node has been coloured in the meantime, or is no
1000
// longer in the document, it should not be returned.
1001
while (found && found.parentNode != this.container)
1002
found = found.parentNode
1003
if (found && (found.dirty || found.nodeType == 3))
1010
// Pick dirty nodes, and highlight them, until
1011
// options.linesPerPass lines have been highlighted. The highlight
1012
// method will continue to next lines as long as it finds dirty
1013
// nodes. It returns an object indicating the amount of lines
1014
// left, and information about the place where it stopped. If
1015
// there are dirty nodes left after this function has spent all
1016
// its lines, it shedules another highlight to finish the job.
1017
highlightDirty: function(force) {
1018
// Prevent FF from raising an error when it is firing timeouts
1019
// on a page that's no longer loaded.
1020
if (!window.select) return;
1022
var lines = force ? Infinity : this.options.linesPerPass;
1023
if (!this.options.readOnly) select.markSelection(this.win);
1025
while (lines > 0 && (start = this.getDirtyNode())){
1026
var result = this.highlight(start, lines);
1028
lines = result.left;
1029
if (result.node && result.dirty)
1030
this.addDirtyNode(result.node);
1033
if (!this.options.readOnly) select.selectMarked();
1035
this.scheduleHighlight();
1036
return this.dirty.length == 0;
1039
// Creates a function that, when called through a timeout, will
1040
// continuously re-parse the document.
1041
documentScanner: function(linesPer) {
1042
var self = this, pos = null;
1044
// If the current node is no longer in the document... oh
1045
// well, we start over.
1046
if (pos && pos.parentNode != self.container)
1048
select.markSelection(self.win);
1049
var result = self.highlight(pos, linesPer, true);
1050
select.selectMarked();
1051
var newPos = result ? (result.node && result.node.nextSibling) : null;
1052
pos = (pos == newPos) ? null : newPos;
1053
self.delayScanning();
1057
// Starts the continuous scanning process for this document after
1058
// a given interval.
1059
delayScanning: function() {
1061
this.parent.clearTimeout(this.documentScan);
1062
this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
1066
// The function that does the actual highlighting/colouring (with
1067
// help from the parser and the DOM normalizer). Its interface is
1068
// rather overcomplicated, because it is used in different
1069
// situations: ensuring that a certain line is highlighted, or
1070
// highlighting up to X lines starting from a certain point. The
1071
// 'from' argument gives the node at which it should start. If
1072
// this is null, it will start at the beginning of the document.
1073
// When a number of lines is given with the 'target' argument, it
1074
// will highlight no more than that amount of lines. If this
1075
// argument holds a DOM node, it will highlight until it reaches
1076
// that node. If at any time it comes across a 'clean' line (no
1077
// dirty nodes), it will stop, except when 'cleanLines' is true.
1078
highlight: function(from, target, cleanLines, maxBacktrack){
1079
var container = this.container, self = this, active = this.options.activeTokens;
1080
var lines = (typeof target == "number" ? target : null);
1082
if (!container.firstChild)
1084
// Backtrack to the first node before from that has a partial
1086
while (from && (!from.parserFromHere || from.dirty)) {
1087
from = from.previousSibling;
1088
if (maxBacktrack != null && from.nodeName == "BR" && (--maxBacktrack) < 0)
1091
// If we are at the end of the document, do nothing.
1092
if (from && !from.nextSibling)
1095
// Check whether a part (<span> node) and the corresponding token
1097
function correctPart(token, part){
1098
return !part.reduced && part.currentText == token.value && part.className == token.style;
1100
// Shorten the text associated with a part by chopping off
1101
// characters from the front. Note that only the currentText
1102
// property gets changed. For efficiency reasons, we leave the
1103
// nodeValue alone -- we set the reduced flag to indicate that
1104
// this part must be replaced.
1105
function shortenPart(part, minus){
1106
part.currentText = part.currentText.substring(minus);
1107
part.reduced = true;
1109
// Create a part corresponding to a given token.
1110
function tokenPart(token){
1111
var part = makePartSpan(token.value, self.doc);
1112
part.className = token.style;
1116
function maybeTouch(node) {
1118
if (node.nextSibling != node.oldNextSibling) {
1119
self.history.touch(node);
1120
node.oldNextSibling = node.nextSibling;
1124
if (self.container.firstChild != self.container.oldFirstChild) {
1125
self.history.touch(node);
1126
self.container.oldFirstChild = self.container.firstChild;
1131
// Get the token stream. If from is null, we start with a new
1132
// parser from the start of the frame, otherwise a partial parse
1134
var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
1135
stream = stringStream(traversal),
1136
parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
1138
// parts is an interface to make it possible to 'delay' fetching
1139
// the next DOM node until we are completely done with the one
1140
// before it. This is necessary because often the next node is
1141
// not yet available when we want to proceed past the current
1145
// Fetch current node.
1148
this.current = traversal.nodes.shift();
1149
return this.current;
1151
// Advance to the next part (do not fetch it yet).
1153
this.current = null;
1155
// Remove the current part from the DOM tree, and move to the
1158
container.removeChild(this.get());
1159
this.current = null;
1161
// Advance to the next part that is not empty, discarding empty
1163
getNonEmpty: function(){
1164
var part = this.get();
1165
// Allow empty nodes when they are alone on a line, needed
1166
// for the FF cursor bug workaround (see select.js,
1167
// insertNewlineAtCursor).
1168
while (part && part.nodeName == "SPAN" && part.currentText == "") {
1172
// Adjust selection information, if any. See select.js for details.
1173
select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
1179
var lineDirty = false, prevLineDirty = true, lineNodes = 0;
1181
// This forEach loops over the tokens from the parsed stream, and
1182
// at the same time uses the parts object to proceed through the
1183
// corresponding DOM nodes.
1184
forEach(parsed, function(token){
1185
var part = parts.getNonEmpty();
1187
if (token.value == "\n"){
1188
// The idea of the two streams actually staying synchronized
1189
// is such a long shot that we explicitly check.
1190
if (part.nodeName != "BR")
1191
throw "Parser out of sync. Expected BR.";
1193
if (part.dirty || !part.indentation) lineDirty = true;
1197
// Every <br> gets a copy of the parser state and a lexical
1198
// context assigned to it. The first is used to be able to
1199
// later resume parsing from this point, the second is used
1201
part.parserFromHere = parsed.copy();
1202
part.indentation = token.indentation;
1205
// If the target argument wasn't an integer, go at least
1207
if (lines == null && part == target) throw StopIteration;
1209
// A clean line with more than one node means we are done.
1210
// Throwing a StopIteration is the way to break out of a
1211
// MochiKit forEach loop.
1212
if ((lines != null && --lines <= 0) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines))
1213
throw StopIteration;
1214
prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
1218
if (part.nodeName != "SPAN")
1219
throw "Parser out of sync. Expected SPAN.";
1224
// If the part matches the token, we can leave it alone.
1225
if (correctPart(token, part)){
1229
// Otherwise, we have to fix it.
1232
// Insert the correct part.
1233
var newPart = tokenPart(token);
1234
container.insertBefore(newPart, part);
1235
if (active) active(newPart, token, self);
1236
var tokensize = token.value.length;
1238
// Eat up parts until the text for this token has been
1239
// removed, adjusting the stored selection info (see
1240
// select.js) in the process.
1241
while (tokensize > 0) {
1243
var partsize = part.currentText.length;
1244
select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
1245
if (partsize > tokensize){
1246
shortenPart(part, tokensize);
1250
tokensize -= partsize;
1259
webkitLastLineHack(this.container);
1261
// The function returns some status information that is used by
1262
// hightlightDirty to determine whether and where it has to
1264
return {left: lines,
1265
node: parts.getNonEmpty(),
1273
addEventHandler(window, "load", function() {
1274
var CodeMirror = window.frameElement.CodeMirror;
1275
CodeMirror.editor = new Editor(CodeMirror.options);
1276
this.parent.setTimeout(method(CodeMirror, "init"), 0);
1281
<script type="text/javascript">
1282
var textarea = document.getElementById('code');
1283
var editor = new CodeMirror(CodeMirror.replace(textarea), {
1286
content: textarea.value,
1287
parserfile: ["tokenizejavascript.js", "parsejavascript.js"],
1288
stylesheet: "css/jscolors.css",
1290
autoMatchParens: true,
1291
initCallback: function(editor){editor.win.document.body.lastChild.scrollIntoView();}