~ivle-dev/ivle/codemirror

« back to all changes in this revision

Viewing changes to ivle/webapp/filesystem/browser/media/codemirror/bigtest.html

  • Committer: David Coles
  • Date: 2010-05-31 10:38:53 UTC
  • Revision ID: coles.david@gmail.com-20100531103853-8xypjpracvwy0qt4
Editor: Added CodeMirror-0.67 Javascript code editor source from 
http://marijn.haverbeke.nl/codemirror/ (zlib-style licence)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<html xmlns="http://www.w3.org/1999/xhtml">
 
2
  <head>
 
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"/>
 
6
  </head>
 
7
  <body style="padding: 20px;">
 
8
 
 
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>
 
13
 
 
14
<div class="border">
 
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 &lt;span> and &lt;br> elements
 
20
 */
 
21
 
 
22
// Make sure a string does not contain two consecutive 'collapseable'
 
23
// whitespace characters.
 
24
function makeWhiteSpace(n) {
 
25
  var buffer = [], nb = true;
 
26
  for (; n > 0; n--) {
 
27
    buffer.push((nb || n == 1) ? nbsp : " ");
 
28
    nb = !nb;
 
29
  }
 
30
  return buffer.join("");
 
31
}
 
32
 
 
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);});
 
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, doc) {
 
47
  var text = value;
 
48
  if (value.nodeType == 3) text = value.nodeValue;
 
49
  else value = doc.createTextNode(text);
 
50
 
 
51
  var span = doc.createElement("SPAN");
 
52
  span.isPart = true;
 
53
  span.appendChild(value);
 
54
  span.currentText = text;
 
55
  return span;
 
56
}
 
57
 
 
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 ?
 
68
  function(container) {
 
69
    var last = container.lastChild;
 
70
    if (!last || !last.isPart || last.textContent != "\u200b")
 
71
      container.appendChild(makePartSpan("\u200b", container.ownerDocument));
 
72
  } : function() {};
 
73
 
 
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};
 
78
 
 
79
  function asEditorLines(string) {
 
80
    return fixSpaces(string.replace(/\t/g, "  ").replace(/\u00a0/g, " ")).replace(/\r\n?/g, "\n").split("\n");
 
81
  }
 
82
 
 
83
  // Helper function for traverseDOM. Flattens an arbitrary DOM node
 
84
  // into an array of textnodes and &lt;br> tags.
 
85
  function simplifyDOM(root) {
 
86
    var doc = root.ownerDocument;
 
87
    var result = [];
 
88
    var leaving = true;
 
89
 
 
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;
 
94
        result.push(node);
 
95
      }
 
96
      else if (node.nodeName == "BR" &amp;&amp; node.childNodes.length == 0) {
 
97
        leaving = true;
 
98
        result.push(node);
 
99
      }
 
100
      else {
 
101
        forEach(node.childNodes, simplifyNode);
 
102
        if (!leaving &amp;&amp; newlineElements.hasOwnProperty(node.nodeName)) {
 
103
          leaving = true;
 
104
          result.push(doc.createElement("BR"));
 
105
        }
 
106
      }
 
107
    }
 
108
 
 
109
    simplifyNode(root);
 
110
    return result;
 
111
  }
 
112
 
 
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
  // &lt;span> and &lt;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;
 
126
    var nodeQueue = [];
 
127
 
 
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);
 
135
      };
 
136
    }
 
137
    var point = null;
 
138
 
 
139
    // Insert a normalized node at the current point. If it is a text
 
140
    // node, wrap it in a &lt;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){
 
146
      var text = "\n";
 
147
      if (part.nodeType == 3) {
 
148
        select.snapshotChanged();
 
149
        part = makePartSpan(part, owner);
 
150
        text = part.currentText;
 
151
      }
 
152
      part.dirty = true;
 
153
      nodeQueue.push(part);
 
154
      point(part);
 
155
      return text;
 
156
    }
 
157
 
 
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){
 
162
      var toYield = [];
 
163
      forEach(simplifyDOM(node), function(part) {
 
164
        toYield.push(insertPart(part));
 
165
      });
 
166
      return yield(toYield.join(""), c);
 
167
    }
 
168
 
 
169
    // Check whether a node is a normalized &lt;span> element.
 
170
    function partNode(node){
 
171
      if (node.isPart &amp;&amp; node.childNodes.length == 1 &amp;&amp; node.firstChild.nodeType == 3) {
 
172
        node.currentText = node.firstChild.nodeValue;
 
173
        return !/[\n\t\r]/.test(node.currentText);
 
174
      }
 
175
      return false;
 
176
    }
 
177
 
 
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);
 
185
 
 
186
      if (partNode(node)){
 
187
        nodeQueue.push(node);
 
188
        return yield(node.currentText, c);
 
189
      }
 
190
      else if (node.nodeName == "BR") {
 
191
        nodeQueue.push(node);
 
192
        return yield("\n", c);
 
193
      }
 
194
      else {
 
195
        point = pointAt(node);
 
196
        removeElement(node);
 
197
        return writeNode(node, c);
 
198
      }
 
199
    }
 
200
 
 
201
    // MochiKit iterators are objects with a next function that
 
202
    // returns the next value or throws StopIteration when there are
 
203
    // no more values.
 
204
    return {next: function(){return cc();}, nodes: nodeQueue};
 
205
  }
 
206
 
 
207
  // Determine the text size of a processed node.
 
208
  function nodeSize(node) {
 
209
    if (node.nodeName == "BR")
 
210
      return 1;
 
211
    else
 
212
      return node.currentText.length;
 
213
  }
 
214
 
 
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 &amp;&amp; node.nodeName != "BR") node = node.previousSibling;
 
219
    return node;
 
220
  }
 
221
  function endOfLine(node, container) {
 
222
    if (!node) node = container.firstChild;
 
223
    else if (node.nodeName == "BR") node = node.nextSibling;
 
224
 
 
225
    while (node &amp;&amp; node.nodeName != "BR") node = node.nextSibling;
 
226
    return node;
 
227
  }
 
228
 
 
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
 
232
  // information.
 
233
  function scrubPasted(container, start, start2) {
 
234
    var end = select.selectionTopNode(container, true),
 
235
        doc = container.ownerDocument;
 
236
    if (start != null &amp;&amp; start.parentNode != container) start = start2;
 
237
    if (start === false) start = null;
 
238
    if (start == end || !end || !container.firstChild) return;
 
239
 
 
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);
 
245
    });
 
246
  }
 
247
 
 
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();
 
258
 
 
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;
 
265
    var cursor;
 
266
    // Start from the cursor when specified and a cursor can be found.
 
267
    if (fromCursor &amp;&amp; (cursor = select.cursorPos(this.editor.container))) {
 
268
      this.line = cursor.node;
 
269
      this.offset = cursor.offset;
 
270
    }
 
271
    else {
 
272
      this.line = null;
 
273
      this.offset = 0;
 
274
    }
 
275
    this.valid = !!string;
 
276
 
 
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.
 
282
      function() {
 
283
        var match = cleanText(self.history.textAfter(self.line).slice(self.offset)).indexOf(string);
 
284
        if (match > -1)
 
285
          return {from: {node: self.line, offset: self.offset + match},
 
286
                  to: {node: self.line, offset: self.offset + match + string.length}};
 
287
      } :
 
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.
 
291
      function() {
 
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)
 
295
          return false;
 
296
        var startOffset = self.offset + match;
 
297
 
 
298
        var line = self.history.nodeAfter(self.line);
 
299
        for (var i = 1; i &lt; target.length - 1; i++) {
 
300
          if (cleanText(self.history.textAfter(line)) != target[i])
 
301
            return false;
 
302
          line = self.history.nodeAfter(line);
 
303
        }
 
304
 
 
305
        if (cleanText(self.history.textAfter(line)).indexOf(target[target.length - 1]) != 0)
 
306
          return false;
 
307
 
 
308
        return {from: {node: self.line, offset: startOffset},
 
309
                to: {node: line, offset: target[target.length - 1].length}};
 
310
      };
 
311
  }
 
312
 
 
313
  SearchCursor.prototype = {
 
314
    findNext: function() {
 
315
      if (!this.valid) return false;
 
316
      this.atOccurrence = false;
 
317
      var self = this;
 
318
 
 
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 &amp;&amp; !this.line.parentNode) {
 
322
        this.line = null;
 
323
        this.offset = 0;
 
324
      }
 
325
 
 
326
      // Set the cursor's position one character after the given
 
327
      // position.
 
328
      function saveAfter(pos) {
 
329
        if (self.history.textAfter(pos.node).length &lt; pos.offset) {
 
330
          self.line = pos.node;
 
331
          self.offset = pos.offset + 1;
 
332
        }
 
333
        else {
 
334
          self.line = self.history.nodeAfter(pos.node);
 
335
          self.offset = 0;
 
336
        }
 
337
      }
 
338
 
 
339
      while (true) {
 
340
        var match = this.matches();
 
341
        // Found the search string.
 
342
        if (match) {
 
343
          this.atOccurrence = match;
 
344
          saveAfter(match.from);
 
345
          return true;
 
346
        }
 
347
        this.line = this.history.nodeAfter(this.line);
 
348
        this.offset = 0;
 
349
        // End of document.
 
350
        if (!this.line) {
 
351
          this.valid = false;
 
352
          return false;
 
353
        }
 
354
      }
 
355
    },
 
