w2tags 0.9.55 → 0.9.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ });