w2tags 0.9.55 → 0.9.56

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/Manifest.txt +31 -0
  2. data/lib/w2tags/parser.rb +2 -5
  3. data/lib/w2tags/try/public/css/csscolors.css +47 -0
  4. data/lib/w2tags/try/public/css/jquery.cluetip.css +130 -0
  5. data/lib/w2tags/try/public/css/jscolors.css +55 -0
  6. data/lib/w2tags/try/public/css/rubycolors.css +82 -0
  7. data/lib/w2tags/try/public/css/xmlcolors.css +51 -0
  8. data/lib/w2tags/try/public/img/loading.gif +0 -0
  9. data/lib/w2tags/try/public/js/codemirror.js +308 -0
  10. data/lib/w2tags/try/public/js/editor.js +1340 -0
  11. data/lib/w2tags/try/public/js/highlight.js +68 -0
  12. data/lib/w2tags/try/public/js/jquery.cluetip.js +470 -0
  13. data/lib/w2tags/try/public/js/jquery.js +4376 -0
  14. data/lib/w2tags/try/public/js/mirrorframe.js +81 -0
  15. data/lib/w2tags/try/public/js/parsecss.js +155 -0
  16. data/lib/w2tags/try/public/js/parsehtmlmixed.js +74 -0
  17. data/lib/w2tags/try/public/js/parsejavascript.js +350 -0
  18. data/lib/w2tags/try/public/js/parsew2tags.js +157 -0
  19. data/lib/w2tags/try/public/js/parsexml.js +292 -0
  20. data/lib/w2tags/try/public/js/select.js +607 -0
  21. data/lib/w2tags/try/public/js/stringstream.js +140 -0
  22. data/lib/w2tags/try/public/js/tokenize.js +57 -0
  23. data/lib/w2tags/try/public/js/tokenizejavascript.js +175 -0
  24. data/lib/w2tags/try/public/js/undo.js +404 -0
  25. data/lib/w2tags/try/public/js/util.js +134 -0
  26. data/lib/w2tags/try/public/w2/basic.w2erb +8 -2
  27. data/lib/w2tags/try/public/w2/erb_base.hot.html +167 -0
  28. data/lib/w2tags/try/public/w2/erb_rails.hot.html +59 -0
  29. data/lib/w2tags/try/public/w2/html.hot.html +1 -0
  30. data/lib/w2tags/try/public/w2/rails.hot.html +37 -0
  31. data/lib/w2tags/try/public/w2/try.rb.hot.html +50 -0
  32. data/lib/w2tags/try/try.rb +3 -2
  33. data/lib/w2tags/try/views/index.erb +85 -15
  34. data/lib/w2tags/try/views/layout.erb +4 -5
  35. data/lib/w2tags/try/views/parse.erb +1 -0
  36. data/tasks/setup.rb +1 -1
  37. metadata +58 -2