356
 
 
357
    select: function() {
 
358
      if (this.atOccurrence) {
 
359
        select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
 
360
        select.scrollToCursor(this.editor.container);
 
361
      }
 
362
    },
 
363
 
 
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;
 
370
      }
 
371
    }
 
372
  };
 
373
 
 
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;
 
379
    this.doc = document;
 
380
    var container = this.container = this.doc.body;
 
381
    this.win = window;
 
382
    this.history = new History(container, options.undoDepth, options.undoDelay,
 
383
                               this, options.onChange);
 
384
    var self = this;
 
385
 
 
386
    if (!Editor.Parser)
 
387
      throw "No parser loaded.";
 
388
    if (options.parserConfig &amp;&amp; Editor.Parser.configure)
 
389
      Editor.Parser.configure(options.parserConfig);
 
390
 
 
391
    if (!options.readOnly)
 
392
      select.setCursorPos(container, {node: null, offset: 0});
 
393
 
 
394
    this.dirty = [];
 
395
    if (options.content)
 
396
      this.importCode(options.content);
 
397
    else // FF acts weird when the editable document is completely empty
 
398
      container.appendChild(this.doc.createElement("BR"));
 
399
 
 
400
    if (!options.readOnly) {
 
401
      if (options.continuousScanning !== false) {
 
402
        this.scanner = this.documentScanner(options.linesPerPass);
 
403
        this.delayScanning();
 
404
      }
 
405
 
 
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 &amp;&amp; internetExplorer)
 
410
          document.body.contentEditable = "true";
 
411
        else
 
412
          document.designMode = "on";
 
413
 
 
414
        document.documentElement.style.borderWidth = "0";
 
415
        if (!options.textWrapping)
 
416
          container.style.whiteSpace = "nowrap";
 
417
      }
 
418
 
 
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).
 
422
      try {
 
423
        setEditable();
 
424
      }
 
425
      catch(e) {
 
426
        var focusEvent = addEventHandler(document, "focus", function() {
 
427
          focusEvent();
 
428
          setEditable();
 
429
        }, true);
 
430
      }
 
431
 
 
432
      addEventHandler(document, "keydown", method(this, "keyDown"));
 
433
      addEventHandler(document, "keypress", method(this, "keyPress"));
 
434
      addEventHandler(document, "keyup", method(this, "keyUp"));
 
435
 
 
436
      function cursorActivity() {self.cursorActivity(false);}
 
437
      addEventHandler(document.body, "mouseup", cursorActivity);
 
438
      addEventHandler(document.body, "paste", function(event) {
 
439
        cursorActivity();
 
440
        if (internetExplorer) {
 
441
          var text = null;
 
442
          try {text = window.clipboardData.getData("Text");}catch(e){}
 
443
          if (text != null) {
 
444
            self.replaceSelection(text);
 
445
            event.stop();
 
446
          }
 
447
          else {
 
448
            var start = select.selectionTopNode(self.container, true),
 
449
                start2 = start &amp;&amp; start.previousSibling;
 
450
            setTimeout(function(){scrubPasted(self.container, start, start2);}, 0);
 
451
          }
 
452
        }
 
453
      });
 
454
      addEventHandler(document.body, "cut", cursorActivity);
 
455
 
 
456
      if (this.options.autoMatchParens)
 
457
        addEventHandler(document.body, "click", method(this, "scheduleParenBlink"));
 
458
    }
 
459
  }
 
460
 
 
461
  function isSafeKey(code) {
 
462
    return (code >= 16 &amp;&amp; code &lt;= 18) || // shift, control, alt
 
463
           (code >= 33 &amp;&amp; code &lt;= 40); // arrows, home, end
 
464
  }
 
465
 
 
466
  Editor.prototype = {
 
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();
 
471
    },
 
472
 
 
473
    // Extract the code from the editor.
 
474
    getCode: function() {
 
475
      if (!this.container.firstChild)
 
476
        return "";
 
477
 
 
478
      var accum = [];
 
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(""));
 
484
    },
 
485
 
 
486
    checkLine: function(node) {
 
487
      if (node === false || !(node == null || node.parentNode == this.container))
 
488
        throw parent.CodeMirror.InvalidLineHandle;
 
489
    },
 
490
 
 
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};
 
496
    },
 
497
 
 
498
    firstLine: function() {
 
499
      return null;
 
500
    },
 
501
 
 
502
    lastLine: function() {
 
503
      if (this.container.lastChild) return startOfLine(this.container.lastChild);
 
504
      else return null;
 
505
    },
 
506
 
 
507
    nextLine: function(line) {
 
508
      this.checkLine(line);
 
509
      var end = endOfLine(line, this.container);
 
510
      return end || false;
 
511
    },
 
512
 
 
513
    prevLine: function(line) {
 
514
      this.checkLine(line);
 
515
      if (line == null) return false;
 
516
      return startOfLine(line.previousSibling);
 
517
    },
 
518
 
 
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};
 
525
      }
 
526
      select.setCursorPos(this.container, start, end);
 
527
      select.scrollToCursor(this.container);
 
528
    },
 
529
 
 
530
    lineContent: function(line) {
 
531
      this.checkLine(line);
 
532
      var accum = [];
 
533
      for (line = line ? line.nextSibling : this.container.firstChild;
 
534
           line &amp;&amp; line.nodeName != "BR"; line = line.nextSibling)
 
535
        accum.push(nodeText(line));
 
536
      return cleanText(accum.join(""));
 
537
    },
 
538
 
 
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},
 
543
                        content);
 
544
      this.addDirtyNode(line);
 
545
      this.scheduleHighlight();
 
546
    },
 
547
 
 
548
    insertIntoLine: function(line, position, content) {
 
549
      var before = null;
 
550
      if (position == "end") {
 
551
        before = endOfLine(line, this.container);
 
552
      }
 
553
      else {
 
554
        for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
 
555
          if (position == 0) {
 
556
            before = cur;
 
557
            break;
 
558
          }
 
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);
 
563
            removeElement(cur);
 
564
            break;
 
565
          }
 
566
          position -= text.length;
 
567
        }
 
568
      }
 
569
 
 
570
      var lines = asEditorLines(content), doc = this.container.ownerDocument;
 
571
      for (var i = 0; i &lt; lines.length; i++) {
 
572
        if (i > 0) this.container.insertBefore(doc.createElement("BR"), before);
 
573
        this.container.insertBefore(makePartSpan(lines[i], doc), before);
 
574
      }
 
575
      this.addDirtyNode(line);
 
576
      this.scheduleHighlight();
 
577
    },
 
578
 
 
579
    // Retrieve the selected text.
 
580
    selectedText: function() {
 
581
      var h = this.history;
 
582
      h.commit();
 
583
 
 
584
      var start = select.cursorPos(this.container, true),
 
585
          end = select.cursorPos(this.container, false);
 
586
      if (!start || !end) return "";
 
587
 
 
588
      if (start.node == end.node)
 
589
        return h.textAfter(start.node).slice(start.offset, end.offset);
 
590
 
 
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"));
 
596
    },
 
597
 
 
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;
 
604
 
 
605
      end = this.replaceRange(start, end, text);
 
606
      select.setCursorPos(this.container, start, end);
 
607
    },
 
608
 
 
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};
 
618
    },
 
619
 
 
620
    getSearchCursor: function(string, fromCursor) {
 
621
      return new SearchCursor(this, string, fromCursor);
 
622
    },
 
623
 
 
624
    // Re-indent the whole buffer
 
625
    reindent: function() {
 
626
      if (this.container.firstChild)
 
627
        this.indentRegion(null, this.container.lastChild);
 
628
    },
 
629
 
 
630
    grabKeys: function(eventHandler, filter) {
 
631
      this.frozen = eventHandler;
 
632
      this.keyFilter = filter;
 
633
    },
 
634
    ungrabKeys: function() {
 
635
      this.frozen = "leave";
 
636
      this.keyFilter = null;
 
637
    },
 
638
 
 
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 &amp;&amp; (!this.keyFilter || this.keyFilter(event.keyCode))) {
 
643
        event.stop();
 
644
        this.frozen(event);
 
645
        return;
 
646
      }
 
647
 
 
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();
 
654
 
 
655
      if (code == 13) { // enter
 
656
        if (event.ctrlKey) {
 
657
          this.reparseBuffer();
 
658
        }
 
659
        else {
 
660
          select.insertNewlineAtCursor(this.win);
 
661
          this.indentAtCursor();
 
662
          select.scrollToCursor(this.container);
 
663
        }
 
664
        event.stop();
 
665
      }
 
