{% block extrabody %}
{{ block.super }}
-<script src="{{ STATIC_URL }}js/lib/codemirror/codemirror.js" type="text/javascript" charset="utf-8">
+<script src="{{ STATIC_URL }}js/lib/codemirror-0.8/codemirror.js" type="text/javascript" charset="utf-8">
</script>
<script src="{{ STATIC_URL }}js/wiki/loader.js" type="text/javascript" charset="utf-8"> </script>
{% endblock %}
{% block extrabody %}
{{ block.super }}
-<script src="{{STATIC_URL}}js/lib/codemirror/codemirror.js" type="text/javascript" charset="utf-8">
+<script src="{{STATIC_URL}}js/lib/codemirror-0.8/codemirror.js" type="text/javascript" charset="utf-8">
</script>
<script src="{{STATIC_URL}}js/wiki/loader_readonly.js" type="text/javascript" charset="utf-8"> </script>
{% endblock %}
--- /dev/null
+/* CodeMirror main module
+ *
+ * Implements the CodeMirror constructor and prototype, which take care
+ * of initializing the editor frame, and providing the outside interface.
+ */
+
+// The CodeMirrorConfig object is used to specify a default
+// configuration. If you specify such an object before loading this
+// file, the values you put into it will override the defaults given
+// below. You can also assign to it after loading.
+var CodeMirrorConfig = window.CodeMirrorConfig || {};
+
+var CodeMirror = (function(){
+ function setDefaults(object, defaults) {
+ for (var option in defaults) {
+ if (!object.hasOwnProperty(option))
+ object[option] = defaults[option];
+ }
+ }
+ function forEach(array, action) {
+ for (var i = 0; i < array.length; i++)
+ action(array[i]);
+ }
+
+ // These default options can be overridden by passing a set of
+ // options to a specific CodeMirror constructor. See manual.html for
+ // their meaning.
+ setDefaults(CodeMirrorConfig, {
+ stylesheet: [],
+ path: "",
+ parserfile: [],
+ basefiles: ["util.js", "stringstream.js", "select.js", "undo.js", "editor.js", "tokenize.js"],
+ iframeClass: null,
+ passDelay: 200,
+ passTime: 50,
+ lineNumberDelay: 200,
+ lineNumberTime: 50,
+ continuousScanning: false,
+ saveFunction: null,
+ onChange: null,
+ undoDepth: 50,
+ undoDelay: 800,
+ disableSpellcheck: true,
+ textWrapping: true,
+ readOnly: false,
+ width: "",
+ height: "300px",
+ minHeight: 100,
+ autoMatchParens: false,
+ parserConfig: null,
+ tabMode: "indent", // or "spaces", "default", "shift"
+ reindentOnLoad: false,
+ activeTokens: null,
+ cursorActivity: null,
+ lineNumbers: false,
+ indentUnit: 2,
+ domain: null
+ });
+
+ function addLineNumberDiv(container) {
+ var nums = document.createElement("DIV"),
+ scroller = document.createElement("DIV");
+ nums.style.position = "absolute";
+ nums.style.height = "100%";
+ if (nums.style.setExpression) {
+ try {nums.style.setExpression("height", "this.previousSibling.offsetHeight + 'px'");}
+ catch(e) {} // Seems to throw 'Not Implemented' on some IE8 versions
+ }
+ nums.style.top = "0px";
+ nums.style.left = "0px";
+ nums.style.overflow = "hidden";
+ container.appendChild(nums);
+ scroller.className = "CodeMirror-line-numbers";
+ nums.appendChild(scroller);
+ scroller.innerHTML = "<div>1</div>";
+ return nums;
+ }
+
+ function frameHTML(options) {
+ if (typeof options.parserfile == "string")
+ options.parserfile = [options.parserfile];
+ if (typeof options.stylesheet == "string")
+ options.stylesheet = [options.stylesheet];
+
+ var html = ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\"><html><head>"];
+ // Hack to work around a bunch of IE8-specific problems.
+ html.push("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=EmulateIE7\"/>");
+ forEach(options.stylesheet, function(file) {
+ html.push("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + file + "\"/>");
+ });
+ forEach(options.basefiles.concat(options.parserfile), function(file) {
+ if (!/^https?:/.test(file)) file = options.path + file;
+ html.push("<script type=\"text/javascript\" src=\"" + file + "\"><" + "/script>");
+ });
+ html.push("</head><body style=\"border-width: 0;\" class=\"editbox\" spellcheck=\"" +
+ (options.disableSpellcheck ? "false" : "true") + "\"></body></html>");
+ return html.join("");
+ }
+
+ var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
+
+ function CodeMirror(place, options) {
+ // Use passed options, if any, to override defaults.
+ this.options = options = options || {};
+ setDefaults(options, CodeMirrorConfig);
+
+ // Backward compatibility for deprecated options.
+ if (options.dumbTabs) options.tabMode = "spaces";
+ else if (options.normalTab) options.tabMode = "default";
+
+ var frame = this.frame = document.createElement("IFRAME");
+ if (options.iframeClass) frame.className = options.iframeClass;
+ frame.frameBorder = 0;
+ frame.style.border = "0";
+ frame.style.width = '100%';
+ frame.style.height = '100%';
+ // display: block occasionally suppresses some Firefox bugs, so we
+ // always add it, redundant as it sounds.
+ frame.style.display = "block";
+
+ var div = this.wrapping = document.createElement("DIV");
+ div.style.position = "relative";
+ div.className = "CodeMirror-wrapping";
+ div.style.width = options.width;
+ div.style.height = (options.height == "dynamic") ? options.minHeight + "px" : options.height;
+ // This is used by Editor.reroutePasteEvent
+ var teHack = this.textareaHack = document.createElement("TEXTAREA");
+ div.appendChild(teHack);
+ teHack.style.position = "absolute";
+ teHack.style.left = "-10000px";
+ teHack.style.width = "10px";
+
+ // Link back to this object, so that the editor can fetch options
+ // and add a reference to itself.
+ frame.CodeMirror = this;
+ if (options.domain && internetExplorer) {
+ this.html = frameHTML(options);
+ frame.src = "javascript:(function(){document.open();" +
+ (options.domain ? "document.domain=\"" + options.domain + "\";" : "") +
+ "document.write(window.frameElement.CodeMirror.html);document.close();})()";
+ }
+ else {
+ frame.src = "javascript:false";
+ }
+
+ if (place.appendChild) place.appendChild(div);
+ else place(div);
+ div.appendChild(frame);
+ if (options.lineNumbers) this.lineNumbers = addLineNumberDiv(div);
+
+ this.win = frame.contentWindow;
+ if (!options.domain || !internetExplorer) {
+ this.win.document.open();
+ this.win.document.write(frameHTML(options));
+ this.win.document.close();
+ }
+ }
+
+ CodeMirror.prototype = {
+ init: function() {
+ if (this.options.initCallback) this.options.initCallback(this);
+ if (this.options.lineNumbers) this.activateLineNumbers();
+ if (this.options.reindentOnLoad) this.reindent();
+ if (this.options.height == "dynamic") this.setDynamicHeight();
+ },
+
+ getCode: function() {return this.editor.getCode();},
+ setCode: function(code) {this.editor.importCode(code);},
+ selection: function() {this.focusIfIE(); return this.editor.selectedText();},
+ reindent: function() {this.editor.reindent();},
+ reindentSelection: function() {this.focusIfIE(); this.editor.reindentSelection(null);},
+
+ focusIfIE: function() {
+ // in IE, a lot of selection-related functionality only works when the frame is focused
+ if (this.win.select.ie_selection) this.focus();
+ },
+ focus: function() {
+ this.win.focus();
+ if (this.editor.selectionSnapshot) // IE hack
+ this.win.select.setBookmark(this.win.document.body, this.editor.selectionSnapshot);
+ },
+ replaceSelection: function(text) {
+ this.focus();
+ this.editor.replaceSelection(text);
+ return true;
+ },
+ replaceChars: function(text, start, end) {
+ this.editor.replaceChars(text, start, end);
+ },
+ getSearchCursor: function(string, fromCursor, caseFold) {
+ return this.editor.getSearchCursor(string, fromCursor, caseFold);
+ },
+
+ undo: function() {this.editor.history.undo();},
+ redo: function() {this.editor.history.redo();},
+ historySize: function() {return this.editor.history.historySize();},
+ clearHistory: function() {this.editor.history.clear();},
+
+ grabKeys: function(callback, filter) {this.editor.grabKeys(callback, filter);},
+ ungrabKeys: function() {this.editor.ungrabKeys();},
+
+ setParser: function(name, parserConfig) {this.editor.setParser(name, parserConfig);},
+ setSpellcheck: function(on) {this.win.document.body.spellcheck = on;},
+ setStylesheet: function(names) {
+ if (typeof names === "string") names = [names];
+ var activeStylesheets = {};
+ var matchedNames = {};
+ var links = this.win.document.getElementsByTagName("link");
+ // Create hashes of active stylesheets and matched names.
+ // This is O(n^2) but n is expected to be very small.
+ for (var x = 0, link; link = links[x]; x++) {
+ if (link.rel.indexOf("stylesheet") !== -1) {
+ for (var y = 0; y < names.length; y++) {
+ var name = names[y];
+ if (link.href.substring(link.href.length - name.length) === name) {
+ activeStylesheets[link.href] = true;
+ matchedNames[name] = true;
+ }
+ }
+ }
+ }
+ // Activate the selected stylesheets and disable the rest.
+ for (var x = 0, link; link = links[x]; x++) {
+ if (link.rel.indexOf("stylesheet") !== -1) {
+ link.disabled = !(link.href in activeStylesheets);
+ }
+ }
+ // Create any new stylesheets.
+ for (var y = 0; y < names.length; y++) {
+ var name = names[y];
+ if (!(name in matchedNames)) {
+ var link = this.win.document.createElement("link");
+ link.rel = "stylesheet";
+ link.type = "text/css";
+ link.href = name;
+ this.win.document.getElementsByTagName('head')[0].appendChild(link);
+ }
+ }
+ },
+ setTextWrapping: function(on) {
+ if (on == this.options.textWrapping) return;
+ this.win.document.body.style.whiteSpace = on ? "" : "nowrap";
+ this.options.textWrapping = on;
+ if (this.lineNumbers) {
+ this.setLineNumbers(false);
+ this.setLineNumbers(true);
+ }
+ },
+ setIndentUnit: function(unit) {this.win.indentUnit = unit;},
+ setUndoDepth: function(depth) {this.editor.history.maxDepth = depth;},
+ setTabMode: function(mode) {this.options.tabMode = mode;},
+ setLineNumbers: function(on) {
+ if (on && !this.lineNumbers) {
+ this.lineNumbers = addLineNumberDiv(this.wrapping);
+ this.activateLineNumbers();
+ }
+ else if (!on && this.lineNumbers) {
+ this.wrapping.removeChild(this.lineNumbers);
+ this.wrapping.style.marginLeft = "";
+ this.lineNumbers = null;
+ }
+ },
+
+ cursorPosition: function(start) {this.focusIfIE(); return this.editor.cursorPosition(start);},
+ firstLine: function() {return this.editor.firstLine();},
+ lastLine: function() {return this.editor.lastLine();},
+ nextLine: function(line) {return this.editor.nextLine(line);},
+ prevLine: function(line) {return this.editor.prevLine(line);},
+ lineContent: function(line) {return this.editor.lineContent(line);},
+ setLineContent: function(line, content) {this.editor.setLineContent(line, content);},
+ removeLine: function(line){this.editor.removeLine(line);},
+ insertIntoLine: function(line, position, content) {this.editor.insertIntoLine(line, position, content);},
+ selectLines: function(startLine, startOffset, endLine, endOffset) {
+ this.win.focus();
+ this.editor.selectLines(startLine, startOffset, endLine, endOffset);
+ },
+ nthLine: function(n) {
+ var line = this.firstLine();
+ for (; n > 1 && line !== false; n--)
+ line = this.nextLine(line);
+ return line;
+ },
+ lineNumber: function(line) {
+ var num = 0;
+ while (line !== false) {
+ num++;
+ line = this.prevLine(line);
+ }
+ return num;
+ },
+ jumpToLine: function(line) {
+ if (typeof line == "number") line = this.nthLine(line);
+ this.selectLines(line, 0);
+ this.win.focus();
+ },
+ currentLine: function() { // Deprecated, but still there for backward compatibility
+ return this.lineNumber(this.cursorLine());
+ },
+ cursorLine: function() {
+ return this.cursorPosition().line;
+ },
+ cursorCoords: function(start) {return this.editor.cursorCoords(start);},
+
+ activateLineNumbers: function() {
+ var frame = this.frame, win = frame.contentWindow, doc = win.document, body = doc.body,
+ nums = this.lineNumbers, scroller = nums.firstChild, self = this;
+ var barWidth = null;
+
+ function sizeBar() {
+ if (frame.offsetWidth == 0) return;
+ for (var root = frame; root.parentNode; root = root.parentNode);
+ if (!nums.parentNode || root != document || !win.Editor) {
+ // Clear event handlers (their nodes might already be collected, so try/catch)
+ try{clear();}catch(e){}
+ clearInterval(sizeInterval);
+ return;
+ }
+
+ /*if (nums.offsetWidth != barWidth) {
+ barWidth = nums.offsetWidth;
+ frame.parentNode.style.paddingLeft = barWidth + "px";
+ }*/
+ }
+ function doScroll() {
+ nums.scrollTop = body.scrollTop || doc.documentElement.scrollTop || 0;
+ }
+ // Cleanup function, registered by nonWrapping and wrapping.
+ var clear = function(){};
+ sizeBar();
+ var sizeInterval = setInterval(sizeBar, 500);
+
+ function ensureEnoughLineNumbers(fill) {
+ var lineHeight = scroller.firstChild.offsetHeight;
+ if (lineHeight == 0) return;
+ var targetHeight = 50 + Math.max(body.offsetHeight, Math.max(frame.offsetHeight, body.scrollHeight || 0)),
+ lastNumber = Math.ceil(targetHeight / lineHeight);
+ for (var i = scroller.childNodes.length; i <= lastNumber; i++) {
+ var div = document.createElement("DIV");
+ div.appendChild(document.createTextNode(fill ? String(i + 1) : "\u00a0"));
+ scroller.appendChild(div);
+ }
+ }
+
+ function nonWrapping() {
+ function update() {
+ ensureEnoughLineNumbers(true);
+ doScroll();
+ }
+ self.updateNumbers = update;
+ var onScroll = win.addEventHandler(win, "scroll", doScroll, true),
+ onResize = win.addEventHandler(win, "resize", update, true);
+ clear = function(){
+ onScroll(); onResize();
+ if (self.updateNumbers == update) self.updateNumbers = null;
+ };
+ update();
+ }
+
+ function wrapping() {
+ var node, lineNum, next, pos, changes = [], styleNums = self.options.styleNumbers;
+
+ function setNum(n, node) {
+ // Does not typically happen (but can, if you mess with the
+ // document during the numbering)
+ if (!lineNum) lineNum = scroller.appendChild(document.createElement("DIV"));
+ if (styleNums) styleNums(lineNum, node, n);
+ // Changes are accumulated, so that the document layout
+ // doesn't have to be recomputed during the pass
+ changes.push(lineNum); changes.push(n);
+ pos = lineNum.offsetHeight + lineNum.offsetTop;
+ lineNum = lineNum.nextSibling;
+ }
+ function commitChanges() {
+ for (var i = 0; i < changes.length; i += 2)
+ changes[i].innerHTML = changes[i + 1];
+ changes = [];
+ }
+ function work() {
+ if (!scroller.parentNode || scroller.parentNode != self.lineNumbers) return;
+
+ var endTime = new Date().getTime() + self.options.lineNumberTime;
+ while (node) {
+ setNum(next++, node.previousSibling);
+ for (; node && !win.isBR(node); node = node.nextSibling) {
+ var bott = node.offsetTop + node.offsetHeight;
+ while (scroller.offsetHeight && bott - 3 > pos) setNum(" ");
+ }
+ if (node) node = node.nextSibling;
+ if (new Date().getTime() > endTime) {
+ commitChanges();
+ pending = setTimeout(work, self.options.lineNumberDelay);
+ return;
+ }
+ }
+ while (lineNum) setNum(next++);
+ commitChanges();
+ doScroll();
+ }
+ function start(firstTime) {
+ doScroll();
+ ensureEnoughLineNumbers(firstTime);
+ node = body.firstChild;
+ lineNum = scroller.firstChild;
+ pos = 0;
+ next = 1;
+ work();
+ }
+
+ start(true);
+ var pending = null;
+ function update() {
+ if (pending) clearTimeout(pending);
+ if (self.editor.allClean()) start();
+ else pending = setTimeout(update, 200);
+ }
+ self.updateNumbers = update;
+ var onScroll = win.addEventHandler(win, "scroll", doScroll, true),
+ onResize = win.addEventHandler(win, "resize", update, true);
+ clear = function(){
+ if (pending) clearTimeout(pending);
+ if (self.updateNumbers == update) self.updateNumbers = null;
+ onScroll();
+ onResize();
+ };
+ }
+ (this.options.textWrapping || this.options.styleNumbers ? wrapping : nonWrapping)();
+ },
+
+ setDynamicHeight: function() {
+ var self = this, activity = self.options.cursorActivity, win = self.win, body = win.document.body,
+ lineHeight = null, timeout = null, vmargin = 2 * self.frame.offsetTop;
+ body.style.overflowY = "hidden";
+ win.document.documentElement.style.overflowY = "hidden";
+ this.frame.scrolling = "no";
+
+ function updateHeight() {
+ for (var span = body.firstChild, sawBR = false; span; span = span.nextSibling)
+ if (win.isSpan(span) && span.offsetHeight) {
+ lineHeight = span.offsetHeight;
+ if (!sawBR) vmargin = 2 * (self.frame.offsetTop + span.offsetTop + body.offsetTop + (internetExplorer ? 10 : 0));
+ break;
+ }
+ if (lineHeight)
+ self.wrapping.style.height = Math.max(vmargin + lineHeight * (body.getElementsByTagName("BR").length + 1),
+ self.options.minHeight) + "px";
+ }
+ setTimeout(updateHeight, 100);
+ self.options.cursorActivity = function(x) {
+ if (activity) activity(x);
+ clearTimeout(timeout);
+ timeout = setTimeout(updateHeight, 200);
+ };
+ }
+ };
+
+ CodeMirror.InvalidLineHandle = {toString: function(){return "CodeMirror.InvalidLineHandle";}};
+
+ CodeMirror.replace = function(element) {
+ if (typeof element == "string")
+ element = document.getElementById(element);
+ return function(newElement) {
+ element.parentNode.replaceChild(newElement, element);
+ };
+ };
+
+ CodeMirror.fromTextArea = function(area, options) {
+ if (typeof area == "string")
+ area = document.getElementById(area);
+
+ options = options || {};
+ if (area.style.width && options.width == null)
+ options.width = area.style.width;
+ if (area.style.height && options.height == null)
+ options.height = area.style.height;
+ if (options.content == null) options.content = area.value;
+
+ if (area.form) {
+ function updateField() {
+ area.value = mirror.getCode();
+ }
+ if (typeof area.form.addEventListener == "function")
+ area.form.addEventListener("submit", updateField, false);
+ else
+ area.form.attachEvent("onsubmit", updateField);
+ var realSubmit = area.form.submit;
+ function wrapSubmit() {
+ updateField();
+ // Can't use realSubmit.apply because IE6 is too stupid
+ area.form.submit = realSubmit;
+ area.form.submit();
+ area.form.submit = wrapSubmit;
+ }
+ area.form.submit = wrapSubmit;
+ }
+
+ function insert(frame) {
+ if (area.nextSibling)
+ area.parentNode.insertBefore(frame, area.nextSibling);
+ else
+ area.parentNode.appendChild(frame);
+ }
+
+ area.style.display = "none";
+ var mirror = new CodeMirror(insert, options);
+ mirror.toTextArea = function() {
+ area.parentNode.removeChild(mirror.wrapping);
+ area.style.display = "";
+ if (area.form) {
+ area.form.submit = realSubmit;
+ if (typeof area.form.removeEventListener == "function")
+ area.form.removeEventListener("submit", updateField, false);
+ else
+ area.form.detachEvent("onsubmit", updateField);
+ }
+ };
+
+ return mirror;
+ };
+
+ CodeMirror.isProbablySupported = function() {
+ // This is rather awful, but can be useful.
+ var match;
+ if (window.opera)
+ return Number(window.opera.version()) >= 9.52;
+ else if (/Apple Computers, Inc/.test(navigator.vendor) && (match = navigator.userAgent.match(/Version\/(\d+(?:\.\d+)?)\./)))
+ return Number(match[1]) >= 3;
+ else if (document.selection && window.ActiveXObject && (match = navigator.userAgent.match(/MSIE (\d+(?:\.\d*)?)\b/)))
+ return Number(match[1]) >= 6;
+ else if (match = navigator.userAgent.match(/gecko\/(\d{8})/i))
+ return Number(match[1]) >= 20050901;
+ else if (match = navigator.userAgent.match(/AppleWebKit\/(\d+)/))
+ return Number(match[1]) >= 525;
+ else
+ return null;
+ };
+
+ return CodeMirror;
+})();
--- /dev/null
+/* The Editor object manages the content of the editable frame. It
+ * catches events, colours nodes, and indents lines. This file also
+ * holds some functions for transforming arbitrary DOM structures into
+ * plain sequences of <span> and <br> elements
+ */
+
+var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
+var webkit = /AppleWebKit/.test(navigator.userAgent);
+var safari = /Apple Computers, Inc/.test(navigator.vendor);
+var gecko = /gecko\/(\d{8})/i.test(navigator.userAgent);
+// TODO this is related to the backspace-at-end-of-line bug. Remove
+// this if Opera gets their act together, make the version check more
+// broad if they don't.
+var brokenOpera = window.opera && /Version\/10.[56]/.test(navigator.userAgent);
+
+// Make sure a string does not contain two consecutive 'collapseable'
+// whitespace characters.
+function makeWhiteSpace(n) {
+ var buffer = [], nb = true;
+ for (; n > 0; n--) {
+ buffer.push((nb || n == 1) ? nbsp : " ");
+ nb ^= true;
+ }
+ return buffer.join("");
+}
+
+// Create a set of white-space characters that will not be collapsed
+// by the browser, but will not break text-wrapping either.
+function fixSpaces(string) {
+ if (string.charAt(0) == " ") string = nbsp + string.slice(1);
+ return string.replace(/\t/g, function() {return makeWhiteSpace(indentUnit);})
+ .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
+}
+
+function cleanText(text) {
+ return text.replace(/\u00a0/g, " ");
+}
+
+// Create a SPAN node with the expected properties for document part
+// spans.
+function makePartSpan(value, doc) {
+ var text = value;
+ if (value.nodeType == 3) text = value.nodeValue;
+ else value = doc.createTextNode(text);
+
+ var span = doc.createElement("SPAN");
+ span.isPart = true;
+ span.appendChild(value);
+ span.currentText = text;
+ return span;
+}
+
+var Editor = (function(){
+ // The HTML elements whose content should be suffixed by a newline
+ // when converting them to flat text.
+ var newlineElements = {"P": true, "DIV": true, "LI": true};
+
+ function asEditorLines(string) {
+ var tab = makeWhiteSpace(indentUnit);
+ return map(string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n").split("\n"), fixSpaces);
+ }
+
+ // Helper function for traverseDOM. Flattens an arbitrary DOM node
+ // into an array of textnodes and <br> tags.
+ function simplifyDOM(root, atEnd) {
+ var doc = root.ownerDocument;
+ var result = [];
+ var leaving = true;
+
+ function simplifyNode(node, top) {
+ if (node.nodeType == 3) {
+ var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/\r/g, "").replace(/\n/g, " "));
+ if (text.length) leaving = false;
+ result.push(node);
+ }
+ else if (isBR(node) && node.childNodes.length == 0) {
+ leaving = true;
+ result.push(node);
+ }
+ else {
+ for (var n = node.firstChild; n; n = n.nextSibling) simplifyNode(n);
+ if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) {
+ leaving = true;
+ if (!atEnd || !top)
+ result.push(doc.createElement("BR"));
+ }
+ }
+ }
+
+ simplifyNode(root, true);
+ return result;
+ }
+
+ // Creates a MochiKit-style iterator that goes over a series of DOM
+ // nodes. The values it yields are strings, the textual content of
+ // the nodes. It makes sure that all nodes up to and including the
+ // one whose text is being yielded have been 'normalized' to be just
+ // <span> and <br> elements.
+ function traverseDOM(start){
+ var owner = start.ownerDocument;
+ var nodeQueue = [];
+
+ // Create a function that can be used to insert nodes after the
+ // one given as argument.
+ function pointAt(node){
+ var parent = node.parentNode;
+ var next = node.nextSibling;
+ return function(newnode) {
+ parent.insertBefore(newnode, next);
+ };
+ }
+ var point = null;
+
+ // This an Opera-specific hack -- always insert an empty span
+ // between two BRs, because Opera's cursor code gets terribly
+ // confused when the cursor is between two BRs.
+ var afterBR = true;
+
+ // Insert a normalized node at the current point. If it is a text
+ // node, wrap it in a <span>, and give that span a currentText
+ // property -- this is used to cache the nodeValue, because
+ // directly accessing nodeValue is horribly slow on some browsers.
+ // The dirty property is used by the highlighter to determine
+ // which parts of the document have to be re-highlighted.
+ function insertPart(part){
+ var text = "\n";
+ if (part.nodeType == 3) {
+ select.snapshotChanged();
+ part = makePartSpan(part, owner);
+ text = part.currentText;
+ afterBR = false;
+ }
+ else {
+ if (afterBR && window.opera)
+ point(makePartSpan("", owner));
+ afterBR = true;
+ }
+ part.dirty = true;
+ nodeQueue.push(part);
+ point(part);
+ return text;
+ }
+
+ // Extract the text and newlines from a DOM node, insert them into
+ // the document, and return the textual content. Used to replace
+ // non-normalized nodes.
+ function writeNode(node, end) {
+ var simplified = simplifyDOM(node, end);
+ for (var i = 0; i < simplified.length; i++)
+ simplified[i] = insertPart(simplified[i]);
+ return simplified.join("");
+ }
+
+ // Check whether a node is a normalized <span> element.
+ function partNode(node){
+ if (node.isPart && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
+ node.currentText = node.firstChild.nodeValue;
+ return !/[\n\t\r]/.test(node.currentText);
+ }
+ return false;
+ }
+
+ // Advance to next node, return string for current node.
+ function next() {
+ if (!start) throw StopIteration;
+ var node = start;
+ start = node.nextSibling;
+
+ if (partNode(node)){
+ nodeQueue.push(node);
+ afterBR = false;
+ return node.currentText;
+ }
+ else if (isBR(node)) {
+ if (afterBR && window.opera)
+ node.parentNode.insertBefore(makePartSpan("", owner), node);
+ nodeQueue.push(node);
+ afterBR = true;
+ return "\n";
+ }
+ else {
+ var end = !node.nextSibling;
+ point = pointAt(node);
+ removeElement(node);
+ return writeNode(node, end);
+ }
+ }
+
+ // MochiKit iterators are objects with a next function that
+ // returns the next value or throws StopIteration when there are
+ // no more values.
+ return {next: next, nodes: nodeQueue};
+ }
+
+ // Determine the text size of a processed node.
+ function nodeSize(node) {
+ return isBR(node) ? 1 : node.currentText.length;
+ }
+
+ // Search backwards through the top-level nodes until the next BR or
+ // the start of the frame.
+ function startOfLine(node) {
+ while (node && !isBR(node)) node = node.previousSibling;
+ return node;
+ }
+ function endOfLine(node, container) {
+ if (!node) node = container.firstChild;
+ else if (isBR(node)) node = node.nextSibling;
+
+ while (node && !isBR(node)) node = node.nextSibling;
+ return node;
+ }
+
+ function time() {return new Date().getTime();}
+
+ // Client interface for searching the content of the editor. Create
+ // these by calling CodeMirror.getSearchCursor. To use, call
+ // findNext on the resulting object -- this returns a boolean
+ // indicating whether anything was found, and can be called again to
+ // skip to the next find. Use the select and replace methods to
+ // actually do something with the found locations.
+ function SearchCursor(editor, string, fromCursor, caseFold) {
+ this.editor = editor;
+ if (caseFold == undefined) {
+ caseFold = (string == string.toLowerCase());
+ }
+ this.caseFold = caseFold;
+ if (caseFold) string = string.toLowerCase();
+ this.history = editor.history;
+ this.history.commit();
+
+ // Are we currently at an occurrence of the search string?
+ this.atOccurrence = false;
+ // The object stores a set of nodes coming after its current
+ // position, so that when the current point is taken out of the
+ // DOM tree, we can still try to continue.
+ this.fallbackSize = 15;
+ var cursor;
+ // Start from the cursor when specified and a cursor can be found.
+ if (fromCursor && (cursor = select.cursorPos(this.editor.container))) {
+ this.line = cursor.node;
+ this.offset = cursor.offset;
+ }
+ else {
+ this.line = null;
+ this.offset = 0;
+ }
+ this.valid = !!string;
+
+ // Create a matcher function based on the kind of string we have.
+ var target = string.split("\n"), self = this;
+ this.matches = (target.length == 1) ?
+ // For one-line strings, searching can be done simply by calling
+ // indexOf on the current line.
+ function() {
+ var line = cleanText(self.history.textAfter(self.line).slice(self.offset));
+ var match = (self.caseFold ? line.toLowerCase() : line).indexOf(string);
+ if (match > -1)
+ return {from: {node: self.line, offset: self.offset + match},
+ to: {node: self.line, offset: self.offset + match + string.length}};
+ } :
+ // Multi-line strings require internal iteration over lines, and
+ // some clunky checks to make sure the first match ends at the
+ // end of the line and the last match starts at the start.
+ function() {
+ var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
+ var match = (self.caseFold ? firstLine.toLowerCase() : firstLine).lastIndexOf(target[0]);
+ if (match == -1 || match != firstLine.length - target[0].length)
+ return false;
+ var startOffset = self.offset + match;
+
+ var line = self.history.nodeAfter(self.line);
+ for (var i = 1; i < target.length - 1; i++) {
+ var lineText = cleanText(self.history.textAfter(line));
+ if ((self.caseFold ? lineText.toLowerCase() : lineText) != target[i])
+ return false;
+ line = self.history.nodeAfter(line);
+ }
+
+ var lastLine = cleanText(self.history.textAfter(line));
+ if ((self.caseFold ? lastLine.toLowerCase() : lastLine).indexOf(target[target.length - 1]) != 0)
+ return false;
+
+ return {from: {node: self.line, offset: startOffset},
+ to: {node: line, offset: target[target.length - 1].length}};
+ };
+ }
+
+ SearchCursor.prototype = {
+ findNext: function() {
+ if (!this.valid) return false;
+ this.atOccurrence = false;
+ var self = this;
+
+ // Go back to the start of the document if the current line is
+ // no longer in the DOM tree.
+ if (this.line && !this.line.parentNode) {
+ this.line = null;
+ this.offset = 0;
+ }
+
+ // Set the cursor's position one character after the given
+ // position.
+ function saveAfter(pos) {
+ if (self.history.textAfter(pos.node).length > pos.offset) {
+ self.line = pos.node;
+ self.offset = pos.offset + 1;
+ }
+ else {
+ self.line = self.history.nodeAfter(pos.node);
+ self.offset = 0;
+ }
+ }
+
+ while (true) {
+ var match = this.matches();
+ // Found the search string.
+ if (match) {
+ this.atOccurrence = match;
+ saveAfter(match.from);
+ return true;
+ }
+ this.line = this.history.nodeAfter(this.line);
+ this.offset = 0;
+ // End of document.
+ if (!this.line) {
+ this.valid = false;
+ return false;
+ }
+ }
+ },
+
+ select: function() {
+ if (this.atOccurrence) {
+ select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
+ select.scrollToCursor(this.editor.container);
+ }
+ },
+
+ replace: function(string) {
+ if (this.atOccurrence) {
+ var end = this.editor.replaceRange(this.atOccurrence.from, this.atOccurrence.to, string);
+ this.line = end.node;
+ this.offset = end.offset;
+ this.atOccurrence = false;
+ }
+ }
+ };
+
+ // The Editor object is the main inside-the-iframe interface.
+ function Editor(options) {
+ this.options = options;
+ window.indentUnit = options.indentUnit;
+ this.parent = parent;
+ this.doc = document;
+ var container = this.container = this.doc.body;
+ this.win = window;
+ this.history = new UndoHistory(container, options.undoDepth, options.undoDelay, this);
+ var self = this;
+
+ if (!Editor.Parser)
+ throw "No parser loaded.";
+ if (options.parserConfig && Editor.Parser.configure)
+ Editor.Parser.configure(options.parserConfig);
+
+ if (!options.readOnly)
+ select.setCursorPos(container, {node: null, offset: 0});
+
+ this.dirty = [];
+ this.importCode(options.content || "");
+ this.history.onChange = options.onChange;
+
+ if (!options.readOnly) {
+ if (options.continuousScanning !== false) {
+ this.scanner = this.documentScanner(options.passTime);
+ this.delayScanning();
+ }
+
+ function setEditable() {
+ // Use contentEditable instead of designMode on IE, since designMode frames
+ // can not run any scripts. It would be nice if we could use contentEditable
+ // everywhere, but it is significantly flakier than designMode on every
+ // single non-IE browser.
+ if (document.body.contentEditable != undefined && internetExplorer)
+ document.body.contentEditable = "true";
+ else
+ document.designMode = "on";
+
+ document.documentElement.style.borderWidth = "0";
+ if (!options.textWrapping)
+ container.style.whiteSpace = "nowrap";
+ }
+
+ // If setting the frame editable fails, try again when the user
+ // focus it (happens when the frame is not visible on
+ // initialisation, in Firefox).
+ try {
+ setEditable();
+ }
+ catch(e) {
+ var focusEvent = addEventHandler(document, "focus", function() {
+ focusEvent();
+ setEditable();
+ }, true);
+ }
+
+ addEventHandler(document, "keydown", method(this, "keyDown"));
+ addEventHandler(document, "keypress", method(this, "keyPress"));
+ addEventHandler(document, "keyup", method(this, "keyUp"));
+
+ function cursorActivity() {self.cursorActivity(false);}
+ addEventHandler(document.body, "mouseup", cursorActivity);
+ addEventHandler(document.body, "cut", cursorActivity);
+
+ // workaround for a gecko bug [?] where going forward and then
+ // back again breaks designmode (no more cursor)
+ if (gecko)
+ addEventHandler(this.win, "pagehide", function(){self.unloaded = true;});
+
+ addEventHandler(document.body, "paste", function(event) {
+ cursorActivity();
+ var text = null;
+ try {
+ var clipboardData = event.clipboardData || window.clipboardData;
+ if (clipboardData) text = clipboardData.getData('Text');
+ }
+ catch(e) {}
+ if (text !== null) {
+ event.stop();
+ self.replaceSelection(text);
+ select.scrollToCursor(self.container);
+ }
+ });
+
+ if (this.options.autoMatchParens)
+ addEventHandler(document.body, "click", method(this, "scheduleParenHighlight"));
+ }
+ else if (!options.textWrapping) {
+ container.style.whiteSpace = "nowrap";
+ }
+ }
+
+ function isSafeKey(code) {
+ return (code >= 16 && code <= 18) || // shift, control, alt
+ (code >= 33 && code <= 40); // arrows, home, end
+ }
+
+ Editor.prototype = {
+ // Import a piece of code into the editor.
+ importCode: function(code) {
+ this.history.push(null, null, asEditorLines(code));
+ this.history.reset();
+ },
+
+ // Extract the code from the editor.
+ getCode: function() {
+ if (!this.container.firstChild)
+ return "";
+
+ var accum = [];
+ select.markSelection(this.win);
+ forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
+ select.selectMarked();
+ return cleanText(accum.join(""));
+ },
+
+ checkLine: function(node) {
+ if (node === false || !(node == null || node.parentNode == this.container))
+ throw parent.CodeMirror.InvalidLineHandle;
+ },
+
+ cursorPosition: function(start) {
+ if (start == null) start = true;
+ var pos = select.cursorPos(this.container, start);
+ if (pos) return {line: pos.node, character: pos.offset};
+ else return {line: null, character: 0};
+ },
+
+ firstLine: function() {
+ return null;
+ },
+
+ lastLine: function() {
+ if (this.container.lastChild) return startOfLine(this.container.lastChild);
+ else return null;
+ },
+
+ nextLine: function(line) {
+ this.checkLine(line);
+ var end = endOfLine(line, this.container);
+ return end || false;
+ },
+
+ prevLine: function(line) {
+ this.checkLine(line);
+ if (line == null) return false;
+ return startOfLine(line.previousSibling);
+ },
+
+ visibleLineCount: function() {
+ var line = this.container.firstChild;
+ while (line && isBR(line)) line = line.nextSibling; // BR heights are unreliable
+ if (!line) return false;
+ var innerHeight = (window.innerHeight
+ || document.documentElement.clientHeight
+ || document.body.clientHeight);
+ return Math.floor(innerHeight / line.offsetHeight);
+ },
+
+ selectLines: function(startLine, startOffset, endLine, endOffset) {
+ this.checkLine(startLine);
+ var start = {node: startLine, offset: startOffset}, end = null;
+ if (endOffset !== undefined) {
+ this.checkLine(endLine);
+ end = {node: endLine, offset: endOffset};
+ }
+ select.setCursorPos(this.container, start, end);
+ select.scrollToCursor(this.container);
+ },
+
+ lineContent: function(line) {
+ var accum = [];
+ for (line = line ? line.nextSibling : this.container.firstChild;
+ line && !isBR(line); line = line.nextSibling)
+ accum.push(nodeText(line));
+ return cleanText(accum.join(""));
+ },
+
+ setLineContent: function(line, content) {
+ this.history.commit();
+ this.replaceRange({node: line, offset: 0},
+ {node: line, offset: this.history.textAfter(line).length},
+ content);
+ this.addDirtyNode(line);
+ this.scheduleHighlight();
+ },
+
+ removeLine: function(line) {
+ var node = line ? line.nextSibling : this.container.firstChild;
+ while (node) {
+ var next = node.nextSibling;
+ removeElement(node);
+ if (isBR(node)) break;
+ node = next;
+ }
+ this.addDirtyNode(line);
+ this.scheduleHighlight();
+ },
+
+ insertIntoLine: function(line, position, content) {
+ var before = null;
+ if (position == "end") {
+ before = endOfLine(line, this.container);
+ }
+ else {
+ for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
+ if (position == 0) {
+ before = cur;
+ break;
+ }
+ var text = nodeText(cur);
+ if (text.length > position) {
+ before = cur.nextSibling;
+ content = text.slice(0, position) + content + text.slice(position);
+ removeElement(cur);
+ break;
+ }
+ position -= text.length;
+ }
+ }
+
+ var lines = asEditorLines(content), doc = this.container.ownerDocument;
+ for (var i = 0; i < lines.length; i++) {
+ if (i > 0) this.container.insertBefore(doc.createElement("BR"), before);
+ this.container.insertBefore(makePartSpan(lines[i], doc), before);
+ }
+ this.addDirtyNode(line);
+ this.scheduleHighlight();
+ },
+
+ // Retrieve the selected text.
+ selectedText: function() {
+ var h = this.history;
+ h.commit();
+
+ var start = select.cursorPos(this.container, true),
+ end = select.cursorPos(this.container, false);
+ if (!start || !end) return "";
+
+ if (start.node == end.node)
+ return h.textAfter(start.node).slice(start.offset, end.offset);
+
+ var text = [h.textAfter(start.node).slice(start.offset)];
+ for (var pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos))
+ text.push(h.textAfter(pos));
+ text.push(h.textAfter(end.node).slice(0, end.offset));
+ return cleanText(text.join("\n"));
+ },
+
+ // Replace the selection with another piece of text.
+ replaceSelection: function(text) {
+ this.history.commit();
+
+ var start = select.cursorPos(this.container, true),
+ end = select.cursorPos(this.container, false);
+ if (!start || !end) return;
+
+ end = this.replaceRange(start, end, text);
+ select.setCursorPos(this.container, end);
+ },
+
+ cursorCoords: function(start) {
+ var sel = select.cursorPos(this.container, start);
+ if (!sel) return null;
+ var off = sel.offset, node = sel.node, doc = this.win.document, self = this;
+ function measureFromNode(node, xOffset) {
+ var y = -(self.win.document.body.scrollTop || self.win.document.documentElement.scrollTop || 0),
+ x = -(self.win.document.body.scrollLeft || self.win.document.documentElement.scrollLeft || 0) + xOffset;
+ forEach([node, self.win.frameElement], function(n) {
+ while (n) {x += n.offsetLeft; y += n.offsetTop;n = n.offsetParent;}
+ });
+ return {x: x, y: y, yBot: y + node.offsetHeight};
+ }
+ function withTempNode(text, f) {
+ var node = doc.createElement("SPAN");
+ node.appendChild(doc.createTextNode(text));
+ try {return f(node);}
+ finally {if (node.parentNode) node.parentNode.removeChild(node);}
+ }
+
+ while (off) {
+ node = node ? node.nextSibling : this.container.firstChild;
+ var txt = nodeText(node);
+ if (off < txt.length)
+ return withTempNode(txt.substr(0, off), function(tmp) {
+ tmp.style.position = "absolute"; tmp.style.visibility = "hidden";
+ tmp.className = node.className;
+ self.container.appendChild(tmp);
+ return measureFromNode(node, tmp.offsetWidth);
+ });
+ off -= txt.length;
+ }
+ if (node && isSpan(node))
+ return measureFromNode(node, node.offsetWidth);
+ else if (node && node.nextSibling && isSpan(node.nextSibling))
+ return measureFromNode(node.nextSibling, 0);
+ else
+ return withTempNode("\u200b", function(tmp) {
+ if (node) node.parentNode.insertBefore(tmp, node.nextSibling);
+ else self.container.insertBefore(tmp, self.container.firstChild);
+ return measureFromNode(tmp, 0);
+ });
+ },
+
+ reroutePasteEvent: function() {
+ if (this.capturingPaste || window.opera) return;
+ this.capturingPaste = true;
+ var te = window.frameElement.CodeMirror.textareaHack;
+ parent.focus();
+ te.value = "";
+ te.focus();
+
+ var self = this;
+ this.parent.setTimeout(function() {
+ self.capturingPaste = false;
+ self.win.focus();
+ if (self.selectionSnapshot) // IE hack
+ self.win.select.setBookmark(self.container, self.selectionSnapshot);
+ var text = te.value;
+ if (text) {
+ self.replaceSelection(text);
+ select.scrollToCursor(self.container);
+ }
+ }, 10);
+ },
+
+ replaceRange: function(from, to, text) {
+ var lines = asEditorLines(text);
+ lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
+ var lastLine = lines[lines.length - 1];
+ lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
+ var end = this.history.nodeAfter(to.node);
+ this.history.push(from.node, end, lines);
+ return {node: this.history.nodeBefore(end),
+ offset: lastLine.length};
+ },
+
+ getSearchCursor: function(string, fromCursor, caseFold) {
+ return new SearchCursor(this, string, fromCursor, caseFold);
+ },
+
+ // Re-indent the whole buffer
+ reindent: function() {
+ if (this.container.firstChild)
+ this.indentRegion(null, this.container.lastChild);
+ },
+
+ reindentSelection: function(direction) {
+ if (!select.somethingSelected(this.win)) {
+ this.indentAtCursor(direction);
+ }
+ else {
+ var start = select.selectionTopNode(this.container, true),
+ end = select.selectionTopNode(this.container, false);
+ if (start === false || end === false) return;
+ this.indentRegion(start, end, direction);
+ }
+ },
+
+ grabKeys: function(eventHandler, filter) {
+ this.frozen = eventHandler;
+ this.keyFilter = filter;
+ },
+ ungrabKeys: function() {
+ this.frozen = "leave";
+ },
+
+ setParser: function(name, parserConfig) {
+ Editor.Parser = window[name];
+ parserConfig = parserConfig || this.options.parserConfig;
+ if (parserConfig && Editor.Parser.configure)
+ Editor.Parser.configure(parserConfig);
+
+ if (this.container.firstChild) {
+ forEach(this.container.childNodes, function(n) {
+ if (n.nodeType != 3) n.dirty = true;
+ });
+ this.addDirtyNode(this.firstChild);
+ this.scheduleHighlight();
+ }
+ },
+
+ // Intercept enter and tab, and assign their new functions.
+ keyDown: function(event) {
+ if (this.frozen == "leave") {this.frozen = null; this.keyFilter = null;}
+ if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode, event))) {
+ event.stop();
+ this.frozen(event);
+ return;
+ }
+
+ var code = this.lastKeyDownCode = event.keyCode;
+ // Don't scan when the user is typing.
+ this.delayScanning();
+ // Schedule a paren-highlight event, if configured.
+ if (this.options.autoMatchParens)
+ this.scheduleParenHighlight();
+
+ // The various checks for !altKey are there because AltGr sets both
+ // ctrlKey and altKey to true, and should not be recognised as
+ // Control.
+ if (code == 13) { // enter
+ if (event.ctrlKey && !event.altKey) {
+ this.reparseBuffer();
+ }
+ else {
+ select.insertNewlineAtCursor(this.win);
+ this.indentAtCursor();
+ select.scrollToCursor(this.container);
+ }
+ event.stop();
+ }
+ else if (code == 9 && this.options.tabMode != "default" && !event.ctrlKey) { // tab
+ this.handleTab(!event.shiftKey);
+ event.stop();
+ }
+ else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
+ this.handleTab(true);
+ event.stop();
+ }
+ else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home
+ if (this.home()) event.stop();
+ }
+ else if (code == 35 && !event.shiftKey && !event.ctrlKey) { // end
+ if (this.end()) event.stop();
+ }
+ // Only in Firefox is the default behavior for PgUp/PgDn correct.
+ else if (code == 33 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgUp
+ if (this.pageUp()) event.stop();
+ }
+ else if (code == 34 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgDn
+ if (this.pageDown()) event.stop();
+ }
+ else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
+ this.highlightParens(event.shiftKey, true);
+ event.stop();
+ }
+ else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right
+ var cursor = select.selectionTopNode(this.container);
+ if (cursor !== false && this.container.firstChild) {
+ if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
+ else {
+ var end = endOfLine(cursor, this.container);
+ select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
+ }
+ event.stop();
+ }
+ }
+ else if ((event.ctrlKey || event.metaKey) && !event.altKey) {
+ if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y
+ select.scrollToNode(this.history.redo());
+ event.stop();
+ }
+ else if (code == 90 || (safari && code == 8)) { // Z, backspace
+ select.scrollToNode(this.history.undo());
+ event.stop();
+ }
+ else if (code == 83 && this.options.saveFunction) { // S
+ this.options.saveFunction();
+ event.stop();
+ }
+ else if (internetExplorer && code == 86) {
+ this.reroutePasteEvent();
+ }
+ }
+ this.keyUpOrPressAfterLastKeyDown = false;
+ },
+
+ // Check for characters that should re-indent the current line,
+ // and prevent Opera from handling enter and tab anyway.
+ keyPress: function(event) {
+ this.keyUpOrPressAfterLastKeyDown = true;
+ var electric = Editor.Parser.electricChars, self = this;
+ // Hack for Opera, and Firefox on OS X, in which stopping a
+ // keydown event does not prevent the associated keypress event
+ // from happening, so we have to cancel enter and tab again
+ // here.
+ if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode || event.code, event))) ||
+ event.code == 13 || (event.code == 9 && this.options.tabMode != "default") ||
+ (event.code == 32 && event.shiftKey && this.options.tabMode == "default"))
+ event.stop();
+ else if (electric && electric.indexOf(event.character) != -1)
+ this.parent.setTimeout(function(){self.indentAtCursor(null);}, 0);
+ else if ((event.character == "v" || event.character == "V")
+ && (event.ctrlKey || event.metaKey) && !event.altKey) // ctrl-V
+ this.reroutePasteEvent();
+ // Work around a bug where pressing backspace at the end of a
+ // line often causes the cursor to jump to the start of the line
+ // in Opera 10.60.
+ else if (brokenOpera && event.code == 8) {
+ var sel = select.selectionTopNode(this.container), self = this,
+ next = sel ? sel.nextSibling : this.container.firstChild;
+ if (sel !== false && next && isBR(next))
+ this.parent.setTimeout(function(){
+ if (select.selectionTopNode(self.container) == next)
+ select.focusAfterNode(next.previousSibling, self.container);
+ }, 20);
+ }
+ },
+
+ // Mark the node at the cursor dirty when a non-safe key is
+ // released.
+ keyUp: function(event) {
+ this.keyUpOrPressAfterLastKeyDown = true;
+ this.cursorActivity(isSafeKey(event.keyCode));
+ },
+
+ // Indent the line following a given <br>, or null for the first
+ // line. If given a <br> element, this must have been highlighted
+ // so that it has an indentation method. Returns the whitespace
+ // element that has been modified or created (if any).
+ indentLineAfter: function(start, direction) {
+ // whiteSpace is the whitespace span at the start of the line,
+ // or null if there is no such node.
+ var whiteSpace = start ? start.nextSibling : this.container.firstChild;
+ if (whiteSpace && !hasClass(whiteSpace, "whitespace"))
+ whiteSpace = null;
+
+ // Sometimes the start of the line can influence the correct
+ // indentation, so we retrieve it.
+ var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
+ var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
+
+ // Ask the lexical context for the correct indentation, and
+ // compute how much this differs from the current indentation.
+ var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
+ if (direction != null && this.options.tabMode == "shift")
+ newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
+ else if (start)
+ newIndent = start.indentation(nextChars, curIndent, direction);
+ else if (Editor.Parser.firstIndentation)
+ newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
+ var indentDiff = newIndent - curIndent;
+
+ // If there is too much, this is just a matter of shrinking a span.
+ if (indentDiff < 0) {
+ if (newIndent == 0) {
+ if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0);
+ removeElement(whiteSpace);
+ whiteSpace = null;
+ }
+ else {
+ select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
+ whiteSpace.currentText = makeWhiteSpace(newIndent);
+ whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
+ }
+ }
+ // Not enough...
+ else if (indentDiff > 0) {
+ // If there is whitespace, we grow it.
+ if (whiteSpace) {
+ whiteSpace.currentText = makeWhiteSpace(newIndent);
+ whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
+ select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
+ }
+ // Otherwise, we have to add a new whitespace node.
+ else {
+ whiteSpace = makePartSpan(makeWhiteSpace(newIndent), this.doc);
+ whiteSpace.className = "whitespace";
+ if (start) insertAfter(whiteSpace, start);
+ else this.container.insertBefore(whiteSpace, this.container.firstChild);
+ select.snapshotMove(firstText && (firstText.firstChild || firstText),
+ whiteSpace.firstChild, newIndent, false, true);
+ }
+ }
+ if (indentDiff != 0) this.addDirtyNode(start);
+ },
+
+ // Re-highlight the selected part of the document.
+ highlightAtCursor: function() {
+ var pos = select.selectionTopNode(this.container, true);
+ var to = select.selectionTopNode(this.container, false);
+ if (pos === false || to === false) return false;
+
+ select.markSelection(this.win);
+ if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
+ return false;
+ select.selectMarked();
+ return true;
+ },
+
+ // When tab is pressed with text selected, the whole selection is
+ // re-indented, when nothing is selected, the line with the cursor
+ // is re-indented.
+ handleTab: function(direction) {
+ if (this.options.tabMode == "spaces")
+ select.insertTabAtCursor(this.win);
+ else
+ this.reindentSelection(direction);
+ },
+
+ // Custom home behaviour that doesn't land the cursor in front of
+ // leading whitespace unless pressed twice.
+ home: function() {
+ var cur = select.selectionTopNode(this.container, true), start = cur;
+ if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild)
+ return false;
+
+ while (cur && !isBR(cur)) cur = cur.previousSibling;
+ var next = cur ? cur.nextSibling : this.container.firstChild;
+ if (next && next != start && next.isPart && hasClass(next, "whitespace"))
+ select.focusAfterNode(next, this.container);
+ else
+ select.focusAfterNode(cur, this.container);
+
+ select.scrollToCursor(this.container);
+ return true;
+ },
+
+ // Some browsers (Opera) don't manage to handle the end key
+ // properly in the face of vertical scrolling.
+ end: function() {
+ var cur = select.selectionTopNode(this.container, true);
+ if (cur === false) return false;
+ cur = endOfLine(cur, this.container);
+ if (!cur) return false;
+ select.focusAfterNode(cur.previousSibling, this.container);
+ select.scrollToCursor(this.container);
+ return true;
+ },
+
+ pageUp: function() {
+ var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
+ if (line === false || scrollAmount === false) return false;
+ // Try to keep one line on the screen.
+ scrollAmount -= 2;
+ for (var i = 0; i < scrollAmount; i++) {
+ line = this.prevLine(line);
+ if (line === false) break;
+ }
+ if (i == 0) return false; // Already at first line
+ select.setCursorPos(this.container, {node: line, offset: 0});
+ select.scrollToCursor(this.container);
+ return true;
+ },
+
+ pageDown: function() {
+ var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
+ if (line === false || scrollAmount === false) return false;
+ // Try to move to the last line of the current page.
+ scrollAmount -= 2;
+ for (var i = 0; i < scrollAmount; i++) {
+ var nextLine = this.nextLine(line);
+ if (nextLine === false) break;
+ line = nextLine;
+ }
+ if (i == 0) return false; // Already at last line
+ select.setCursorPos(this.container, {node: line, offset: 0});
+ select.scrollToCursor(this.container);
+ return true;
+ },
+
+ // Delay (or initiate) the next paren highlight event.
+ scheduleParenHighlight: function() {
+ if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
+ var self = this;
+ this.parenEvent = this.parent.setTimeout(function(){self.highlightParens();}, 300);
+ },
+
+ // Take the token before the cursor. If it contains a character in
+ // '()[]{}', search for the matching paren/brace/bracket, and
+ // highlight them in green for a moment, or red if no proper match
+ // was found.
+ highlightParens: function(jump, fromKey) {
+ var self = this;
+ // give the relevant nodes a colour.
+ function highlight(node, ok) {
+ if (!node) return;
+ if (self.options.markParen) {
+ self.options.markParen(node, ok);
+ }
+ else {
+ node.style.fontWeight = "bold";
+ node.style.color = ok ? "#8F8" : "#F88";
+ }
+ }
+ function unhighlight(node) {
+ if (!node) return;
+ if (self.options.unmarkParen) {
+ self.options.unmarkParen(node);
+ }
+ else {
+ node.style.fontWeight = "";
+ node.style.color = "";
+ }
+ }
+ if (!fromKey && self.highlighted) {
+ unhighlight(self.highlighted[0]);
+ unhighlight(self.highlighted[1]);
+ }
+
+ if (!window.select) return;
+ // Clear the event property.
+ if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
+ this.parenEvent = null;
+
+ // Extract a 'paren' from a piece of text.
+ function paren(node) {
+ if (node.currentText) {
+ var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
+ return match && match[1];
+ }
+ }
+ // Determine the direction a paren is facing.
+ function forward(ch) {
+ return /[\(\[\{]/.test(ch);
+ }
+
+ var ch, cursor = select.selectionTopNode(this.container, true);
+ if (!cursor || !this.highlightAtCursor()) return;
+ cursor = select.selectionTopNode(this.container, true);
+ if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor)))))
+ return;
+ // We only look for tokens with the same className.
+ var className = cursor.className, dir = forward(ch), match = matching[ch];
+
+ // Since parts of the document might not have been properly
+ // highlighted, and it is hard to know in advance which part we
+ // have to scan, we just try, and when we find dirty nodes we
+ // abort, parse them, and re-try.
+ function tryFindMatch() {
+ var stack = [], ch, ok = true;
+ for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
+ if (runner.className == className && isSpan(runner) && (ch = paren(runner))) {
+ if (forward(ch) == dir)
+ stack.push(ch);
+ else if (!stack.length)
+ ok = false;
+ else if (stack.pop() != matching[ch])
+ ok = false;
+ if (!stack.length) break;
+ }
+ else if (runner.dirty || !isSpan(runner) && !isBR(runner)) {
+ return {node: runner, status: "dirty"};
+ }
+ }
+ return {node: runner, status: runner && ok};
+ }
+
+ while (true) {
+ var found = tryFindMatch();
+ if (found.status == "dirty") {
+ this.highlight(found.node, endOfLine(found.node));
+ // Needed because in some corner cases a highlight does not
+ // reach a node.
+ found.node.dirty = false;
+ continue;
+ }
+ else {
+ highlight(cursor, found.status);
+ highlight(found.node, found.status);
+ if (fromKey)
+ self.parent.setTimeout(function() {unhighlight(cursor); unhighlight(found.node);}, 500);
+ else
+ self.highlighted = [cursor, found.node];
+ if (jump && found.node)
+ select.focusAfterNode(found.node.previousSibling, this.container);
+ break;
+ }
+ }
+ },
+
+ // Adjust the amount of whitespace at the start of the line that
+ // the cursor is on so that it is indented properly.
+ indentAtCursor: function(direction) {
+ if (!this.container.firstChild) return;
+ // The line has to have up-to-date lexical information, so we
+ // highlight it first.
+ if (!this.highlightAtCursor()) return;
+ var cursor = select.selectionTopNode(this.container, false);
+ // If we couldn't determine the place of the cursor,
+ // there's nothing to indent.
+ if (cursor === false)
+ return;
+ select.markSelection(this.win);
+ this.indentLineAfter(startOfLine(cursor), direction);
+ select.selectMarked();
+ },
+
+ // Indent all lines whose start falls inside of the current
+ // selection.
+ indentRegion: function(start, end, direction) {
+ var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling);
+ if (!isBR(end)) end = endOfLine(end, this.container);
+ this.addDirtyNode(start);
+
+ do {
+ var next = endOfLine(current, this.container);
+ if (current) this.highlight(before, next, true);
+ this.indentLineAfter(current, direction);
+ before = current;
+ current = next;
+ } while (current != end);
+ select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
+ },
+
+ // Find the node that the cursor is in, mark it as dirty, and make
+ // sure a highlight pass is scheduled.
+ cursorActivity: function(safe) {
+ // pagehide event hack above
+ if (this.unloaded) {
+ this.win.document.designMode = "off";
+ this.win.document.designMode = "on";
+ this.unloaded = false;
+ }
+
+ if (internetExplorer) {
+ this.container.createTextRange().execCommand("unlink");
+ this.selectionSnapshot = select.getBookmark(this.container);
+ }
+
+ var activity = this.options.cursorActivity;
+ if (!safe || activity) {
+ var cursor = select.selectionTopNode(this.container, false);
+ if (cursor === false || !this.container.firstChild) return;
+ cursor = cursor || this.container.firstChild;
+ if (activity) activity(cursor);
+ if (!safe) {
+ this.scheduleHighlight();
+ this.addDirtyNode(cursor);
+ }
+ }
+ },
+
+ reparseBuffer: function() {
+ forEach(this.container.childNodes, function(node) {node.dirty = true;});
+ if (this.container.firstChild)
+ this.addDirtyNode(this.container.firstChild);
+ },
+
+ // Add a node to the set of dirty nodes, if it isn't already in
+ // there.
+ addDirtyNode: function(node) {
+ node = node || this.container.firstChild;
+ if (!node) return;
+
+ for (var i = 0; i < this.dirty.length; i++)
+ if (this.dirty[i] == node) return;
+
+ if (node.nodeType != 3)
+ node.dirty = true;
+ this.dirty.push(node);
+ },
+
+ allClean: function() {
+ return !this.dirty.length;
+ },
+
+ // Cause a highlight pass to happen in options.passDelay
+ // milliseconds. Clear the existing timeout, if one exists. This
+ // way, the passes do not happen while the user is typing, and
+ // should as unobtrusive as possible.
+ scheduleHighlight: function() {
+ // Timeouts are routed through the parent window, because on
+ // some browsers designMode windows do not fire timeouts.
+ var self = this;
+ this.parent.clearTimeout(this.highlightTimeout);
+ this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
+ },
+
+ // Fetch one dirty node, and remove it from the dirty set.
+ getDirtyNode: function() {
+ while (this.dirty.length > 0) {
+ var found = this.dirty.pop();
+ // IE8 sometimes throws an unexplainable 'invalid argument'
+ // exception for found.parentNode
+ try {
+ // If the node has been coloured in the meantime, or is no
+ // longer in the document, it should not be returned.
+ while (found && found.parentNode != this.container)
+ found = found.parentNode;
+ if (found && (found.dirty || found.nodeType == 3))
+ return found;
+ } catch (e) {}
+ }
+ return null;
+ },
+
+ // Pick dirty nodes, and highlight them, until options.passTime
+ // milliseconds have gone by. The highlight method will continue
+ // to next lines as long as it finds dirty nodes. It returns
+ // information about the place where it stopped. If there are
+ // dirty nodes left after this function has spent all its lines,
+ // it shedules another highlight to finish the job.
+ highlightDirty: function(force) {
+ // Prevent FF from raising an error when it is firing timeouts
+ // on a page that's no longer loaded.
+ if (!window.select) return false;
+
+ if (!this.options.readOnly) select.markSelection(this.win);
+ var start, endTime = force ? null : time() + this.options.passTime;
+ while ((time() < endTime || force) && (start = this.getDirtyNode())) {
+ var result = this.highlight(start, endTime);
+ if (result && result.node && result.dirty)
+ this.addDirtyNode(result.node);
+ }
+ if (!this.options.readOnly) select.selectMarked();
+ if (start) this.scheduleHighlight();
+ return this.dirty.length == 0;
+ },
+
+ // Creates a function that, when called through a timeout, will
+ // continuously re-parse the document.
+ documentScanner: function(passTime) {
+ var self = this, pos = null;
+ return function() {
+ // FF timeout weirdness workaround.
+ if (!window.select) return;
+ // If the current node is no longer in the document... oh
+ // well, we start over.
+ if (pos && pos.parentNode != self.container)
+ pos = null;
+ select.markSelection(self.win);
+ var result = self.highlight(pos, time() + passTime, true);
+ select.selectMarked();
+ var newPos = result ? (result.node && result.node.nextSibling) : null;
+ pos = (pos == newPos) ? null : newPos;
+ self.delayScanning();
+ };
+ },
+
+ // Starts the continuous scanning process for this document after
+ // a given interval.
+ delayScanning: function() {
+ if (this.scanner) {
+ this.parent.clearTimeout(this.documentScan);
+ this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
+ }
+ },
+
+ isIMEOn: function() {
+ // chrome: keyDown keyCode is 229 while IME on
+ // firefox: no keyUps or keyPresses fires after first keyDown while IME on
+ return this.lastKeyDownCode == 229 || this.keyUpOrPressAfterLastKeyDown === false;
+ },
+
+ // The function that does the actual highlighting/colouring (with
+ // help from the parser and the DOM normalizer). Its interface is
+ // rather overcomplicated, because it is used in different
+ // situations: ensuring that a certain line is highlighted, or
+ // highlighting up to X milliseconds starting from a certain
+ // point. The 'from' argument gives the node at which it should
+ // start. If this is null, it will start at the beginning of the
+ // document. When a timestamp is given with the 'target' argument,
+ // it will stop highlighting at that time. If this argument holds
+ // a DOM node, it will highlight until it reaches that node. If at
+ // any time it comes across two 'clean' lines (no dirty nodes), it
+ // will stop, except when 'cleanLines' is true. maxBacktrack is
+ // the maximum number of lines to backtrack to find an existing
+ // parser instance. This is used to give up in situations where a
+ // highlight would take too long and freeze the browser interface.
+ highlight: function(from, target, cleanLines, maxBacktrack){
+ var container = this.container, self = this, active = this.options.activeTokens;
+ var endTime = (typeof target == "number" ? target : null);
+
+ if (!container.firstChild || this.isIMEOn())
+ return false;
+ // Backtrack to the first node before from that has a partial
+ // parse stored.
+ while (from && (!from.parserFromHere || from.dirty)) {
+ if (maxBacktrack != null && isBR(from) && (--maxBacktrack) < 0)
+ return false;
+ from = from.previousSibling;
+ }
+ // If we are at the end of the document, do nothing.
+ if (from && !from.nextSibling)
+ return false;
+
+ // Check whether a part (<span> node) and the corresponding token
+ // match.
+ function correctPart(token, part){
+ return !part.reduced && part.currentText == token.value && part.className == token.style;
+ }
+ // Shorten the text associated with a part by chopping off
+ // characters from the front. Note that only the currentText
+ // property gets changed. For efficiency reasons, we leave the
+ // nodeValue alone -- we set the reduced flag to indicate that
+ // this part must be replaced.
+ function shortenPart(part, minus){
+ part.currentText = part.currentText.substring(minus);
+ part.reduced = true;
+ }
+ // Create a part corresponding to a given token.
+ function tokenPart(token){
+ var part = makePartSpan(token.value, self.doc);
+ part.className = token.style;
+ return part;
+ }
+
+ function maybeTouch(node) {
+ if (node) {
+ var old = node.oldNextSibling;
+ if (lineDirty || old === undefined || node.nextSibling != old)
+ self.history.touch(node);
+ node.oldNextSibling = node.nextSibling;
+ }
+ else {
+ var old = self.container.oldFirstChild;
+ if (lineDirty || old === undefined || self.container.firstChild != old)
+ self.history.touch(null);
+ self.container.oldFirstChild = self.container.firstChild;
+ }
+ }
+
+ // Get the token stream. If from is null, we start with a new
+ // parser from the start of the frame, otherwise a partial parse
+ // is resumed.
+ var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
+ stream = stringStream(traversal),
+ parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
+
+ function surroundedByBRs(node) {
+ return (node.previousSibling == null || isBR(node.previousSibling)) &&
+ (node.nextSibling == null || isBR(node.nextSibling));
+ }
+
+ // parts is an interface to make it possible to 'delay' fetching
+ // the next DOM node until we are completely done with the one
+ // before it. This is necessary because often the next node is
+ // not yet available when we want to proceed past the current
+ // one.
+ var parts = {
+ current: null,
+ // Fetch current node.
+ get: function(){
+ if (!this.current)
+ this.current = traversal.nodes.shift();
+ return this.current;
+ },
+ // Advance to the next part (do not fetch it yet).
+ next: function(){
+ this.current = null;
+ },
+ // Remove the current part from the DOM tree, and move to the
+ // next.
+ remove: function(){
+ container.removeChild(this.get());
+ this.current = null;
+ },
+ // Advance to the next part that is not empty, discarding empty
+ // parts.
+ getNonEmpty: function(){
+ var part = this.get();
+ // Allow empty nodes when they are alone on a line, needed
+ // for the FF cursor bug workaround (see select.js,
+ // insertNewlineAtCursor).
+ while (part && isSpan(part) && part.currentText == "") {
+ // Leave empty nodes that are alone on a line alone in
+ // Opera, since that browsers doesn't deal well with
+ // having 2 BRs in a row.
+ if (window.opera && surroundedByBRs(part)) {
+ this.next();
+ part = this.get();
+ }
+ else {
+ var old = part;
+ this.remove();
+ part = this.get();
+ // Adjust selection information, if any. See select.js for details.
+ select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
+ }
+ }
+
+ return part;
+ }
+ };
+
+ var lineDirty = false, prevLineDirty = true, lineNodes = 0;
+
+ // This forEach loops over the tokens from the parsed stream, and
+ // at the same time uses the parts object to proceed through the
+ // corresponding DOM nodes.
+ forEach(parsed, function(token){
+ var part = parts.getNonEmpty();
+
+ if (token.value == "\n"){
+ // The idea of the two streams actually staying synchronized
+ // is such a long shot that we explicitly check.
+ if (!isBR(part))
+ throw "Parser out of sync. Expected BR.";
+
+ if (part.dirty || !part.indentation) lineDirty = true;
+ maybeTouch(from);
+ from = part;
+
+ // Every <br> gets a copy of the parser state and a lexical
+ // context assigned to it. The first is used to be able to
+ // later resume parsing from this point, the second is used
+ // for indentation.
+ part.parserFromHere = parsed.copy();
+ part.indentation = token.indentation;
+ part.dirty = false;
+
+ // If the target argument wasn't an integer, go at least
+ // until that node.
+ if (endTime == null && part == target) throw StopIteration;
+
+ // A clean line with more than one node means we are done.
+ // Throwing a StopIteration is the way to break out of a
+ // MochiKit forEach loop.
+ if ((endTime != null && time() >= endTime) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines))
+ throw StopIteration;
+ prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
+ parts.next();
+ }
+ else {
+ if (!isSpan(part))
+ throw "Parser out of sync. Expected SPAN.";
+ if (part.dirty)
+ lineDirty = true;
+ lineNodes++;
+
+ // If the part matches the token, we can leave it alone.
+ if (correctPart(token, part)){
+ part.dirty = false;
+ parts.next();
+ }
+ // Otherwise, we have to fix it.
+ else {
+ lineDirty = true;
+ // Insert the correct part.
+ var newPart = tokenPart(token);
+ container.insertBefore(newPart, part);
+ if (active) active(newPart, token, self);
+ var tokensize = token.value.length;
+ var offset = 0;
+ // Eat up parts until the text for this token has been
+ // removed, adjusting the stored selection info (see
+ // select.js) in the process.
+ while (tokensize > 0) {
+ part = parts.get();
+ var partsize = part.currentText.length;
+ select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
+ if (partsize > tokensize){
+ shortenPart(part, tokensize);
+ tokensize = 0;
+ }
+ else {
+ tokensize -= partsize;
+ offset += partsize;
+ parts.remove();
+ }
+ }
+ }
+ }
+ });
+ maybeTouch(from);
+
+ // The function returns some status information that is used by
+ // hightlightDirty to determine whether and where it has to
+ // continue.
+ return {node: parts.getNonEmpty(),
+ dirty: lineDirty};
+ }
+ };
+
+ return Editor;
+})();
+
+addEventHandler(window, "load", function() {
+ var CodeMirror = window.frameElement.CodeMirror;
+ var e = CodeMirror.editor = new Editor(CodeMirror.options);
+ this.parent.setTimeout(method(CodeMirror, "init"), 0);
+});
--- /dev/null
+/* This file defines an XML parser, with a few kludges to make it
+ * useable for HTML. autoSelfClosers defines a set of tag names that
+ * are expected to not have a closing tag, and doNotIndent specifies
+ * the tags inside of which no indentation should happen (see Config
+ * object). These can be disabled by passing the editor an object like
+ * {useHTMLKludges: false} as parserConfig option.
+ */
+
+var XMLParser = Editor.Parser = (function() {
+ var Kludges = {
+ autoSelfClosers: {"br": true, "img": true, "hr": true, "link": true, "input": true,
+ "meta": true, "col": true, "frame": true, "base": true, "area": true},
+ doNotIndent: {"pre": true, "!cdata": true}
+ };
+ var NoKludges = {autoSelfClosers: {}, doNotIndent: {"!cdata": true}};
+ var UseKludges = Kludges;
+ var alignCDATA = false;
+
+ // Simple stateful tokenizer for XML documents. Returns a
+ // MochiKit-style iterator, with a state property that contains a
+ // function encapsulating the current state. See tokenize.js.
+ var tokenizeXML = (function() {
+ function inText(source, setState) {
+ var ch = source.next();
+ if (ch == "<") {
+ if (source.equals("!")) {
+ source.next();
+ if (source.equals("[")) {
+ if (source.lookAhead("[CDATA[", true)) {
+ setState(inBlock("xml-cdata", "]]>"));
+ return null;
+ }
+ else {
+ return "xml-text";
+ }
+ }
+ else if (source.lookAhead("--", true)) {
+ setState(inBlock("xml-comment", "-->"));
+ return null;
+ }
+ else {
+ return "xml-text";
+ }
+ }
+ else if (source.equals("?")) {
+ source.next();
+ source.nextWhileMatches(/[\w\._\-]/);
+ setState(inBlock("xml-processing", "?>"));
+ return "xml-processing";
+ }
+ else {
+ if (source.equals("/")) source.next();
+ setState(inTag);
+ return "xml-punctuation";
+ }
+ }
+ else if (ch == "&") {
+ while (!source.endOfLine()) {
+ if (source.next() == ";")
+ break;
+ }
+ return "xml-entity";
+ }
+ else {
+ source.nextWhileMatches(/[^&<\n]/);
+ return "xml-text";
+ }
+ }
+
+ function inTag(source, setState) {
+ var ch = source.next();
+ if (ch == ">") {
+ setState(inText);
+ return "xml-punctuation";
+ }
+ else if (/[?\/]/.test(ch) && source.equals(">")) {
+ source.next();
+ setState(inText);
+ return "xml-punctuation";
+ }
+ else if (ch == "=") {
+ return "xml-punctuation";
+ }
+ else if (/[\'\"]/.test(ch)) {
+ setState(inAttribute(ch));
+ return null;
+ }
+ else {
+ source.nextWhileMatches(/[^\s\u00a0=<>\"\'\/?]/);
+ return "xml-name";
+ }
+ }
+
+ function inAttribute(quote) {
+ return function(source, setState) {
+ while (!source.endOfLine()) {
+ if (source.next() == quote) {
+ setState(inTag);
+ break;
+ }
+ }
+ return "xml-attribute";
+ };
+ }
+
+ function inBlock(style, terminator) {
+ return function(source, setState) {
+ while (!source.endOfLine()) {
+ if (source.lookAhead(terminator, true)) {
+ setState(inText);
+ break;
+ }
+ source.next();
+ }
+ return style;
+ };
+ }
+
+ return function(source, startState) {
+ return tokenizer(source, startState || inText);
+ };
+ })();
+
+ // The parser. The structure of this function largely follows that of
+ // parseJavaScript in parsejavascript.js (there is actually a bit more
+ // shared code than I'd like), but it is quite a bit simpler.
+ function parseXML(source) {
+ var tokens = tokenizeXML(source), token;
+ var cc = [base];
+ var tokenNr = 0, indented = 0;
+ var currentTag = null, context = null;
+ var consume;
+
+ function push(fs) {
+ for (var i = fs.length - 1; i >= 0; i--)
+ cc.push(fs[i]);
+ }
+ function cont() {
+ push(arguments);
+ consume = true;
+ }
+ function pass() {
+ push(arguments);
+ consume = false;
+ }
+
+ function markErr() {
+ token.style += " xml-error";
+ }
+ function expect(text) {
+ return function(style, content) {
+ if (content == text) cont();
+ else {markErr(); cont(arguments.callee);}
+ };
+ }
+
+ function pushContext(tagname, startOfLine) {
+ var noIndent = UseKludges.doNotIndent.hasOwnProperty(tagname) || (context && context.noIndent);
+ context = {prev: context, name: tagname, indent: indented, startOfLine: startOfLine, noIndent: noIndent};
+ }
+ function popContext() {
+ context = context.prev;
+ }
+ function computeIndentation(baseContext) {
+ return function(nextChars, current) {
+ var context = baseContext;
+ if (context && context.noIndent)
+ return current;
+ if (alignCDATA && /<!\[CDATA\[/.test(nextChars))
+ return 0;
+ if (context && /^<\//.test(nextChars))
+ context = context.prev;
+ while (context && !context.startOfLine)
+ context = context.prev;
+ if (context)
+ return context.indent + indentUnit;
+ else
+ return 0;
+ };
+ }
+
+ function base() {
+ return pass(element, base);
+ }
+ var harmlessTokens = {"xml-text": true, "xml-entity": true, "xml-comment": true, "xml-processing": true};
+ function element(style, content) {
+ if (content == "<") cont(tagname, attributes, endtag(tokenNr == 1));
+ else if (content == "</") cont(closetagname, expect(">"));
+ else if (style == "xml-cdata") {
+ if (!context || context.name != "!cdata") pushContext("!cdata");
+ if (/\]\]>$/.test(content)) popContext();
+ cont();
+ }
+ else if (harmlessTokens.hasOwnProperty(style)) cont();
+ else {markErr(); cont();}
+ }
+ function tagname(style, content) {
+ if (style == "xml-name") {
+ currentTag = content.toLowerCase();
+ token.style = "xml-tagname";
+ cont();
+ }
+ else {
+ currentTag = null;
+ pass();
+ }
+ }
+ function closetagname(style, content) {
+ if (style == "xml-name") {
+ token.style = "xml-tagname";
+ if (context && content.toLowerCase() == context.name) popContext();
+ else markErr();
+ }
+ cont();
+ }
+ function endtag(startOfLine) {
+ return function(style, content) {
+ if (content == "/>" || (content == ">" && UseKludges.autoSelfClosers.hasOwnProperty(currentTag))) cont();
+ else if (content == ">") {pushContext(currentTag, startOfLine); cont();}
+ else {markErr(); cont(arguments.callee);}
+ };
+ }
+ function attributes(style) {
+ if (style == "xml-name") {token.style = "xml-attname"; cont(attribute, attributes);}
+ else pass();
+ }
+ function attribute(style, content) {
+ if (content == "=") cont(value);
+ else if (content == ">" || content == "/>") pass(endtag);
+ else pass();
+ }
+ function value(style) {
+ if (style == "xml-attribute") cont(value);
+ else pass();
+ }
+
+ return {
+ indentation: function() {return indented;},
+
+ next: function(){
+ token = tokens.next();
+ if (token.style == "whitespace" && tokenNr == 0)
+ indented = token.value.length;
+ else
+ tokenNr++;
+ if (token.content == "\n") {
+ indented = tokenNr = 0;
+ token.indentation = computeIndentation(context);
+ }
+
+ if (token.style == "whitespace" || token.type == "xml-comment")
+ return token;
+
+ while(true){
+ consume = false;
+ cc.pop()(token.style, token.content);
+ if (consume) return token;
+ }
+ },
+
+ copy: function(){
+ var _cc = cc.concat([]), _tokenState = tokens.state, _context = context;
+ var parser = this;
+
+ return function(input){
+ cc = _cc.concat([]);
+ tokenNr = indented = 0;
+ context = _context;
+ tokens = tokenizeXML(input, _tokenState);
+ return parser;
+ };
+ }
+ };
+ }
+
+ return {
+ make: parseXML,
+ electricChars: "/",
+ configure: function(config) {
+ if (config.useHTMLKludges != null)
+ UseKludges = config.useHTMLKludges ? Kludges : NoKludges;
+ if (config.alignCDATA)
+ alignCDATA = config.alignCDATA;
+ }
+ };
+})();
--- /dev/null
+/* Functionality for finding, storing, and restoring selections
+ *
+ * This does not provide a generic API, just the minimal functionality
+ * required by the CodeMirror system.
+ */
+
+// Namespace object.
+var select = {};
+
+(function() {
+ select.ie_selection = document.selection && document.selection.createRangeCollection;
+
+ // Find the 'top-level' (defined as 'a direct child of the node
+ // passed as the top argument') node that the given node is
+ // contained in. Return null if the given node is not inside the top
+ // node.
+ function topLevelNodeAt(node, top) {
+ while (node && node.parentNode != top)
+ node = node.parentNode;
+ return node;
+ }
+
+ // Find the top-level node that contains the node before this one.
+ function topLevelNodeBefore(node, top) {
+ while (!node.previousSibling && node.parentNode != top)
+ node = node.parentNode;
+ return topLevelNodeAt(node.previousSibling, top);
+ }
+
+ var fourSpaces = "\u00a0\u00a0\u00a0\u00a0";
+
+ select.scrollToNode = function(node, cursor) {
+ if (!node) return;
+ var element = node,
+ doc = element.ownerDocument, body = doc.body,
+ win = (doc.defaultView || doc.parentWindow),
+ html = doc.documentElement,
+ atEnd = !element.nextSibling || !element.nextSibling.nextSibling
+ || !element.nextSibling.nextSibling.nextSibling;
+ // In Opera (and recent Webkit versions), BR elements *always*
+ // have a offsetTop property of zero.
+ var compensateHack = 0;
+ while (element && !element.offsetTop) {
+ compensateHack++;
+ element = element.previousSibling;
+ }
+ // atEnd is another kludge for these browsers -- if the cursor is
+ // at the end of the document, and the node doesn't have an
+ // offset, just scroll to the end.
+ if (compensateHack == 0) atEnd = false;
+
+ // WebKit has a bad habit of (sometimes) happily returning bogus
+ // offsets when the document has just been changed. This seems to
+ // always be 5/5, so we don't use those.
+ if (webkit && element && element.offsetTop == 5 && element.offsetLeft == 5)
+ return;
+
+ var y = compensateHack * (element ? element.offsetHeight : 0), x = 0,
+ width = (node ? node.offsetWidth : 0), pos = element;
+ while (pos && pos.offsetParent) {
+ y += pos.offsetTop;
+ // Don't count X offset for <br> nodes
+ if (!isBR(pos))
+ x += pos.offsetLeft;
+ pos = pos.offsetParent;
+ }
+
+ var scroll_x = body.scrollLeft || html.scrollLeft || 0,
+ scroll_y = body.scrollTop || html.scrollTop || 0,
+ scroll = false, screen_width = win.innerWidth || html.clientWidth || 0;
+
+ if (cursor || width < screen_width) {
+ if (cursor) {
+ var off = select.offsetInNode(win, node), size = nodeText(node).length;
+ if (size) x += width * (off / size);
+ }
+ var screen_x = x - scroll_x;
+ if (screen_x < 0 || screen_x > screen_width) {
+ scroll_x = x;
+ scroll = true;
+ }
+ }
+ var screen_y = y - scroll_y;
+ if (screen_y < 0 || atEnd || screen_y > (win.innerHeight || html.clientHeight || 0) - 50) {
+ scroll_y = atEnd ? 1e6 : y;
+ scroll = true;
+ }
+ if (scroll) win.scrollTo(scroll_x, scroll_y);
+ };
+
+ select.scrollToCursor = function(container) {
+ select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild, true);
+ };
+
+ // Used to prevent restoring a selection when we do not need to.
+ var currentSelection = null;
+
+ select.snapshotChanged = function() {
+ if (currentSelection) currentSelection.changed = true;
+ };
+
+ // This is called by the code in editor.js whenever it is replacing
+ // a text node. The function sees whether the given oldNode is part
+ // of the current selection, and updates this selection if it is.
+ // Because nodes are often only partially replaced, the length of
+ // the part that gets replaced has to be taken into account -- the
+ // selection might stay in the oldNode if the newNode is smaller
+ // than the selection's offset. The offset argument is needed in
+ // case the selection does move to the new object, and the given
+ // length is not the whole length of the new node (part of it might
+ // have been used to replace another node).
+ select.snapshotReplaceNode = function(from, to, length, offset) {
+ if (!currentSelection) return;
+
+ function replace(point) {
+ if (from == point.node) {
+ currentSelection.changed = true;
+ if (length && point.offset > length) {
+ point.offset -= length;
+ }
+ else {
+ point.node = to;
+ point.offset += (offset || 0);
+ }
+ }
+ }
+ replace(currentSelection.start);
+ replace(currentSelection.end);
+ };
+
+ select.snapshotMove = function(from, to, distance, relative, ifAtStart) {
+ if (!currentSelection) return;
+
+ function move(point) {
+ if (from == point.node && (!ifAtStart || point.offset == 0)) {
+ currentSelection.changed = true;
+ point.node = to;
+ if (relative) point.offset = Math.max(0, point.offset + distance);
+ else point.offset = distance;
+ }
+ }
+ move(currentSelection.start);
+ move(currentSelection.end);
+ };
+
+ // Most functions are defined in two ways, one for the IE selection
+ // model, one for the W3C one.
+ if (select.ie_selection) {
+ function selectionNode(win, start) {
+ var range = win.document.selection.createRange();
+ range.collapse(start);
+
+ function nodeAfter(node) {
+ var found = null;
+ while (!found && node) {
+ found = node.nextSibling;
+ node = node.parentNode;
+ }
+ return nodeAtStartOf(found);
+ }
+
+ function nodeAtStartOf(node) {
+ while (node && node.firstChild) node = node.firstChild;
+ return {node: node, offset: 0};
+ }
+
+ var containing = range.parentElement();
+ if (!isAncestor(win.document.body, containing)) return null;
+ if (!containing.firstChild) return nodeAtStartOf(containing);
+
+ var working = range.duplicate();
+ working.moveToElementText(containing);
+ working.collapse(true);
+ for (var cur = containing.firstChild; cur; cur = cur.nextSibling) {
+ if (cur.nodeType == 3) {
+ var size = cur.nodeValue.length;
+ working.move("character", size);
+ }
+ else {
+ working.moveToElementText(cur);
+ working.collapse(false);
+ }
+
+ var dir = range.compareEndPoints("StartToStart", working);
+ if (dir == 0) return nodeAfter(cur);
+ if (dir == 1) continue;
+ if (cur.nodeType != 3) return nodeAtStartOf(cur);
+
+ working.setEndPoint("StartToEnd", range);
+ return {node: cur, offset: size - working.text.length};
+ }
+ return nodeAfter(containing);
+ }
+
+ select.markSelection = function(win) {
+ currentSelection = null;
+ var sel = win.document.selection;
+ if (!sel) return;
+ var start = selectionNode(win, true),
+ end = selectionNode(win, false);
+ if (!start || !end) return;
+ currentSelection = {start: start, end: end, window: win, changed: false};
+ };
+
+ select.selectMarked = function() {
+ if (!currentSelection || !currentSelection.changed) return;
+ var win = currentSelection.window, doc = win.document;
+
+ function makeRange(point) {
+ var range = doc.body.createTextRange(),
+ node = point.node;
+ if (!node) {
+ range.moveToElementText(currentSelection.window.document.body);
+ range.collapse(false);
+ }
+ else if (node.nodeType == 3) {
+ range.moveToElementText(node.parentNode);
+ var offset = point.offset;
+ while (node.previousSibling) {
+ node = node.previousSibling;
+ offset += (node.innerText || "").length;
+ }
+ range.move("character", offset);
+ }
+ else {
+ range.moveToElementText(node);
+ range.collapse(true);
+ }
+ return range;
+ }
+
+ var start = makeRange(currentSelection.start), end = makeRange(currentSelection.end);
+ start.setEndPoint("StartToEnd", end);
+ start.select();
+ };
+
+ select.offsetInNode = function(win, node) {
+ var sel = win.document.selection;
+ if (!sel) return 0;
+ var range = sel.createRange(), range2 = range.duplicate();
+ try {range2.moveToElementText(node);} catch(e){return 0;}
+ range.setEndPoint("StartToStart", range2);
+ return range.text.length;
+ };
+
+ // Get the top-level node that one end of the cursor is inside or
+ // after. Note that this returns false for 'no cursor', and null
+ // for 'start of document'.
+ select.selectionTopNode = function(container, start) {
+ var selection = container.ownerDocument.selection;
+ if (!selection) return false;
+
+ var range = selection.createRange(), range2 = range.duplicate();
+ range.collapse(start);
+ var around = range.parentElement();
+ if (around && isAncestor(container, around)) {
+ // Only use this node if the selection is not at its start.
+ range2.moveToElementText(around);
+ if (range.compareEndPoints("StartToStart", range2) == 1)
+ return topLevelNodeAt(around, container);
+ }
+
+ // Move the start of a range to the start of a node,
+ // compensating for the fact that you can't call
+ // moveToElementText with text nodes.
+ function moveToNodeStart(range, node) {
+ if (node.nodeType == 3) {
+ var count = 0, cur = node.previousSibling;
+ while (cur && cur.nodeType == 3) {
+ count += cur.nodeValue.length;
+ cur = cur.previousSibling;
+ }
+ if (cur) {
+ try{range.moveToElementText(cur);}
+ catch(e){return false;}
+ range.collapse(false);
+ }
+ else range.moveToElementText(node.parentNode);
+ if (count) range.move("character", count);
+ }
+ else {
+ try{range.moveToElementText(node);}
+ catch(e){return false;}
+ }
+ return true;
+ }
+
+ // Do a binary search through the container object, comparing
+ // the start of each node to the selection
+ var start = 0, end = container.childNodes.length - 1;
+ while (start < end) {
+ var middle = Math.ceil((end + start) / 2), node = container.childNodes[middle];
+ if (!node) return false; // Don't ask. IE6 manages this sometimes.
+ if (!moveToNodeStart(range2, node)) return false;
+ if (range.compareEndPoints("StartToStart", range2) == 1)
+ start = middle;
+ else
+ end = middle - 1;
+ }
+ return container.childNodes[start] || null;
+ };
+
+ // Place the cursor after this.start. This is only useful when
+ // manually moving the cursor instead of restoring it to its old
+ // position.
+ select.focusAfterNode = function(node, container) {
+ var range = container.ownerDocument.body.createTextRange();
+ range.moveToElementText(node || container);
+ range.collapse(!node);
+ range.select();
+ };
+
+ select.somethingSelected = function(win) {
+ var sel = win.document.selection;
+ return sel && (sel.createRange().text != "");
+ };
+
+ function insertAtCursor(window, html) {
+ var selection = window.document.selection;
+ if (selection) {
+ var range = selection.createRange();
+ range.pasteHTML(html);
+ range.collapse(false);
+ range.select();
+ }
+ }
+
+ // Used to normalize the effect of the enter key, since browsers
+ // do widely different things when pressing enter in designMode.
+ select.insertNewlineAtCursor = function(window) {
+ insertAtCursor(window, "<br>");
+ };
+
+ select.insertTabAtCursor = function(window) {
+ insertAtCursor(window, fourSpaces);
+ };
+
+ // Get the BR node at the start of the line on which the cursor
+ // currently is, and the offset into the line. Returns null as
+ // node if cursor is on first line.
+ select.cursorPos = function(container, start) {
+ var selection = container.ownerDocument.selection;
+ if (!selection) return null;
+
+ var topNode = select.selectionTopNode(container, start);
+ while (topNode && !isBR(topNode))
+ topNode = topNode.previousSibling;
+
+ var range = selection.createRange(), range2 = range.duplicate();
+ range.collapse(start);
+ if (topNode) {
+ range2.moveToElementText(topNode);
+ range2.collapse(false);
+ }
+ else {
+ // When nothing is selected, we can get all kinds of funky errors here.
+ try { range2.moveToElementText(container); }
+ catch (e) { return null; }
+ range2.collapse(true);
+ }
+ range.setEndPoint("StartToStart", range2);
+
+ return {node: topNode, offset: range.text.length};
+ };
+
+ select.setCursorPos = function(container, from, to) {
+ function rangeAt(pos) {
+ var range = container.ownerDocument.body.createTextRange();
+ if (!pos.node) {
+ range.moveToElementText(container);
+ range.collapse(true);
+ }
+ else {
+ range.moveToElementText(pos.node);
+ range.collapse(false);
+ }
+ range.move("character", pos.offset);
+ return range;
+ }
+
+ var range = rangeAt(from);
+ if (to && to != from)
+ range.setEndPoint("EndToEnd", rangeAt(to));
+ range.select();
+ }
+
+ // Some hacks for storing and re-storing the selection when the editor loses and regains focus.
+ select.getBookmark = function (container) {
+ var from = select.cursorPos(container, true), to = select.cursorPos(container, false);
+ if (from && to) return {from: from, to: to};
+ };
+
+ // Restore a stored selection.
+ select.setBookmark = function(container, mark) {
+ if (!mark) return;
+ select.setCursorPos(container, mark.from, mark.to);
+ };
+ }
+ // W3C model
+ else {
+ // Find the node right at the cursor, not one of its
+ // ancestors with a suitable offset. This goes down the DOM tree
+ // until a 'leaf' is reached (or is it *up* the DOM tree?).
+ function innerNode(node, offset) {
+ while (node.nodeType != 3 && !isBR(node)) {
+ var newNode = node.childNodes[offset] || node.nextSibling;
+ offset = 0;
+ while (!newNode && node.parentNode) {
+ node = node.parentNode;
+ newNode = node.nextSibling;
+ }
+ node = newNode;
+ if (!newNode) break;
+ }
+ return {node: node, offset: offset};
+ }
+
+ // Store start and end nodes, and offsets within these, and refer
+ // back to the selection object from those nodes, so that this
+ // object can be updated when the nodes are replaced before the
+ // selection is restored.
+ select.markSelection = function (win) {
+ var selection = win.getSelection();
+ if (!selection || selection.rangeCount == 0)
+ return (currentSelection = null);
+ var range = selection.getRangeAt(0);
+
+ currentSelection = {
+ start: innerNode(range.startContainer, range.startOffset),
+ end: innerNode(range.endContainer, range.endOffset),
+ window: win,
+ changed: false
+ };
+ };
+
+ select.selectMarked = function () {
+ var cs = currentSelection;
+ // on webkit-based browsers, it is apparently possible that the
+ // selection gets reset even when a node that is not one of the
+ // endpoints get messed with. the most common situation where
+ // this occurs is when a selection is deleted or overwitten. we
+ // check for that here.
+ function focusIssue() {
+ if (cs.start.node == cs.end.node && cs.start.offset == cs.end.offset) {
+ var selection = cs.window.getSelection();
+ if (!selection || selection.rangeCount == 0) return true;
+ var range = selection.getRangeAt(0), point = innerNode(range.startContainer, range.startOffset);
+ return cs.start.node != point.node || cs.start.offset != point.offset;
+ }
+ }
+ if (!cs || !(cs.changed || (webkit && focusIssue()))) return;
+ var win = cs.window, range = win.document.createRange();
+
+ function setPoint(point, which) {
+ if (point.node) {
+ // Some magic to generalize the setting of the start and end
+ // of a range.
+ if (point.offset == 0)
+ range["set" + which + "Before"](point.node);
+ else
+ range["set" + which](point.node, point.offset);
+ }
+ else {
+ range.setStartAfter(win.document.body.lastChild || win.document.body);
+ }
+ }
+
+ setPoint(cs.end, "End");
+ setPoint(cs.start, "Start");
+ selectRange(range, win);
+ };
+
+ // Helper for selecting a range object.
+ function selectRange(range, window) {
+ var selection = window.getSelection();
+ if (!selection) return;
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ function selectionRange(window) {
+ var selection = window.getSelection();
+ if (!selection || selection.rangeCount == 0)
+ return false;
+ else
+ return selection.getRangeAt(0);
+ }
+
+ // Finding the top-level node at the cursor in the W3C is, as you
+ // can see, quite an involved process.
+ select.selectionTopNode = function(container, start) {
+ var range = selectionRange(container.ownerDocument.defaultView);
+ if (!range) return false;
+
+ var node = start ? range.startContainer : range.endContainer;
+ var offset = start ? range.startOffset : range.endOffset;
+ // Work around (yet another) bug in Opera's selection model.
+ if (window.opera && !start && range.endContainer == container && range.endOffset == range.startOffset + 1 &&
+ container.childNodes[range.startOffset] && isBR(container.childNodes[range.startOffset]))
+ offset--;
+
+ // For text nodes, we look at the node itself if the cursor is
+ // inside, or at the node before it if the cursor is at the
+ // start.
+ if (node.nodeType == 3){
+ if (offset > 0)
+ return topLevelNodeAt(node, container);
+ else
+ return topLevelNodeBefore(node, container);
+ }
+ // Occasionally, browsers will return the HTML node as
+ // selection. If the offset is 0, we take the start of the frame
+ // ('after null'), otherwise, we take the last node.
+ else if (node.nodeName.toUpperCase() == "HTML") {
+ return (offset == 1 ? null : container.lastChild);
+ }
+ // If the given node is our 'container', we just look up the
+ // correct node by using the offset.
+ else if (node == container) {
+ return (offset == 0) ? null : node.childNodes[offset - 1];
+ }
+ // In any other case, we have a regular node. If the cursor is
+ // at the end of the node, we use the node itself, if it is at
+ // the start, we use the node before it, and in any other
+ // case, we look up the child before the cursor and use that.
+ else {
+ if (offset == node.childNodes.length)
+ return topLevelNodeAt(node, container);
+ else if (offset == 0)
+ return topLevelNodeBefore(node, container);
+ else
+ return topLevelNodeAt(node.childNodes[offset - 1], container);
+ }
+ };
+
+ select.focusAfterNode = function(node, container) {
+ var win = container.ownerDocument.defaultView,
+ range = win.document.createRange();
+ range.setStartBefore(container.firstChild || container);
+ // In Opera, setting the end of a range at the end of a line
+ // (before a BR) will cause the cursor to appear on the next
+ // line, so we set the end inside of the start node when
+ // possible.
+ if (node && !node.firstChild)
+ range.setEndAfter(node);
+ else if (node)
+ range.setEnd(node, node.childNodes.length);
+ else
+ range.setEndBefore(container.firstChild || container);
+ range.collapse(false);
+ selectRange(range, win);
+ };
+
+ select.somethingSelected = function(win) {
+ var range = selectionRange(win);
+ return range && !range.collapsed;
+ };
+
+ select.offsetInNode = function(win, node) {
+ var range = selectionRange(win);
+ if (!range) return 0;
+ range = range.cloneRange();
+ range.setStartBefore(node);
+ return range.toString().length;
+ };
+
+ function insertNodeAtCursor(window, node) {
+ var range = selectionRange(window);
+ if (!range) return;
+
+ range.deleteContents();
+ range.insertNode(node);
+
+ // work around weirdness where Opera will magically insert a new
+ // BR node when a BR node inside a span is moved around. makes
+ // sure the BR ends up outside of spans.
+ if (window.opera && isBR(node) && isSpan(node.parentNode)) {
+ var next = node.nextSibling, p = node.parentNode, outer = p.parentNode;
+ outer.insertBefore(node, p.nextSibling);
+ var textAfter = "";
+ for (; next && next.nodeType == 3; next = next.nextSibling) {
+ textAfter += next.nodeValue;
+ removeElement(next);
+ }
+ outer.insertBefore(makePartSpan(textAfter, window.document), node.nextSibling);
+ }
+ range = window.document.createRange();
+ range.selectNode(node);
+ range.collapse(false);
+ selectRange(range, window);
+ }
+
+ select.insertNewlineAtCursor = function(window) {
+ if (webkit)
+ document.execCommand('insertLineBreak');
+ else
+ insertNodeAtCursor(window, window.document.createElement("BR"));
+ };
+
+ select.insertTabAtCursor = function(window) {
+ insertNodeAtCursor(window, window.document.createTextNode(fourSpaces));
+ };
+
+ select.cursorPos = function(container, start) {
+ var range = selectionRange(window);
+ if (!range) return;
+
+ var topNode = select.selectionTopNode(container, start);
+ while (topNode && !isBR(topNode))
+ topNode = topNode.previousSibling;
+
+ range = range.cloneRange();
+ range.collapse(start);
+ if (topNode)
+ range.setStartAfter(topNode);
+ else
+ range.setStartBefore(container);
+
+ return {node: topNode, offset: range.toString().length};
+ };
+
+ select.setCursorPos = function(container, from, to) {
+ var win = container.ownerDocument.defaultView,
+ range = win.document.createRange();
+
+ function setPoint(node, offset, side) {
+ if (offset == 0 && node && !node.nextSibling) {
+ range["set" + side + "After"](node);
+ return true;
+ }
+
+ if (!node)
+ node = container.firstChild;
+ else
+ node = node.nextSibling;
+
+ if (!node) return;
+
+ if (offset == 0) {
+ range["set" + side + "Before"](node);
+ return true;
+ }
+
+ var backlog = []
+ function decompose(node) {
+ if (node.nodeType == 3)
+ backlog.push(node);
+ else
+ forEach(node.childNodes, decompose);
+ }
+ while (true) {
+ while (node && !backlog.length) {
+ decompose(node);
+ node = node.nextSibling;
+ }
+ var cur = backlog.shift();
+ if (!cur) return false;
+
+ var length = cur.nodeValue.length;
+ if (length >= offset) {
+ range["set" + side](cur, offset);
+ return true;
+ }
+ offset -= length;
+ }
+ }
+
+ to = to || from;
+ if (setPoint(to.node, to.offset, "End") && setPoint(from.node, from.offset, "Start"))
+ selectRange(range, win);
+ };
+ }
+})();
--- /dev/null
+/* String streams are the things fed to parsers (which can feed them
+ * to a tokenizer if they want). They provide peek and next methods
+ * for looking at the current character (next 'consumes' this
+ * character, peek does not), and a get method for retrieving all the
+ * text that was consumed since the last time get was called.
+ *
+ * An easy mistake to make is to let a StopIteration exception finish
+ * the token stream while there are still characters pending in the
+ * string stream (hitting the end of the buffer while parsing a
+ * token). To make it easier to detect such errors, the stringstreams
+ * throw an exception when this happens.
+ */
+
+// Make a stringstream stream out of an iterator that returns strings.
+// This is applied to the result of traverseDOM (see codemirror.js),
+// and the resulting stream is fed to the parser.
+var stringStream = function(source){
+ // String that's currently being iterated over.
+ var current = "";
+ // Position in that string.
+ var pos = 0;
+ // Accumulator for strings that have been iterated over but not
+ // get()-ed yet.
+ var accum = "";
+ // Make sure there are more characters ready, or throw
+ // StopIteration.
+ function ensureChars() {
+ while (pos == current.length) {
+ accum += current;
+ current = ""; // In case source.next() throws
+ pos = 0;
+ try {current = source.next();}
+ catch (e) {
+ if (e != StopIteration) throw e;
+ else return false;
+ }
+ }
+ return true;
+ }
+
+ return {
+ // Return the next character in the stream.
+ peek: function() {
+ if (!ensureChars()) return null;
+ return current.charAt(pos);
+ },
+ // Get the next character, throw StopIteration if at end, check
+ // for unused content.
+ next: function() {
+ if (!ensureChars()) {
+ if (accum.length > 0)
+ throw "End of stringstream reached without emptying buffer ('" + accum + "').";
+ else
+ throw StopIteration;
+ }
+ return current.charAt(pos++);
+ },
+ // Return the characters iterated over since the last call to
+ // .get().
+ get: function() {
+ var temp = accum;
+ accum = "";
+ if (pos > 0){
+ temp += current.slice(0, pos);
+ current = current.slice(pos);
+ pos = 0;
+ }
+ return temp;
+ },
+ // Push a string back into the stream.
+ push: function(str) {
+ current = current.slice(0, pos) + str + current.slice(pos);
+ },
+ lookAhead: function(str, consume, skipSpaces, caseInsensitive) {
+ function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
+ str = cased(str);
+ var found = false;
+
+ var _accum = accum, _pos = pos;
+ if (skipSpaces) this.nextWhileMatches(/[\s\u00a0]/);
+
+ while (true) {
+ var end = pos + str.length, left = current.length - pos;
+ if (end <= current.length) {
+ found = str == cased(current.slice(pos, end));
+ pos = end;
+ break;
+ }
+ else if (str.slice(0, left) == cased(current.slice(pos))) {
+ accum += current; current = "";
+ try {current = source.next();}
+ catch (e) {break;}
+ pos = 0;
+ str = str.slice(left);
+ }
+ else {
+ break;
+ }
+ }
+
+ if (!(found && consume)) {
+ current = accum.slice(_accum.length) + current;
+ pos = _pos;
+ accum = _accum;
+ }
+
+ return found;
+ },
+
+ // Utils built on top of the above
+ more: function() {
+ return this.peek() !== null;
+ },
+ applies: function(test) {
+ var next = this.peek();
+ return (next !== null && test(next));
+ },
+ nextWhile: function(test) {
+ var next;
+ while ((next = this.peek()) !== null && test(next))
+ this.next();
+ },
+ matches: function(re) {
+ var next = this.peek();
+ return (next !== null && re.test(next));
+ },
+ nextWhileMatches: function(re) {
+ var next;
+ while ((next = this.peek()) !== null && re.test(next))
+ this.next();
+ },
+ equals: function(ch) {
+ return ch === this.peek();
+ },
+ endOfLine: function() {
+ var next = this.peek();
+ return next == null || next == "\n";
+ }
+ };
+};
--- /dev/null
+// A framework for simple tokenizers. Takes care of newlines and
+// white-space, and of getting the text from the source stream into
+// the token object. A state is a function of two arguments -- a
+// string stream and a setState function. The second can be used to
+// change the tokenizer's state, and can be ignored for stateless
+// tokenizers. This function should advance the stream over a token
+// and return a string or object containing information about the next
+// token, or null to pass and have the (new) state be called to finish
+// the token. When a string is given, it is wrapped in a {style, type}
+// object. In the resulting object, the characters consumed are stored
+// under the content property. Any whitespace following them is also
+// automatically consumed, and added to the value property. (Thus,
+// content is the actual meaningful part of the token, while value
+// contains all the text it spans.)
+
+function tokenizer(source, state) {
+ // Newlines are always a separate token.
+ function isWhiteSpace(ch) {
+ // The messy regexp is because IE's regexp matcher is of the
+ // opinion that non-breaking spaces are no whitespace.
+ return ch != "\n" && /^[\s\u00a0]*$/.test(ch);
+ }
+
+ var tokenizer = {
+ state: state,
+
+ take: function(type) {
+ if (typeof(type) == "string")
+ type = {style: type, type: type};
+
+ type.content = (type.content || "") + source.get();
+ if (!/\n$/.test(type.content))
+ source.nextWhile(isWhiteSpace);
+ type.value = type.content + source.get();
+ return type;
+ },
+
+ next: function () {
+ if (!source.more()) throw StopIteration;
+
+ var type;
+ if (source.equals("\n")) {
+ source.next();
+ return this.take("whitespace");
+ }
+
+ if (source.applies(isWhiteSpace))
+ type = "whitespace";
+ else
+ while (!type)
+ type = this.state(source, function(s) {tokenizer.state = s;});
+
+ return this.take(type);
+ }
+ };
+ return tokenizer;
+}
--- /dev/null
+/**
+ * Storage and control for undo information within a CodeMirror
+ * editor. 'Why on earth is such a complicated mess required for
+ * that?', I hear you ask. The goal, in implementing this, was to make
+ * the complexity of storing and reverting undo information depend
+ * only on the size of the edited or restored content, not on the size
+ * of the whole document. This makes it necessary to use a kind of
+ * 'diff' system, which, when applied to a DOM tree, causes some
+ * complexity and hackery.
+ *
+ * In short, the editor 'touches' BR elements as it parses them, and
+ * the UndoHistory stores these. When nothing is touched in commitDelay
+ * milliseconds, the changes are committed: It goes over all touched
+ * nodes, throws out the ones that did not change since last commit or
+ * are no longer in the document, and assembles the rest into zero or
+ * more 'chains' -- arrays of adjacent lines. Links back to these
+ * chains are added to the BR nodes, while the chain that previously
+ * spanned these nodes is added to the undo history. Undoing a change
+ * means taking such a chain off the undo history, restoring its
+ * content (text is saved per line) and linking it back into the
+ * document.
+ */
+
+// A history object needs to know about the DOM container holding the
+// document, the maximum amount of undo levels it should store, the
+// delay (of no input) after which it commits a set of changes, and,
+// unfortunately, the 'parent' window -- a window that is not in
+// designMode, and on which setTimeout works in every browser.
+function UndoHistory(container, maxDepth, commitDelay, editor) {
+ this.container = container;
+ this.maxDepth = maxDepth; this.commitDelay = commitDelay;
+ this.editor = editor; this.parent = editor.parent;
+ // This line object represents the initial, empty editor.
+ var initial = {text: "", from: null, to: null};
+ // As the borders between lines are represented by BR elements, the
+ // start of the first line and the end of the last one are
+ // represented by null. Since you can not store any properties
+ // (links to line objects) in null, these properties are used in
+ // those cases.
+ this.first = initial; this.last = initial;
+ // Similarly, a 'historyTouched' property is added to the BR in
+ // front of lines that have already been touched, and 'firstTouched'
+ // is used for the first line.
+ this.firstTouched = false;
+ // History is the set of committed changes, touched is the set of
+ // nodes touched since the last commit.
+ this.history = []; this.redoHistory = []; this.touched = [];
+}
+
+UndoHistory.prototype = {
+ // Schedule a commit (if no other touches come in for commitDelay
+ // milliseconds).
+ scheduleCommit: function() {
+ var self = this;
+ this.parent.clearTimeout(this.commitTimeout);
+ this.commitTimeout = this.parent.setTimeout(function(){self.tryCommit();}, this.commitDelay);
+ },
+
+ // Mark a node as touched. Null is a valid argument.
+ touch: function(node) {
+ this.setTouched(node);
+ this.scheduleCommit();
+ },
+
+ // Undo the last change.
+ undo: function() {
+ // Make sure pending changes have been committed.
+ this.commit();
+
+ if (this.history.length) {
+ // Take the top diff from the history, apply it, and store its
+ // shadow in the redo history.
+ var item = this.history.pop();
+ this.redoHistory.push(this.updateTo(item, "applyChain"));
+ this.notifyEnvironment();
+ return this.chainNode(item);
+ }
+ },
+
+ // Redo the last undone change.
+ redo: function() {
+ this.commit();
+ if (this.redoHistory.length) {
+ // The inverse of undo, basically.
+ var item = this.redoHistory.pop();
+ this.addUndoLevel(this.updateTo(item, "applyChain"));
+ this.notifyEnvironment();
+ return this.chainNode(item);
+ }
+ },
+
+ clear: function() {
+ this.history = [];
+ this.redoHistory = [];
+ },
+
+ // Ask for the size of the un/redo histories.
+ historySize: function() {
+ return {undo: this.history.length, redo: this.redoHistory.length};
+ },
+
+ // Push a changeset into the document.
+ push: function(from, to, lines) {
+ var chain = [];
+ for (var i = 0; i < lines.length; i++) {
+ var end = (i == lines.length - 1) ? to : this.container.ownerDocument.createElement("BR");
+ chain.push({from: from, to: end, text: cleanText(lines[i])});
+ from = end;
+ }
+ this.pushChains([chain], from == null && to == null);
+ this.notifyEnvironment();
+ },
+
+ pushChains: function(chains, doNotHighlight) {
+ this.commit(doNotHighlight);
+ this.addUndoLevel(this.updateTo(chains, "applyChain"));
+ this.redoHistory = [];
+ },
+
+ // Retrieve a DOM node from a chain (for scrolling to it after undo/redo).
+ chainNode: function(chains) {
+ for (var i = 0; i < chains.length; i++) {
+ var start = chains[i][0], node = start && (start.from || start.to);
+ if (node) return node;
+ }
+ },
+
+ // Clear the undo history, make the current document the start
+ // position.
+ reset: function() {
+ this.history = []; this.redoHistory = [];
+ },
+
+ textAfter: function(br) {
+ return this.after(br).text;
+ },
+
+ nodeAfter: function(br) {
+ return this.after(br).to;
+ },
+
+ nodeBefore: function(br) {
+ return this.before(br).from;
+ },
+
+ // Commit unless there are pending dirty nodes.
+ tryCommit: function() {
+ if (!window.UndoHistory) return; // Stop when frame has been unloaded
+ if (this.editor.highlightDirty()) this.commit(true);
+ else this.scheduleCommit();
+ },
+
+ // Check whether the touched nodes hold any changes, if so, commit
+ // them.
+ commit: function(doNotHighlight) {
+ this.parent.clearTimeout(this.commitTimeout);
+ // Make sure there are no pending dirty nodes.
+ if (!doNotHighlight) this.editor.highlightDirty(true);
+ // Build set of chains.
+ var chains = this.touchedChains(), self = this;
+
+ if (chains.length) {
+ this.addUndoLevel(this.updateTo(chains, "linkChain"));
+ this.redoHistory = [];
+ this.notifyEnvironment();
+ }
+ },
+
+ // [ end of public interface ]
+
+ // Update the document with a given set of chains, return its
+ // shadow. updateFunc should be "applyChain" or "linkChain". In the
+ // second case, the chains are taken to correspond the the current
+ // document, and only the state of the line data is updated. In the
+ // first case, the content of the chains is also pushed iinto the
+ // document.
+ updateTo: function(chains, updateFunc) {
+ var shadows = [], dirty = [];
+ for (var i = 0; i < chains.length; i++) {
+ shadows.push(this.shadowChain(chains[i]));
+ dirty.push(this[updateFunc](chains[i]));
+ }
+ if (updateFunc == "applyChain")
+ this.notifyDirty(dirty);
+ return shadows;
+ },
+
+ // Notify the editor that some nodes have changed.
+ notifyDirty: function(nodes) {
+ forEach(nodes, method(this.editor, "addDirtyNode"))
+ this.editor.scheduleHighlight();
+ },
+
+ notifyEnvironment: function() {
+ if (this.onChange) this.onChange();
+ // Used by the line-wrapping line-numbering code.
+ if (window.frameElement && window.frameElement.CodeMirror.updateNumbers)
+ window.frameElement.CodeMirror.updateNumbers();
+ },
+
+ // Link a chain into the DOM nodes (or the first/last links for null
+ // nodes).
+ linkChain: function(chain) {
+ for (var i = 0; i < chain.length; i++) {
+ var line = chain[i];
+ if (line.from) line.from.historyAfter = line;
+ else this.first = line;
+ if (line.to) line.to.historyBefore = line;
+ else this.last = line;
+ }
+ },
+
+ // Get the line object after/before a given node.
+ after: function(node) {
+ return node ? node.historyAfter : this.first;
+ },
+ before: function(node) {
+ return node ? node.historyBefore : this.last;
+ },
+
+ // Mark a node as touched if it has not already been marked.
+ setTouched: function(node) {
+ if (node) {
+ if (!node.historyTouched) {
+ this.touched.push(node);
+ node.historyTouched = true;
+ }
+ }
+ else {
+ this.firstTouched = true;
+ }
+ },
+
+ // Store a new set of undo info, throw away info if there is more of
+ // it than allowed.
+ addUndoLevel: function(diffs) {
+ this.history.push(diffs);
+ if (this.history.length > this.maxDepth)
+ this.history.shift();
+ },
+
+ // Build chains from a set of touched nodes.
+ touchedChains: function() {
+ var self = this;
+
+ // The temp system is a crummy hack to speed up determining
+ // whether a (currently touched) node has a line object associated
+ // with it. nullTemp is used to store the object for the first
+ // line, other nodes get it stored in their historyTemp property.
+ var nullTemp = null;
+ function temp(node) {return node ? node.historyTemp : nullTemp;}
+ function setTemp(node, line) {
+ if (node) node.historyTemp = line;
+ else nullTemp = line;
+ }
+
+ function buildLine(node) {
+ var text = [];
+ for (var cur = node ? node.nextSibling : self.container.firstChild;
+ cur && !isBR(cur); cur = cur.nextSibling)
+ if (cur.currentText) text.push(cur.currentText);
+ return {from: node, to: cur, text: cleanText(text.join(""))};
+ }
+
+ // Filter out unchanged lines and nodes that are no longer in the
+ // document. Build up line objects for remaining nodes.
+ var lines = [];
+ if (self.firstTouched) self.touched.push(null);
+ forEach(self.touched, function(node) {
+ if (node && node.parentNode != self.container) return;
+
+ if (node) node.historyTouched = false;
+ else self.firstTouched = false;
+
+ var line = buildLine(node), shadow = self.after(node);
+ if (!shadow || shadow.text != line.text || shadow.to != line.to) {
+ lines.push(line);
+ setTemp(node, line);
+ }
+ });
+
+ // Get the BR element after/before the given node.
+ function nextBR(node, dir) {
+ var link = dir + "Sibling", search = node[link];
+ while (search && !isBR(search))
+ search = search[link];
+ return search;
+ }
+
+ // Assemble line objects into chains by scanning the DOM tree
+ // around them.
+ var chains = []; self.touched = [];
+ forEach(lines, function(line) {
+ // Note that this makes the loop skip line objects that have
+ // been pulled into chains by lines before them.
+ if (!temp(line.from)) return;
+
+ var chain = [], curNode = line.from, safe = true;
+ // Put any line objects (referred to by temp info) before this
+ // one on the front of the array.
+ while (true) {
+ var curLine = temp(curNode);
+ if (!curLine) {
+ if (safe) break;
+ else curLine = buildLine(curNode);
+ }
+ chain.unshift(curLine);
+ setTemp(curNode, null);
+ if (!curNode) break;
+ safe = self.after(curNode);
+ curNode = nextBR(curNode, "previous");
+ }
+ curNode = line.to; safe = self.before(line.from);
+ // Add lines after this one at end of array.
+ while (true) {
+ if (!curNode) break;
+ var curLine = temp(curNode);
+ if (!curLine) {
+ if (safe) break;
+ else curLine = buildLine(curNode);
+ }
+ chain.push(curLine);
+ setTemp(curNode, null);
+ safe = self.before(curNode);
+ curNode = nextBR(curNode, "next");
+ }
+ chains.push(chain);
+ });
+
+ return chains;
+ },
+
+ // Find the 'shadow' of a given chain by following the links in the
+ // DOM nodes at its start and end.
+ shadowChain: function(chain) {
+ var shadows = [], next = this.after(chain[0].from), end = chain[chain.length - 1].to;
+ while (true) {
+ shadows.push(next);
+ var nextNode = next.to;
+ if (!nextNode || nextNode == end)
+ break;
+ else
+ next = nextNode.historyAfter || this.before(end);
+ // (The this.before(end) is a hack -- FF sometimes removes
+ // properties from BR nodes, in which case the best we can hope
+ // for is to not break.)
+ }
+ return shadows;
+ },
+
+ // Update the DOM tree to contain the lines specified in a given
+ // chain, link this chain into the DOM nodes.
+ applyChain: function(chain) {
+ // Some attempt is made to prevent the cursor from jumping
+ // randomly when an undo or redo happens. It still behaves a bit
+ // strange sometimes.
+ var cursor = select.cursorPos(this.container, false), self = this;
+
+ // Remove all nodes in the DOM tree between from and to (null for
+ // start/end of container).
+ function removeRange(from, to) {
+ var pos = from ? from.nextSibling : self.container.firstChild;
+ while (pos != to) {
+ var temp = pos.nextSibling;
+ removeElement(pos);
+ pos = temp;
+ }
+ }
+
+ var start = chain[0].from, end = chain[chain.length - 1].to;
+ // Clear the space where this change has to be made.
+ removeRange(start, end);
+
+ // Insert the content specified by the chain into the DOM tree.
+ for (var i = 0; i < chain.length; i++) {
+ var line = chain[i];
+ // The start and end of the space are already correct, but BR
+ // tags inside it have to be put back.
+ if (i > 0)
+ self.container.insertBefore(line.from, end);
+
+ // Add the text.
+ var node = makePartSpan(fixSpaces(line.text), this.container.ownerDocument);
+ self.container.insertBefore(node, end);
+ // See if the cursor was on this line. Put it back, adjusting
+ // for changed line length, if it was.
+ if (cursor && cursor.node == line.from) {
+ var cursordiff = 0;
+ var prev = this.after(line.from);
+ if (prev && i == chain.length - 1) {
+ // Only adjust if the cursor is after the unchanged part of
+ // the line.
+ for (var match = 0; match < cursor.offset &&
+ line.text.charAt(match) == prev.text.charAt(match); match++);
+ if (cursor.offset > match)
+ cursordiff = line.text.length - prev.text.length;
+ }
+ select.setCursorPos(this.container, {node: line.from, offset: Math.max(0, cursor.offset + cursordiff)});
+ }
+ // Cursor was in removed line, this is last new line.
+ else if (cursor && (i == chain.length - 1) && cursor.node && cursor.node.parentNode != this.container) {
+ select.setCursorPos(this.container, {node: line.from, offset: line.text.length});
+ }
+ }
+
+ // Anchor the chain in the DOM tree.
+ this.linkChain(chain);
+ return start;
+ }
+};
--- /dev/null
+/* A few useful utility functions. */
+
+// Capture a method on an object.
+function method(obj, name) {
+ return function() {obj[name].apply(obj, arguments);};
+}
+
+// The value used to signal the end of a sequence in iterators.
+var StopIteration = {toString: function() {return "StopIteration"}};
+
+// Apply a function to each element in a sequence.
+function forEach(iter, f) {
+ if (iter.next) {
+ try {while (true) f(iter.next());}
+ catch (e) {if (e != StopIteration) throw e;}
+ }
+ else {
+ for (var i = 0; i < iter.length; i++)
+ f(iter[i]);
+ }
+}
+
+// Map a function over a sequence, producing an array of results.
+function map(iter, f) {
+ var accum = [];
+ forEach(iter, function(val) {accum.push(f(val));});
+ return accum;
+}
+
+// Create a predicate function that tests a string againsts a given
+// regular expression. No longer used but might be used by 3rd party
+// parsers.
+function matcher(regexp){
+ return function(value){return regexp.test(value);};
+}
+
+// Test whether a DOM node has a certain CSS class. Much faster than
+// the MochiKit equivalent, for some reason.
+function hasClass(element, className){
+ var classes = element.className;
+ return classes && new RegExp("(^| )" + className + "($| )").test(classes);
+}
+
+// Insert a DOM node after another node.
+function insertAfter(newNode, oldNode) {
+ var parent = oldNode.parentNode;
+ parent.insertBefore(newNode, oldNode.nextSibling);
+ return newNode;
+}
+
+function removeElement(node) {
+ if (node.parentNode)
+ node.parentNode.removeChild(node);
+}
+
+function clearElement(node) {
+ while (node.firstChild)
+ node.removeChild(node.firstChild);
+}
+
+// Check whether a node is contained in another one.
+function isAncestor(node, child) {
+ while (child = child.parentNode) {
+ if (node == child)
+ return true;
+ }
+ return false;
+}
+
+// The non-breaking space character.
+var nbsp = "\u00a0";
+var matching = {"{": "}", "[": "]", "(": ")",
+ "}": "{", "]": "[", ")": "("};
+
+// Standardize a few unportable event properties.
+function normalizeEvent(event) {
+ if (!event.stopPropagation) {
+ event.stopPropagation = function() {this.cancelBubble = true;};
+ event.preventDefault = function() {this.returnValue = false;};
+ }
+ if (!event.stop) {
+ event.stop = function() {
+ this.stopPropagation();
+ this.preventDefault();
+ };
+ }
+
+ if (event.type == "keypress") {
+ event.code = (event.charCode == null) ? event.keyCode : event.charCode;
+ event.character = String.fromCharCode(event.code);
+ }
+ return event;
+}
+
+// Portably register event handlers.
+function addEventHandler(node, type, handler, removeFunc) {
+ function wrapHandler(event) {
+ handler(normalizeEvent(event || window.event));
+ }
+ if (typeof node.addEventListener == "function") {
+ node.addEventListener(type, wrapHandler, false);
+ if (removeFunc) return function() {node.removeEventListener(type, wrapHandler, false);};
+ }
+ else {
+ node.attachEvent("on" + type, wrapHandler);
+ if (removeFunc) return function() {node.detachEvent("on" + type, wrapHandler);};
+ }
+}
+
+function nodeText(node) {
+ return node.textContent || node.innerText || node.nodeValue || "";
+}
+
+function nodeTop(node) {
+ var top = 0;
+ while (node.offsetParent) {
+ top += node.offsetTop;
+ node = node.offsetParent;
+ }
+ return top;
+}
+
+function isBR(node) {
+ var nn = node.nodeName;
+ return nn == "BR" || nn == "br";
+}
+function isSpan(node) {
+ var nn = node.nodeName;
+ return nn == "SPAN" || nn == "span";
+}
+++ /dev/null
-/* CodeMirror main module
- *
- * Implements the CodeMirror constructor and prototype, which take care
- * of initializing the editor frame, and providing the outside interface.
- */
-
-// The CodeMirrorConfig object is used to specify a default
-// configuration. If you specify such an object before loading this
-// file, the values you put into it will override the defaults given
-// below. You can also assign to it after loading.
-var CodeMirrorConfig = window.CodeMirrorConfig || {};
-
-var CodeMirror = (function(){
- function setDefaults(object, defaults) {
- for (var option in defaults) {
- if (!object.hasOwnProperty(option))
- object[option] = defaults[option];
- }
- }
- function forEach(array, action) {
- for (var i = 0; i < array.length; i++)
- action(array[i]);
- }
-
- // These default options can be overridden by passing a set of
- // options to a specific CodeMirror constructor. See manual.html for
- // their meaning.
- setDefaults(CodeMirrorConfig, {
- stylesheet: [],
- path: "",
- parserfile: [],
- basefiles: ["util.js", "stringstream.js", "select.js", "undo.js", "editor.js", "tokenize.js"],
- iframeClass: null,
- passDelay: 200,
- passTime: 50,
- lineNumberDelay: 200,
- lineNumberTime: 50,
- continuousScanning: false,
- saveFunction: null,
- onChange: null,
- undoDepth: 50,
- undoDelay: 800,
- disableSpellcheck: true,
- textWrapping: true,
- readOnly: false,
- width: "",
- height: "300px",
- minHeight: 100,
- autoMatchParens: false,
- parserConfig: null,
- tabMode: "indent", // or "spaces", "default", "shift"
- reindentOnLoad: false,
- activeTokens: null,
- cursorActivity: null,
- lineNumbers: false,
- indentUnit: 2,
- domain: null
- });
-
- function addLineNumberDiv(container) {
- var nums = document.createElement("DIV"),
- scroller = document.createElement("DIV");
- nums.style.position = "absolute";
- nums.style.height = "100%";
- if (nums.style.setExpression) {
- try {nums.style.setExpression("height", "this.previousSibling.offsetHeight + 'px'");}
- catch(e) {} // Seems to throw 'Not Implemented' on some IE8 versions
- }
- nums.style.top = "0px";
- nums.style.left = "0px";
- nums.style.overflow = "hidden";
- container.appendChild(nums);
- scroller.className = "CodeMirror-line-numbers";
- nums.appendChild(scroller);
- scroller.innerHTML = "<div>1</div>";
- return nums;
- }
-
- function frameHTML(options) {
- if (typeof options.parserfile == "string")
- options.parserfile = [options.parserfile];
- if (typeof options.stylesheet == "string")
- options.stylesheet = [options.stylesheet];
-
- var html = ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\"><html><head>"];
- // Hack to work around a bunch of IE8-specific problems.
- html.push("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=EmulateIE7\"/>");
- forEach(options.stylesheet, function(file) {
- html.push("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + file + "\"/>");
- });
- forEach(options.basefiles.concat(options.parserfile), function(file) {
- if (!/^https?:/.test(file)) file = options.path + file;
- html.push("<script type=\"text/javascript\" src=\"" + file + "\"><" + "/script>");
- });
- html.push("</head><body style=\"border-width: 0;\" class=\"editbox\" spellcheck=\"" +
- (options.disableSpellcheck ? "false" : "true") + "\"></body></html>");
- return html.join("");
- }
-
- var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
-
- function CodeMirror(place, options) {
- // Use passed options, if any, to override defaults.
- this.options = options = options || {};
- setDefaults(options, CodeMirrorConfig);
-
- // Backward compatibility for deprecated options.
- if (options.dumbTabs) options.tabMode = "spaces";
- else if (options.normalTab) options.tabMode = "default";
-
- var frame = this.frame = document.createElement("IFRAME");
- if (options.iframeClass) frame.className = options.iframeClass;
- frame.frameBorder = 0;
- frame.style.border = "0";
- frame.style.width = '100%';
- frame.style.height = '100%';
- // display: block occasionally suppresses some Firefox bugs, so we
- // always add it, redundant as it sounds.
- frame.style.display = "block";
-
- var div = this.wrapping = document.createElement("DIV");
- div.style.position = "relative";
- div.className = "CodeMirror-wrapping";
- div.style.width = options.width;
- div.style.height = (options.height == "dynamic") ? options.minHeight + "px" : options.height;
- // This is used by Editor.reroutePasteEvent
- var teHack = this.textareaHack = document.createElement("TEXTAREA");
- div.appendChild(teHack);
- teHack.style.position = "absolute";
- teHack.style.left = "-10000px";
- teHack.style.width = "10px";
-
- // Link back to this object, so that the editor can fetch options
- // and add a reference to itself.
- frame.CodeMirror = this;
- if (options.domain && internetExplorer) {
- this.html = frameHTML(options);
- frame.src = "javascript:(function(){document.open();" +
- (options.domain ? "document.domain=\"" + options.domain + "\";" : "") +
- "document.write(window.frameElement.CodeMirror.html);document.close();})()";
- }
- else {
- frame.src = "javascript:false";
- }
-
- if (place.appendChild) place.appendChild(div);
- else place(div);
- div.appendChild(frame);
- if (options.lineNumbers) this.lineNumbers = addLineNumberDiv(div);
-
- this.win = frame.contentWindow;
- if (!options.domain || !internetExplorer) {
- this.win.document.open();
- this.win.document.write(frameHTML(options));
- this.win.document.close();
- }
- }
-
- CodeMirror.prototype = {
- init: function() {
- if (this.options.initCallback) this.options.initCallback(this);
- if (this.options.lineNumbers) this.activateLineNumbers();
- if (this.options.reindentOnLoad) this.reindent();
- if (this.options.height == "dynamic") this.setDynamicHeight();
- },
-
- getCode: function() {return this.editor.getCode();},
- setCode: function(code) {this.editor.importCode(code);},
- selection: function() {this.focusIfIE(); return this.editor.selectedText();},
- reindent: function() {this.editor.reindent();},
- reindentSelection: function() {this.focusIfIE(); this.editor.reindentSelection(null);},
-
- focusIfIE: function() {
- // in IE, a lot of selection-related functionality only works when the frame is focused
- if (this.win.select.ie_selection) this.focus();
- },
- focus: function() {
- this.win.focus();
- if (this.editor.selectionSnapshot) // IE hack
- this.win.select.setBookmark(this.win.document.body, this.editor.selectionSnapshot);
- },
- replaceSelection: function(text) {
- this.focus();
- this.editor.replaceSelection(text);
- return true;
- },
- replaceChars: function(text, start, end) {
- this.editor.replaceChars(text, start, end);
- },
- getSearchCursor: function(string, fromCursor, caseFold) {
- return this.editor.getSearchCursor(string, fromCursor, caseFold);
- },
-
- undo: function() {this.editor.history.undo();},
- redo: function() {this.editor.history.redo();},
- historySize: function() {return this.editor.history.historySize();},
- clearHistory: function() {this.editor.history.clear();},
-
- grabKeys: function(callback, filter) {this.editor.grabKeys(callback, filter);},
- ungrabKeys: function() {this.editor.ungrabKeys();},
-
- setParser: function(name, parserConfig) {this.editor.setParser(name, parserConfig);},
- setSpellcheck: function(on) {this.win.document.body.spellcheck = on;},
- setStylesheet: function(names) {
- if (typeof names === "string") names = [names];
- var activeStylesheets = {};
- var matchedNames = {};
- var links = this.win.document.getElementsByTagName("link");
- // Create hashes of active stylesheets and matched names.
- // This is O(n^2) but n is expected to be very small.
- for (var x = 0, link; link = links[x]; x++) {
- if (link.rel.indexOf("stylesheet") !== -1) {
- for (var y = 0; y < names.length; y++) {
- var name = names[y];
- if (link.href.substring(link.href.length - name.length) === name) {
- activeStylesheets[link.href] = true;
- matchedNames[name] = true;
- }
- }
- }
- }
- // Activate the selected stylesheets and disable the rest.
- for (var x = 0, link; link = links[x]; x++) {
- if (link.rel.indexOf("stylesheet") !== -1) {
- link.disabled = !(link.href in activeStylesheets);
- }
- }
- // Create any new stylesheets.
- for (var y = 0; y < names.length; y++) {
- var name = names[y];
- if (!(name in matchedNames)) {
- var link = this.win.document.createElement("link");
- link.rel = "stylesheet";
- link.type = "text/css";
- link.href = name;
- this.win.document.getElementsByTagName('head')[0].appendChild(link);
- }
- }
- },
- setTextWrapping: function(on) {
- if (on == this.options.textWrapping) return;
- this.win.document.body.style.whiteSpace = on ? "" : "nowrap";
- this.options.textWrapping = on;
- if (this.lineNumbers) {
- this.setLineNumbers(false);
- this.setLineNumbers(true);
- }
- },
- setIndentUnit: function(unit) {this.win.indentUnit = unit;},
- setUndoDepth: function(depth) {this.editor.history.maxDepth = depth;},
- setTabMode: function(mode) {this.options.tabMode = mode;},
- setLineNumbers: function(on) {
- if (on && !this.lineNumbers) {
- this.lineNumbers = addLineNumberDiv(this.wrapping);
- this.activateLineNumbers();
- }
- else if (!on && this.lineNumbers) {
- this.wrapping.removeChild(this.lineNumbers);
- this.wrapping.style.marginLeft = "";
- this.lineNumbers = null;
- }
- },
-
- cursorPosition: function(start) {this.focusIfIE(); return this.editor.cursorPosition(start);},
- firstLine: function() {return this.editor.firstLine();},
- lastLine: function() {return this.editor.lastLine();},
- nextLine: function(line) {return this.editor.nextLine(line);},
- prevLine: function(line) {return this.editor.prevLine(line);},
- lineContent: function(line) {return this.editor.lineContent(line);},
- setLineContent: function(line, content) {this.editor.setLineContent(line, content);},
- removeLine: function(line){this.editor.removeLine(line);},
- insertIntoLine: function(line, position, content) {this.editor.insertIntoLine(line, position, content);},
- selectLines: function(startLine, startOffset, endLine, endOffset) {
- this.win.focus();
- this.editor.selectLines(startLine, startOffset, endLine, endOffset);
- },
- nthLine: function(n) {
- var line = this.firstLine();
- for (; n > 1 && line !== false; n--)
- line = this.nextLine(line);
- return line;
- },
- lineNumber: function(line) {
- var num = 0;
- while (line !== false) {
- num++;
- line = this.prevLine(line);
- }
- return num;
- },
- jumpToLine: function(line) {
- if (typeof line == "number") line = this.nthLine(line);
- this.selectLines(line, 0);
- this.win.focus();
- },
- currentLine: function() { // Deprecated, but still there for backward compatibility
- return this.lineNumber(this.cursorLine());
- },
- cursorLine: function() {
- return this.cursorPosition().line;
- },
- cursorCoords: function(start) {return this.editor.cursorCoords(start);},
-
- activateLineNumbers: function() {
- var frame = this.frame, win = frame.contentWindow, doc = win.document, body = doc.body,
- nums = this.lineNumbers, scroller = nums.firstChild, self = this;
- var barWidth = null;
-
- function sizeBar() {
- if (frame.offsetWidth == 0) return;
- for (var root = frame; root.parentNode; root = root.parentNode);
- if (!nums.parentNode || root != document || !win.Editor) {
- // Clear event handlers (their nodes might already be collected, so try/catch)
- try{clear();}catch(e){}
- clearInterval(sizeInterval);
- return;
- }
-
- /*if (nums.offsetWidth != barWidth) {
- barWidth = nums.offsetWidth;
- frame.parentNode.style.paddingLeft = barWidth + "px";
- }*/
- }
- function doScroll() {
- nums.scrollTop = body.scrollTop || doc.documentElement.scrollTop || 0;
- }
- // Cleanup function, registered by nonWrapping and wrapping.
- var clear = function(){};
- sizeBar();
- var sizeInterval = setInterval(sizeBar, 500);
-
- function ensureEnoughLineNumbers(fill) {
- var lineHeight = scroller.firstChild.offsetHeight;
- if (lineHeight == 0) return;
- var targetHeight = 50 + Math.max(body.offsetHeight, Math.max(frame.offsetHeight, body.scrollHeight || 0)),
- lastNumber = Math.ceil(targetHeight / lineHeight);
- for (var i = scroller.childNodes.length; i <= lastNumber; i++) {
- var div = document.createElement("DIV");
- div.appendChild(document.createTextNode(fill ? String(i + 1) : "\u00a0"));
- scroller.appendChild(div);
- }
- }
-
- function nonWrapping() {
- function update() {
- ensureEnoughLineNumbers(true);
- doScroll();
- }
- self.updateNumbers = update;
- var onScroll = win.addEventHandler(win, "scroll", doScroll, true),
- onResize = win.addEventHandler(win, "resize", update, true);
- clear = function(){
- onScroll(); onResize();
- if (self.updateNumbers == update) self.updateNumbers = null;
- };
- update();
- }
-
- function wrapping() {
- var node, lineNum, next, pos, changes = [], styleNums = self.options.styleNumbers;
-
- function setNum(n, node) {
- // Does not typically happen (but can, if you mess with the
- // document during the numbering)
- if (!lineNum) lineNum = scroller.appendChild(document.createElement("DIV"));
- if (styleNums) styleNums(lineNum, node, n);
- // Changes are accumulated, so that the document layout
- // doesn't have to be recomputed during the pass
- changes.push(lineNum); changes.push(n);
- pos = lineNum.offsetHeight + lineNum.offsetTop;
- lineNum = lineNum.nextSibling;
- }
- function commitChanges() {
- for (var i = 0; i < changes.length; i += 2)
- changes[i].innerHTML = changes[i + 1];
- changes = [];
- }
- function work() {
- if (!scroller.parentNode || scroller.parentNode != self.lineNumbers) return;
-
- var endTime = new Date().getTime() + self.options.lineNumberTime;
- while (node) {
- setNum(next++, node.previousSibling);
- for (; node && !win.isBR(node); node = node.nextSibling) {
- var bott = node.offsetTop + node.offsetHeight;
- while (scroller.offsetHeight && bott - 3 > pos) setNum(" ");
- }
- if (node) node = node.nextSibling;
- if (new Date().getTime() > endTime) {
- commitChanges();
- pending = setTimeout(work, self.options.lineNumberDelay);
- return;
- }
- }
- while (lineNum) setNum(next++);
- commitChanges();
- doScroll();
- }
- function start(firstTime) {
- doScroll();
- ensureEnoughLineNumbers(firstTime);
- node = body.firstChild;
- lineNum = scroller.firstChild;
- pos = 0;
- next = 1;
- work();
- }
-
- start(true);
- var pending = null;
- function update() {
- if (pending) clearTimeout(pending);
- if (self.editor.allClean()) start();
- else pending = setTimeout(update, 200);
- }
- self.updateNumbers = update;
- var onScroll = win.addEventHandler(win, "scroll", doScroll, true),
- onResize = win.addEventHandler(win, "resize", update, true);
- clear = function(){
- if (pending) clearTimeout(pending);
- if (self.updateNumbers == update) self.updateNumbers = null;
- onScroll();
- onResize();
- };
- }
- (this.options.textWrapping || this.options.styleNumbers ? wrapping : nonWrapping)();
- },
-
- setDynamicHeight: function() {
- var self = this, activity = self.options.cursorActivity, win = self.win, body = win.document.body,
- lineHeight = null, timeout = null, vmargin = 2 * self.frame.offsetTop;
- body.style.overflowY = "hidden";
- win.document.documentElement.style.overflowY = "hidden";
- this.frame.scrolling = "no";
-
- function updateHeight() {
- for (var span = body.firstChild, sawBR = false; span; span = span.nextSibling)
- if (win.isSpan(span) && span.offsetHeight) {
- lineHeight = span.offsetHeight;
- if (!sawBR) vmargin = 2 * (self.frame.offsetTop + span.offsetTop + body.offsetTop + (internetExplorer ? 10 : 0));
- break;
- }
- if (lineHeight)
- self.wrapping.style.height = Math.max(vmargin + lineHeight * (body.getElementsByTagName("BR").length + 1),
- self.options.minHeight) + "px";
- }
- setTimeout(updateHeight, 100);
- self.options.cursorActivity = function(x) {
- if (activity) activity(x);
- clearTimeout(timeout);
- timeout = setTimeout(updateHeight, 200);
- };
- }
- };
-
- CodeMirror.InvalidLineHandle = {toString: function(){return "CodeMirror.InvalidLineHandle";}};
-
- CodeMirror.replace = function(element) {
- if (typeof element == "string")
- element = document.getElementById(element);
- return function(newElement) {
- element.parentNode.replaceChild(newElement, element);
- };
- };
-
- CodeMirror.fromTextArea = function(area, options) {
- if (typeof area == "string")
- area = document.getElementById(area);
-
- options = options || {};
- if (area.style.width && options.width == null)
- options.width = area.style.width;
- if (area.style.height && options.height == null)
- options.height = area.style.height;
- if (options.content == null) options.content = area.value;
-
- if (area.form) {
- function updateField() {
- area.value = mirror.getCode();
- }
- if (typeof area.form.addEventListener == "function")
- area.form.addEventListener("submit", updateField, false);
- else
- area.form.attachEvent("onsubmit", updateField);
- var realSubmit = area.form.submit;
- function wrapSubmit() {
- updateField();
- // Can't use realSubmit.apply because IE6 is too stupid
- area.form.submit = realSubmit;
- area.form.submit();
- area.form.submit = wrapSubmit;
- }
- area.form.submit = wrapSubmit;
- }
-
- function insert(frame) {
- if (area.nextSibling)
- area.parentNode.insertBefore(frame, area.nextSibling);
- else
- area.parentNode.appendChild(frame);
- }
-
- area.style.display = "none";
- var mirror = new CodeMirror(insert, options);
- mirror.toTextArea = function() {
- area.parentNode.removeChild(mirror.wrapping);
- area.style.display = "";
- if (area.form) {
- area.form.submit = realSubmit;
- if (typeof area.form.removeEventListener == "function")
- area.form.removeEventListener("submit", updateField, false);
- else
- area.form.detachEvent("onsubmit", updateField);
- }
- };
-
- return mirror;
- };
-
- CodeMirror.isProbablySupported = function() {
- // This is rather awful, but can be useful.
- var match;
- if (window.opera)
- return Number(window.opera.version()) >= 9.52;
- else if (/Apple Computers, Inc/.test(navigator.vendor) && (match = navigator.userAgent.match(/Version\/(\d+(?:\.\d+)?)\./)))
- return Number(match[1]) >= 3;
- else if (document.selection && window.ActiveXObject && (match = navigator.userAgent.match(/MSIE (\d+(?:\.\d*)?)\b/)))
- return Number(match[1]) >= 6;
- else if (match = navigator.userAgent.match(/gecko\/(\d{8})/i))
- return Number(match[1]) >= 20050901;
- else if (match = navigator.userAgent.match(/AppleWebKit\/(\d+)/))
- return Number(match[1]) >= 525;
- else
- return null;
- };
-
- return CodeMirror;
-})();
+++ /dev/null
-/* The Editor object manages the content of the editable frame. It
- * catches events, colours nodes, and indents lines. This file also
- * holds some functions for transforming arbitrary DOM structures into
- * plain sequences of <span> and <br> elements
- */
-
-var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
-var webkit = /AppleWebKit/.test(navigator.userAgent);
-var safari = /Apple Computers, Inc/.test(navigator.vendor);
-var gecko = /gecko\/(\d{8})/i.test(navigator.userAgent);
-// TODO this is related to the backspace-at-end-of-line bug. Remove
-// this if Opera gets their act together, make the version check more
-// broad if they don't.
-var brokenOpera = window.opera && /Version\/10.[56]/.test(navigator.userAgent);
-
-// Make sure a string does not contain two consecutive 'collapseable'
-// whitespace characters.
-function makeWhiteSpace(n) {
- var buffer = [], nb = true;
- for (; n > 0; n--) {
- buffer.push((nb || n == 1) ? nbsp : " ");
- nb ^= true;
- }
- return buffer.join("");
-}
-
-// Create a set of white-space characters that will not be collapsed
-// by the browser, but will not break text-wrapping either.
-function fixSpaces(string) {
- if (string.charAt(0) == " ") string = nbsp + string.slice(1);
- return string.replace(/\t/g, function() {return makeWhiteSpace(indentUnit);})
- .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
-}
-
-function cleanText(text) {
- return text.replace(/\u00a0/g, " ");
-}
-
-// Create a SPAN node with the expected properties for document part
-// spans.
-function makePartSpan(value, doc) {
- var text = value;
- if (value.nodeType == 3) text = value.nodeValue;
- else value = doc.createTextNode(text);
-
- var span = doc.createElement("SPAN");
- span.isPart = true;
- span.appendChild(value);
- span.currentText = text;
- return span;
-}
-
-var Editor = (function(){
- // The HTML elements whose content should be suffixed by a newline
- // when converting them to flat text.
- var newlineElements = {"P": true, "DIV": true, "LI": true};
-
- function asEditorLines(string) {
- var tab = makeWhiteSpace(indentUnit);
- return map(string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n").split("\n"), fixSpaces);
- }
-
- // Helper function for traverseDOM. Flattens an arbitrary DOM node
- // into an array of textnodes and <br> tags.
- function simplifyDOM(root, atEnd) {
- var doc = root.ownerDocument;
- var result = [];
- var leaving = true;
-
- function simplifyNode(node, top) {
- if (node.nodeType == 3) {
- var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/\r/g, "").replace(/\n/g, " "));
- if (text.length) leaving = false;
- result.push(node);
- }
- else if (isBR(node) && node.childNodes.length == 0) {
- leaving = true;
- result.push(node);
- }
- else {
- for (var n = node.firstChild; n; n = n.nextSibling) simplifyNode(n);
- if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) {
- leaving = true;
- if (!atEnd || !top)
- result.push(doc.createElement("BR"));
- }
- }
- }
-
- simplifyNode(root, true);
- return result;
- }
-
- // Creates a MochiKit-style iterator that goes over a series of DOM
- // nodes. The values it yields are strings, the textual content of
- // the nodes. It makes sure that all nodes up to and including the
- // one whose text is being yielded have been 'normalized' to be just
- // <span> and <br> elements.
- function traverseDOM(start){
- var owner = start.ownerDocument;
- var nodeQueue = [];
-
- // Create a function that can be used to insert nodes after the
- // one given as argument.
- function pointAt(node){
- var parent = node.parentNode;
- var next = node.nextSibling;
- return function(newnode) {
- parent.insertBefore(newnode, next);
- };
- }
- var point = null;
-
- // This an Opera-specific hack -- always insert an empty span
- // between two BRs, because Opera's cursor code gets terribly
- // confused when the cursor is between two BRs.
- var afterBR = true;
-
- // Insert a normalized node at the current point. If it is a text
- // node, wrap it in a <span>, and give that span a currentText
- // property -- this is used to cache the nodeValue, because
- // directly accessing nodeValue is horribly slow on some browsers.
- // The dirty property is used by the highlighter to determine
- // which parts of the document have to be re-highlighted.
- function insertPart(part){
- var text = "\n";
- if (part.nodeType == 3) {
- select.snapshotChanged();
- part = makePartSpan(part, owner);
- text = part.currentText;
- afterBR = false;
- }
- else {
- if (afterBR && window.opera)
- point(makePartSpan("", owner));
- afterBR = true;
- }
- part.dirty = true;
- nodeQueue.push(part);
- point(part);
- return text;
- }
-
- // Extract the text and newlines from a DOM node, insert them into
- // the document, and return the textual content. Used to replace
- // non-normalized nodes.
- function writeNode(node, end) {
- var simplified = simplifyDOM(node, end);
- for (var i = 0; i < simplified.length; i++)
- simplified[i] = insertPart(simplified[i]);
- return simplified.join("");
- }
-
- // Check whether a node is a normalized <span> element.
- function partNode(node){
- if (node.isPart && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
- node.currentText = node.firstChild.nodeValue;
- return !/[\n\t\r]/.test(node.currentText);
- }
- return false;
- }
-
- // Advance to next node, return string for current node.
- function next() {
- if (!start) throw StopIteration;
- var node = start;
- start = node.nextSibling;
-
- if (partNode(node)){
- nodeQueue.push(node);
- afterBR = false;
- return node.currentText;
- }
- else if (isBR(node)) {
- if (afterBR && window.opera)
- node.parentNode.insertBefore(makePartSpan("", owner), node);
- nodeQueue.push(node);
- afterBR = true;
- return "\n";
- }
- else {
- var end = !node.nextSibling;
- point = pointAt(node);
- removeElement(node);
- return writeNode(node, end);
- }
- }
-
- // MochiKit iterators are objects with a next function that
- // returns the next value or throws StopIteration when there are
- // no more values.
- return {next: next, nodes: nodeQueue};
- }
-
- // Determine the text size of a processed node.
- function nodeSize(node) {
- return isBR(node) ? 1 : node.currentText.length;
- }
-
- // Search backwards through the top-level nodes until the next BR or
- // the start of the frame.
- function startOfLine(node) {
- while (node && !isBR(node)) node = node.previousSibling;
- return node;
- }
- function endOfLine(node, container) {
- if (!node) node = container.firstChild;
- else if (isBR(node)) node = node.nextSibling;
-
- while (node && !isBR(node)) node = node.nextSibling;
- return node;
- }
-
- function time() {return new Date().getTime();}
-
- // Client interface for searching the content of the editor. Create
- // these by calling CodeMirror.getSearchCursor. To use, call
- // findNext on the resulting object -- this returns a boolean
- // indicating whether anything was found, and can be called again to
- // skip to the next find. Use the select and replace methods to
- // actually do something with the found locations.
- function SearchCursor(editor, string, fromCursor, caseFold) {
- this.editor = editor;
- if (caseFold == undefined) {
- caseFold = (string == string.toLowerCase());
- }
- this.caseFold = caseFold;
- if (caseFold) string = string.toLowerCase();
- this.history = editor.history;
- this.history.commit();
-
- // Are we currently at an occurrence of the search string?
- this.atOccurrence = false;
- // The object stores a set of nodes coming after its current
- // position, so that when the current point is taken out of the
- // DOM tree, we can still try to continue.
- this.fallbackSize = 15;
- var cursor;
- // Start from the cursor when specified and a cursor can be found.
- if (fromCursor && (cursor = select.cursorPos(this.editor.container))) {
- this.line = cursor.node;
- this.offset = cursor.offset;
- }
- else {
- this.line = null;
- this.offset = 0;
- }
- this.valid = !!string;
-
- // Create a matcher function based on the kind of string we have.
- var target = string.split("\n"), self = this;
- this.matches = (target.length == 1) ?
- // For one-line strings, searching can be done simply by calling
- // indexOf on the current line.
- function() {
- var line = cleanText(self.history.textAfter(self.line).slice(self.offset));
- var match = (self.caseFold ? line.toLowerCase() : line).indexOf(string);
- if (match > -1)
- return {from: {node: self.line, offset: self.offset + match},
- to: {node: self.line, offset: self.offset + match + string.length}};
- } :
- // Multi-line strings require internal iteration over lines, and
- // some clunky checks to make sure the first match ends at the
- // end of the line and the last match starts at the start.
- function() {
- var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
- var match = (self.caseFold ? firstLine.toLowerCase() : firstLine).lastIndexOf(target[0]);
- if (match == -1 || match != firstLine.length - target[0].length)
- return false;
- var startOffset = self.offset + match;
-
- var line = self.history.nodeAfter(self.line);
- for (var i = 1; i < target.length - 1; i++) {
- var lineText = cleanText(self.history.textAfter(line));
- if ((self.caseFold ? lineText.toLowerCase() : lineText) != target[i])
- return false;
- line = self.history.nodeAfter(line);
- }
-
- var lastLine = cleanText(self.history.textAfter(line));
- if ((self.caseFold ? lastLine.toLowerCase() : lastLine).indexOf(target[target.length - 1]) != 0)
- return false;
-
- return {from: {node: self.line, offset: startOffset},
- to: {node: line, offset: target[target.length - 1].length}};
- };
- }
-
- SearchCursor.prototype = {
- findNext: function() {
- if (!this.valid) return false;
- this.atOccurrence = false;
- var self = this;
-
- // Go back to the start of the document if the current line is
- // no longer in the DOM tree.
- if (this.line && !this.line.parentNode) {
- this.line = null;
- this.offset = 0;
- }
-
- // Set the cursor's position one character after the given
- // position.
- function saveAfter(pos) {
- if (self.history.textAfter(pos.node).length > pos.offset) {
- self.line = pos.node;
- self.offset = pos.offset + 1;
- }
- else {
- self.line = self.history.nodeAfter(pos.node);
- self.offset = 0;
- }
- }
-
- while (true) {
- var match = this.matches();
- // Found the search string.
- if (match) {
- this.atOccurrence = match;
- saveAfter(match.from);
- return true;
- }
- this.line = this.history.nodeAfter(this.line);
- this.offset = 0;
- // End of document.
- if (!this.line) {
- this.valid = false;
- return false;
- }
- }
- },
-
- select: function() {
- if (this.atOccurrence) {
- select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
- select.scrollToCursor(this.editor.container);
- }
- },
-
- replace: function(string) {
- if (this.atOccurrence) {
- var end = this.editor.replaceRange(this.atOccurrence.from, this.atOccurrence.to, string);
- this.line = end.node;
- this.offset = end.offset;
- this.atOccurrence = false;
- }
- }
- };
-
- // The Editor object is the main inside-the-iframe interface.
- function Editor(options) {
- this.options = options;
- window.indentUnit = options.indentUnit;
- this.parent = parent;
- this.doc = document;
- var container = this.container = this.doc.body;
- this.win = window;
- this.history = new UndoHistory(container, options.undoDepth, options.undoDelay, this);
- var self = this;
-
- if (!Editor.Parser)
- throw "No parser loaded.";
- if (options.parserConfig && Editor.Parser.configure)
- Editor.Parser.configure(options.parserConfig);
-
- if (!options.readOnly)
- select.setCursorPos(container, {node: null, offset: 0});
-
- this.dirty = [];
- this.importCode(options.content || "");
- this.history.onChange = options.onChange;
-
- if (!options.readOnly) {
- if (options.continuousScanning !== false) {
- this.scanner = this.documentScanner(options.passTime);
- this.delayScanning();
- }
-
- function setEditable() {
- // Use contentEditable instead of designMode on IE, since designMode frames
- // can not run any scripts. It would be nice if we could use contentEditable
- // everywhere, but it is significantly flakier than designMode on every
- // single non-IE browser.
- if (document.body.contentEditable != undefined && internetExplorer)
- document.body.contentEditable = "true";
- else
- document.designMode = "on";
-
- document.documentElement.style.borderWidth = "0";
- if (!options.textWrapping)
- container.style.whiteSpace = "nowrap";
- }
-
- // If setting the frame editable fails, try again when the user
- // focus it (happens when the frame is not visible on
- // initialisation, in Firefox).
- try {
- setEditable();
- }
- catch(e) {
- var focusEvent = addEventHandler(document, "focus", function() {
- focusEvent();
- setEditable();
- }, true);
- }
-
- addEventHandler(document, "keydown", method(this, "keyDown"));
- addEventHandler(document, "keypress", method(this, "keyPress"));
- addEventHandler(document, "keyup", method(this, "keyUp"));
-
- function cursorActivity() {self.cursorActivity(false);}
- addEventHandler(document.body, "mouseup", cursorActivity);
- addEventHandler(document.body, "cut", cursorActivity);
-
- // workaround for a gecko bug [?] where going forward and then
- // back again breaks designmode (no more cursor)
- if (gecko)
- addEventHandler(this.win, "pagehide", function(){self.unloaded = true;});
-
- addEventHandler(document.body, "paste", function(event) {
- cursorActivity();
- var text = null;
- try {
- var clipboardData = event.clipboardData || window.clipboardData;
- if (clipboardData) text = clipboardData.getData('Text');
- }
- catch(e) {}
- if (text !== null) {
- event.stop();
- self.replaceSelection(text);
- select.scrollToCursor(self.container);
- }
- });
-
- if (this.options.autoMatchParens)
- addEventHandler(document.body, "click", method(this, "scheduleParenHighlight"));
- }
- else if (!options.textWrapping) {
- container.style.whiteSpace = "nowrap";
- }
- }
-
- function isSafeKey(code) {
- return (code >= 16 && code <= 18) || // shift, control, alt
- (code >= 33 && code <= 40); // arrows, home, end
- }
-
- Editor.prototype = {
- // Import a piece of code into the editor.
- importCode: function(code) {
- this.history.push(null, null, asEditorLines(code));
- this.history.reset();
- },
-
- // Extract the code from the editor.
- getCode: function() {
- if (!this.container.firstChild)
- return "";
-
- var accum = [];
- select.markSelection(this.win);
- forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
- select.selectMarked();
- return cleanText(accum.join(""));
- },
-
- checkLine: function(node) {
- if (node === false || !(node == null || node.parentNode == this.container))
- throw parent.CodeMirror.InvalidLineHandle;
- },
-
- cursorPosition: function(start) {
- if (start == null) start = true;
- var pos = select.cursorPos(this.container, start);
- if (pos) return {line: pos.node, character: pos.offset};
- else return {line: null, character: 0};
- },
-
- firstLine: function() {
- return null;
- },
-
- lastLine: function() {
- if (this.container.lastChild) return startOfLine(this.container.lastChild);
- else return null;
- },
-
- nextLine: function(line) {
- this.checkLine(line);
- var end = endOfLine(line, this.container);
- return end || false;
- },
-
- prevLine: function(line) {
- this.checkLine(line);
- if (line == null) return false;
- return startOfLine(line.previousSibling);
- },
-
- visibleLineCount: function() {
- var line = this.container.firstChild;
- while (line && isBR(line)) line = line.nextSibling; // BR heights are unreliable
- if (!line) return false;
- var innerHeight = (window.innerHeight
- || document.documentElement.clientHeight
- || document.body.clientHeight);
- return Math.floor(innerHeight / line.offsetHeight);
- },
-
- selectLines: function(startLine, startOffset, endLine, endOffset) {
- this.checkLine(startLine);
- var start = {node: startLine, offset: startOffset}, end = null;
- if (endOffset !== undefined) {
- this.checkLine(endLine);
- end = {node: endLine, offset: endOffset};
- }
- select.setCursorPos(this.container, start, end);
- select.scrollToCursor(this.container);
- },
-
- lineContent: function(line) {
- var accum = [];
- for (line = line ? line.nextSibling : this.container.firstChild;
- line && !isBR(line); line = line.nextSibling)
- accum.push(nodeText(line));
- return cleanText(accum.join(""));
- },
-
- setLineContent: function(line, content) {
- this.history.commit();
- this.replaceRange({node: line, offset: 0},
- {node: line, offset: this.history.textAfter(line).length},
- content);
- this.addDirtyNode(line);
- this.scheduleHighlight();
- },
-
- removeLine: function(line) {
- var node = line ? line.nextSibling : this.container.firstChild;
- while (node) {
- var next = node.nextSibling;
- removeElement(node);
- if (isBR(node)) break;
- node = next;
- }
- this.addDirtyNode(line);
- this.scheduleHighlight();
- },
-
- insertIntoLine: function(line, position, content) {
- var before = null;
- if (position == "end") {
- before = endOfLine(line, this.container);
- }
- else {
- for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
- if (position == 0) {
- before = cur;
- break;
- }
- var text = nodeText(cur);
- if (text.length > position) {
- before = cur.nextSibling;
- content = text.slice(0, position) + content + text.slice(position);
- removeElement(cur);
- break;
- }
- position -= text.length;
- }
- }
-
- var lines = asEditorLines(content), doc = this.container.ownerDocument;
- for (var i = 0; i < lines.length; i++) {
- if (i > 0) this.container.insertBefore(doc.createElement("BR"), before);
- this.container.insertBefore(makePartSpan(lines[i], doc), before);
- }
- this.addDirtyNode(line);
- this.scheduleHighlight();
- },
-
- // Retrieve the selected text.
- selectedText: function() {
- var h = this.history;
- h.commit();
-
- var start = select.cursorPos(this.container, true),
- end = select.cursorPos(this.container, false);
- if (!start || !end) return "";
-
- if (start.node == end.node)
- return h.textAfter(start.node).slice(start.offset, end.offset);
-
- var text = [h.textAfter(start.node).slice(start.offset)];
- for (var pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos))
- text.push(h.textAfter(pos));
- text.push(h.textAfter(end.node).slice(0, end.offset));
- return cleanText(text.join("\n"));
- },
-
- // Replace the selection with another piece of text.
- replaceSelection: function(text) {
- this.history.commit();
-
- var start = select.cursorPos(this.container, true),
- end = select.cursorPos(this.container, false);
- if (!start || !end) return;
-
- end = this.replaceRange(start, end, text);
- select.setCursorPos(this.container, end);
- },
-
- cursorCoords: function(start) {
- var sel = select.cursorPos(this.container, start);
- if (!sel) return null;
- var off = sel.offset, node = sel.node, doc = this.win.document, self = this;
- function measureFromNode(node, xOffset) {
- var y = -(self.win.document.body.scrollTop || self.win.document.documentElement.scrollTop || 0),
- x = -(self.win.document.body.scrollLeft || self.win.document.documentElement.scrollLeft || 0) + xOffset;
- forEach([node, self.win.frameElement], function(n) {
- while (n) {x += n.offsetLeft; y += n.offsetTop;n = n.offsetParent;}
- });
- return {x: x, y: y, yBot: y + node.offsetHeight};
- }
- function withTempNode(text, f) {
- var node = doc.createElement("SPAN");
- node.appendChild(doc.createTextNode(text));
- try {return f(node);}
- finally {if (node.parentNode) node.parentNode.removeChild(node);}
- }
-
- while (off) {
- node = node ? node.nextSibling : this.container.firstChild;
- var txt = nodeText(node);
- if (off < txt.length)
- return withTempNode(txt.substr(0, off), function(tmp) {
- tmp.style.position = "absolute"; tmp.style.visibility = "hidden";
- tmp.className = node.className;
- self.container.appendChild(tmp);
- return measureFromNode(node, tmp.offsetWidth);
- });
- off -= txt.length;
- }
- if (node && isSpan(node))
- return measureFromNode(node, node.offsetWidth);
- else if (node && node.nextSibling && isSpan(node.nextSibling))
- return measureFromNode(node.nextSibling, 0);
- else
- return withTempNode("\u200b", function(tmp) {
- if (node) node.parentNode.insertBefore(tmp, node.nextSibling);
- else self.container.insertBefore(tmp, self.container.firstChild);
- return measureFromNode(tmp, 0);
- });
- },
-
- reroutePasteEvent: function() {
- if (this.capturingPaste || window.opera) return;
- this.capturingPaste = true;
- var te = window.frameElement.CodeMirror.textareaHack;
- parent.focus();
- te.value = "";
- te.focus();
-
- var self = this;
- this.parent.setTimeout(function() {
- self.capturingPaste = false;
- self.win.focus();
- if (self.selectionSnapshot) // IE hack
- self.win.select.setBookmark(self.container, self.selectionSnapshot);
- var text = te.value;
- if (text) {
- self.replaceSelection(text);
- select.scrollToCursor(self.container);
- }
- }, 10);
- },
-
- replaceRange: function(from, to, text) {
- var lines = asEditorLines(text);
- lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
- var lastLine = lines[lines.length - 1];
- lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
- var end = this.history.nodeAfter(to.node);
- this.history.push(from.node, end, lines);
- return {node: this.history.nodeBefore(end),
- offset: lastLine.length};
- },
-
- getSearchCursor: function(string, fromCursor, caseFold) {
- return new SearchCursor(this, string, fromCursor, caseFold);
- },
-
- // Re-indent the whole buffer
- reindent: function() {
- if (this.container.firstChild)
- this.indentRegion(null, this.container.lastChild);
- },
-
- reindentSelection: function(direction) {
- if (!select.somethingSelected(this.win)) {
- this.indentAtCursor(direction);
- }
- else {
- var start = select.selectionTopNode(this.container, true),
- end = select.selectionTopNode(this.container, false);
- if (start === false || end === false) return;
- this.indentRegion(start, end, direction);
- }
- },
-
- grabKeys: function(eventHandler, filter) {
- this.frozen = eventHandler;
- this.keyFilter = filter;
- },
- ungrabKeys: function() {
- this.frozen = "leave";
- },
-
- setParser: function(name, parserConfig) {
- Editor.Parser = window[name];
- parserConfig = parserConfig || this.options.parserConfig;
- if (parserConfig && Editor.Parser.configure)
- Editor.Parser.configure(parserConfig);
-
- if (this.container.firstChild) {
- forEach(this.container.childNodes, function(n) {
- if (n.nodeType != 3) n.dirty = true;
- });
- this.addDirtyNode(this.firstChild);
- this.scheduleHighlight();
- }
- },
-
- // Intercept enter and tab, and assign their new functions.
- keyDown: function(event) {
- if (this.frozen == "leave") {this.frozen = null; this.keyFilter = null;}
- if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode, event))) {
- event.stop();
- this.frozen(event);
- return;
- }
-
- var code = this.lastKeyDownCode = event.keyCode;
- // Don't scan when the user is typing.
- this.delayScanning();
- // Schedule a paren-highlight event, if configured.
- if (this.options.autoMatchParens)
- this.scheduleParenHighlight();
-
- // The various checks for !altKey are there because AltGr sets both
- // ctrlKey and altKey to true, and should not be recognised as
- // Control.
- if (code == 13) { // enter
- if (event.ctrlKey && !event.altKey) {
- this.reparseBuffer();
- }
- else {
- select.insertNewlineAtCursor(this.win);
- this.indentAtCursor();
- select.scrollToCursor(this.container);
- }
- event.stop();
- }
- else if (code == 9 && this.options.tabMode != "default" && !event.ctrlKey) { // tab
- this.handleTab(!event.shiftKey);
- event.stop();
- }
- else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
- this.handleTab(true);
- event.stop();
- }
- else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home
- if (this.home()) event.stop();
- }
- else if (code == 35 && !event.shiftKey && !event.ctrlKey) { // end
- if (this.end()) event.stop();
- }
- // Only in Firefox is the default behavior for PgUp/PgDn correct.
- else if (code == 33 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgUp
- if (this.pageUp()) event.stop();
- }
- else if (code == 34 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgDn
- if (this.pageDown()) event.stop();
- }
- else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
- this.highlightParens(event.shiftKey, true);
- event.stop();
- }
- else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right
- var cursor = select.selectionTopNode(this.container);
- if (cursor !== false && this.container.firstChild) {
- if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
- else {
- var end = endOfLine(cursor, this.container);
- select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
- }
- event.stop();
- }
- }
- else if ((event.ctrlKey || event.metaKey) && !event.altKey) {
- if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y
- select.scrollToNode(this.history.redo());
- event.stop();
- }
- else if (code == 90 || (safari && code == 8)) { // Z, backspace
- select.scrollToNode(this.history.undo());
- event.stop();
- }
- else if (code == 83 && this.options.saveFunction) { // S
- this.options.saveFunction();
- event.stop();
- }
- else if (internetExplorer && code == 86) {
- this.reroutePasteEvent();
- }
- }
- this.keyUpOrPressAfterLastKeyDown = false;
- },
-
- // Check for characters that should re-indent the current line,
- // and prevent Opera from handling enter and tab anyway.
- keyPress: function(event) {
- this.keyUpOrPressAfterLastKeyDown = true;
- var electric = Editor.Parser.electricChars, self = this;
- // Hack for Opera, and Firefox on OS X, in which stopping a
- // keydown event does not prevent the associated keypress event
- // from happening, so we have to cancel enter and tab again
- // here.
- if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode || event.code, event))) ||
- event.code == 13 || (event.code == 9 && this.options.tabMode != "default") ||
- (event.code == 32 && event.shiftKey && this.options.tabMode == "default"))
- event.stop();
- else if (electric && electric.indexOf(event.character) != -1)
- this.parent.setTimeout(function(){self.indentAtCursor(null);}, 0);
- else if ((event.character == "v" || event.character == "V")
- && (event.ctrlKey || event.metaKey) && !event.altKey) // ctrl-V
- this.reroutePasteEvent();
- // Work around a bug where pressing backspace at the end of a
- // line often causes the cursor to jump to the start of the line
- // in Opera 10.60.
- else if (brokenOpera && event.code == 8) {
- var sel = select.selectionTopNode(this.container), self = this,
- next = sel ? sel.nextSibling : this.container.firstChild;
- if (sel !== false && next && isBR(next))
- this.parent.setTimeout(function(){
- if (select.selectionTopNode(self.container) == next)
- select.focusAfterNode(next.previousSibling, self.container);
- }, 20);
- }
- },
-
- // Mark the node at the cursor dirty when a non-safe key is
- // released.
- keyUp: function(event) {
- this.keyUpOrPressAfterLastKeyDown = true;
- this.cursorActivity(isSafeKey(event.keyCode));
- },
-
- // Indent the line following a given <br>, or null for the first
- // line. If given a <br> element, this must have been highlighted
- // so that it has an indentation method. Returns the whitespace
- // element that has been modified or created (if any).
- indentLineAfter: function(start, direction) {
- // whiteSpace is the whitespace span at the start of the line,
- // or null if there is no such node.
- var whiteSpace = start ? start.nextSibling : this.container.firstChild;
- if (whiteSpace && !hasClass(whiteSpace, "whitespace"))
- whiteSpace = null;
-
- // Sometimes the start of the line can influence the correct
- // indentation, so we retrieve it.
- var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
- var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
-
- // Ask the lexical context for the correct indentation, and
- // compute how much this differs from the current indentation.
- var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
- if (direction != null && this.options.tabMode == "shift")
- newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
- else if (start)
- newIndent = start.indentation(nextChars, curIndent, direction);
- else if (Editor.Parser.firstIndentation)
- newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
- var indentDiff = newIndent - curIndent;
-
- // If there is too much, this is just a matter of shrinking a span.
- if (indentDiff < 0) {
- if (newIndent == 0) {
- if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0);
- removeElement(whiteSpace);
- whiteSpace = null;
- }
- else {
- select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
- whiteSpace.currentText = makeWhiteSpace(newIndent);
- whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
- }
- }
- // Not enough...
- else if (indentDiff > 0) {
- // If there is whitespace, we grow it.
- if (whiteSpace) {
- whiteSpace.currentText = makeWhiteSpace(newIndent);
- whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
- select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
- }
- // Otherwise, we have to add a new whitespace node.
- else {
- whiteSpace = makePartSpan(makeWhiteSpace(newIndent), this.doc);
- whiteSpace.className = "whitespace";
- if (start) insertAfter(whiteSpace, start);
- else this.container.insertBefore(whiteSpace, this.container.firstChild);
- select.snapshotMove(firstText && (firstText.firstChild || firstText),
- whiteSpace.firstChild, newIndent, false, true);
- }
- }
- if (indentDiff != 0) this.addDirtyNode(start);
- },
-
- // Re-highlight the selected part of the document.
- highlightAtCursor: function() {
- var pos = select.selectionTopNode(this.container, true);
- var to = select.selectionTopNode(this.container, false);
- if (pos === false || to === false) return false;
-
- select.markSelection(this.win);
- if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
- return false;
- select.selectMarked();
- return true;
- },
-
- // When tab is pressed with text selected, the whole selection is
- // re-indented, when nothing is selected, the line with the cursor
- // is re-indented.
- handleTab: function(direction) {
- if (this.options.tabMode == "spaces")
- select.insertTabAtCursor(this.win);
- else
- this.reindentSelection(direction);
- },
-
- // Custom home behaviour that doesn't land the cursor in front of
- // leading whitespace unless pressed twice.
- home: function() {
- var cur = select.selectionTopNode(this.container, true), start = cur;
- if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild)
- return false;
-
- while (cur && !isBR(cur)) cur = cur.previousSibling;
- var next = cur ? cur.nextSibling : this.container.firstChild;
- if (next && next != start && next.isPart && hasClass(next, "whitespace"))
- select.focusAfterNode(next, this.container);
- else
- select.focusAfterNode(cur, this.container);
-
- select.scrollToCursor(this.container);
- return true;
- },
-
- // Some browsers (Opera) don't manage to handle the end key
- // properly in the face of vertical scrolling.
- end: function() {
- var cur = select.selectionTopNode(this.container, true);
- if (cur === false) return false;
- cur = endOfLine(cur, this.container);
- if (!cur) return false;
- select.focusAfterNode(cur.previousSibling, this.container);
- select.scrollToCursor(this.container);
- return true;
- },
-
- pageUp: function() {
- var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
- if (line === false || scrollAmount === false) return false;
- // Try to keep one line on the screen.
- scrollAmount -= 2;
- for (var i = 0; i < scrollAmount; i++) {
- line = this.prevLine(line);
- if (line === false) break;
- }
- if (i == 0) return false; // Already at first line
- select.setCursorPos(this.container, {node: line, offset: 0});
- select.scrollToCursor(this.container);
- return true;
- },
-
- pageDown: function() {
- var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
- if (line === false || scrollAmount === false) return false;
- // Try to move to the last line of the current page.
- scrollAmount -= 2;
- for (var i = 0; i < scrollAmount; i++) {
- var nextLine = this.nextLine(line);
- if (nextLine === false) break;
- line = nextLine;
- }
- if (i == 0) return false; // Already at last line
- select.setCursorPos(this.container, {node: line, offset: 0});
- select.scrollToCursor(this.container);
- return true;
- },
-
- // Delay (or initiate) the next paren highlight event.
- scheduleParenHighlight: function() {
- if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
- var self = this;
- this.parenEvent = this.parent.setTimeout(function(){self.highlightParens();}, 300);
- },
-
- // Take the token before the cursor. If it contains a character in
- // '()[]{}', search for the matching paren/brace/bracket, and
- // highlight them in green for a moment, or red if no proper match
- // was found.
- highlightParens: function(jump, fromKey) {
- var self = this;
- // give the relevant nodes a colour.
- function highlight(node, ok) {
- if (!node) return;
- if (self.options.markParen) {
- self.options.markParen(node, ok);
- }
- else {
- node.style.fontWeight = "bold";
- node.style.color = ok ? "#8F8" : "#F88";
- }
- }
- function unhighlight(node) {
- if (!node) return;
- if (self.options.unmarkParen) {
- self.options.unmarkParen(node);
- }
- else {
- node.style.fontWeight = "";
- node.style.color = "";
- }
- }
- if (!fromKey && self.highlighted) {
- unhighlight(self.highlighted[0]);
- unhighlight(self.highlighted[1]);
- }
-
- if (!window.select) return;
- // Clear the event property.
- if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
- this.parenEvent = null;
-
- // Extract a 'paren' from a piece of text.
- function paren(node) {
- if (node.currentText) {
- var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
- return match && match[1];
- }
- }
- // Determine the direction a paren is facing.
- function forward(ch) {
- return /[\(\[\{]/.test(ch);
- }
-
- var ch, cursor = select.selectionTopNode(this.container, true);
- if (!cursor || !this.highlightAtCursor()) return;
- cursor = select.selectionTopNode(this.container, true);
- if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor)))))
- return;
- // We only look for tokens with the same className.
- var className = cursor.className, dir = forward(ch), match = matching[ch];
-
- // Since parts of the document might not have been properly
- // highlighted, and it is hard to know in advance which part we
- // have to scan, we just try, and when we find dirty nodes we
- // abort, parse them, and re-try.
- function tryFindMatch() {
- var stack = [], ch, ok = true;
- for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
- if (runner.className == className && isSpan(runner) && (ch = paren(runner))) {
- if (forward(ch) == dir)
- stack.push(ch);
- else if (!stack.length)
- ok = false;
- else if (stack.pop() != matching[ch])
- ok = false;
- if (!stack.length) break;
- }
- else if (runner.dirty || !isSpan(runner) && !isBR(runner)) {
- return {node: runner, status: "dirty"};
- }
- }
- return {node: runner, status: runner && ok};
- }
-
- while (true) {
- var found = tryFindMatch();
- if (found.status == "dirty") {
- this.highlight(found.node, endOfLine(found.node));
- // Needed because in some corner cases a highlight does not
- // reach a node.
- found.node.dirty = false;
- continue;
- }
- else {
- highlight(cursor, found.status);
- highlight(found.node, found.status);
- if (fromKey)
- self.parent.setTimeout(function() {unhighlight(cursor); unhighlight(found.node);}, 500);
- else
- self.highlighted = [cursor, found.node];
- if (jump && found.node)
- select.focusAfterNode(found.node.previousSibling, this.container);
- break;
- }
- }
- },
-
- // Adjust the amount of whitespace at the start of the line that
- // the cursor is on so that it is indented properly.
- indentAtCursor: function(direction) {
- if (!this.container.firstChild) return;
- // The line has to have up-to-date lexical information, so we
- // highlight it first.
- if (!this.highlightAtCursor()) return;
- var cursor = select.selectionTopNode(this.container, false);
- // If we couldn't determine the place of the cursor,
- // there's nothing to indent.
- if (cursor === false)
- return;
- select.markSelection(this.win);
- this.indentLineAfter(startOfLine(cursor), direction);
- select.selectMarked();
- },
-
- // Indent all lines whose start falls inside of the current
- // selection.
- indentRegion: function(start, end, direction) {
- var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling);
- if (!isBR(end)) end = endOfLine(end, this.container);
- this.addDirtyNode(start);
-
- do {
- var next = endOfLine(current, this.container);
- if (current) this.highlight(before, next, true);
- this.indentLineAfter(current, direction);
- before = current;
- current = next;
- } while (current != end);
- select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
- },
-
- // Find the node that the cursor is in, mark it as dirty, and make
- // sure a highlight pass is scheduled.
- cursorActivity: function(safe) {
- // pagehide event hack above
- if (this.unloaded) {
- this.win.document.designMode = "off";
- this.win.document.designMode = "on";
- this.unloaded = false;
- }
-
- if (internetExplorer) {
- this.container.createTextRange().execCommand("unlink");
- this.selectionSnapshot = select.getBookmark(this.container);
- }
-
- var activity = this.options.cursorActivity;
- if (!safe || activity) {
- var cursor = select.selectionTopNode(this.container, false);
- if (cursor === false || !this.container.firstChild) return;
- cursor = cursor || this.container.firstChild;
- if (activity) activity(cursor);
- if (!safe) {
- this.scheduleHighlight();
- this.addDirtyNode(cursor);
- }
- }
- },
-
- reparseBuffer: function() {
- forEach(this.container.childNodes, function(node) {node.dirty = true;});
- if (this.container.firstChild)
- this.addDirtyNode(this.container.firstChild);
- },
-
- // Add a node to the set of dirty nodes, if it isn't already in
- // there.
- addDirtyNode: function(node) {
- node = node || this.container.firstChild;
- if (!node) return;
-
- for (var i = 0; i < this.dirty.length; i++)
- if (this.dirty[i] == node) return;
-
- if (node.nodeType != 3)
- node.dirty = true;
- this.dirty.push(node);
- },
-
- allClean: function() {
- return !this.dirty.length;
- },
-
- // Cause a highlight pass to happen in options.passDelay
- // milliseconds. Clear the existing timeout, if one exists. This
- // way, the passes do not happen while the user is typing, and
- // should as unobtrusive as possible.
- scheduleHighlight: function() {
- // Timeouts are routed through the parent window, because on
- // some browsers designMode windows do not fire timeouts.
- var self = this;
- this.parent.clearTimeout(this.highlightTimeout);
- this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
- },
-
- // Fetch one dirty node, and remove it from the dirty set.
- getDirtyNode: function() {
- while (this.dirty.length > 0) {
- var found = this.dirty.pop();
- // IE8 sometimes throws an unexplainable 'invalid argument'
- // exception for found.parentNode
- try {
- // If the node has been coloured in the meantime, or is no
- // longer in the document, it should not be returned.
- while (found && found.parentNode != this.container)
- found = found.parentNode;
- if (found && (found.dirty || found.nodeType == 3))
- return found;
- } catch (e) {}
- }
- return null;
- },
-
- // Pick dirty nodes, and highlight them, until options.passTime
- // milliseconds have gone by. The highlight method will continue
- // to next lines as long as it finds dirty nodes. It returns
- // information about the place where it stopped. If there are
- // dirty nodes left after this function has spent all its lines,
- // it shedules another highlight to finish the job.
- highlightDirty: function(force) {
- // Prevent FF from raising an error when it is firing timeouts
- // on a page that's no longer loaded.
- if (!window.select) return false;
-
- if (!this.options.readOnly) select.markSelection(this.win);
- var start, endTime = force ? null : time() + this.options.passTime;
- while ((time() < endTime || force) && (start = this.getDirtyNode())) {
- var result = this.highlight(start, endTime);
- if (result && result.node && result.dirty)
- this.addDirtyNode(result.node);
- }
- if (!this.options.readOnly) select.selectMarked();
- if (start) this.scheduleHighlight();
- return this.dirty.length == 0;
- },
-
- // Creates a function that, when called through a timeout, will
- // continuously re-parse the document.
- documentScanner: function(passTime) {
- var self = this, pos = null;
- return function() {
- // FF timeout weirdness workaround.
- if (!window.select) return;
- // If the current node is no longer in the document... oh
- // well, we start over.
- if (pos && pos.parentNode != self.container)
- pos = null;
- select.markSelection(self.win);
- var result = self.highlight(pos, time() + passTime, true);
- select.selectMarked();
- var newPos = result ? (result.node && result.node.nextSibling) : null;
- pos = (pos == newPos) ? null : newPos;
- self.delayScanning();
- };
- },
-
- // Starts the continuous scanning process for this document after
- // a given interval.
- delayScanning: function() {
- if (this.scanner) {
- this.parent.clearTimeout(this.documentScan);
- this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
- }
- },
-
- isIMEOn: function() {
- // chrome: keyDown keyCode is 229 while IME on
- // firefox: no keyUps or keyPresses fires after first keyDown while IME on
- return this.lastKeyDownCode == 229 || this.keyUpOrPressAfterLastKeyDown === false;
- },
-
- // The function that does the actual highlighting/colouring (with
- // help from the parser and the DOM normalizer). Its interface is
- // rather overcomplicated, because it is used in different
- // situations: ensuring that a certain line is highlighted, or
- // highlighting up to X milliseconds starting from a certain
- // point. The 'from' argument gives the node at which it should
- // start. If this is null, it will start at the beginning of the
- // document. When a timestamp is given with the 'target' argument,
- // it will stop highlighting at that time. If this argument holds
- // a DOM node, it will highlight until it reaches that node. If at
- // any time it comes across two 'clean' lines (no dirty nodes), it
- // will stop, except when 'cleanLines' is true. maxBacktrack is
- // the maximum number of lines to backtrack to find an existing
- // parser instance. This is used to give up in situations where a
- // highlight would take too long and freeze the browser interface.
- highlight: function(from, target, cleanLines, maxBacktrack){
- var container = this.container, self = this, active = this.options.activeTokens;
- var endTime = (typeof target == "number" ? target : null);
-
- if (!container.firstChild || this.isIMEOn())
- return false;
- // Backtrack to the first node before from that has a partial
- // parse stored.
- while (from && (!from.parserFromHere || from.dirty)) {
- if (maxBacktrack != null && isBR(from) && (--maxBacktrack) < 0)
- return false;
- from = from.previousSibling;
- }
- // If we are at the end of the document, do nothing.
- if (from && !from.nextSibling)
- return false;
-
- // Check whether a part (<span> node) and the corresponding token
- // match.
- function correctPart(token, part){
- return !part.reduced && part.currentText == token.value && part.className == token.style;
- }
- // Shorten the text associated with a part by chopping off
- // characters from the front. Note that only the currentText
- // property gets changed. For efficiency reasons, we leave the
- // nodeValue alone -- we set the reduced flag to indicate that
- // this part must be replaced.
- function shortenPart(part, minus){
- part.currentText = part.currentText.substring(minus);
- part.reduced = true;
- }
- // Create a part corresponding to a given token.
- function tokenPart(token){
- var part = makePartSpan(token.value, self.doc);
- part.className = token.style;
- return part;
- }
-
- function maybeTouch(node) {
- if (node) {
- var old = node.oldNextSibling;
- if (lineDirty || old === undefined || node.nextSibling != old)
- self.history.touch(node);
- node.oldNextSibling = node.nextSibling;
- }
- else {
- var old = self.container.oldFirstChild;
- if (lineDirty || old === undefined || self.container.firstChild != old)
- self.history.touch(null);
- self.container.oldFirstChild = self.container.firstChild;
- }
- }
-
- // Get the token stream. If from is null, we start with a new
- // parser from the start of the frame, otherwise a partial parse
- // is resumed.
- var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
- stream = stringStream(traversal),
- parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
-
- function surroundedByBRs(node) {
- return (node.previousSibling == null || isBR(node.previousSibling)) &&
- (node.nextSibling == null || isBR(node.nextSibling));
- }
-
- // parts is an interface to make it possible to 'delay' fetching
- // the next DOM node until we are completely done with the one
- // before it. This is necessary because often the next node is
- // not yet available when we want to proceed past the current
- // one.
- var parts = {
- current: null,
- // Fetch current node.
- get: function(){
- if (!this.current)
- this.current = traversal.nodes.shift();
- return this.current;
- },
- // Advance to the next part (do not fetch it yet).
- next: function(){
- this.current = null;
- },
- // Remove the current part from the DOM tree, and move to the
- // next.
- remove: function(){
- container.removeChild(this.get());
- this.current = null;
- },
- // Advance to the next part that is not empty, discarding empty
- // parts.
- getNonEmpty: function(){
- var part = this.get();
- // Allow empty nodes when they are alone on a line, needed
- // for the FF cursor bug workaround (see select.js,
- // insertNewlineAtCursor).
- while (part && isSpan(part) && part.currentText == "") {
- // Leave empty nodes that are alone on a line alone in
- // Opera, since that browsers doesn't deal well with
- // having 2 BRs in a row.
- if (window.opera && surroundedByBRs(part)) {
- this.next();
- part = this.get();
- }
- else {
- var old = part;
- this.remove();
- part = this.get();
- // Adjust selection information, if any. See select.js for details.
- select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
- }
- }
-
- return part;
- }
- };
-
- var lineDirty = false, prevLineDirty = true, lineNodes = 0;
-
- // This forEach loops over the tokens from the parsed stream, and
- // at the same time uses the parts object to proceed through the
- // corresponding DOM nodes.
- forEach(parsed, function(token){
- var part = parts.getNonEmpty();
-
- if (token.value == "\n"){
- // The idea of the two streams actually staying synchronized
- // is such a long shot that we explicitly check.
- if (!isBR(part))
- throw "Parser out of sync. Expected BR.";
-
- if (part.dirty || !part.indentation) lineDirty = true;
- maybeTouch(from);
- from = part;
-
- // Every <br> gets a copy of the parser state and a lexical
- // context assigned to it. The first is used to be able to
- // later resume parsing from this point, the second is used
- // for indentation.
- part.parserFromHere = parsed.copy();
- part.indentation = token.indentation;
- part.dirty = false;
-
- // If the target argument wasn't an integer, go at least
- // until that node.
- if (endTime == null && part == target) throw StopIteration;
-
- // A clean line with more than one node means we are done.
- // Throwing a StopIteration is the way to break out of a
- // MochiKit forEach loop.
- if ((endTime != null && time() >= endTime) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines))
- throw StopIteration;
- prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
- parts.next();
- }
- else {
- if (!isSpan(part))
- throw "Parser out of sync. Expected SPAN.";
- if (part.dirty)
- lineDirty = true;
- lineNodes++;
-
- // If the part matches the token, we can leave it alone.
- if (correctPart(token, part)){
- part.dirty = false;
- parts.next();
- }
- // Otherwise, we have to fix it.
- else {
- lineDirty = true;
- // Insert the correct part.
- var newPart = tokenPart(token);
- container.insertBefore(newPart, part);
- if (active) active(newPart, token, self);
- var tokensize = token.value.length;
- var offset = 0;
- // Eat up parts until the text for this token has been
- // removed, adjusting the stored selection info (see
- // select.js) in the process.
- while (tokensize > 0) {
- part = parts.get();
- var partsize = part.currentText.length;
- select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
- if (partsize > tokensize){
- shortenPart(part, tokensize);
- tokensize = 0;
- }
- else {
- tokensize -= partsize;
- offset += partsize;
- parts.remove();
- }
- }
- }
- }
- });
- maybeTouch(from);
-
- // The function returns some status information that is used by
- // hightlightDirty to determine whether and where it has to
- // continue.
- return {node: parts.getNonEmpty(),
- dirty: lineDirty};
- }
- };
-
- return Editor;
-})();
-
-addEventHandler(window, "load", function() {
- var CodeMirror = window.frameElement.CodeMirror;
- var e = CodeMirror.editor = new Editor(CodeMirror.options);
- this.parent.setTimeout(method(CodeMirror, "init"), 0);
-});
+++ /dev/null
-/* This file defines an XML parser, with a few kludges to make it
- * useable for HTML. autoSelfClosers defines a set of tag names that
- * are expected to not have a closing tag, and doNotIndent specifies
- * the tags inside of which no indentation should happen (see Config
- * object). These can be disabled by passing the editor an object like
- * {useHTMLKludges: false} as parserConfig option.
- */
-
-var XMLParser = Editor.Parser = (function() {
- var Kludges = {
- autoSelfClosers: {"br": true, "img": true, "hr": true, "link": true, "input": true,
- "meta": true, "col": true, "frame": true, "base": true, "area": true},
- doNotIndent: {"pre": true, "!cdata": true}
- };
- var NoKludges = {autoSelfClosers: {}, doNotIndent: {"!cdata": true}};
- var UseKludges = Kludges;
- var alignCDATA = false;
-
- // Simple stateful tokenizer for XML documents. Returns a
- // MochiKit-style iterator, with a state property that contains a
- // function encapsulating the current state. See tokenize.js.
- var tokenizeXML = (function() {
- function inText(source, setState) {
- var ch = source.next();
- if (ch == "<") {
- if (source.equals("!")) {
- source.next();
- if (source.equals("[")) {
- if (source.lookAhead("[CDATA[", true)) {
- setState(inBlock("xml-cdata", "]]>"));
- return null;
- }
- else {
- return "xml-text";
- }
- }
- else if (source.lookAhead("--", true)) {
- setState(inBlock("xml-comment", "-->"));
- return null;
- }
- else {
- return "xml-text";
- }
- }
- else if (source.equals("?")) {
- source.next();
- source.nextWhileMatches(/[\w\._\-]/);
- setState(inBlock("xml-processing", "?>"));
- return "xml-processing";
- }
- else {
- if (source.equals("/")) source.next();
- setState(inTag);
- return "xml-punctuation";
- }
- }
- else if (ch == "&") {
- while (!source.endOfLine()) {
- if (source.next() == ";")
- break;
- }
- return "xml-entity";
- }
- else {
- source.nextWhileMatches(/[^&<\n]/);
- return "xml-text";
- }
- }
-
- function inTag(source, setState) {
- var ch = source.next();
- if (ch == ">") {
- setState(inText);
- return "xml-punctuation";
- }
- else if (/[?\/]/.test(ch) && source.equals(">")) {
- source.next();
- setState(inText);
- return "xml-punctuation";
- }
- else if (ch == "=") {
- return "xml-punctuation";
- }
- else if (/[\'\"]/.test(ch)) {
- setState(inAttribute(ch));
- return null;
- }
- else {
- source.nextWhileMatches(/[^\s\u00a0=<>\"\'\/?]/);
- return "xml-name";
- }
- }
-
- function inAttribute(quote) {
- return function(source, setState) {
- while (!source.endOfLine()) {
- if (source.next() == quote) {
- setState(inTag);
- break;
- }
- }
- return "xml-attribute";
- };
- }
-
- function inBlock(style, terminator) {
- return function(source, setState) {
- while (!source.endOfLine()) {
- if (source.lookAhead(terminator, true)) {
- setState(inText);
- break;
- }
- source.next();
- }
- return style;
- };
- }
-
- return function(source, startState) {
- return tokenizer(source, startState || inText);
- };
- })();
-
- // The parser. The structure of this function largely follows that of
- // parseJavaScript in parsejavascript.js (there is actually a bit more
- // shared code than I'd like), but it is quite a bit simpler.
- function parseXML(source) {
- var tokens = tokenizeXML(source), token;
- var cc = [base];
- var tokenNr = 0, indented = 0;
- var currentTag = null, context = null;
- var consume;
-
- function push(fs) {
- for (var i = fs.length - 1; i >= 0; i--)
- cc.push(fs[i]);
- }
- function cont() {
- push(arguments);
- consume = true;
- }
- function pass() {
- push(arguments);
- consume = false;
- }
-
- function markErr() {
- token.style += " xml-error";
- }
- function expect(text) {
- return function(style, content) {
- if (content == text) cont();
- else {markErr(); cont(arguments.callee);}
- };
- }
-
- function pushContext(tagname, startOfLine) {
- var noIndent = UseKludges.doNotIndent.hasOwnProperty(tagname) || (context && context.noIndent);
- context = {prev: context, name: tagname, indent: indented, startOfLine: startOfLine, noIndent: noIndent};
- }
- function popContext() {
- context = context.prev;
- }
- function computeIndentation(baseContext) {
- return function(nextChars, current) {
- var context = baseContext;
- if (context && context.noIndent)
- return current;
- if (alignCDATA && /<!\[CDATA\[/.test(nextChars))
- return 0;
- if (context && /^<\//.test(nextChars))
- context = context.prev;
- while (context && !context.startOfLine)
- context = context.prev;
- if (context)
- return context.indent + indentUnit;
- else
- return 0;
- };
- }
-
- function base() {
- return pass(element, base);
- }
- var harmlessTokens = {"xml-text": true, "xml-entity": true, "xml-comment": true, "xml-processing": true};
- function element(style, content) {
- if (content == "<") cont(tagname, attributes, endtag(tokenNr == 1));
- else if (content == "</") cont(closetagname, expect(">"));
- else if (style == "xml-cdata") {
- if (!context || context.name != "!cdata") pushContext("!cdata");
- if (/\]\]>$/.test(content)) popContext();
- cont();
- }
- else if (harmlessTokens.hasOwnProperty(style)) cont();
- else {markErr(); cont();}
- }
- function tagname(style, content) {
- if (style == "xml-name") {
- currentTag = content.toLowerCase();
- token.style = "xml-tagname";
- cont();
- }
- else {
- currentTag = null;
- pass();
- }
- }
- function closetagname(style, content) {
- if (style == "xml-name") {
- token.style = "xml-tagname";
- if (context && content.toLowerCase() == context.name) popContext();
- else markErr();
- }
- cont();
- }
- function endtag(startOfLine) {
- return function(style, content) {
- if (content == "/>" || (content == ">" && UseKludges.autoSelfClosers.hasOwnProperty(currentTag))) cont();
- else if (content == ">") {pushContext(currentTag, startOfLine); cont();}
- else {markErr(); cont(arguments.callee);}
- };
- }
- function attributes(style) {
- if (style == "xml-name") {token.style = "xml-attname"; cont(attribute, attributes);}
- else pass();
- }
- function attribute(style, content) {
- if (content == "=") cont(value);
- else if (content == ">" || content == "/>") pass(endtag);
- else pass();
- }
- function value(style) {
- if (style == "xml-attribute") cont(value);
- else pass();
- }
-
- return {
- indentation: function() {return indented;},
-
- next: function(){
- token = tokens.next();
- if (token.style == "whitespace" && tokenNr == 0)
- indented = token.value.length;
- else
- tokenNr++;
- if (token.content == "\n") {
- indented = tokenNr = 0;
- token.indentation = computeIndentation(context);
- }
-
- if (token.style == "whitespace" || token.type == "xml-comment")
- return token;
-
- while(true){
- consume = false;
- cc.pop()(token.style, token.content);
- if (consume) return token;
- }
- },
-
- copy: function(){
- var _cc = cc.concat([]), _tokenState = tokens.state, _context = context;
- var parser = this;
-
- return function(input){
- cc = _cc.concat([]);
- tokenNr = indented = 0;
- context = _context;
- tokens = tokenizeXML(input, _tokenState);
- return parser;
- };
- }
- };
- }
-
- return {
- make: parseXML,
- electricChars: "/",
- configure: function(config) {
- if (config.useHTMLKludges != null)
- UseKludges = config.useHTMLKludges ? Kludges : NoKludges;
- if (config.alignCDATA)
- alignCDATA = config.alignCDATA;
- }
- };
-})();
+++ /dev/null
-/* Functionality for finding, storing, and restoring selections
- *
- * This does not provide a generic API, just the minimal functionality
- * required by the CodeMirror system.
- */
-
-// Namespace object.
-var select = {};
-
-(function() {
- select.ie_selection = document.selection && document.selection.createRangeCollection;
-
- // Find the 'top-level' (defined as 'a direct child of the node
- // passed as the top argument') node that the given node is
- // contained in. Return null if the given node is not inside the top
- // node.
- function topLevelNodeAt(node, top) {
- while (node && node.parentNode != top)
- node = node.parentNode;
- return node;
- }
-
- // Find the top-level node that contains the node before this one.
- function topLevelNodeBefore(node, top) {
- while (!node.previousSibling && node.parentNode != top)
- node = node.parentNode;
- return topLevelNodeAt(node.previousSibling, top);
- }
-
- var fourSpaces = "\u00a0\u00a0\u00a0\u00a0";
-
- select.scrollToNode = function(node, cursor) {
- if (!node) return;
- var element = node,
- doc = element.ownerDocument, body = doc.body,
- win = (doc.defaultView || doc.parentWindow),
- html = doc.documentElement,
- atEnd = !element.nextSibling || !element.nextSibling.nextSibling
- || !element.nextSibling.nextSibling.nextSibling;
- // In Opera (and recent Webkit versions), BR elements *always*
- // have a offsetTop property of zero.
- var compensateHack = 0;
- while (element && !element.offsetTop) {
- compensateHack++;
- element = element.previousSibling;
- }
- // atEnd is another kludge for these browsers -- if the cursor is
- // at the end of the document, and the node doesn't have an
- // offset, just scroll to the end.
- if (compensateHack == 0) atEnd = false;
-
- // WebKit has a bad habit of (sometimes) happily returning bogus
- // offsets when the document has just been changed. This seems to
- // always be 5/5, so we don't use those.
- if (webkit && element && element.offsetTop == 5 && element.offsetLeft == 5)
- return;
-
- var y = compensateHack * (element ? element.offsetHeight : 0), x = 0,
- width = (node ? node.offsetWidth : 0), pos = element;
- while (pos && pos.offsetParent) {
- y += pos.offsetTop;
- // Don't count X offset for <br> nodes
- if (!isBR(pos))
- x += pos.offsetLeft;
- pos = pos.offsetParent;
- }
-
- var scroll_x = body.scrollLeft || html.scrollLeft || 0,
- scroll_y = body.scrollTop || html.scrollTop || 0,
- scroll = false, screen_width = win.innerWidth || html.clientWidth || 0;
-
- if (cursor || width < screen_width) {
- if (cursor) {
- var off = select.offsetInNode(win, node), size = nodeText(node).length;
- if (size) x += width * (off / size);
- }
- var screen_x = x - scroll_x;
- if (screen_x < 0 || screen_x > screen_width) {
- scroll_x = x;
- scroll = true;
- }
- }
- var screen_y = y - scroll_y;
- if (screen_y < 0 || atEnd || screen_y > (win.innerHeight || html.clientHeight || 0) - 50) {
- scroll_y = atEnd ? 1e6 : y;
- scroll = true;
- }
- if (scroll) win.scrollTo(scroll_x, scroll_y);
- };
-
- select.scrollToCursor = function(container) {
- select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild, true);
- };
-
- // Used to prevent restoring a selection when we do not need to.
- var currentSelection = null;
-
- select.snapshotChanged = function() {
- if (currentSelection) currentSelection.changed = true;
- };
-
- // This is called by the code in editor.js whenever it is replacing
- // a text node. The function sees whether the given oldNode is part
- // of the current selection, and updates this selection if it is.
- // Because nodes are often only partially replaced, the length of
- // the part that gets replaced has to be taken into account -- the
- // selection might stay in the oldNode if the newNode is smaller
- // than the selection's offset. The offset argument is needed in
- // case the selection does move to the new object, and the given
- // length is not the whole length of the new node (part of it might
- // have been used to replace another node).
- select.snapshotReplaceNode = function(from, to, length, offset) {
- if (!currentSelection) return;
-
- function replace(point) {
- if (from == point.node) {
- currentSelection.changed = true;
- if (length && point.offset > length) {
- point.offset -= length;
- }
- else {
- point.node = to;
- point.offset += (offset || 0);
- }
- }
- }
- replace(currentSelection.start);
- replace(currentSelection.end);
- };
-
- select.snapshotMove = function(from, to, distance, relative, ifAtStart) {
- if (!currentSelection) return;
-
- function move(point) {
- if (from == point.node && (!ifAtStart || point.offset == 0)) {
- currentSelection.changed = true;
- point.node = to;
- if (relative) point.offset = Math.max(0, point.offset + distance);
- else point.offset = distance;
- }
- }
- move(currentSelection.start);
- move(currentSelection.end);
- };
-
- // Most functions are defined in two ways, one for the IE selection
- // model, one for the W3C one.
- if (select.ie_selection) {
- function selectionNode(win, start) {
- var range = win.document.selection.createRange();
- range.collapse(start);
-
- function nodeAfter(node) {
- var found = null;
- while (!found && node) {
- found = node.nextSibling;
- node = node.parentNode;
- }
- return nodeAtStartOf(found);
- }
-
- function nodeAtStartOf(node) {
- while (node && node.firstChild) node = node.firstChild;
- return {node: node, offset: 0};
- }
-
- var containing = range.parentElement();
- if (!isAncestor(win.document.body, containing)) return null;
- if (!containing.firstChild) return nodeAtStartOf(containing);
-
- var working = range.duplicate();
- working.moveToElementText(containing);
- working.collapse(true);
- for (var cur = containing.firstChild; cur; cur = cur.nextSibling) {
- if (cur.nodeType == 3) {
- var size = cur.nodeValue.length;
- working.move("character", size);
- }
- else {
- working.moveToElementText(cur);
- working.collapse(false);
- }
-
- var dir = range.compareEndPoints("StartToStart", working);
- if (dir == 0) return nodeAfter(cur);
- if (dir == 1) continue;
- if (cur.nodeType != 3) return nodeAtStartOf(cur);
-
- working.setEndPoint("StartToEnd", range);
- return {node: cur, offset: size - working.text.length};
- }
- return nodeAfter(containing);
- }
-
- select.markSelection = function(win) {
- currentSelection = null;
- var sel = win.document.selection;
- if (!sel) return;
- var start = selectionNode(win, true),
- end = selectionNode(win, false);
- if (!start || !end) return;
- currentSelection = {start: start, end: end, window: win, changed: false};
- };
-
- select.selectMarked = function() {
- if (!currentSelection || !currentSelection.changed) return;
- var win = currentSelection.window, doc = win.document;
-
- function makeRange(point) {
- var range = doc.body.createTextRange(),
- node = point.node;
- if (!node) {
- range.moveToElementText(currentSelection.window.document.body);
- range.collapse(false);
- }
- else if (node.nodeType == 3) {
- range.moveToElementText(node.parentNode);
- var offset = point.offset;
- while (node.previousSibling) {
- node = node.previousSibling;
- offset += (node.innerText || "").length;
- }
- range.move("character", offset);
- }
- else {
- range.moveToElementText(node);
- range.collapse(true);
- }
- return range;
- }
-
- var start = makeRange(currentSelection.start), end = makeRange(currentSelection.end);
- start.setEndPoint("StartToEnd", end);
- start.select();
- };
-
- select.offsetInNode = function(win, node) {
- var sel = win.document.selection;
- if (!sel) return 0;
- var range = sel.createRange(), range2 = range.duplicate();
- try {range2.moveToElementText(node);} catch(e){return 0;}
- range.setEndPoint("StartToStart", range2);
- return range.text.length;
- };
-
- // Get the top-level node that one end of the cursor is inside or
- // after. Note that this returns false for 'no cursor', and null
- // for 'start of document'.
- select.selectionTopNode = function(container, start) {
- var selection = container.ownerDocument.selection;
- if (!selection) return false;
-
- var range = selection.createRange(), range2 = range.duplicate();
- range.collapse(start);
- var around = range.parentElement();
- if (around && isAncestor(container, around)) {
- // Only use this node if the selection is not at its start.
- range2.moveToElementText(around);
- if (range.compareEndPoints("StartToStart", range2) == 1)
- return topLevelNodeAt(around, container);
- }
-
- // Move the start of a range to the start of a node,
- // compensating for the fact that you can't call
- // moveToElementText with text nodes.
- function moveToNodeStart(range, node) {
- if (node.nodeType == 3) {
- var count = 0, cur = node.previousSibling;
- while (cur && cur.nodeType == 3) {
- count += cur.nodeValue.length;
- cur = cur.previousSibling;
- }
- if (cur) {
- try{range.moveToElementText(cur);}
- catch(e){return false;}
- range.collapse(false);
- }
- else range.moveToElementText(node.parentNode);
- if (count) range.move("character", count);
- }
- else {
- try{range.moveToElementText(node);}
- catch(e){return false;}
- }
- return true;
- }
-
- // Do a binary search through the container object, comparing
- // the start of each node to the selection
- var start = 0, end = container.childNodes.length - 1;
- while (start < end) {
- var middle = Math.ceil((end + start) / 2), node = container.childNodes[middle];
- if (!node) return false; // Don't ask. IE6 manages this sometimes.
- if (!moveToNodeStart(range2, node)) return false;
- if (range.compareEndPoints("StartToStart", range2) == 1)
- start = middle;
- else
- end = middle - 1;
- }
- return container.childNodes[start] || null;
- };
-
- // Place the cursor after this.start. This is only useful when
- // manually moving the cursor instead of restoring it to its old
- // position.
- select.focusAfterNode = function(node, container) {
- var range = container.ownerDocument.body.createTextRange();
- range.moveToElementText(node || container);
- range.collapse(!node);
- range.select();
- };
-
- select.somethingSelected = function(win) {
- var sel = win.document.selection;
- return sel && (sel.createRange().text != "");
- };
-
- function insertAtCursor(window, html) {
- var selection = window.document.selection;
- if (selection) {
- var range = selection.createRange();
- range.pasteHTML(html);
- range.collapse(false);
- range.select();
- }
- }
-
- // Used to normalize the effect of the enter key, since browsers
- // do widely different things when pressing enter in designMode.
- select.insertNewlineAtCursor = function(window) {
- insertAtCursor(window, "<br>");
- };
-
- select.insertTabAtCursor = function(window) {
- insertAtCursor(window, fourSpaces);
- };
-
- // Get the BR node at the start of the line on which the cursor
- // currently is, and the offset into the line. Returns null as
- // node if cursor is on first line.
- select.cursorPos = function(container, start) {
- var selection = container.ownerDocument.selection;
- if (!selection) return null;
-
- var topNode = select.selectionTopNode(container, start);
- while (topNode && !isBR(topNode))
- topNode = topNode.previousSibling;
-
- var range = selection.createRange(), range2 = range.duplicate();
- range.collapse(start);
- if (topNode) {
- range2.moveToElementText(topNode);
- range2.collapse(false);
- }
- else {
- // When nothing is selected, we can get all kinds of funky errors here.
- try { range2.moveToElementText(container); }
- catch (e) { return null; }
- range2.collapse(true);
- }
- range.setEndPoint("StartToStart", range2);
-
- return {node: topNode, offset: range.text.length};
- };
-
- select.setCursorPos = function(container, from, to) {
- function rangeAt(pos) {
- var range = container.ownerDocument.body.createTextRange();
- if (!pos.node) {
- range.moveToElementText(container);
- range.collapse(true);
- }
- else {
- range.moveToElementText(pos.node);
- range.collapse(false);
- }
- range.move("character", pos.offset);
- return range;
- }
-
- var range = rangeAt(from);
- if (to && to != from)
- range.setEndPoint("EndToEnd", rangeAt(to));
- range.select();
- }
-
- // Some hacks for storing and re-storing the selection when the editor loses and regains focus.
- select.getBookmark = function (container) {
- var from = select.cursorPos(container, true), to = select.cursorPos(container, false);
- if (from && to) return {from: from, to: to};
- };
-
- // Restore a stored selection.
- select.setBookmark = function(container, mark) {
- if (!mark) return;
- select.setCursorPos(container, mark.from, mark.to);
- };
- }
- // W3C model
- else {
- // Find the node right at the cursor, not one of its
- // ancestors with a suitable offset. This goes down the DOM tree
- // until a 'leaf' is reached (or is it *up* the DOM tree?).
- function innerNode(node, offset) {
- while (node.nodeType != 3 && !isBR(node)) {
- var newNode = node.childNodes[offset] || node.nextSibling;
- offset = 0;
- while (!newNode && node.parentNode) {
- node = node.parentNode;
- newNode = node.nextSibling;
- }
- node = newNode;
- if (!newNode) break;
- }
- return {node: node, offset: offset};
- }
-
- // Store start and end nodes, and offsets within these, and refer
- // back to the selection object from those nodes, so that this
- // object can be updated when the nodes are replaced before the
- // selection is restored.
- select.markSelection = function (win) {
- var selection = win.getSelection();
- if (!selection || selection.rangeCount == 0)
- return (currentSelection = null);
- var range = selection.getRangeAt(0);
-
- currentSelection = {
- start: innerNode(range.startContainer, range.startOffset),
- end: innerNode(range.endContainer, range.endOffset),
- window: win,
- changed: false
- };
- };
-
- select.selectMarked = function () {
- var cs = currentSelection;
- // on webkit-based browsers, it is apparently possible that the
- // selection gets reset even when a node that is not one of the
- // endpoints get messed with. the most common situation where
- // this occurs is when a selection is deleted or overwitten. we
- // check for that here.
- function focusIssue() {
- if (cs.start.node == cs.end.node && cs.start.offset == cs.end.offset) {
- var selection = cs.window.getSelection();
- if (!selection || selection.rangeCount == 0) return true;
- var range = selection.getRangeAt(0), point = innerNode(range.startContainer, range.startOffset);
- return cs.start.node != point.node || cs.start.offset != point.offset;
- }
- }
- if (!cs || !(cs.changed || (webkit && focusIssue()))) return;
- var win = cs.window, range = win.document.createRange();
-
- function setPoint(point, which) {
- if (point.node) {
- // Some magic to generalize the setting of the start and end
- // of a range.
- if (point.offset == 0)
- range["set" + which + "Before"](point.node);
- else
- range["set" + which](point.node, point.offset);
- }
- else {
- range.setStartAfter(win.document.body.lastChild || win.document.body);
- }
- }
-
- setPoint(cs.end, "End");
- setPoint(cs.start, "Start");
- selectRange(range, win);
- };
-
- // Helper for selecting a range object.
- function selectRange(range, window) {
- var selection = window.getSelection();
- if (!selection) return;
- selection.removeAllRanges();
- selection.addRange(range);
- }
- function selectionRange(window) {
- var selection = window.getSelection();
- if (!selection || selection.rangeCount == 0)
- return false;
- else
- return selection.getRangeAt(0);
- }
-
- // Finding the top-level node at the cursor in the W3C is, as you
- // can see, quite an involved process.
- select.selectionTopNode = function(container, start) {
- var range = selectionRange(container.ownerDocument.defaultView);
- if (!range) return false;
-
- var node = start ? range.startContainer : range.endContainer;
- var offset = start ? range.startOffset : range.endOffset;
- // Work around (yet another) bug in Opera's selection model.
- if (window.opera && !start && range.endContainer == container && range.endOffset == range.startOffset + 1 &&
- container.childNodes[range.startOffset] && isBR(container.childNodes[range.startOffset]))
- offset--;
-
- // For text nodes, we look at the node itself if the cursor is
- // inside, or at the node before it if the cursor is at the
- // start.
- if (node.nodeType == 3){
- if (offset > 0)
- return topLevelNodeAt(node, container);
- else
- return topLevelNodeBefore(node, container);
- }
- // Occasionally, browsers will return the HTML node as
- // selection. If the offset is 0, we take the start of the frame
- // ('after null'), otherwise, we take the last node.
- else if (node.nodeName.toUpperCase() == "HTML") {
- return (offset == 1 ? null : container.lastChild);
- }
- // If the given node is our 'container', we just look up the
- // correct node by using the offset.
- else if (node == container) {
- return (offset == 0) ? null : node.childNodes[offset - 1];
- }
- // In any other case, we have a regular node. If the cursor is
- // at the end of the node, we use the node itself, if it is at
- // the start, we use the node before it, and in any other
- // case, we look up the child before the cursor and use that.
- else {
- if (offset == node.childNodes.length)
- return topLevelNodeAt(node, container);
- else if (offset == 0)
- return topLevelNodeBefore(node, container);
- else
- return topLevelNodeAt(node.childNodes[offset - 1], container);
- }
- };
-
- select.focusAfterNode = function(node, container) {
- var win = container.ownerDocument.defaultView,
- range = win.document.createRange();
- range.setStartBefore(container.firstChild || container);
- // In Opera, setting the end of a range at the end of a line
- // (before a BR) will cause the cursor to appear on the next
- // line, so we set the end inside of the start node when
- // possible.
- if (node && !node.firstChild)
- range.setEndAfter(node);
- else if (node)
- range.setEnd(node, node.childNodes.length);
- else
- range.setEndBefore(container.firstChild || container);
- range.collapse(false);
- selectRange(range, win);
- };
-
- select.somethingSelected = function(win) {
- var range = selectionRange(win);
- return range && !range.collapsed;
- };
-
- select.offsetInNode = function(win, node) {
- var range = selectionRange(win);
- if (!range) return 0;
- range = range.cloneRange();
- range.setStartBefore(node);
- return range.toString().length;
- };
-
- function insertNodeAtCursor(window, node) {
- var range = selectionRange(window);
- if (!range) return;
-
- range.deleteContents();
- range.insertNode(node);
-
- // work around weirdness where Opera will magically insert a new
- // BR node when a BR node inside a span is moved around. makes
- // sure the BR ends up outside of spans.
- if (window.opera && isBR(node) && isSpan(node.parentNode)) {
- var next = node.nextSibling, p = node.parentNode, outer = p.parentNode;
- outer.insertBefore(node, p.nextSibling);
- var textAfter = "";
- for (; next && next.nodeType == 3; next = next.nextSibling) {
- textAfter += next.nodeValue;
- removeElement(next);
- }
- outer.insertBefore(makePartSpan(textAfter, window.document), node.nextSibling);
- }
- range = window.document.createRange();
- range.selectNode(node);
- range.collapse(false);
- selectRange(range, window);
- }
-
- select.insertNewlineAtCursor = function(window) {
- if (webkit)
- document.execCommand('insertLineBreak');
- else
- insertNodeAtCursor(window, window.document.createElement("BR"));
- };
-
- select.insertTabAtCursor = function(window) {
- insertNodeAtCursor(window, window.document.createTextNode(fourSpaces));
- };
-
- select.cursorPos = function(container, start) {
- var range = selectionRange(window);
- if (!range) return;
-
- var topNode = select.selectionTopNode(container, start);
- while (topNode && !isBR(topNode))
- topNode = topNode.previousSibling;
-
- range = range.cloneRange();
- range.collapse(start);
- if (topNode)
- range.setStartAfter(topNode);
- else
- range.setStartBefore(container);
-
- return {node: topNode, offset: range.toString().length};
- };
-
- select.setCursorPos = function(container, from, to) {
- var win = container.ownerDocument.defaultView,
- range = win.document.createRange();
-
- function setPoint(node, offset, side) {
- if (offset == 0 && node && !node.nextSibling) {
- range["set" + side + "After"](node);
- return true;
- }
-
- if (!node)
- node = container.firstChild;
- else
- node = node.nextSibling;
-
- if (!node) return;
-
- if (offset == 0) {
- range["set" + side + "Before"](node);
- return true;
- }
-
- var backlog = []
- function decompose(node) {
- if (node.nodeType == 3)
- backlog.push(node);
- else
- forEach(node.childNodes, decompose);
- }
- while (true) {
- while (node && !backlog.length) {
- decompose(node);
- node = node.nextSibling;
- }
- var cur = backlog.shift();
- if (!cur) return false;
-
- var length = cur.nodeValue.length;
- if (length >= offset) {
- range["set" + side](cur, offset);
- return true;
- }
- offset -= length;
- }
- }
-
- to = to || from;
- if (setPoint(to.node, to.offset, "End") && setPoint(from.node, from.offset, "Start"))
- selectRange(range, win);
- };
- }
-})();
+++ /dev/null
-/* String streams are the things fed to parsers (which can feed them
- * to a tokenizer if they want). They provide peek and next methods
- * for looking at the current character (next 'consumes' this
- * character, peek does not), and a get method for retrieving all the
- * text that was consumed since the last time get was called.
- *
- * An easy mistake to make is to let a StopIteration exception finish
- * the token stream while there are still characters pending in the
- * string stream (hitting the end of the buffer while parsing a
- * token). To make it easier to detect such errors, the stringstreams
- * throw an exception when this happens.
- */
-
-// Make a stringstream stream out of an iterator that returns strings.
-// This is applied to the result of traverseDOM (see codemirror.js),
-// and the resulting stream is fed to the parser.
-var stringStream = function(source){
- // String that's currently being iterated over.
- var current = "";
- // Position in that string.
- var pos = 0;
- // Accumulator for strings that have been iterated over but not
- // get()-ed yet.
- var accum = "";
- // Make sure there are more characters ready, or throw
- // StopIteration.
- function ensureChars() {
- while (pos == current.length) {
- accum += current;
- current = ""; // In case source.next() throws
- pos = 0;
- try {current = source.next();}
- catch (e) {
- if (e != StopIteration) throw e;
- else return false;
- }
- }
- return true;
- }
-
- return {
- // Return the next character in the stream.
- peek: function() {
- if (!ensureChars()) return null;
- return current.charAt(pos);
- },
- // Get the next character, throw StopIteration if at end, check
- // for unused content.
- next: function() {
- if (!ensureChars()) {
- if (accum.length > 0)
- throw "End of stringstream reached without emptying buffer ('" + accum + "').";
- else
- throw StopIteration;
- }
- return current.charAt(pos++);
- },
- // Return the characters iterated over since the last call to
- // .get().
- get: function() {
- var temp = accum;
- accum = "";
- if (pos > 0){
- temp += current.slice(0, pos);
- current = current.slice(pos);
- pos = 0;
- }
- return temp;
- },
- // Push a string back into the stream.
- push: function(str) {
- current = current.slice(0, pos) + str + current.slice(pos);
- },
- lookAhead: function(str, consume, skipSpaces, caseInsensitive) {
- function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
- str = cased(str);
- var found = false;
-
- var _accum = accum, _pos = pos;
- if (skipSpaces) this.nextWhileMatches(/[\s\u00a0]/);
-
- while (true) {
- var end = pos + str.length, left = current.length - pos;
- if (end <= current.length) {
- found = str == cased(current.slice(pos, end));
- pos = end;
- break;
- }
- else if (str.slice(0, left) == cased(current.slice(pos))) {
- accum += current; current = "";
- try {current = source.next();}
- catch (e) {break;}
- pos = 0;
- str = str.slice(left);
- }
- else {
- break;
- }
- }
-
- if (!(found && consume)) {
- current = accum.slice(_accum.length) + current;
- pos = _pos;
- accum = _accum;
- }
-
- return found;
- },
-
- // Utils built on top of the above
- more: function() {
- return this.peek() !== null;
- },
- applies: function(test) {
- var next = this.peek();
- return (next !== null && test(next));
- },
- nextWhile: function(test) {
- var next;
- while ((next = this.peek()) !== null && test(next))
- this.next();
- },
- matches: function(re) {
- var next = this.peek();
- return (next !== null && re.test(next));
- },
- nextWhileMatches: function(re) {
- var next;
- while ((next = this.peek()) !== null && re.test(next))
- this.next();
- },
- equals: function(ch) {
- return ch === this.peek();
- },
- endOfLine: function() {
- var next = this.peek();
- return next == null || next == "\n";
- }
- };
-};
+++ /dev/null
-// A framework for simple tokenizers. Takes care of newlines and
-// white-space, and of getting the text from the source stream into
-// the token object. A state is a function of two arguments -- a
-// string stream and a setState function. The second can be used to
-// change the tokenizer's state, and can be ignored for stateless
-// tokenizers. This function should advance the stream over a token
-// and return a string or object containing information about the next
-// token, or null to pass and have the (new) state be called to finish
-// the token. When a string is given, it is wrapped in a {style, type}
-// object. In the resulting object, the characters consumed are stored
-// under the content property. Any whitespace following them is also
-// automatically consumed, and added to the value property. (Thus,
-// content is the actual meaningful part of the token, while value
-// contains all the text it spans.)
-
-function tokenizer(source, state) {
- // Newlines are always a separate token.
- function isWhiteSpace(ch) {
- // The messy regexp is because IE's regexp matcher is of the
- // opinion that non-breaking spaces are no whitespace.
- return ch != "\n" && /^[\s\u00a0]*$/.test(ch);
- }
-
- var tokenizer = {
- state: state,
-
- take: function(type) {
- if (typeof(type) == "string")
- type = {style: type, type: type};
-
- type.content = (type.content || "") + source.get();
- if (!/\n$/.test(type.content))
- source.nextWhile(isWhiteSpace);
- type.value = type.content + source.get();
- return type;
- },
-
- next: function () {
- if (!source.more()) throw StopIteration;
-
- var type;
- if (source.equals("\n")) {
- source.next();
- return this.take("whitespace");
- }
-
- if (source.applies(isWhiteSpace))
- type = "whitespace";
- else
- while (!type)
- type = this.state(source, function(s) {tokenizer.state = s;});
-
- return this.take(type);
- }
- };
- return tokenizer;
-}
+++ /dev/null
-/**
- * Storage and control for undo information within a CodeMirror
- * editor. 'Why on earth is such a complicated mess required for
- * that?', I hear you ask. The goal, in implementing this, was to make
- * the complexity of storing and reverting undo information depend
- * only on the size of the edited or restored content, not on the size
- * of the whole document. This makes it necessary to use a kind of
- * 'diff' system, which, when applied to a DOM tree, causes some
- * complexity and hackery.
- *
- * In short, the editor 'touches' BR elements as it parses them, and
- * the UndoHistory stores these. When nothing is touched in commitDelay
- * milliseconds, the changes are committed: It goes over all touched
- * nodes, throws out the ones that did not change since last commit or
- * are no longer in the document, and assembles the rest into zero or
- * more 'chains' -- arrays of adjacent lines. Links back to these
- * chains are added to the BR nodes, while the chain that previously
- * spanned these nodes is added to the undo history. Undoing a change
- * means taking such a chain off the undo history, restoring its
- * content (text is saved per line) and linking it back into the
- * document.
- */
-
-// A history object needs to know about the DOM container holding the
-// document, the maximum amount of undo levels it should store, the
-// delay (of no input) after which it commits a set of changes, and,
-// unfortunately, the 'parent' window -- a window that is not in
-// designMode, and on which setTimeout works in every browser.
-function UndoHistory(container, maxDepth, commitDelay, editor) {
- this.container = container;
- this.maxDepth = maxDepth; this.commitDelay = commitDelay;
- this.editor = editor; this.parent = editor.parent;
- // This line object represents the initial, empty editor.
- var initial = {text: "", from: null, to: null};
- // As the borders between lines are represented by BR elements, the
- // start of the first line and the end of the last one are
- // represented by null. Since you can not store any properties
- // (links to line objects) in null, these properties are used in
- // those cases.
- this.first = initial; this.last = initial;
- // Similarly, a 'historyTouched' property is added to the BR in
- // front of lines that have already been touched, and 'firstTouched'
- // is used for the first line.
- this.firstTouched = false;
- // History is the set of committed changes, touched is the set of
- // nodes touched since the last commit.
- this.history = []; this.redoHistory = []; this.touched = [];
-}
-
-UndoHistory.prototype = {
- // Schedule a commit (if no other touches come in for commitDelay
- // milliseconds).
- scheduleCommit: function() {
- var self = this;
- this.parent.clearTimeout(this.commitTimeout);
- this.commitTimeout = this.parent.setTimeout(function(){self.tryCommit();}, this.commitDelay);
- },
-
- // Mark a node as touched. Null is a valid argument.
- touch: function(node) {
- this.setTouched(node);
- this.scheduleCommit();
- },
-
- // Undo the last change.
- undo: function() {
- // Make sure pending changes have been committed.
- this.commit();
-
- if (this.history.length) {
- // Take the top diff from the history, apply it, and store its
- // shadow in the redo history.
- var item = this.history.pop();
- this.redoHistory.push(this.updateTo(item, "applyChain"));
- this.notifyEnvironment();
- return this.chainNode(item);
- }
- },
-
- // Redo the last undone change.
- redo: function() {
- this.commit();
- if (this.redoHistory.length) {
- // The inverse of undo, basically.
- var item = this.redoHistory.pop();
- this.addUndoLevel(this.updateTo(item, "applyChain"));
- this.notifyEnvironment();
- return this.chainNode(item);
- }
- },
-
- clear: function() {
- this.history = [];
- this.redoHistory = [];
- },
-
- // Ask for the size of the un/redo histories.
- historySize: function() {
- return {undo: this.history.length, redo: this.redoHistory.length};
- },
-
- // Push a changeset into the document.
- push: function(from, to, lines) {
- var chain = [];
- for (var i = 0; i < lines.length; i++) {
- var end = (i == lines.length - 1) ? to : this.container.ownerDocument.createElement("BR");
- chain.push({from: from, to: end, text: cleanText(lines[i])});
- from = end;
- }
- this.pushChains([chain], from == null && to == null);
- this.notifyEnvironment();
- },
-
- pushChains: function(chains, doNotHighlight) {
- this.commit(doNotHighlight);
- this.addUndoLevel(this.updateTo(chains, "applyChain"));
- this.redoHistory = [];
- },
-
- // Retrieve a DOM node from a chain (for scrolling to it after undo/redo).
- chainNode: function(chains) {
- for (var i = 0; i < chains.length; i++) {
- var start = chains[i][0], node = start && (start.from || start.to);
- if (node) return node;
- }
- },
-
- // Clear the undo history, make the current document the start
- // position.
- reset: function() {
- this.history = []; this.redoHistory = [];
- },
-
- textAfter: function(br) {
- return this.after(br).text;
- },
-
- nodeAfter: function(br) {
- return this.after(br).to;
- },
-
- nodeBefore: function(br) {
- return this.before(br).from;
- },
-
- // Commit unless there are pending dirty nodes.
- tryCommit: function() {
- if (!window.UndoHistory) return; // Stop when frame has been unloaded
- if (this.editor.highlightDirty()) this.commit(true);
- else this.scheduleCommit();
- },
-
- // Check whether the touched nodes hold any changes, if so, commit
- // them.
- commit: function(doNotHighlight) {
- this.parent.clearTimeout(this.commitTimeout);
- // Make sure there are no pending dirty nodes.
- if (!doNotHighlight) this.editor.highlightDirty(true);
- // Build set of chains.
- var chains = this.touchedChains(), self = this;
-
- if (chains.length) {
- this.addUndoLevel(this.updateTo(chains, "linkChain"));
- this.redoHistory = [];
- this.notifyEnvironment();
- }
- },
-
- // [ end of public interface ]
-
- // Update the document with a given set of chains, return its
- // shadow. updateFunc should be "applyChain" or "linkChain". In the
- // second case, the chains are taken to correspond the the current
- // document, and only the state of the line data is updated. In the
- // first case, the content of the chains is also pushed iinto the
- // document.
- updateTo: function(chains, updateFunc) {
- var shadows = [], dirty = [];
- for (var i = 0; i < chains.length; i++) {
- shadows.push(this.shadowChain(chains[i]));
- dirty.push(this[updateFunc](chains[i]));
- }
- if (updateFunc == "applyChain")
- this.notifyDirty(dirty);
- return shadows;
- },
-
- // Notify the editor that some nodes have changed.
- notifyDirty: function(nodes) {
- forEach(nodes, method(this.editor, "addDirtyNode"))
- this.editor.scheduleHighlight();
- },
-
- notifyEnvironment: function() {
- if (this.onChange) this.onChange();
- // Used by the line-wrapping line-numbering code.
- if (window.frameElement && window.frameElement.CodeMirror.updateNumbers)
- window.frameElement.CodeMirror.updateNumbers();
- },
-
- // Link a chain into the DOM nodes (or the first/last links for null
- // nodes).
- linkChain: function(chain) {
- for (var i = 0; i < chain.length; i++) {
- var line = chain[i];
- if (line.from) line.from.historyAfter = line;
- else this.first = line;
- if (line.to) line.to.historyBefore = line;
- else this.last = line;
- }
- },
-
- // Get the line object after/before a given node.
- after: function(node) {
- return node ? node.historyAfter : this.first;
- },
- before: function(node) {
- return node ? node.historyBefore : this.last;
- },
-
- // Mark a node as touched if it has not already been marked.
- setTouched: function(node) {
- if (node) {
- if (!node.historyTouched) {
- this.touched.push(node);
- node.historyTouched = true;
- }
- }
- else {
- this.firstTouched = true;
- }
- },
-
- // Store a new set of undo info, throw away info if there is more of
- // it than allowed.
- addUndoLevel: function(diffs) {
- this.history.push(diffs);
- if (this.history.length > this.maxDepth)
- this.history.shift();
- },
-
- // Build chains from a set of touched nodes.
- touchedChains: function() {
- var self = this;
-
- // The temp system is a crummy hack to speed up determining
- // whether a (currently touched) node has a line object associated
- // with it. nullTemp is used to store the object for the first
- // line, other nodes get it stored in their historyTemp property.
- var nullTemp = null;
- function temp(node) {return node ? node.historyTemp : nullTemp;}
- function setTemp(node, line) {
- if (node) node.historyTemp = line;
- else nullTemp = line;
- }
-
- function buildLine(node) {
- var text = [];
- for (var cur = node ? node.nextSibling : self.container.firstChild;
- cur && !isBR(cur); cur = cur.nextSibling)
- if (cur.currentText) text.push(cur.currentText);
- return {from: node, to: cur, text: cleanText(text.join(""))};
- }
-
- // Filter out unchanged lines and nodes that are no longer in the
- // document. Build up line objects for remaining nodes.
- var lines = [];
- if (self.firstTouched) self.touched.push(null);
- forEach(self.touched, function(node) {
- if (node && node.parentNode != self.container) return;
-
- if (node) node.historyTouched = false;
- else self.firstTouched = false;
-
- var line = buildLine(node), shadow = self.after(node);
- if (!shadow || shadow.text != line.text || shadow.to != line.to) {
- lines.push(line);
- setTemp(node, line);
- }
- });
-
- // Get the BR element after/before the given node.
- function nextBR(node, dir) {
- var link = dir + "Sibling", search = node[link];
- while (search && !isBR(search))
- search = search[link];
- return search;
- }
-
- // Assemble line objects into chains by scanning the DOM tree
- // around them.
- var chains = []; self.touched = [];
- forEach(lines, function(line) {
- // Note that this makes the loop skip line objects that have
- // been pulled into chains by lines before them.
- if (!temp(line.from)) return;
-
- var chain = [], curNode = line.from, safe = true;
- // Put any line objects (referred to by temp info) before this
- // one on the front of the array.
- while (true) {
- var curLine = temp(curNode);
- if (!curLine) {
- if (safe) break;
- else curLine = buildLine(curNode);
- }
- chain.unshift(curLine);
- setTemp(curNode, null);
- if (!curNode) break;
- safe = self.after(curNode);
- curNode = nextBR(curNode, "previous");
- }
- curNode = line.to; safe = self.before(line.from);
- // Add lines after this one at end of array.
- while (true) {
- if (!curNode) break;
- var curLine = temp(curNode);
- if (!curLine) {
- if (safe) break;
- else curLine = buildLine(curNode);
- }
- chain.push(curLine);
- setTemp(curNode, null);
- safe = self.before(curNode);
- curNode = nextBR(curNode, "next");
- }
- chains.push(chain);
- });
-
- return chains;
- },
-
- // Find the 'shadow' of a given chain by following the links in the
- // DOM nodes at its start and end.
- shadowChain: function(chain) {
- var shadows = [], next = this.after(chain[0].from), end = chain[chain.length - 1].to;
- while (true) {
- shadows.push(next);
- var nextNode = next.to;
- if (!nextNode || nextNode == end)
- break;
- else
- next = nextNode.historyAfter || this.before(end);
- // (The this.before(end) is a hack -- FF sometimes removes
- // properties from BR nodes, in which case the best we can hope
- // for is to not break.)
- }
- return shadows;
- },
-
- // Update the DOM tree to contain the lines specified in a given
- // chain, link this chain into the DOM nodes.
- applyChain: function(chain) {
- // Some attempt is made to prevent the cursor from jumping
- // randomly when an undo or redo happens. It still behaves a bit
- // strange sometimes.
- var cursor = select.cursorPos(this.container, false), self = this;
-
- // Remove all nodes in the DOM tree between from and to (null for
- // start/end of container).
- function removeRange(from, to) {
- var pos = from ? from.nextSibling : self.container.firstChild;
- while (pos != to) {
- var temp = pos.nextSibling;
- removeElement(pos);
- pos = temp;
- }
- }
-
- var start = chain[0].from, end = chain[chain.length - 1].to;
- // Clear the space where this change has to be made.
- removeRange(start, end);
-
- // Insert the content specified by the chain into the DOM tree.
- for (var i = 0; i < chain.length; i++) {
- var line = chain[i];
- // The start and end of the space are already correct, but BR
- // tags inside it have to be put back.
- if (i > 0)
- self.container.insertBefore(line.from, end);
-
- // Add the text.
- var node = makePartSpan(fixSpaces(line.text), this.container.ownerDocument);
- self.container.insertBefore(node, end);
- // See if the cursor was on this line. Put it back, adjusting
- // for changed line length, if it was.
- if (cursor && cursor.node == line.from) {
- var cursordiff = 0;
- var prev = this.after(line.from);
- if (prev && i == chain.length - 1) {
- // Only adjust if the cursor is after the unchanged part of
- // the line.
- for (var match = 0; match < cursor.offset &&
- line.text.charAt(match) == prev.text.charAt(match); match++);
- if (cursor.offset > match)
- cursordiff = line.text.length - prev.text.length;
- }
- select.setCursorPos(this.container, {node: line.from, offset: Math.max(0, cursor.offset + cursordiff)});
- }
- // Cursor was in removed line, this is last new line.
- else if (cursor && (i == chain.length - 1) && cursor.node && cursor.node.parentNode != this.container) {
- select.setCursorPos(this.container, {node: line.from, offset: line.text.length});
- }
- }
-
- // Anchor the chain in the DOM tree.
- this.linkChain(chain);
- return start;
- }
-};
+++ /dev/null
-/* A few useful utility functions. */
-
-// Capture a method on an object.
-function method(obj, name) {
- return function() {obj[name].apply(obj, arguments);};
-}
-
-// The value used to signal the end of a sequence in iterators.
-var StopIteration = {toString: function() {return "StopIteration"}};
-
-// Apply a function to each element in a sequence.
-function forEach(iter, f) {
- if (iter.next) {
- try {while (true) f(iter.next());}
- catch (e) {if (e != StopIteration) throw e;}
- }
- else {
- for (var i = 0; i < iter.length; i++)
- f(iter[i]);
- }
-}
-
-// Map a function over a sequence, producing an array of results.
-function map(iter, f) {
- var accum = [];
- forEach(iter, function(val) {accum.push(f(val));});
- return accum;
-}
-
-// Create a predicate function that tests a string againsts a given
-// regular expression. No longer used but might be used by 3rd party
-// parsers.
-function matcher(regexp){
- return function(value){return regexp.test(value);};
-}
-
-// Test whether a DOM node has a certain CSS class. Much faster than
-// the MochiKit equivalent, for some reason.
-function hasClass(element, className){
- var classes = element.className;
- return classes && new RegExp("(^| )" + className + "($| )").test(classes);
-}
-
-// Insert a DOM node after another node.
-function insertAfter(newNode, oldNode) {
- var parent = oldNode.parentNode;
- parent.insertBefore(newNode, oldNode.nextSibling);
- return newNode;
-}
-
-function removeElement(node) {
- if (node.parentNode)
- node.parentNode.removeChild(node);
-}
-
-function clearElement(node) {
- while (node.firstChild)
- node.removeChild(node.firstChild);
-}
-
-// Check whether a node is contained in another one.
-function isAncestor(node, child) {
- while (child = child.parentNode) {
- if (node == child)
- return true;
- }
- return false;
-}
-
-// The non-breaking space character.
-var nbsp = "\u00a0";
-var matching = {"{": "}", "[": "]", "(": ")",
- "}": "{", "]": "[", ")": "("};
-
-// Standardize a few unportable event properties.
-function normalizeEvent(event) {
- if (!event.stopPropagation) {
- event.stopPropagation = function() {this.cancelBubble = true;};
- event.preventDefault = function() {this.returnValue = false;};
- }
- if (!event.stop) {
- event.stop = function() {
- this.stopPropagation();
- this.preventDefault();
- };
- }
-
- if (event.type == "keypress") {
- event.code = (event.charCode == null) ? event.keyCode : event.charCode;
- event.character = String.fromCharCode(event.code);
- }
- return event;
-}
-
-// Portably register event handlers.
-function addEventHandler(node, type, handler, removeFunc) {
- function wrapHandler(event) {
- handler(normalizeEvent(event || window.event));
- }
- if (typeof node.addEventListener == "function") {
- node.addEventListener(type, wrapHandler, false);
- if (removeFunc) return function() {node.removeEventListener(type, wrapHandler, false);};
- }
- else {
- node.attachEvent("on" + type, wrapHandler);
- if (removeFunc) return function() {node.detachEvent("on" + type, wrapHandler);};
- }
-}
-
-function nodeText(node) {
- return node.textContent || node.innerText || node.nodeValue || "";
-}
-
-function nodeTop(node) {
- var top = 0;
- while (node.offsetParent) {
- top += node.offsetTop;
- node = node.offsetParent;
- }
- return top;
-}
-
-function isBR(node) {
- var nn = node.nodeName;
- return nn == "BR" || nn == "br";
-}
-function isSpan(node) {
- var nn = node.nodeName;
- return nn == "SPAN" || nn == "span";
-}
this.codemirror = CodeMirror.fromTextArea('codemirror_placeholder', {
parserfile: 'parsexml.js',
- path: STATIC_URL + "js/lib/codemirror/",
+ path: STATIC_URL + "js/lib/codemirror-0.8/",
stylesheet: STATIC_URL + "css/xmlcolors_20100906.css",
parserConfig: {
useHTMLKludges: false