@@ -0,0 +1,308 @@
1
+ /* CodeMirror main module
2
+ *
3
+ * Implements the CodeMirror constructor and prototype, which take care
4
+ * of initializing the editor frame, and providing the outside interface.
5
+ */
6
+
7
+ // The CodeMirrorConfig object is used to specify a default
8
+ // configuration. If you specify such an object before loading this
9
+ // file, the values you put into it will override the defaults given
10
+ // below. You can also assign to it after loading.
11
+ var CodeMirrorConfig = window.CodeMirrorConfig || {};
12
+
13
+ var CodeMirror = (function(){
14
+ function setDefaults(object, defaults) {
15
+ for (var option in defaults) {
16
+ if (!object.hasOwnProperty(option))
17
+ object[option] = defaults[option];
18
+ }
19
+ }
20
+ function forEach(array, action) {
21
+ for (var i = 0; i < array.length; i++)
22
+ action(array[i]);
23
+ }
24
+
25
+ // These default options can be overridden by passing a set of
26
+ // options to a specific CodeMirror constructor. See manual.html for
27
+ // their meaning.
28
+ setDefaults(CodeMirrorConfig, {
29
+ stylesheet: "",
30
+ path: "",
31
+ parserfile: [],
32
+ basefiles: ["util.js", "stringstream.js", "select.js", "undo.js", "editor.js", "tokenize.js"],
33
+ iframeClass: null,
34
+ passDelay: 200,
35
+ passTime: 50,
36
+ continuousScanning: false,
37
+ saveFunction: null,
38
+ onChange: null,
39
+ undoDepth: 50,
40
+ undoDelay: 800,
41
+ disableSpellcheck: true,
42
+ textWrapping: true,
43
+ readOnly: false,
44
+ width: "100%",
45
+ height: "300px",
46
+ autoMatchParens: false,
47
+ parserConfig: null,
48
+ tabMode: "indent", // or "spaces", "default", "shift"
49
+ reindentOnLoad: false,
50
+ activeTokens: null,
51
+ cursorActivity: null,
52
+ lineNumbers: false,
53
+ indentUnit: 2
54
+ });
55
+
56
+ function wrapLineNumberDiv(place) {
57
+ return function(node) {
58
+ var container = document.createElement("DIV"),
59
+ nums = document.createElement("DIV"),
60
+ scroller = document.createElement("DIV");
61
+ container.style.position = "relative";
62
+ nums.style.position = "absolute";
63
+ nums.style.height = "100%";
64
+ if (nums.style.setExpression) {
65
+ try {nums.style.setExpression("height", "this.previousSibling.offsetHeight + 'px'");}
66
+ catch(e) {} // Seems to throw 'Not Implemented' on some IE8 versions
67
+ }
68
+ nums.style.top = "0px";
69
+ nums.style.overflow = "hidden";
70
+ place(container);
71
+ container.appendChild(node);
72
+ container.appendChild(nums);
73
+ scroller.className = "CodeMirror-line-numbers";
74
+ nums.appendChild(scroller);
75
+ }
76
+ }
77
+
78
+ function applyLineNumbers(frame) {
79
+ var win = frame.contentWindow, doc = win.document,
80
+ nums = frame.nextSibling, scroller = nums.firstChild;
81
+
82
+ var nextNum = 1, barWidth = null;
83
+ function sizeBar() {
84
+ if (!frame.offsetWidth || !win.Editor) {
85
+ for (var cur = frame; cur.parentNode; cur = cur.parentNode) {
86
+ if (cur != document) {
87
+ clearInterval(sizeInterval);
88
+ return;
89
+ }
90
+ }
91
+ }
92
+
93
+ if (nums.offsetWidth != barWidth) {
94
+ barWidth = nums.offsetWidth;
95
+ nums.style.left = "-" + (frame.parentNode.style.marginLeft = barWidth + "px");
96
+ }
97
+ }
98
+ function update() {
99
+ var diff = 20 + Math.max(doc.body.offsetHeight, frame.offsetHeight) - scroller.offsetHeight;
100
+ for (var n = Math.ceil(diff / 10); n > 0; n--) {
101
+ var div = document.createElement("DIV");
102
+ div.appendChild(document.createTextNode(nextNum++));
103
+ scroller.appendChild(div);
104
+ }
105
+ nums.scrollTop = doc.body.scrollTop || doc.documentElement.scrollTop || 0;
106
+ }
107
+ sizeBar();
108
+ update();
109
+ win.addEventHandler(win, "scroll", update);
110
+ win.addEventHandler(win, "resize", update);
111
+ var sizeInterval = setInterval(sizeBar, 500);
112
+ }
113
+
114
+ function CodeMirror(place, options) {
115
+ // Backward compatibility for deprecated options.
116
+ if (options.dumbTabs) options.tabMode = "spaces";
117
+ else if (options.normalTab) options.tabMode = "default";
118
+
119
+ // Use passed options, if any, to override defaults.
120
+ this.options = options = options || {};
121
+ setDefaults(options, CodeMirrorConfig);
122
+
123
+ var frame = this.frame = document.createElement("IFRAME");
124
+ if (options.iframeClass) frame.className = options.iframeClass;
125
+ frame.frameBorder = 0;
126
+ frame.src = "javascript:false;";
127
+ frame.style.border = "0";
128
+ frame.style.width = options.width;
129
+ frame.style.height = options.height;
130
+ // display: block occasionally suppresses some Firefox bugs, so we
131
+ // always add it, redundant as it sounds.
132
+ frame.style.display = "block";
133
+
134
+ if (place.appendChild) {
135
+ var node = place;
136
+ place = function(n){node.appendChild(n);};
137
+ }
138
+ if (options.lineNumbers) place = wrapLineNumberDiv(place);
139
+ place(frame);
140
+
141
+ // Link back to this object, so that the editor can fetch options
142
+ // and add a reference to itself.
143
+ frame.CodeMirror = this;
144
+ this.win = frame.contentWindow;
145
+
146
+ if (typeof options.parserfile == "string")
147
+ options.parserfile = [options.parserfile];
148
+ if (typeof options.stylesheet == "string")
149
+ options.stylesheet = [options.stylesheet];
150
+
151
+ var html = ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\"><html><head>"];
152
+ forEach(options.stylesheet, function(file) {
153
+ html.push("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + file + "\"/>");
154
+ });
155
+ forEach(options.basefiles.concat(options.parserfile), function(file) {
156
+ html.push("<script type=\"text/javascript\" src=\"" + options.path + file + "\"></script>");
157
+ });
158
+ html.push("</head><body style=\"border-width: 0;\" class=\"editbox\" spellcheck=\"" +
159
+ (options.disableSpellcheck ? "false" : "true") + "\"></body></html>");
160
+
161
+ var doc = this.win.document;
162
+ doc.open();
163
+ doc.write(html.join(""));
164
+ doc.close();
165
+ }
166
+
167
+ CodeMirror.prototype = {
168
+ init: function() {
169
+ if (this.options.initCallback) this.options.initCallback(this);
170
+ if (this.options.lineNumbers) applyLineNumbers(this.frame);
171
+ if (this.options.reindentOnLoad) this.reindent();
172
+ },
173
+
174
+ getCode: function() {return this.editor.getCode();},
175
+ setCode: function(code) {this.editor.importCode(code);},
176
+ selection: function() {return this.editor.selectedText();},
177
+ reindent: function() {this.editor.reindent();},
178
+ reindentSelection: function() {this.editor.reindentSelection(null);},
179
+
180
+ focus: function() {
181
+ this.win.focus();
182
+ if (this.editor.selectionSnapshot) // IE hack
183
+ this.win.select.selectCoords(this.win, this.editor.selectionSnapshot);
184
+ },
185
+ replaceSelection: function(text) {
186
+ this.focus();
187
+ this.editor.replaceSelection(text);
188
+ return true;
189
+ },
190
+ replaceChars: function(text, start, end) {
191
+ this.editor.replaceChars(text, start, end);
192
+ },
193
+ getSearchCursor: function(string, fromCursor) {
194
+ return this.editor.getSearchCursor(string, fromCursor);
195
+ },
196
+
197
+ undo: function() {this.editor.history.undo();},
198
+ redo: function() {this.editor.history.redo();},
199
+ historySize: function() {return this.editor.history.historySize();},
200
+ clearHistory: function() {this.editor.history.clear();},
201
+
202
+ grabKeys: function(callback, filter) {this.editor.grabKeys(callback, filter);},
203
+ ungrabKeys: function() {this.editor.ungrabKeys();},
204
+
205
+ setParser: function(name) {this.editor.setParser(name);},
206
+
207
+ cursorPosition: function(start) {
208
+ if (this.win.select.ie_selection) this.focus();
209
+ return this.editor.cursorPosition(start);
210
+ },
211
+ firstLine: function() {return this.editor.firstLine();},
212
+ lastLine: function() {return this.editor.lastLine();},
213
+ nextLine: function(line) {return this.editor.nextLine(line);},
214
+ prevLine: function(line) {return this.editor.prevLine(line);},
215
+ lineContent: function(line) {return this.editor.lineContent(line);},
216
+ setLineContent: function(line, content) {this.editor.setLineContent(line, content);},
217
+ insertIntoLine: function(line, position, content) {this.editor.insertIntoLine(line, position, content);},
218
+ selectLines: function(startLine, startOffset, endLine, endOffset) {
219
+ this.win.focus();
220
+ this.editor.selectLines(startLine, startOffset, endLine, endOffset);
221
+ },
222
+ nthLine: function(n) {
223
+ var line = this.firstLine();
224
+ for (; n > 1 && line !== false; n--)
225
+ line = this.nextLine(line);
226
+ return line;
227
+ },
228
+ lineNumber: function(line) {
229
+ var num = 0;
230
+ while (line !== false) {
231
+ num++;
232
+ line = this.prevLine(line);
233
+ }
234
+ return num;
235
+ },
236
+
237
+ // Old number-based line interface
238
+ jumpToLine: function(n) {
239
+ this.selectLines(this.nthLine(n), 0);
240
+ this.win.focus();
241
+ },
242
+ currentLine: function() {
243
+ return this.lineNumber(this.cursorPosition().line);
244
+ }
245
+ };
246
+
247
+ CodeMirror.InvalidLineHandle = {toString: function(){return "CodeMirror.InvalidLineHandle";}};
248
+
249
+ CodeMirror.replace = function(element) {
250
+ if (typeof element == "string")
251
+ element = document.getElementById(element);
252
+ return function(newElement) {
253
+ element.parentNode.replaceChild(newElement, element);
254
+ };
255
+ };
256
+
257
+ CodeMirror.fromTextArea = function(area, options) {
258
+ if (typeof area == "string")
259
+ area = document.getElementById(area);
260
+
261
+ options = options || {};
262
+ if (area.style.width && options.width == null)
263
+ options.width = area.style.width;
264
+ if (area.style.height && options.height == null)
265
+ options.height = area.style.height;
266
+ if (options.content == null) options.content = area.value;
267
+
268
+ if (area.form) {
269
+ function updateField() {
270
+ area.value = mirror.getCode();
271
+ }
272
+ if (typeof area.form.addEventListener == "function")
273
+ area.form.addEventListener("submit", updateField, false);
274
+ else
275
+ area.form.attachEvent("onsubmit", updateField);
276
+ }
277
+
278
+ function insert(frame) {
279
+ if (area.nextSibling)
280
+ area.parentNode.insertBefore(frame, area.nextSibling);
281
+ else
282
+ area.parentNode.appendChild(frame);
283
+ }
284
+
285
+ area.style.display = "none";
286
+ var mirror = new CodeMirror(insert, options);
287
+ return mirror;
288
+ };
289
+
290
+ CodeMirror.isProbablySupported = function() {
291
+ // This is rather awful, but can be useful.
292
+ var match;
293
+ if (window.opera)
294
+ return Number(window.opera.version()) >= 9.52;
295
+ else if (/Apple Computers, Inc/.test(navigator.vendor) && (match = navigator.userAgent.match(/Version\/(\d+(?:\.\d+)?)\./)))
296
+ return Number(match[1]) >= 3;
297
+ else if (document.selection && window.ActiveXObject && (match = navigator.userAgent.match(/MSIE (\d+(?:\.\d*)?)\b/)))
298
+ return Number(match[1]) >= 6;
299
+ else if (match = navigator.userAgent.match(/gecko\/(\d{8})/i))
300
+ return Number(match[1]) >= 20050901;
301
+ else if (match = navigator.userAgent.match(/AppleWebKit\/(\d+)/))
302
+ return Number(match[1]) >= 525;
303
+ else
304
+ return null;
305
+ };
306
+
307
+ return CodeMirror;
308
+ })();
@@ -0,0 +1,1340 @@
1
+ /* The Editor object manages the content of the editable frame. It
2
+ * catches events, colours nodes, and indents lines. This file also
3
+ * holds some functions for transforming arbitrary DOM structures into
4
+ * plain sequences of <span> and <br> elements
5
+ */
6
+
7
+ // Make sure a string does not contain two consecutive 'collapseable'
8
+ // whitespace characters.
9
+ function makeWhiteSpace(n) {
10
+ var buffer = [], nb = true;
11
+ for (; n > 0; n--) {
12
+ buffer.push((nb || n == 1) ? nbsp : " ");
13
+ nb = !nb;
14
+ }
15
+ return buffer.join("");
16
+ }
17
+
18
+ // Create a set of white-space characters that will not be collapsed
19
+ // by the browser, but will not break text-wrapping either.
20
+ function fixSpaces(string) {
21
+ if (string.charAt(0) == " ") string = nbsp + string.slice(1);
22
+ return string.replace(/\t/g, function(){return makeWhiteSpace(indentUnit);})
23
+ .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
24
+ }
25
+
26
+ function cleanText(text) {
27
+ return text.replace(/\u00a0/g, " ").replace(/\u200b/g, "");
28
+ }
29
+
30
+ // Create a SPAN node with the expected properties for document part
31
+ // spans.
32
+ function makePartSpan(value, doc) {
33
+ var text = value;
34
+ if (value.nodeType == 3) text = value.nodeValue;
35
+ else value = doc.createTextNode(text);
36
+
37
+ var span = doc.createElement("SPAN");
38
+ span.isPart = true;
39
+ span.appendChild(value);
40
+ span.currentText = text;
41
+ return span;
42
+ }
43
+
44
+ // On webkit, when the last BR of the document does not have text
45
+ // behind it, the cursor can not be put on the line after it. This
46
+ // makes pressing enter at the end of the document occasionally do
47
+ // nothing (or at least seem to do nothing). To work around it, this
48
+ // function makes sure the document ends with a span containing a
49
+ // zero-width space character. The traverseDOM iterator filters such
50
+ // character out again, so that the parsers won't see them. This
51
+ // function is called from a few strategic places to make sure the
52
+ // zwsp is restored after the highlighting process eats it.
53
+ var webkitLastLineHack = webkit ?
54
+ function(container) {
55
+ var last = container.lastChild;
56
+ if (!last || !last.isPart || last.textContent != "\u200b")
57
+ container.appendChild(makePartSpan("\u200b", container.ownerDocument));
58
+ } : function() {};
59
+
60
+ var Editor = (function(){
61
+ // The HTML elements whose content should be suffixed by a newline
62
+ // when converting them to flat text.
63
+ var newlineElements = {"P": true, "DIV": true, "LI": true};
64
+
65
+ function asEditorLines(string) {
66
+ var tab = makeWhiteSpace(indentUnit);
67
+ return map(string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n").split("\n"), fixSpaces);
68
+ }
69
+
70
+ // Helper function for traverseDOM. Flattens an arbitrary DOM node
71
+ // into an array of textnodes and <br> tags.
72
+ function simplifyDOM(root, atEnd) {
73
+ var doc = root.ownerDocument;
74
+ var result = [];
75
+ var leaving = true;
76
+
77
+ function simplifyNode(node, top) {
78
+ if (node.nodeType == 3) {
79
+ var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " "));
80
+ if (text.length) leaving = false;
81
+ result.push(node);
82
+ }
83
+ else if (isBR(node) && node.childNodes.length == 0) {
84
+ leaving = true;
85
+ result.push(node);
86
+ }
87
+ else {
88
+ forEach(node.childNodes, simplifyNode);
89
+ if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) {
90
+ leaving = true;
91
+ if (!atEnd || !top)
92
+ result.push(doc.createElement("BR"));
93
+ }
94
+ }
95
+ }
96
+
97
+ simplifyNode(root, true);
98
+ return result;
99
+ }
100
+
101
+ // Creates a MochiKit-style iterator that goes over a series of DOM
102
+ // nodes. The values it yields are strings, the textual content of
103
+ // the nodes. It makes sure that all nodes up to and including the
104
+ // one whose text is being yielded have been 'normalized' to be just
105
+ // <span> and <br> elements.
106
+ // See the story.html file for some short remarks about the use of
107
+ // continuation-passing style in this iterator.
108
+ function traverseDOM(start){
109
+ function yield(value, c){cc = c; return value;}
110
+ function push(fun, arg, c){return function(){return fun(arg, c);};}
111
+ function stop(){cc = stop; throw StopIteration;};
112
+ var cc = push(scanNode, start, stop);
113
+ var owner = start.ownerDocument;
114
+ var nodeQueue = [];
115
+
116
+ // Create a function that can be used to insert nodes after the
117
+ // one given as argument.
118
+ function pointAt(node){
119
+ var parent = node.parentNode;
120
+ var next = node.nextSibling;
121
+ return function(newnode) {
122
+ parent.insertBefore(newnode, next);
123
+ };
124
+ }
125
+ var point = null;
126
+
127
+ // Insert a normalized node at the current point. If it is a text
128
+ // node, wrap it in a <span>, and give that span a currentText
129
+ // property -- this is used to cache the nodeValue, because
130
+ // directly accessing nodeValue is horribly slow on some browsers.
131
+ // The dirty property is used by the highlighter to determine
132
+ // which parts of the document have to be re-highlighted.
133
+ function insertPart(part){
134
+ var text = "\n";
135
+ if (part.nodeType == 3) {
136
+ select.snapshotChanged();
137
+ part = makePartSpan(part, owner);
138
+ text = part.currentText;
139
+ }
140
+ part.dirty = true;
141
+ nodeQueue.push(part);
142
+ point(part);
143
+ return text;
144
+ }
145
+
146
+ // Extract the text and newlines from a DOM node, insert them into
147
+ // the document, and yield the textual content. Used to replace
148
+ // non-normalized nodes.
149
+ function writeNode(node, c, end) {
150
+ var toYield = [];
151
+ forEach(simplifyDOM(node, end), function(part) {
152
+ toYield.push(insertPart(part));
153
+ });
154
+ return yield(toYield.join(""), c);
155
+ }
156
+
157
+ // Check whether a node is a normalized <span> element.
158
+ function partNode(node){
159
+ if (node.isPart && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
160
+ node.currentText = node.firstChild.nodeValue;
161
+ return !/[\n\t\r]/.test(node.currentText);
162
+ }
163
+ return false;
164
+ }
165
+
166
+ // Handle a node. Add its successor to the continuation if there
167
+ // is one, find out whether the node is normalized. If it is,
168
+ // yield its content, otherwise, normalize it (writeNode will take
169
+ // care of yielding).
170
+ function scanNode(node, c){
171
+ if (node.nextSibling)
172
+ c = push(scanNode, node.nextSibling, c);
173
+
174
+ if (partNode(node)){
175
+ nodeQueue.push(node);
176
+ return yield(node.currentText, c);
177
+ }
178
+ else if (isBR(node)) {
179
+ nodeQueue.push(node);
180
+ return yield("\n", c);
181
+ }
182
+ else {
183
+ var end = !node.nextSibling;
184
+ point = pointAt(node);
185
+ removeElement(node);
186
+ return writeNode(node, c, end);
187
+ }
188
+ }
189
+
190
+ // MochiKit iterators are objects with a next function that
191
+ // returns the next value or throws StopIteration when there are
192
+ // no more values.
193
+ return {next: function(){return cc();}, nodes: nodeQueue};
194
+ }
195
+
196
+ // Determine the text size of a processed node.
197
+ function nodeSize(node) {
198
+ return isBR(node) ? 1 : node.currentText.length;
199
+ }
200
+
201
+ // Search backwards through the top-level nodes until the next BR or
202
+ // the start of the frame.
203
+ function startOfLine(node) {
204
+ while (node && !isBR(node)) node = node.previousSibling;
205
+ return node;
206
+ }
207
+ function endOfLine(node, container) {
208
+ if (!node) node = container.firstChild;
209
+ else if (isBR(node)) node = node.nextSibling;
210
+
211
+ while (node && !isBR(node)) node = node.nextSibling;
212
+ return node;
213
+ }
214
+
215
+ function time() {return new Date().getTime();}
216
+
217
+ // Client interface for searching the content of the editor. Create
218
+ // these by calling CodeMirror.getSearchCursor. To use, call
219
+ // findNext on the resulting object -- this returns a boolean
220
+ // indicating whether anything was found, and can be called again to
221
+ // skip to the next find. Use the select and replace methods to
222
+ // actually do something with the found locations.
223
+ function SearchCursor(editor, string, fromCursor, caseFold) {
224
+ this.editor = editor;
225
+ this.caseFold = caseFold;
226
+ if (caseFold) string = string.toLowerCase();
227
+ this.history = editor.history;
228
+ this.history.commit();
229
+
230
+ // Are we currently at an occurrence of the search string?
231
+ this.atOccurrence = false;
232
+ // The object stores a set of nodes coming after its current
233
+ // position, so that when the current point is taken out of the
234
+ // DOM tree, we can still try to continue.
235
+ this.fallbackSize = 15;
236
+ var cursor;
237
+ // Start from the cursor when specified and a cursor can be found.
238
+ if (fromCursor && (cursor = select.cursorPos(this.editor.container))) {
239
+ this.line = cursor.node;
240
+ this.offset = cursor.offset;
241
+ }
242
+ else {
243
+ this.line = null;
244
+ this.offset = 0;
245
+ }
246
+ this.valid = !!string;
247
+
248
+ // Create a matcher function based on the kind of string we have.
249
+ var target = string.split("\n"), self = this;
250
+ this.matches = (target.length == 1) ?
251
+ // For one-line strings, searching can be done simply by calling
252
+ // indexOf on the current line.
253
+ function() {
254
+ var line = cleanText(self.history.textAfter(self.line).slice(self.offset));
255
+ var match = (self.caseFold ? line.toLowerCase() : line).indexOf(string);
256
+ if (match > -1)
257
+ return {from: {node: self.line, offset: self.offset + match},
258
+ to: {node: self.line, offset: self.offset + match + string.length}};
259
+ } :
260
+ // Multi-line strings require internal iteration over lines, and
261
+ // some clunky checks to make sure the first match ends at the
262
+ // end of the line and the last match starts at the start.
263
+ function() {
264
+ var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
265
+ var match = (self.caseFold ? firstLine.toLowerCase() : firstLine).lastIndexOf(target[0]);
266
+ if (match == -1 || match != firstLine.length - target[0].length)
267
+ return false;
268
+ var startOffset = self.offset + match;
269
+
270
+ var line = self.history.nodeAfter(self.line);
271
+ for (var i = 1; i < target.length - 1; i++) {
272
+ var line = cleanText(self.history.textAfter(line));
273
+ if ((self.caseFold ? line.toLowerCase() : line) != target[i])
274
+ return false;
275
+ line = self.history.nodeAfter(line);
276
+ }
277
+
278
+ var lastLine = cleanText(self.history.textAfter(line));
279
+ if ((self.caseFold ? lastLine.toLowerCase() : lastLine).indexOf(target[target.length - 1]) != 0)
280
+ return false;
281
+
282
+ return {from: {node: self.line, offset: startOffset},
283
+ to: {node: line, offset: target[target.length - 1].length}};
284
+ };
285
+ }
286
+
287
+ SearchCursor.prototype = {
288
+ findNext: function() {
289
+ if (!this.valid) return false;
290
+ this.atOccurrence = false;
291
+ var self = this;
292
+
293
+ // Go back to the start of the document if the current line is
294
+ // no longer in the DOM tree.
295
+ if (this.line && !this.line.parentNode) {
296
+ this.line = null;
297
+ this.offset = 0;
298
+ }
299
+
300
+ // Set the cursor's position one character after the given
301
+ // position.
302
+ function saveAfter(pos) {
303
+ if (self.history.textAfter(pos.node).length > pos.offset) {
304
+ self.line = pos.node;
305
+ self.offset = pos.offset + 1;
306
+ }
307
+ else {
308
+ self.line = self.history.nodeAfter(pos.node);
309
+ self.offset = 0;
310
+ }
311
+ }
312
+
313
+ while (true) {
314
+ var match = this.matches();
315
+ // Found the search string.
316
+ if (match) {
317
+ this.atOccurrence = match;
318
+ saveAfter(match.from);
319
+ return true;
320
+ }
321
+ this.line = this.history.nodeAfter(this.line);
322
+ this.offset = 0;
323
+ // End of document.
324
+ if (!this.line) {
325
+ this.valid = false;
326
+ return false;
327
+ }
328
+ }
329
+ },
330
+
331
+ select: function() {
332
+ if (this.atOccurrence) {
333
+ select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
334
+ select.scrollToCursor(this.editor.container);
335
+ }
336
+ },
337
+
338
+ replace: function(string) {
339
+ if (this.atOccurrence) {
340
+ var end = this.editor.replaceRange(this.atOccurrence.from, this.atOccurrence.to, string);
341
+ this.line = end.node;
342
+ this.offset = end.offset;
343
+ this.atOccurrence = false;
344
+ }
345
+ }
346
+ };
347
+
348
+ // The Editor object is the main inside-the-iframe interface.
349
+ function Editor(options) {
350
+ this.options = options;
351
+ window.indentUnit = options.indentUnit;
352
+ this.parent = parent;
353
+ this.doc = document;
354
+ var container = this.container = this.doc.body;
355
+ this.win = window;
356
+ this.history = new History(container, options.undoDepth, options.undoDelay,
357
+ this, options.onChange);
358
+ var self = this;
359
+
360
+ if (!Editor.Parser)
361
+ throw "No parser loaded.";
362
+ if (options.parserConfig && Editor.Parser.configure)
363
+ Editor.Parser.configure(options.parserConfig);
364
+
365
+ if (!options.readOnly)
366
+ select.setCursorPos(container, {node: null, offset: 0});
367
+
368
+ this.dirty = [];
369
+ if (options.content)
370
+ this.importCode(options.content);
371
+
372
+ if (!options.readOnly) {
373
+ if (options.continuousScanning !== false) {
374
+ this.scanner = this.documentScanner(options.passTime);
375
+ this.delayScanning();
376
+ }
377
+
378
+ function setEditable() {
379
+ // In IE, designMode frames can not run any scripts, so we use
380
+ // contentEditable instead.
381
+ if (document.body.contentEditable != undefined && internetExplorer)
382
+ document.body.contentEditable = "true";
383
+ else
384
+ document.designMode = "on";
385
+
386
+ document.documentElement.style.borderWidth = "0";
387
+ if (!options.textWrapping)
388
+ container.style.whiteSpace = "nowrap";
389
+ }
390
+
391
+ // If setting the frame editable fails, try again when the user
392
+ // focus it (happens when the frame is not visible on
393
+ // initialisation, in Firefox).
394
+ try {
395
+ setEditable();
396
+ }
397
+ catch(e) {
398
+ var focusEvent = addEventHandler(document, "focus", function() {
399
+ focusEvent();
400
+ setEditable();
401
+ }, true);
402
+ }
403
+
404
+ addEventHandler(document, "keydown", method(this, "keyDown"));
405
+ addEventHandler(document, "keypress", method(this, "keyPress"));
406
+ addEventHandler(document, "keyup", method(this, "keyUp"));
407
+
408
+ function cursorActivity() {self.cursorActivity(false);}
409
+ addEventHandler(document.body, "mouseup", cursorActivity);
410
+ addEventHandler(document.body, "cut", cursorActivity);
411
+
412
+ addEventHandler(document.body, "paste", function(event) {
413
+ cursorActivity();
414
+ var text = null;
415
+ try {
416
+ var clipboardData = event.clipboardData || window.clipboardData;
417
+ if (clipboardData) text = clipboardData.getData('Text');
418
+ }
419
+ catch(e) {}
420
+ if (text !== null) {
421
+ self.replaceSelection(text);
422
+ select.scrollToCursor(this.container);
423
+ event.stop();
424
+ }
425
+ });
426
+
427
+ if (this.options.autoMatchParens)
428
+ addEventHandler(document.body, "click", method(this, "scheduleParenBlink"));
429
+ }
430
+ else if (!options.textWrapping) {
431
+ container.style.whiteSpace = "nowrap";
432
+ }
433
+ }
434
+
435
+ function isSafeKey(code) {
436
+ return (code >= 16 && code <= 18) || // shift, control, alt
437
+ (code >= 33 && code <= 40); // arrows, home, end
438
+ }
439
+
440
+ Editor.prototype = {
441
+ // Import a piece of code into the editor.
442
+ importCode: function(code) {
443
+ this.history.push(null, null, asEditorLines(code));
444
+ this.history.reset();
445
+ },
446
+
447
+ // Extract the code from the editor.
448
+ getCode: function() {
449
+ if (!this.container.firstChild)
450
+ return "";
451
+
452
+ var accum = [];
453
+ select.markSelection(this.win);
454
+ forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
455
+ webkitLastLineHack(this.container);
456
+ select.selectMarked();
457
+ return cleanText(accum.join(""));
458
+ },
459
+
460
+ checkLine: function(node) {
461
+ if (node === false || !(node == null || node.parentNode == this.container))
462
+ throw parent.CodeMirror.InvalidLineHandle;
463
+ },
464
+
465
+ cursorPosition: function(start) {
466
+ if (start == null) start = true;
467
+ var pos = select.cursorPos(this.container, start);
468
+ if (pos) return {line: pos.node, character: pos.offset};
469
+ else return {line: null, character: 0};
470
+ },
471
+
472
+ firstLine: function() {
473
+ return null;
474
+ },
475
+
476
+ lastLine: function() {
477
+ if (this.container.lastChild) return startOfLine(this.container.lastChild);
478
+ else return null;
479
+ },
480
+
481
+ nextLine: function(line) {
482
+ this.checkLine(line);
483
+ var end = endOfLine(line, this.container);
484
+ return end || false;
485
+ },
486
+
487
+ prevLine: function(line) {
488
+ this.checkLine(line);
489
+ if (line == null) return false;
490
+ return startOfLine(line.previousSibling);
491
+ },
492
+
493
+ selectLines: function(startLine, startOffset, endLine, endOffset) {
494
+ this.checkLine(startLine);
495
+ var start = {node: startLine, offset: startOffset}, end = null;
496
+ if (endOffset !== undefined) {
497
+ this.checkLine(endLine);
498
+ end = {node: endLine, offset: endOffset};
499
+ }
500
+ select.setCursorPos(this.container, start, end);
501
+ select.scrollToCursor(this.container);
502
+ },
503
+
504
+ lineContent: function(line) {
505
+ this.checkLine(line);
506
+ var accum = [];
507
+ for (line = line ? line.nextSibling : this.container.firstChild;
508
+ line && !isBR(line); line = line.nextSibling)
509
+ accum.push(nodeText(line));
510
+ return cleanText(accum.join(""));
511
+ },
512
+
513
+ setLineContent: function(line, content) {
514
+ this.history.commit();
515
+ this.replaceRange({node: line, offset: 0},
516
+ {node: line, offset: this.history.textAfter(line).length},
517
+ content);
518
+ this.addDirtyNode(line);
519
+ this.scheduleHighlight();
520
+ },
521
+
522
+ insertIntoLine: function(line, position, content) {
523
+ var before = null;
524
+ if (position == "end") {
525
+ before = endOfLine(line, this.container);
526
+ }
527
+ else {
528
+ for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
529
+ if (position == 0) {
530
+ before = cur;
531
+ break;
532
+ }
533
+ var text = nodeText(cur);
534
+ if (text.length > position) {
535
+ before = cur.nextSibling;
536
+ content = text.slice(0, position) + content + text.slice(position);
537
+ removeElement(cur);
538
+ break;
539
+ }
540
+ position -= text.length;
541
+ }
542
+ }
543
+
544
+ var lines = asEditorLines(content), doc = this.container.ownerDocument;
545
+ for (var i = 0; i < lines.length; i++) {
546
+ if (i > 0) this.container.insertBefore(doc.createElement("BR"), before);
547
+ this.container.insertBefore(makePartSpan(lines[i], doc), before);
548
+ }
549
+ this.addDirtyNode(line);
550
+ this.scheduleHighlight();
551
+ },
552
+
553
+ // Retrieve the selected text.
554
+ selectedText: function() {
555
+ var h = this.history;
556
+ h.commit();
557
+
558
+ var start = select.cursorPos(this.container, true),
559
+ end = select.cursorPos(this.container, false);
560
+ if (!start || !end) return "";
561
+
562
+ if (start.node == end.node)
563
+ return h.textAfter(start.node).slice(start.offset, end.offset);
564
+
565
+ var text = [h.textAfter(start.node).slice(start.offset)];
566
+ for (var pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos))
567
+ text.push(h.textAfter(pos));
568
+ text.push(h.textAfter(end.node).slice(0, end.offset));
569
+ return cleanText(text.join("\n"));
570
+ },
571
+
572
+ // Replace the selection with another piece of text.
573
+ replaceSelection: function(text) {
574
+ this.history.commit();
575
+
576
+ var start = select.cursorPos(this.container, true),
577
+ end = select.cursorPos(this.container, false);
578
+ if (!start || !end) return;
579
+
580
+ end = this.replaceRange(start, end, text);
581
+ select.setCursorPos(this.container, end);
582
+ webkitLastLineHack(this.container);
583
+ },
584
+
585
+ reroutePasteEvent: function() {
586
+ if (this.capturingPaste || window.opera) return;
587
+ this.capturingPaste = true;
588
+ var te = parent.document.createElement("TEXTAREA");
589
+ te.style.position = "absolute";
590
+ te.style.left = "-10000px";
591
+ te.style.width = "10px";
592
+ te.style.top = nodeTop(frameElement) + "px";
593
+ window.frameElement.CodeMirror.wrapping.appendChild(te);
594
+ parent.focus();
595
+ te.focus();
596
+
597
+ var self = this;
598
+ this.parent.setTimeout(function() {
599
+ self.capturingPaste = false;
600
+ self.win.focus();
601
+ if (self.selectionSnapshot) // IE hack
602
+ self.win.select.setBookmark(self.container, self.selectionSnapshot);
603
+ var text = te.value;
604
+ if (text) {
605
+ self.replaceSelection(text);
606
+ select.scrollToCursor(self.container);
607
+ }
608
+ removeElement(te);
609
+ }, 10);
610
+ },
611
+
612
+ replaceRange: function(from, to, text) {
613
+ var lines = asEditorLines(text);
614
+ lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
615
+ var lastLine = lines[lines.length - 1];
616
+ lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
617
+ var end = this.history.nodeAfter(to.node);
618
+ this.history.push(from.node, end, lines);
619
+ return {node: this.history.nodeBefore(end),
620
+ offset: lastLine.length};
621
+ },
622
+
623
+ getSearchCursor: function(string, fromCursor, caseFold) {
624
+ return new SearchCursor(this, string, fromCursor, caseFold);
625
+ },
626
+
627
+ // Re-indent the whole buffer
628
+ reindent: function() {
629
+ if (this.container.firstChild)
630
+ this.indentRegion(null, this.container.lastChild);
631
+ },
632
+
633
+ reindentSelection: function(direction) {
634
+ if (!select.somethingSelected(this.win)) {
635
+ this.indentAtCursor(direction);
636
+ }
637
+ else {
638
+ var start = select.selectionTopNode(this.container, true),
639
+ end = select.selectionTopNode(this.container, false);
640
+ if (start === false || end === false) return;
641
+ this.indentRegion(start, end, direction);
642
+ }
643
+ },
644
+
645
+ grabKeys: function(eventHandler, filter) {
646
+ this.frozen = eventHandler;
647
+ this.keyFilter = filter;
648
+ },
649
+ ungrabKeys: function() {
650
+ this.frozen = "leave";
651
+ this.keyFilter = null;
652
+ },
653
+
654
+ setParser: function(name) {
655
+ Editor.Parser = window[name];
656
+ if (this.container.firstChild) {
657
+ forEach(this.container.childNodes, function(n) {
658
+ if (n.nodeType != 3) n.dirty = true;
659
+ });
660
+ this.addDirtyNode(this.firstChild);
661
+ this.scheduleHighlight();
662
+ }
663
+ },
664
+
665
+ // Intercept enter and tab, and assign their new functions.
666
+ keyDown: function(event) {
667
+ if (this.frozen == "leave") this.frozen = null;
668
+ if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode))) {
669
+ event.stop();
670
+ this.frozen(event);
671
+ return;
672
+ }
673
+
674
+ var code = event.keyCode;
675
+ // Don't scan when the user is typing.
676
+ this.delayScanning();
677
+ // Schedule a paren-highlight event, if configured.
678
+ if (this.options.autoMatchParens)
679
+ this.scheduleParenBlink();
680
+
681
+ // The various checks for !altKey are there because AltGr sets both
682
+ // ctrlKey and altKey to true, and should not be recognised as
683
+ // Control.
684
+ if (code == 13) { // enter
685
+ if (event.ctrlKey && !event.altKey) {
686
+ this.reparseBuffer();
687
+ }
688
+ else {
689
+ select.insertNewlineAtCursor(this.win);
690
+ this.indentAtCursor();
691
+ select.scrollToCursor(this.container);
692
+ }
693
+ event.stop();
694
+ }
695
+ else if (code == 9 && this.options.tabMode != "default") { // tab
696
+ this.handleTab(!event.ctrlKey && !event.shiftKey);
697
+ event.stop();
698
+ }
699
+ else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
700
+ this.handleTab(true);
701
+ event.stop();
702
+ }
703
+ else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home
704
+ if (this.home()) event.stop();
705
+ }
706
+ else if (code == 35 && !event.shiftKey && !event.ctrlKey) { // end
707
+ if (this.end()) event.stop();
708
+ }
709
+ else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
710
+ this.blinkParens(event.shiftKey);
711
+ event.stop();
712
+ }
713
+ else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right
714
+ var cursor = select.selectionTopNode(this.container);
715
+ if (cursor === false || !this.container.firstChild) return;
716
+
717
+ if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
718
+ else {
719
+ var end = endOfLine(cursor, this.container);
720
+ select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
721
+ }
722
+ event.stop();
723
+ }
724
+ else if ((event.ctrlKey || event.metaKey) && !event.altKey) {
725
+ if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y
726
+ select.scrollToNode(this.history.redo());
727
+ event.stop();
728
+ }
729
+ else if (code == 90 || (safari && code == 8)) { // Z, backspace
730
+ select.scrollToNode(this.history.undo());
731
+ event.stop();
732
+ }
733
+ else if (code == 83 && this.options.saveFunction) { // S
734
+ this.options.saveFunction();
735
+ event.stop();
736
+ }
737
+ else if (internetExplorer && code == 86) {
738
+ this.reroutePasteEvent();
739
+ }
740
+ }
741
+ },
742
+
743
+ // Check for characters that should re-indent the current line,
744
+ // and prevent Opera from handling enter and tab anyway.
745
+ keyPress: function(event) {
746
+ var electric = Editor.Parser.electricChars, self = this;
747
+ // Hack for Opera, and Firefox on OS X, in which stopping a
748
+ // keydown event does not prevent the associated keypress event
749
+ // from happening, so we have to cancel enter and tab again
750
+ // here.
751
+ if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode))) ||
752
+ event.code == 13 || (event.code == 9 && this.options.tabMode != "default") ||
753
+ (event.keyCode == 32 && event.shiftKey && this.options.tabMode == "default"))
754
+ event.stop();
755
+ else if (electric && electric.indexOf(event.character) != -1)
756
+ this.parent.setTimeout(function(){self.indentAtCursor(null);}, 0);
757
+ else if ((event.character == "v" || event.character == "V")
758
+ && (event.ctrlKey || event.metaKey) && !event.altKey) // ctrl-V
759
+ this.reroutePasteEvent();
760
+ },
761
+
762
+ // Mark the node at the cursor dirty when a non-safe key is
763
+ // released.
764
+ keyUp: function(event) {
765
+ this.cursorActivity(isSafeKey(event.keyCode));
766
+ },
767
+
768
+ // Indent the line following a given <br>, or null for the first
769
+ // line. If given a <br> element, this must have been highlighted
770
+ // so that it has an indentation method. Returns the whitespace
771
+ // element that has been modified or created (if any).
772
+ indentLineAfter: function(start, direction) {
773
+ // whiteSpace is the whitespace span at the start of the line,
774
+ // or null if there is no such node.
775
+ var whiteSpace = start ? start.nextSibling : this.container.firstChild;
776
+ if (whiteSpace && !hasClass(whiteSpace, "whitespace"))
777
+ whiteSpace = null;
778
+
779
+ // Sometimes the start of the line can influence the correct
780
+ // indentation, so we retrieve it.
781
+ var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
782
+ var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
783
+
784
+ // Ask the lexical context for the correct indentation, and
785
+ // compute how much this differs from the current indentation.
786
+ var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
787
+ if (direction != null && this.options.tabMode == "shift")
788
+ newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
789
+ else if (start)
790
+ newIndent = start.indentation(nextChars, curIndent, direction);
791
+ else if (Editor.Parser.firstIndentation)
792
+ newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
793
+ var indentDiff = newIndent - curIndent;
794
+
795
+ // If there is too much, this is just a matter of shrinking a span.
796
+ if (indentDiff < 0) {
797
+ if (newIndent == 0) {
798
+ if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0);
799
+ removeElement(whiteSpace);
800
+ whiteSpace = null;
801
+ }
802
+ else {
803
+ select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
804
+ whiteSpace.currentText = makeWhiteSpace(newIndent);
805
+ whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
806
+ }
807
+ }
808
+ // Not enough...
809
+ else if (indentDiff > 0) {
810
+ // If there is whitespace, we grow it.
811
+ if (whiteSpace) {
812
+ whiteSpace.currentText = makeWhiteSpace(newIndent);
813
+ whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
814
+ }
815
+ // Otherwise, we have to add a new whitespace node.
816
+ else {
817
+ whiteSpace = makePartSpan(makeWhiteSpace(newIndent), this.doc);
818
+ whiteSpace.className = "whitespace";
819
+ if (start) insertAfter(whiteSpace, start);
820
+ else this.container.insertBefore(whiteSpace, this.container.firstChild);
821
+ }
822
+ if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
823
+ }
824
+ if (indentDiff != 0) this.addDirtyNode(start);
825
+ return whiteSpace;
826
+ },
827
+
828
+ // Re-highlight the selected part of the document.
829
+ highlightAtCursor: function() {
830
+ var pos = select.selectionTopNode(this.container, true);
831
+ var to = select.selectionTopNode(this.container, false);
832
+ if (pos === false || to === false) return;
833
+
834
+ select.markSelection(this.win);
835
+ if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
836
+ return false;
837
+ select.selectMarked();
838
+ return true;
839
+ },
840
+
841
+ // When tab is pressed with text selected, the whole selection is
842
+ // re-indented, when nothing is selected, the line with the cursor
843
+ // is re-indented.
844
+ handleTab: function(direction) {
845
+ if (this.options.tabMode == "spaces")
846
+ select.insertTabAtCursor(this.win);
847
+ else
848
+ this.reindentSelection(direction);
849
+ },
850
+
851
+ // Custom home behaviour that doesn't land the cursor in front of
852
+ // leading whitespace unless pressed twice.
853
+ home: function() {
854
+ var cur = select.selectionTopNode(this.container, true), start = cur;
855
+ if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild)
856
+ return false;
857
+
858
+ while (cur && !isBR(cur)) cur = cur.previousSibling;
859
+ var next = cur ? cur.nextSibling : this.container.firstChild;
860
+ if (next && next != start && next.isPart && hasClass(next, "whitespace"))
861
+ select.focusAfterNode(next, this.container);
862
+ else
863
+ select.focusAfterNode(cur, this.container);
864
+
865
+ select.scrollToCursor(this.container);
866
+ return true;
867
+ },
868
+
869
+ // Some browsers (Opera) don't manage to handle the end key
870
+ // properly in the face of vertical scrolling.
871
+ end: function() {
872
+ var cur = select.selectionTopNode(this.container, true);
873
+ if (cur === false) return false;
874
+ cur = endOfLine(cur, this.container);
875
+ if (!cur) return false;
876
+ select.focusAfterNode(cur.previousSibling, this.container);
877
+ select.scrollToCursor(this.container);
878
+ return true;
879
+ },
880
+
881
+ // Delay (or initiate) the next paren blink event.
882
+ scheduleParenBlink: function() {
883
+ if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
884
+ var self = this;
885
+ this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300);
886
+ },
887
+
888
+ // Take the token before the cursor. If it contains a character in
889
+ // '()[]{}', search for the matching paren/brace/bracket, and
890
+ // highlight them in green for a moment, or red if no proper match
891
+ // was found.
892
+ blinkParens: function(jump) {
893
+ if (!window.select) return;
894
+ // Clear the event property.
895
+ if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
896
+ this.parenEvent = null;
897
+
898
+ // Extract a 'paren' from a piece of text.
899
+ function paren(node) {
900
+ if (node.currentText) {
901
+ var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
902
+ return match && match[1];
903
+ }
904
+ }
905
+ // Determine the direction a paren is facing.
906
+ function forward(ch) {
907
+ return /[\(\[\{]/.test(ch);
908
+ }
909
+
910
+ var ch, self = this, cursor = select.selectionTopNode(this.container, true);
911
+ if (!cursor || !this.highlightAtCursor()) return;
912
+ cursor = select.selectionTopNode(this.container, true);
913
+ if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor)))))
914
+ return;
915
+ // We only look for tokens with the same className.
916
+ var className = cursor.className, dir = forward(ch), match = matching[ch];
917
+
918
+ // Since parts of the document might not have been properly
919
+ // highlighted, and it is hard to know in advance which part we
920
+ // have to scan, we just try, and when we find dirty nodes we
921
+ // abort, parse them, and re-try.
922
+ function tryFindMatch() {
923
+ var stack = [], ch, ok = true;;
924
+ for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
925
+ if (runner.className == className && isSpan(runner) && (ch = paren(runner))) {
926
+ if (forward(ch) == dir)
927
+ stack.push(ch);
928
+ else if (!stack.length)
929
+ ok = false;
930
+ else if (stack.pop() != matching[ch])
931
+ ok = false;
932
+ if (!stack.length) break;
933
+ }
934
+ else if (runner.dirty || !isSpan(runner) && !isBR(runner)) {
935
+ return {node: runner, status: "dirty"};
936
+ }
937
+ }
938
+ return {node: runner, status: runner && ok};
939
+ }
940
+ // Temporarily give the relevant nodes a colour.
941
+ function blink(node, ok) {
942
+ node.style.fontWeight = "bold";
943
+ node.style.color = ok ? "#8F8" : "#F88";
944
+ self.parent.setTimeout(function() {node.style.fontWeight = ""; node.style.color = "";}, 500);
945
+ }
946
+
947
+ while (true) {
948
+ var found = tryFindMatch();
949
+ if (found.status == "dirty") {
950
+ this.highlight(found.node, endOfLine(found.node));
951
+ // Needed because in some corner cases a highlight does not
952
+ // reach a node.
953
+ found.node.dirty = false;
954
+ continue;
955
+ }
956
+ else {
957
+ blink(cursor, found.status);
958
+ if (found.node) {
959
+ blink(found.node, found.status);
960
+ if (jump) select.focusAfterNode(found.node.previousSibling, this.container);
961
+ }
962
+ break;
963
+ }
964
+ }
965
+ },
966
+
967
+ // Adjust the amount of whitespace at the start of the line that
968
+ // the cursor is on so that it is indented properly.
969
+ indentAtCursor: function(direction) {
970
+ if (!this.container.firstChild) return;
971
+ // The line has to have up-to-date lexical information, so we
972
+ // highlight it first.
973
+ if (!this.highlightAtCursor()) return;
974
+ var cursor = select.selectionTopNode(this.container, false);
975
+ // If we couldn't determine the place of the cursor,
976
+ // there's nothing to indent.
977
+ if (cursor === false)
978
+ return;
979
+ var lineStart = startOfLine(cursor);
980
+ var whiteSpace = this.indentLineAfter(lineStart, direction);
981
+ if (cursor == lineStart && whiteSpace)
982
+ cursor = whiteSpace;
983
+ // This means the indentation has probably messed up the cursor.
984
+ if (cursor == whiteSpace)
985
+ select.focusAfterNode(cursor, this.container);
986
+ },
987
+
988
+ // Indent all lines whose start falls inside of the current
989
+ // selection.
990
+ indentRegion: function(start, end, direction) {
991
+ var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling);
992
+ if (!isBR(end)) end = endOfLine(end, this.container);
993
+
994
+ do {
995
+ var next = endOfLine(current, this.container);
996
+ if (current) this.highlight(before, next, true);
997
+ this.indentLineAfter(current, direction);
998
+ before = current;
999
+ current = next;
1000
+ } while (current != end);
1001
+ select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
1002
+ },
1003
+
1004
+ // Find the node that the cursor is in, mark it as dirty, and make
1005
+ // sure a highlight pass is scheduled.
1006
+ cursorActivity: function(safe) {
1007
+ if (internetExplorer) {
1008
+ this.container.createTextRange().execCommand("unlink");
1009
+ this.selectionSnapshot = select.getBookmark(this.container);
1010
+ }
1011
+
1012
+ var activity = this.options.cursorActivity;
1013
+ if (!safe || activity) {
1014
+ var cursor = select.selectionTopNode(this.container, false);
1015
+ if (cursor === false || !this.container.firstChild) return;
1016
+ cursor = cursor || this.container.firstChild;
1017
+ if (activity) activity(cursor);
1018
+ if (!safe) {
1019
+ this.scheduleHighlight();
1020
+ this.addDirtyNode(cursor);
1021
+ }
1022
+ }
1023
+ },
1024
+
1025
+ reparseBuffer: function() {
1026
+ forEach(this.container.childNodes, function(node) {node.dirty = true;});
1027
+ if (this.container.firstChild)
1028
+ this.addDirtyNode(this.container.firstChild);
1029
+ },
1030
+
1031
+ // Add a node to the set of dirty nodes, if it isn't already in
1032
+ // there.
1033
+ addDirtyNode: function(node) {
1034
+ node = node || this.container.firstChild;
1035
+ if (!node) return;
1036
+
1037
+ for (var i = 0; i < this.dirty.length; i++)
1038
+ if (this.dirty[i] == node) return;
1039
+
1040
+ if (node.nodeType != 3)
1041
+ node.dirty = true;
1042
+ this.dirty.push(node);
1043
+ },
1044
+
1045
+ // Cause a highlight pass to happen in options.passDelay
1046
+ // milliseconds. Clear the existing timeout, if one exists. This
1047
+ // way, the passes do not happen while the user is typing, and
1048
+ // should as unobtrusive as possible.
1049
+ scheduleHighlight: function() {
1050
+ // Timeouts are routed through the parent window, because on
1051
+ // some browsers designMode windows do not fire timeouts.
1052
+ var self = this;
1053
+ this.parent.clearTimeout(this.highlightTimeout);
1054
+ this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
1055
+ },
1056
+
1057
+ // Fetch one dirty node, and remove it from the dirty set.
1058
+ getDirtyNode: function() {
1059
+ while (this.dirty.length > 0) {
1060
+ var found = this.dirty.pop();
1061
+ // IE8 sometimes throws an unexplainable 'invalid argument'
1062
+ // exception for found.parentNode
1063
+ try {
1064
+ // If the node has been coloured in the meantime, or is no
1065
+ // longer in the document, it should not be returned.
1066
+ while (found && found.parentNode != this.container)
1067
+ found = found.parentNode
1068
+ if (found && (found.dirty || found.nodeType == 3))
1069
+ return found;
1070
+ } catch (e) {}
1071
+ }
1072
+ return null;
1073
+ },
1074
+
1075
+ // Pick dirty nodes, and highlight them, until options.passTime
1076
+ // milliseconds have gone by. The highlight method will continue
1077
+ // to next lines as long as it finds dirty nodes. It returns
1078
+ // information about the place where it stopped. If there are
1079
+ // dirty nodes left after this function has spent all its lines,
1080
+ // it shedules another highlight to finish the job.
1081
+ highlightDirty: function(force) {
1082
+ // Prevent FF from raising an error when it is firing timeouts
1083
+ // on a page that's no longer loaded.
1084
+ if (!window.select) return;
1085
+
1086
+ if (!this.options.readOnly) select.markSelection(this.win);
1087
+ var start, endTime = force ? null : time() + this.options.passTime;
1088
+ while ((time() < endTime || force) && (start = this.getDirtyNode())) {
1089
+ var result = this.highlight(start, endTime);
1090
+ if (result && result.node && result.dirty)
1091
+ this.addDirtyNode(result.node);
1092
+ }
1093
+ if (!this.options.readOnly) select.selectMarked();
1094
+ if (start) this.scheduleHighlight();
1095
+ return this.dirty.length == 0;
1096
+ },
1097
+
1098
+ // Creates a function that, when called through a timeout, will
1099
+ // continuously re-parse the document.
1100
+ documentScanner: function(passTime) {
1101
+ var self = this, pos = null;
1102
+ return function() {
1103
+ // FF timeout weirdness workaround.
1104
+ if (!window.select) return;
1105
+ // If the current node is no longer in the document... oh
1106
+ // well, we start over.
1107
+ if (pos && pos.parentNode != self.container)
1108
+ pos = null;
1109
+ select.markSelection(self.win);
1110
+ var result = self.highlight(pos, time() + passTime, true);
1111
+ select.selectMarked();
1112
+ var newPos = result ? (result.node && result.node.nextSibling) : null;
1113
+ pos = (pos == newPos) ? null : newPos;
1114
+ self.delayScanning();
1115
+ };
1116
+ },
1117
+
1118
+ // Starts the continuous scanning process for this document after
1119
+ // a given interval.
1120
+ delayScanning: function() {
1121
+ if (this.scanner) {
1122
+ this.parent.clearTimeout(this.documentScan);
1123
+ this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
1124
+ }
1125
+ },
1126
+
1127
+ // The function that does the actual highlighting/colouring (with
1128
+ // help from the parser and the DOM normalizer). Its interface is
1129
+ // rather overcomplicated, because it is used in different
1130
+ // situations: ensuring that a certain line is highlighted, or
1131
+ // highlighting up to X milliseconds starting from a certain
1132
+ // point. The 'from' argument gives the node at which it should
1133
+ // start. If this is null, it will start at the beginning of the
1134
+ // document. When a timestamp is given with the 'target' argument,
1135
+ // it will stop highlighting at that time. If this argument holds
1136
+ // a DOM node, it will highlight until it reaches that node. If at
1137
+ // any time it comes across two 'clean' lines (no dirty nodes), it
1138
+ // will stop, except when 'cleanLines' is true. maxBacktrack is
1139
+ // the maximum number of lines to backtrack to find an existing
1140
+ // parser instance. This is used to give up in situations where a
1141
+ // highlight would take too long and freeze the browser interface.
1142
+ highlight: function(from, target, cleanLines, maxBacktrack){
1143
+ var container = this.container, self = this, active = this.options.activeTokens;
1144
+ var endTime = (typeof target == "number" ? target : null);
1145
+
1146
+ if (!container.firstChild)
1147
+ return;
1148
+ // Backtrack to the first node before from that has a partial
1149
+ // parse stored.
1150
+ while (from && (!from.parserFromHere || from.dirty)) {
1151
+ if (maxBacktrack != null && isBR(from) && (--maxBacktrack) < 0)
1152
+ return false;
1153
+ from = from.previousSibling;
1154
+ }
1155
+ // If we are at the end of the document, do nothing.
1156
+ if (from && !from.nextSibling)
1157
+ return;
1158
+
1159
+ // Check whether a part (<span> node) and the corresponding token
1160
+ // match.
1161
+ function correctPart(token, part){
1162
+ return !part.reduced && part.currentText == token.value && part.className == token.style;
1163
+ }
1164
+ // Shorten the text associated with a part by chopping off
1165
+ // characters from the front. Note that only the currentText
1166
+ // property gets changed. For efficiency reasons, we leave the
1167
+ // nodeValue alone -- we set the reduced flag to indicate that
1168
+ // this part must be replaced.
1169
+ function shortenPart(part, minus){
1170
+ part.currentText = part.currentText.substring(minus);
1171
+ part.reduced = true;
1172
+ }
1173
+ // Create a part corresponding to a given token.
1174
+ function tokenPart(token){
1175
+ var part = makePartSpan(token.value, self.doc);
1176
+ part.className = token.style;
1177
+ return part;
1178
+ }
1179
+
1180
+ function maybeTouch(node) {
1181
+ if (node) {
1182
+ var old = node.oldNextSibling;
1183
+ if (lineDirty || old === undefined || node.nextSibling != old)
1184
+ self.history.touch(node);
1185
+ node.oldNextSibling = node.nextSibling;
1186
+ }
1187
+ else {
1188
+ var old = self.container.oldFirstChild;
1189
+ if (lineDirty || old === undefined || self.container.firstChild != old)
1190
+ self.history.touch(null);
1191
+ self.container.oldFirstChild = self.container.firstChild;
1192
+ }
1193
+ }
1194
+
1195
+ // Get the token stream. If from is null, we start with a new
1196
+ // parser from the start of the frame, otherwise a partial parse
1197
+ // is resumed.
1198
+ var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
1199
+ stream = stringStream(traversal),
1200
+ parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
1201
+
1202
+ // parts is an interface to make it possible to 'delay' fetching
1203
+ // the next DOM node until we are completely done with the one
1204
+ // before it. This is necessary because often the next node is
1205
+ // not yet available when we want to proceed past the current
1206
+ // one.
1207
+ var parts = {
1208
+ current: null,
1209
+ // Fetch current node.
1210
+ get: function(){
1211
+ if (!this.current)
1212
+ this.current = traversal.nodes.shift();
1213
+ return this.current;
1214
+ },
1215
+ // Advance to the next part (do not fetch it yet).
1216
+ next: function(){
1217
+ this.current = null;
1218
+ },
1219
+ // Remove the current part from the DOM tree, and move to the
1220
+ // next.
1221
+ remove: function(){
1222
+ container.removeChild(this.get());
1223
+ this.current = null;
1224
+ },
1225
+ // Advance to the next part that is not empty, discarding empty
1226
+ // parts.
1227
+ getNonEmpty: function(){
1228
+ var part = this.get();
1229
+ // Allow empty nodes when they are alone on a line, needed
1230
+ // for the FF cursor bug workaround (see select.js,
1231
+ // insertNewlineAtCursor).
1232
+ while (part && isSpan(part) && part.currentText == "") {
1233
+ var old = part;
1234
+ this.remove();
1235
+ part = this.get();
1236
+ // Adjust selection information, if any. See select.js for details.
1237
+ select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
1238
+ }
1239
+ return part;
1240
+ }
1241
+ };
1242
+
1243
+ var lineDirty = false, prevLineDirty = true, lineNodes = 0;
1244
+
1245
+ // This forEach loops over the tokens from the parsed stream, and
1246
+ // at the same time uses the parts object to proceed through the
1247
+ // corresponding DOM nodes.
1248
+ forEach(parsed, function(token){
1249
+ var part = parts.getNonEmpty();
1250
+
1251
+ if (token.value == "\n"){
1252
+ // The idea of the two streams actually staying synchronized
1253
+ // is such a long shot that we explicitly check.
1254
+ if (!isBR(part))
1255
+ throw "Parser out of sync. Expected BR.";
1256
+
1257
+ if (part.dirty || !part.indentation) lineDirty = true;
1258
+ maybeTouch(from);
1259
+ from = part;
1260
+
1261
+ // Every <br> gets a copy of the parser state and a lexical
1262
+ // context assigned to it. The first is used to be able to
1263
+ // later resume parsing from this point, the second is used
1264
+ // for indentation.
1265
+ part.parserFromHere = parsed.copy();
1266
+ part.indentation = token.indentation;
1267
+ part.dirty = false;
1268
+
1269
+ // If the target argument wasn't an integer, go at least
1270
+ // until that node.
1271
+ if (endTime == null && part == target) throw StopIteration;
1272
+
1273
+ // A clean line with more than one node means we are done.
1274
+ // Throwing a StopIteration is the way to break out of a
1275
+ // MochiKit forEach loop.
1276
+ if ((endTime != null && time() >= endTime) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines))
1277
+ throw StopIteration;
1278
+ prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
1279
+ parts.next();
1280
+ }
1281
+ else {
1282
+ if (!isSpan(part))
1283
+ throw "Parser out of sync. Expected SPAN.";
1284
+ if (part.dirty)
1285
+ lineDirty = true;
1286
+ lineNodes++;
1287
+
1288
+ // If the part matches the token, we can leave it alone.
1289
+ if (correctPart(token, part)){
1290
+ part.dirty = false;
1291
+ parts.next();
1292
+ }
1293
+ // Otherwise, we have to fix it.
1294
+ else {
1295
+ lineDirty = true;
1296
+ // Insert the correct part.
1297
+ var newPart = tokenPart(token);
1298
+ container.insertBefore(newPart, part);
1299
+ if (active) active(newPart, token, self);
1300
+ var tokensize = token.value.length;
1301
+ var offset = 0;
1302
+ // Eat up parts until the text for this token has been
1303
+ // removed, adjusting the stored selection info (see
1304
+ // select.js) in the process.
1305
+ while (tokensize > 0) {
1306
+ part = parts.get();
1307
+ var partsize = part.currentText.length;
1308
+ select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
1309
+ if (partsize > tokensize){
1310
+ shortenPart(part, tokensize);
1311
+ tokensize = 0;
1312
+ }
1313
+ else {
1314
+ tokensize -= partsize;
1315
+ offset += partsize;
1316
+ parts.remove();
1317
+ }
1318
+ }
1319
+ }
1320
+ }
1321
+ });
1322
+ maybeTouch(from);
1323
+ webkitLastLineHack(this.container);
1324
+
1325
+ // The function returns some status information that is used by
1326
+ // hightlightDirty to determine whether and where it has to
1327
+ // continue.
1328
+ return {node: parts.getNonEmpty(),
1329
+ dirty: lineDirty};
1330
+ }
1331
+ };
1332
+
1333
+ return Editor;
1334
+ })();
1335
+
1336
+ addEventHandler(window, "load", function() {
1337
+ var CodeMirror = window.frameElement.CodeMirror;
1338
+ CodeMirror.editor = new Editor(CodeMirror.options);
1339
+ this.parent.setTimeout(method(CodeMirror, "init"), 0);
1340
+ });