666
      else if (code == 9 &amp;&amp; this.options.tabMode != "default") { // tab
 
667
        this.handleTab(!event.ctrlKey &amp;&amp; !event.shiftKey);
 
668
        event.stop();
 
669
      }
 
670
      else if (code == 32 &amp;&amp; event.shiftKey &amp;&amp; this.options.tabMode == "default") { // space
 
671
        this.handleTab(true);
 
672
        event.stop();
 
673
      }
 
674
      else if ((code == 219 || code == 221) &amp;&amp; event.ctrlKey) {
 
675
        this.blinkParens(event.shiftKey);
 
676
        event.stop();
 
677
      }
 
678
      else if (event.metaKey &amp;&amp; (code == 37 || code == 39)) { // Meta-left/right
 
679
        var cursor = select.selectionTopNode(this.container);
 
680
        if (cursor === false || !this.container.firstChild) return;
 
681
 
 
682
        if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
 
683
        else {
 
684
          end = endOfLine(cursor, this.container);
 
685
          select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
 
686
        }
 
687
        event.stop();
 
688
      }
 
689
      else if (event.ctrlKey || event.metaKey) {
 
690
        if ((event.shiftKey &amp;&amp; code == 90) || code == 89) { // shift-Z, Y
 
691
          select.scrollToNode(this.history.redo());
 
692
          event.stop();
 
693
        }
 
694
        else if (code == 90 || code == 8) { // Z, backspace
 
695
          select.scrollToNode(this.history.undo());
 
696
          event.stop();
 
697
        }
 
698
        else if (code == 83 &amp;&amp; this.options.saveFunction) { // S
 
699
          this.options.saveFunction();
 
700
          event.stop();
 
701
        }
 
702
      }
 
703
    },
 
704
 
 
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) &amp;&amp; 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
 
712
      // here.
 
713
      if ((this.frozen &amp;&amp; (!this.keyFilter || this.keyFilter(event.keyCode))) ||
 
714
          event.code == 13 || (event.code == 9 &amp;&amp; this.options.tabMode != "default") ||
 
715
          (event.keyCode == 32 &amp;&amp; event.shiftKey &amp;&amp; this.options.tabMode == "default"))
 
716
        event.stop();
 
717
      else if (electric &amp;&amp; electric.indexOf(event.character) != -1)
 
718
        this.parent.setTimeout(method(this, "indentAtCursor"), 0);
 
719
    },
 
720
 
 
721
    // Mark the node at the cursor dirty when a non-safe key is
 
722
    // released.
 
723
    keyUp: function(event) {
 
724
      this.cursorActivity(isSafeKey(event.keyCode));
 
725
    },
 
726
 
 
727
    // Indent the line following a given &lt;br>, or null for the first
 
728
    // line. If given a &lt;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 &amp;&amp; !hasClass(whiteSpace, "whitespace"))
 
736
        whiteSpace = null;
 
737
 
 
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 &amp;&amp; firstText &amp;&amp; firstText.currentText) ? firstText.currentText : "";
 
742
 
 
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 &amp;&amp; this.options.tabMode == "shift")
 
747
        newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
 
748
      else if (start)
 
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;
 
753
 
 
754
      // If there is too much, this is just a matter of shrinking a span.
 
755
      if (indentDiff &lt; 0) {
 
756
        if (newIndent == 0) {
 
757
          if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0);
 
758
          removeElement(whiteSpace);
 
759
          whiteSpace = null;
 
760
        }
 
761
        else {
 
762
          select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
 
763
          whiteSpace.currentText = makeWhiteSpace(newIndent);
 
764
          whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
 
765
        }
 
766
      }
 
767
      // Not enough...
 
768
      else if (indentDiff > 0) {
 
769
        // If there is whitespace, we grow it.
 
770
        if (whiteSpace) {
 
771
          whiteSpace.currentText = makeWhiteSpace(newIndent);
 
772
          whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
 
773
        }
 
774
        // Otherwise, we have to add a new whitespace node.
 
775
        else {
 
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);
 
780
        }
 
781
        if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
 
782
      }
 
783
      if (indentDiff != 0) this.addDirtyNode(start);
 
784
      return whiteSpace;
 
785
    },
 
786
 
 
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;
 
792
 
 
793
      select.markSelection(this.win);
 
794
      if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
 
795
        return false;
 
796
      select.selectMarked();
 
797
      return true;
 
798
    },
 
799
 
 
800
    // When tab is pressed with text selected, the whole selection is
 
801
    // re-indented, when nothing is selected, the line with the cursor
 
802
    // is re-indented.
 
803
    handleTab: function(direction) {
 
804
      if (this.options.tabMode == "spaces") {
 
805
        select.insertTabAtCursor(this.win);
 
806
      }
 
807
      else if (!select.somethingSelected(this.win)) {
 
808
        this.indentAtCursor(direction);
 
809
      }
 
810
      else {
 
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);
 
815
      }
 
816
    },
 
817
 
 
818
    // Delay (or initiate) the next paren blink event.
 
819
    scheduleParenBlink: function() {
 
820
      if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
 
821
      var self = this;
 
822
      this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300);
 
823
    },
 
824
 
 
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
 
828
    // was found.
 
829
    blinkParens: function(jump) {
 
830
      // Clear the event property.
 
831
      if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
 
832
      this.parenEvent = null;
 
833
 
 
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 &amp;&amp; match[1];
 
839
        }
 
840
      }
 
841
      // Determine the direction a paren is facing.
 
842
      function forward(ch) {
 
843
        return /[\(\[\{]/.test(ch);
 
844
      }
 
845
 
 
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 &amp;&amp; ((ch = paren(cursor)) || (cursor = cursor.nextSibling) &amp;&amp; (ch = paren(cursor)))))
 
850
        return;
 
851
      // We only look for tokens with the same className.
 
852
      var className = cursor.className, dir = forward(ch), match = matching[ch];
 
853
 
 
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 &amp;&amp; runner.nodeName == "SPAN" &amp;&amp; (ch = paren(runner))) {
 
862
            if (forward(ch) == dir)
 
863
              stack.push(ch);
 
864
            else if (!stack.length)
 
865
              ok = false;
 
866
            else if (stack.pop() != matching[ch])
 
867
              ok = false;
 
868
            if (!stack.length) break;
 
869
          }
 
870
          else if (runner.dirty || runner.nodeName != "SPAN" &amp;&amp; runner.nodeName != "BR") {
 
871
            return {node: runner, status: "dirty"};
 
872
          }
 
873
        }
 
874
        return {node: runner, status: runner &amp;&amp; ok};
 
875
      }
 
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);
 
881
      }
 
882
 
 
883
      while (true) {
 
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
 
888
          // reach a node.
 
889
          found.node.dirty = false;
 
890
          continue;
 
891
        }
 
892
        else {
 
893
          blink(cursor, found.status);
 
894
          if (found.node) {
 
895
            blink(found.node, found.status);
 
896
            if (jump) select.focusAfterNode(found.node.previousSibling, this.container);
 
897
          }
 
898
          break;
 
899
        }
 
900
      }
 
901
    },
 
902
 
 
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)
 
914
        return;
 
915
      var lineStart = startOfLine(cursor);
 
916
      var whiteSpace = this.indentLineAfter(lineStart, direction);
 
917
      if (cursor == lineStart &amp;&amp; whiteSpace)
 
918
          cursor = whiteSpace;
 
919
      // This means the indentation has probably messed up the cursor.
 
920
      if (cursor == whiteSpace)
 
921
        select.focusAfterNode(cursor, this.container);
 
922
    },
 
923
 
 
924
    // Indent all lines whose start falls inside of the current
 
925
    // selection.
 
926
    indentRegion: function(start, end, direction) {
 
927
      var current = (start = startOfLine(start)), before = start &amp;&amp; startOfLine(start.previousSibling);
 
928
      if (end.nodeName != "BR") end = endOfLine(end, this.container);
 
929
 
 
930
      do {
 
931
        if (current) this.highlight(before, current, true);
 
932
        this.indentLineAfter(current, direction);
 
933
        before = current;
 
934
        current = endOfLine(current, this.container);
 
935
      } while (current != end);
 
936
      select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
 
937
    },
 
938
 
 
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);
 
945
      }
 
946
 
 
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);
 
953
        if (!safe) {
 
954
          this.scheduleHighlight();
 
955
          this.addDirtyNode(cursor);
 
956
        }
 
957
      }
 
958
    },
 
959
 
 
960
    reparseBuffer: function() {
 
961
      forEach(this.container.childNodes, function(node) {node.dirty = true;});
 
962
      if (this.container.firstChild)
 
963
        this.addDirtyNode(this.container.firstChild);
 
964
    },
 
965
 
 
966
    // Add a node to the set of dirty nodes, if it isn't already in
 
967
    // there.
 
968
    addDirtyNode: function(node) {
 
969
      node = node || this.container.firstChild;
 
970
      if (!node) return;
 
971
 
 
972
      for (var i = 0; i &lt; this.dirty.length; i++)
 
973
        if (this.dirty[i] == node) return;
 
974
 
 
975
      if (node.nodeType != 3)
 
976
        node.dirty = true;
 
977
      this.dirty.push(node);
 
978
    },
 
979
 
 
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.
 
987
      var self = this;
 
988
      this.parent.clearTimeout(this.highlightTimeout);
 
989
      this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
 
990
    },
 
991
 
 
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
 
998
        try {
 
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 &amp;&amp; found.parentNode != this.container)
 
1002
            found = found.parentNode
 
1003
          if (found &amp;&amp; (found.dirty || found.nodeType == 3))
 
1004
            return found;
 
1005
        } catch (e) {}
 
1006
      }
 
1007
      return null;
 
1008
    },
 
1009
 
 
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;
 
1021
 
 
1022
      var lines = force ? Infinity : this.options.linesPerPass;
 
1023
      if (!this.options.readOnly) select.markSelection(this.win);
 
1024
      var start;
 
1025
      while (lines > 0 &amp;&amp; (start = this.getDirtyNode())){
 
1026
        var result = this.highlight(start, lines);
 
1027
        if (result) {
 
1028
          lines = result.left;
 
1029
          if (result.node &amp;&amp; result.dirty)
 
1030
            this.addDirtyNode(result.node);
 
1031
        }
 
1032
      }
 
1033
      if (!this.options.readOnly) select.selectMarked();
 
1034
      if (start)
 
1035
        this.scheduleHighlight();
 
1036
      return this.dirty.length == 0;
 
1037
    },
 
1038
 
 
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;
 
1043
      return function() {
 
1044
        // If the current node is no longer in the document... oh
 
1045
        // well, we start over.
 
1046
        if (pos &amp;&amp; pos.parentNode != self.container)
 
1047
          pos = null;
 
1048
        select.markSelection(self.win);
 
1049
        var result = self.highlight(pos, linesPer, true);
 
1050
        select.selectMarked();
 
1051
        var newPos = result ? (result.node &amp;&amp; result.node.nextSibling) : null;
 
1052
        pos = (pos == newPos) ? null : newPos;
 
1053
        self.delayScanning();
 
1054
      };
 
1055
    },
 
1056
 
 
1057
    // Starts the continuous scanning process for this document after
 
1058
    // a given interval.
 
1059
    delayScanning: function() {
 
1060
      if (this.scanner) {
 
1061
        this.parent.clearTimeout(this.documentScan);
 
1062
        this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
 
1063
      }
 
1064
    },
 
1065
 
 
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);
 
1081
 
 
1082
      if (!container.firstChild)
 
1083
        return;
 
1084
      // Backtrack to the first node before from that has a partial
 
1085
      // parse stored.
 
1086
      while (from &amp;&amp; (!from.parserFromHere || from.dirty)) {
 
1087
        from = from.previousSibling;
 
1088
        if (maxBacktrack != null &amp;&amp; from.nodeName == "BR" &amp;&amp; (--maxBacktrack) &lt; 0)
 
1089
          return false;
 
1090
      }
 
1091
      // If we are at the end of the document, do nothing.
 
1092
      if (from &amp;&amp; !from.nextSibling)
 
1093
        return;
 
1094
 
 
1095
      // Check whether a part (&lt;span> node) and the corresponding token
 
1096
      // match.
 
1097
      function correctPart(token, part){
 
1098
        return !part.reduced &amp;&amp; part.currentText == token.value &amp;&amp; part.className == token.style;
 
1099
      }
 
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;
 
1108
      }
 
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;
 
1113
        return part;
 
1114
      }
 
1115
 
 
1116
      function maybeTouch(node) {
 
1117
        if (node) {
 
1118
          if (node.nextSibling != node.oldNextSibling) {
 
1119
            self.history.touch(node);
 
1120
            node.oldNextSibling = node.nextSibling;
 
1121
          }
 
1122
        }
 
1123
        else {
 
1124
          if (self.container.firstChild != self.container.oldFirstChild) {
 
1125
            self.history.touch(node);
 
1126
            self.container.oldFirstChild = self.container.firstChild;
 
1127
          }
 
1128
        }
 
1129
      }
 
1130
 
 
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
 
1133
      // is resumed.
 
1134
      var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
 
1135
          stream = stringStream(traversal),
 
1136
          parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
 
1137
 
 
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
 
1142
      // one.
 
1143
      var parts = {
 
1144
        current: null,
 
1145
        // Fetch current node.
 
1146
        get: function(){
 
1147
          if (!this.current)
 
1148
            this.current = traversal.nodes.shift();
 
1149
          return this.current;
 
1150
        },
 
1151
        // Advance to the next part (do not fetch it yet).
 
1152
        next: function(){
 
1153
          this.current = null;
 
1154
        },
 
1155
        // Remove the current part from the DOM tree, and move to the
 
1156
        // next.
 
1157
        remove: function(){
 
1158
          container.removeChild(this.get());
 
1159
          this.current = null;
 
1160
        },
 
1161
        // Advance to the next part that is not empty, discarding empty
 
1162
        // parts.
 
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 &amp;&amp; part.nodeName == "SPAN" &amp;&amp; part.currentText == "") {
 
1169
            var old = part;
 
1170
            this.remove();
 
1171
            part = this.get();
 
1172
            // Adjust selection information, if any. See select.js for details.
 
1173
            select.snapshotMove(old.firstChild, part &amp;&amp; (part.firstChild || part), 0);
 
1174
          }
 
1175
          return part;
 
1176
        }
 
1177
      };
 
1178
 
 
1179
      var lineDirty = false, prevLineDirty = true, lineNodes = 0;
 
1180
 
 
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();
 
1186
 
 
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.";
 
1192
 
 
1193
          if (part.dirty || !part.indentation) lineDirty = true;
 
1194
          maybeTouch(from);
 
1195
          from = part;
 
1196
 
 
1197
          // Every &lt;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
 
1200
          // for indentation.
 
1201
          part.parserFromHere = parsed.copy();
 
1202
          part.indentation = token.indentation;
 
1203
          part.dirty = false;
 
1204
 
 
1205
          // If the target argument wasn't an integer, go at least
 
1206
          // until that node.
 
1207
          if (lines == null &amp;&amp; part == target) throw StopIteration;
 
1208
 
 
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 &amp;&amp; --lines &lt;= 0) || (!lineDirty &amp;&amp; !prevLineDirty &amp;&amp; lineNodes > 1 &amp;&amp; !cleanLines))
 
1213
            throw StopIteration;
 
1214
          prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
 
1215
          parts.next();
 
1216
        }
 
1217
        else {
 
1218
          if (part.nodeName != "SPAN")
 
1219
            throw "Parser out of sync. Expected SPAN.";
 
1220
          if (part.dirty)
 
1221
            lineDirty = true;
 
1222
          lineNodes++;
 
1223
 
 
1224
          // If the part matches the token, we can leave it alone.
 
1225
          if (correctPart(token, part)){
 
1226
            part.dirty = false;
 
1227
            parts.next();
 
1228
          }
 
1229
          // Otherwise, we have to fix it.
 
1230
          else {
 
1231
            lineDirty = true;
 
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;
 
1237
            var offset = 0;
 
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) {
 
1242
              part = parts.get();
 
1243
              var partsize = part.currentText.length;
 
1244
              select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
 
1245
              if (partsize > tokensize){
 
1246
                shortenPart(part, tokensize);
 
1247
                tokensize = 0;
 
1248
              }
 
1249
              else {
 
1250
                tokensize -= partsize;
 
1251
                offset += partsize;
 
1252
                parts.remove();
 
1253
              }
 
1254
            }
 
1255
          }
 
1256
        }
 
1257
      });
 
1258
      maybeTouch(from);
 
1259
      webkitLastLineHack(this.container);
 
1260
 
 
1261
      // The function returns some status information that is used by
 
1262
      // hightlightDirty to determine whether and where it has to
 
1263
      // continue.
 
1264
      return {left: lines,
 
1265
              node: parts.getNonEmpty(),
 
1266
              dirty: lineDirty};
 
1267
    }
 
1268
  };
 
1269
 
 
1270
  return Editor;
 
1271
})();
 
1272
 
 
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);
 
1277
  });
 
1278
</textarea>
 
1279
</div>
 
1280
 
 
1281
<script type="text/javascript">
 
1282
  var textarea = document.getElementById('code');
 
1283
  var editor = new CodeMirror(CodeMirror.replace(textarea), {
 
1284
    height: "750px",
 
1285
    width: "100%",
 
1286
    content: textarea.value,
 
1287
    parserfile: ["tokenizejavascript.js", "parsejavascript.js"],
 
1288
    stylesheet: "css/jscolors.css",
 
1289
    path: "js/",
 
1290
    autoMatchParens: true,
 
1291
    initCallback: function(editor){editor.win.document.body.lastChild.scrollIntoView();}
 
1292
  });
 
1293
</script>
 
1294
 
 
1295
  </body>
 
1296
</html>