diff --git a/AUTHORS.md b/AUTHORS.md index a10a34c..37f9464 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -6,3 +6,5 @@ * Alexander https://github.com/sanek (Adding JSON-API) * abma https://github.com/abma (.htaccess improvement) * Chris https://github.com/ch0wnag3 (favicon URL improvement) +* Gabriel Wanzek https://github.com/GabrielWanzek (gabdark & gabdark3 themes) +* Luc https://github.com/ltribolet (Bootstrap theme) diff --git a/CREATING_THEMES.md b/CREATING_THEMES.md new file mode 100644 index 0000000..5fa6183 --- /dev/null +++ b/CREATING_THEMES.md @@ -0,0 +1,14 @@ +How to create your own theme +---------------------------- + +1. Make a copy of the folder htdocs/themes/default, and name it as you like. +2. Start customizing your files! +3. Delete everything that has not been changed. + +For example: If you've only modified the main.css, create a folder named "css" in your theme folder, and put your main.css in there. +The theme engine will load your css, and falls back to files in the default theme that aren't in your theme folder. + +Examples: + +* gabdark - a theme with only a modified main.css +* bootstrap - a full theme with custom html, css, js and images diff --git a/README.md b/README.md index 35ed950..49ab739 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,17 @@ Changelog ### Version 0.8.5: -Todo. Planned: Refactoring of pastes model, unit tests, diff-view, easier spamadmin, documentation, and more +* Themes! Configure a different theme in config/stikked.php - or create your own + +#### Upgrade instructions + +The following line must be present config/stikked.php + +```php +$config['theme'] = 'default'; +``` + +You can choose between default, bootstrap, gabdark and gabdark3. ### Version 0.8.4: diff --git a/htdocs/application/.htaccess b/htdocs/application/.htaccess index 14249c5..3a42882 100644 --- a/htdocs/application/.htaccess +++ b/htdocs/application/.htaccess @@ -1 +1 @@ -Deny from all \ No newline at end of file +Deny from all diff --git a/htdocs/application/config/routes.php b/htdocs/application/config/routes.php index 755c3d6..eafc508 100644 --- a/htdocs/application/config/routes.php +++ b/htdocs/application/config/routes.php @@ -64,5 +64,9 @@ $route['iphone/view/:any'] = 'iphone/view'; $route['404_override'] = 'main/error_404'; +$route['themes/:any/css/:any'] = 'theme_assets/css'; +$route['themes/:any/images/:any'] = 'theme_assets/images'; +$route['themes/:any/js/:any'] = 'theme_assets/js'; + /* End of file routes.php */ /* Location: ./application/config/routes.php */ diff --git a/htdocs/application/config/stikked.php b/htdocs/application/config/stikked.php old mode 100755 new mode 100644 index 78d46e8..082337a --- a/htdocs/application/config/stikked.php +++ b/htdocs/application/config/stikked.php @@ -2,7 +2,7 @@ /** * Site Name - * + * * The name of your site * */ @@ -10,7 +10,7 @@ $config['site_name'] = 'Stikked'; /** * Database connection - * + * * Credentials for your database * The database structure will be created automatically * @@ -21,8 +21,17 @@ $config['db_username'] = 'stikked'; $config['db_password'] = 'stikked'; /** - * Combine JS & CSS files - * + * Theme + * + * Which theme to use + * Folder name in htdocs/themes/ + * +*/ +$config['theme'] = 'default'; + +/** + * Combine JS & CSS files (recommended) + * * htdocs/static/asset/ folder must be writeable * */ diff --git a/htdocs/application/controllers/api.php b/htdocs/application/controllers/api.php old mode 100755 new mode 100644 diff --git a/htdocs/application/controllers/backup.php b/htdocs/application/controllers/backup.php old mode 100755 new mode 100644 diff --git a/htdocs/application/controllers/index.html b/htdocs/application/controllers/index.html old mode 100755 new mode 100644 diff --git a/htdocs/application/controllers/iphone.php b/htdocs/application/controllers/iphone.php old mode 100755 new mode 100644 diff --git a/htdocs/application/controllers/main.php b/htdocs/application/controllers/main.php old mode 100755 new mode 100644 diff --git a/htdocs/application/controllers/spamadmin.php b/htdocs/application/controllers/spamadmin.php old mode 100755 new mode 100644 diff --git a/htdocs/application/controllers/theme_assets.php b/htdocs/application/controllers/theme_assets.php new file mode 100644 index 0000000..5d02a88 --- /dev/null +++ b/htdocs/application/controllers/theme_assets.php @@ -0,0 +1,87 @@ +theme = config_item('theme'); + } + + function css() + { + $css_file = $this->uri->segment(4); + + //file path + $file_path = 'themes/' . $this->theme . '/css/' . $css_file; + + //fallback to default css if view in theme not found + + if (!file_exists($file_path)) + { + $file_path = 'themes/default/css/' . $css_file; + } + + //send + header('Content-type: text/css'); + $this->_expires_header(1); + readfile($file_path); + } + + function images() + { + $image_file = $this->uri->segment(4); + + //file path + $file_path = 'themes/' . $this->theme . '/images/' . $image_file; + + //fallback to default css if view in theme not found + + if (!file_exists($file_path)) + { + $file_path = 'themes/default/images/' . $image_file; + } + + //send + header('Content-type: ' . mime_content_type($file_path)); + $this->_expires_header(30); + readfile($file_path); + } + + function js() + { + + //get js + $segments = $this->uri->segment_array(); + array_shift($segments); + array_shift($segments); + array_shift($segments); + $js_file = implode('/', $segments); + $js_file = str_replace('../', '', $js_file); + + //file path + $file_path = 'themes/' . $this->theme . '/js/' . $js_file; + + //send + header('Content-type: application/x-javascript'); + $this->_expires_header(30); + readfile($file_path); + } + private + function _expires_header($days) + { + header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', time() + 60 * 60 * 24 * $days)); + } +} diff --git a/htdocs/application/controllers/unittest.php b/htdocs/application/controllers/unittest.php old mode 100755 new mode 100644 diff --git a/htdocs/application/core/Input.php b/htdocs/application/core/Input.php old mode 100755 new mode 100644 diff --git a/htdocs/application/core/MY_Loader.php b/htdocs/application/core/MY_Loader.php new file mode 100644 index 0000000..54b77e1 --- /dev/null +++ b/htdocs/application/core/MY_Loader.php @@ -0,0 +1,204 @@ +_ci_load(array( + '_ci_view' => $view_path, + '_ci_vars' => $this->_ci_object_to_array($vars) , + '_ci_return' => $return + )); + } + /** + * Loader + * + * This function is used to load views and files. + * Variables are prefixed with _ci_ to avoid symbol collision with + * variables made available to view files + * + * @param array + * @return void + */ + protected + function _ci_load($_ci_data) + { + + // Set the default data variables + foreach (array( + '_ci_view', + '_ci_vars', + '_ci_path', + '_ci_return' + ) as $_ci_val) + { + $$_ci_val = (!isset($_ci_data[$_ci_val])) ? FALSE : $_ci_data[$_ci_val]; + } + $file_exists = FALSE; + + // Set the path to the requested file + + if ($_ci_path != '') + { + $_ci_x = explode('/', $_ci_path); + $_ci_file = end($_ci_x); + } + else + { + $_ci_ext = pathinfo($_ci_view, PATHINFO_EXTENSION); + $_ci_file = ($_ci_ext == '') ? $_ci_view . '.php' : $_ci_view; + foreach ($this->_ci_view_paths as $view_file => $cascade) + { + + /* *** modification for stikked themes *** + * + * we are by default in the htdocs/application/views folder, which is bad. + * for security reasons, themes folder should be outside the application dir. + * but file_exists() doesn't work with ../../ in filenames :-( + * so, applying the full FrontControllerPATH here, making ../../ superfluous. + * + */ + + if (file_exists(FCPATH . $_ci_file)) + { + $_ci_path = FCPATH . $_ci_file; + $file_exists = TRUE; + break; + } + + if (!$cascade) + { + break; + } + } + } + + if (!$file_exists && !file_exists($_ci_path)) + { + show_error('Unable to load the requested file: ' . $_ci_file); + } + + // This allows anything loaded using $this->load (views, files, etc.) + // to become accessible from within the Controller and Model functions. + + $_ci_CI = & get_instance(); + foreach (get_object_vars($_ci_CI) as $_ci_key => $_ci_var) + { + + if (!isset($this->$_ci_key)) + { + $this->$_ci_key = & $_ci_CI->$_ci_key; + } + } + + /* + * Extract and cache variables + * + * You can either set variables using the dedicated $this->load_vars() + * function or via the second parameter of this function. We'll merge + * the two types and cache them so that views that are embedded within + * other views can have access to these variables. + */ + + if (is_array($_ci_vars)) + { + $this->_ci_cached_vars = array_merge($this->_ci_cached_vars, $_ci_vars); + } + extract($this->_ci_cached_vars); + + /* + * Buffer the output + * + * We buffer the output for two reasons: + * 1. Speed. You get a significant speed boost. + * 2. So that the final rendered template can be + * post-processed by the output class. Why do we + * need post processing? For one thing, in order to + * show the elapsed page load time. Unless we + * can intercept the content right before it's sent to + * the browser and then stop the timer it won't be accurate. + */ + ob_start(); + + // If the PHP installation does not support short tags we'll + // do a little string replacement, changing the short tags + + // to standard PHP echo statements. + + + if ((bool)@ini_get('short_open_tag') === FALSE AND config_item('rewrite_short_tags') == TRUE) + { + echo eval('?>' . preg_replace("/;*\s*\?>/", "; ?>", str_replace('=', ' $this->_ci_ob_level + 1) + { + ob_end_flush(); + } + else + { + $_ci_CI->output->append_output(ob_get_contents()); + @ob_end_clean(); + } + } +} diff --git a/htdocs/application/errors/error_404.php b/htdocs/application/errors/error_404.php old mode 100755 new mode 100644 diff --git a/htdocs/application/errors/error_db.php b/htdocs/application/errors/error_db.php old mode 100755 new mode 100644 index 45225c7..b396cda --- a/htdocs/application/errors/error_db.php +++ b/htdocs/application/errors/error_db.php @@ -1,51 +1,62 @@ - + + +
+' + // Absolutely positioned blinky cursor + ' ' + // Used to force a width + '' + // DIVs containing the selection and the actual code + '
' + + line.getHTML(makeTab) + ''; + // Kludge to make sure the styled element lies behind the selection (by z-index) + if (line.bgClassName) + html = '
' + html + "
' : ""), text); + for (var j = 1; j < line.height; ++j) html.push(""); + if (!marker) normalNode = i; + } + ++i; + }); + gutter.style.display = "none"; + gutterText.innerHTML = html.join(""); + // Make sure scrolling doesn't cause number gutter size to pop + if (normalNode != null && options.lineNumbers) { + var node = gutterText.childNodes[normalNode - showingFrom]; + var minwidth = String(doc.size).length, val = eltText(node.firstChild), pad = ""; + while (val.length + pad.length < minwidth) pad += "\u00a0"; + if (pad) node.insertBefore(document.createTextNode(pad), node.firstChild); + } + gutter.style.display = ""; + var resized = Math.abs((parseInt(lineSpace.style.marginLeft) || 0) - gutter.offsetWidth) > 2; + lineSpace.style.marginLeft = gutter.offsetWidth + "px"; + gutterDirty = false; + return resized; + } + function updateSelection() { + var collapsed = posEq(sel.from, sel.to); + var fromPos = localCoords(sel.from, true); + var toPos = collapsed ? fromPos : localCoords(sel.to, true); + var headPos = sel.inverted ? fromPos : toPos, th = textHeight(); + var wrapOff = eltOffset(wrapper), lineOff = eltOffset(lineDiv); + inputDiv.style.top = Math.max(0, Math.min(scroller.offsetHeight, headPos.y + lineOff.top - wrapOff.top)) + "px"; + inputDiv.style.left = Math.max(0, Math.min(scroller.offsetWidth, headPos.x + lineOff.left - wrapOff.left)) + "px"; + if (collapsed) { + cursor.style.top = headPos.y + "px"; + cursor.style.left = (options.lineWrapping ? Math.min(headPos.x, lineSpace.offsetWidth) : headPos.x) + "px"; + cursor.style.display = ""; + selectionDiv.style.display = "none"; + } else { + var sameLine = fromPos.y == toPos.y, html = ""; + var clientWidth = lineSpace.clientWidth || lineSpace.offsetWidth; + var clientHeight = lineSpace.clientHeight || lineSpace.offsetHeight; + function add(left, top, right, height) { + var rstyle = quirksMode ? "width: " + (!right ? clientWidth : clientWidth - right - left) + "px" + : "right: " + right + "px"; + html += ''; + } + if (sel.from.ch && fromPos.y >= 0) { + var right = sameLine ? clientWidth - toPos.x : 0; + add(fromPos.x, fromPos.y, right, th); + } + var middleStart = Math.max(0, fromPos.y + (sel.from.ch ? th : 0)); + var middleHeight = Math.min(toPos.y, clientHeight) - middleStart; + if (middleHeight > 0.2 * th) + add(0, middleStart, 0, middleHeight); + if ((!sameLine || !sel.from.ch) && toPos.y < clientHeight - .5 * th) + add(0, toPos.y, clientWidth - toPos.x, th); + selectionDiv.innerHTML = html; + cursor.style.display = "none"; + selectionDiv.style.display = ""; + } + } + + function setShift(val) { + if (val) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from); + else shiftSelecting = null; + } + function setSelectionUser(from, to) { + var sh = shiftSelecting && clipPos(shiftSelecting); + if (sh) { + if (posLess(sh, from)) from = sh; + else if (posLess(to, sh)) to = sh; + } + setSelection(from, to); + userSelChange = true; + } + // Update the selection. Last two args are only used by + // updateLines, since they have to be expressed in the line + // numbers before the update. + function setSelection(from, to, oldFrom, oldTo) { + goalColumn = null; + if (oldFrom == null) {oldFrom = sel.from.line; oldTo = sel.to.line;} + if (posEq(sel.from, from) && posEq(sel.to, to)) return; + if (posLess(to, from)) {var tmp = to; to = from; from = tmp;} + + // Skip over hidden lines. + if (from.line != oldFrom) { + var from1 = skipHidden(from, oldFrom, sel.from.ch); + // If there is no non-hidden line left, force visibility on current line + if (!from1) setLineHidden(from.line, false); + else from = from1; + } + if (to.line != oldTo) to = skipHidden(to, oldTo, sel.to.ch); + + if (posEq(from, to)) sel.inverted = false; + else if (posEq(from, sel.to)) sel.inverted = false; + else if (posEq(to, sel.from)) sel.inverted = true; + + if (options.autoClearEmptyLines && posEq(sel.from, sel.to)) { + var head = sel.inverted ? from : to; + if (head.line != sel.from.line && sel.from.line < doc.size) { + var oldLine = getLine(sel.from.line); + if (/^\s+$/.test(oldLine.text)) + setTimeout(operation(function() { + if (oldLine.parent && /^\s+$/.test(oldLine.text)) { + var no = lineNo(oldLine); + replaceRange("", {line: no, ch: 0}, {line: no, ch: oldLine.text.length}); + } + }, 10)); + } + } + + sel.from = from; sel.to = to; + selectionChanged = true; + } + function skipHidden(pos, oldLine, oldCh) { + function getNonHidden(dir) { + var lNo = pos.line + dir, end = dir == 1 ? doc.size : -1; + while (lNo != end) { + var line = getLine(lNo); + if (!line.hidden) { + var ch = pos.ch; + if (toEnd || ch > oldCh || ch > line.text.length) ch = line.text.length; + return {line: lNo, ch: ch}; + } + lNo += dir; + } + } + var line = getLine(pos.line); + var toEnd = pos.ch == line.text.length && pos.ch != oldCh; + if (!line.hidden) return pos; + if (pos.line >= oldLine) return getNonHidden(1) || getNonHidden(-1); + else return getNonHidden(-1) || getNonHidden(1); + } + function setCursor(line, ch, user) { + var pos = clipPos({line: line, ch: ch || 0}); + (user ? setSelectionUser : setSelection)(pos, pos); + } + + function clipLine(n) {return Math.max(0, Math.min(n, doc.size-1));} + function clipPos(pos) { + if (pos.line < 0) return {line: 0, ch: 0}; + if (pos.line >= doc.size) return {line: doc.size-1, ch: getLine(doc.size-1).text.length}; + var ch = pos.ch, linelen = getLine(pos.line).text.length; + if (ch == null || ch > linelen) return {line: pos.line, ch: linelen}; + else if (ch < 0) return {line: pos.line, ch: 0}; + else return pos; + } + + function findPosH(dir, unit) { + var end = sel.inverted ? sel.from : sel.to, line = end.line, ch = end.ch; + var lineObj = getLine(line); + function findNextLine() { + for (var l = line + dir, e = dir < 0 ? -1 : doc.size; l != e; l += dir) { + var lo = getLine(l); + if (!lo.hidden) { line = l; lineObj = lo; return true; } + } + } + function moveOnce(boundToLine) { + if (ch == (dir < 0 ? 0 : lineObj.text.length)) { + if (!boundToLine && findNextLine()) ch = dir < 0 ? lineObj.text.length : 0; + else return false; + } else ch += dir; + return true; + } + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word") { + var sawWord = false; + for (;;) { + if (dir < 0) if (!moveOnce()) break; + if (isWordChar(lineObj.text.charAt(ch))) sawWord = true; + else if (sawWord) {if (dir < 0) {dir = 1; moveOnce();} break;} + if (dir > 0) if (!moveOnce()) break; + } + } + return {line: line, ch: ch}; + } + function moveH(dir, unit) { + var pos = dir < 0 ? sel.from : sel.to; + if (shiftSelecting || posEq(sel.from, sel.to)) pos = findPosH(dir, unit); + setCursor(pos.line, pos.ch, true); + } + function deleteH(dir, unit) { + if (!posEq(sel.from, sel.to)) replaceRange("", sel.from, sel.to); + else if (dir < 0) replaceRange("", findPosH(dir, unit), sel.to); + else replaceRange("", sel.from, findPosH(dir, unit)); + userSelChange = true; + } + var goalColumn = null; + function moveV(dir, unit) { + var dist = 0, pos = localCoords(sel.inverted ? sel.from : sel.to, true); + if (goalColumn != null) pos.x = goalColumn; + if (unit == "page") dist = Math.min(scroller.clientHeight, window.innerHeight || document.documentElement.clientHeight); + else if (unit == "line") dist = textHeight(); + var target = coordsChar(pos.x, pos.y + dist * dir + 2); + if (unit == "page") scrollbar.scrollTop += localCoords(target, true).y - pos.y; + setCursor(target.line, target.ch, true); + goalColumn = pos.x; + } + + function selectWordAt(pos) { + var line = getLine(pos.line).text; + var start = pos.ch, end = pos.ch; + while (start > 0 && isWordChar(line.charAt(start - 1))) --start; + while (end < line.length && isWordChar(line.charAt(end))) ++end; + setSelectionUser({line: pos.line, ch: start}, {line: pos.line, ch: end}); + } + function selectLine(line) { + setSelectionUser({line: line, ch: 0}, clipPos({line: line + 1, ch: 0})); + } + function indentSelected(mode) { + if (posEq(sel.from, sel.to)) return indentLine(sel.from.line, mode); + var e = sel.to.line - (sel.to.ch ? 0 : 1); + for (var i = sel.from.line; i <= e; ++i) indentLine(i, mode); + } + + function indentLine(n, how) { + if (!how) how = "add"; + if (how == "smart") { + if (!mode.indent) how = "prev"; + else var state = getStateBefore(n); + } + + var line = getLine(n), curSpace = line.indentation(options.tabSize), + curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (how == "smart") { + indentation = mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass) how = "prev"; + } + if (how == "prev") { + if (n) indentation = getLine(n-1).indentation(options.tabSize); + else indentation = 0; + } + else if (how == "add") indentation = curSpace + options.indentUnit; + else if (how == "subtract") indentation = curSpace - options.indentUnit; + indentation = Math.max(0, indentation); + var diff = indentation - curSpace; + + if (!diff) { + if (sel.from.line != n && sel.to.line != n) return; + var indentString = curSpaceString; + } else { + var indentString = "", pos = 0; + if (options.indentWithTabs) + for (var i = Math.floor(indentation / options.tabSize); i; --i) {pos += options.tabSize; indentString += "\t";} + while (pos < indentation) {++pos; indentString += " ";} + } + + replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length}); + } + + function loadMode() { + mode = CodeMirror.getMode(options, options.mode); + doc.iter(0, doc.size, function(line) { line.stateAfter = null; }); + work = [0]; + startWorker(); + } + function gutterChanged() { + var visible = options.gutter || options.lineNumbers; + gutter.style.display = visible ? "" : "none"; + if (visible) gutterDirty = true; + else lineDiv.parentNode.style.marginLeft = 0; + } + function wrappingChanged(from, to) { + if (options.lineWrapping) { + wrapper.className += " CodeMirror-wrap"; + var perLine = scroller.clientWidth / charWidth() - 3; + doc.iter(0, doc.size, function(line) { + if (line.hidden) return; + var guess = Math.ceil(line.text.length / perLine) || 1; + if (guess != 1) updateLineHeight(line, guess); + }); + lineSpace.style.width = code.style.width = ""; + widthForcer.style.left = ""; + } else { + wrapper.className = wrapper.className.replace(" CodeMirror-wrap", ""); + maxLine = ""; maxLineChanged = true; + doc.iter(0, doc.size, function(line) { + if (line.height != 1 && !line.hidden) updateLineHeight(line, 1); + if (line.text.length > maxLine.length) maxLine = line.text; + }); + } + changes.push({from: 0, to: doc.size}); + } + function makeTab(col) { + var w = options.tabSize - col % options.tabSize, cached = tabCache[w]; + if (cached) return cached; + for (var str = '', i = 0; i < w; ++i) str += " "; + return (tabCache[w] = {html: str + "", width: w}); + } + function themeChanged() { + scroller.className = scroller.className.replace(/\s*cm-s-\S+/g, "") + + options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + } + function keyMapChanged() { + var style = keyMap[options.keyMap].style; + wrapper.className = wrapper.className.replace(/\s*cm-keymap-\S+/g, "") + + (style ? " cm-keymap-" + style : ""); + } + + function TextMarker() { this.set = []; } + TextMarker.prototype.clear = operation(function() { + var min = Infinity, max = -Infinity; + for (var i = 0, e = this.set.length; i < e; ++i) { + var line = this.set[i], mk = line.marked; + if (!mk || !line.parent) continue; + var lineN = lineNo(line); + min = Math.min(min, lineN); max = Math.max(max, lineN); + for (var j = 0; j < mk.length; ++j) + if (mk[j].marker == this) mk.splice(j--, 1); + } + if (min != Infinity) + changes.push({from: min, to: max + 1}); + }); + TextMarker.prototype.find = function() { + var from, to; + for (var i = 0, e = this.set.length; i < e; ++i) { + var line = this.set[i], mk = line.marked; + for (var j = 0; j < mk.length; ++j) { + var mark = mk[j]; + if (mark.marker == this) { + if (mark.from != null || mark.to != null) { + var found = lineNo(line); + if (found != null) { + if (mark.from != null) from = {line: found, ch: mark.from}; + if (mark.to != null) to = {line: found, ch: mark.to}; + } + } + } + } + } + return {from: from, to: to}; + }; + + function markText(from, to, className) { + from = clipPos(from); to = clipPos(to); + var tm = new TextMarker(); + if (!posLess(from, to)) return tm; + function add(line, from, to, className) { + getLine(line).addMark(new MarkedText(from, to, className, tm)); + } + if (from.line == to.line) add(from.line, from.ch, to.ch, className); + else { + add(from.line, from.ch, null, className); + for (var i = from.line + 1, e = to.line; i < e; ++i) + add(i, null, null, className); + add(to.line, null, to.ch, className); + } + changes.push({from: from.line, to: to.line + 1}); + return tm; + } + + function setBookmark(pos) { + pos = clipPos(pos); + var bm = new Bookmark(pos.ch); + getLine(pos.line).addMark(bm); + return bm; + } + + function findMarksAt(pos) { + pos = clipPos(pos); + var markers = [], marked = getLine(pos.line).marked; + if (!marked) return markers; + for (var i = 0, e = marked.length; i < e; ++i) { + var m = marked[i]; + if ((m.from == null || m.from <= pos.ch) && + (m.to == null || m.to >= pos.ch)) + markers.push(m.marker || m); + } + return markers; + } + + function addGutterMarker(line, text, className) { + if (typeof line == "number") line = getLine(clipLine(line)); + line.gutterMarker = {text: text, style: className}; + gutterDirty = true; + return line; + } + function removeGutterMarker(line) { + if (typeof line == "number") line = getLine(clipLine(line)); + line.gutterMarker = null; + gutterDirty = true; + } + + function changeLine(handle, op) { + var no = handle, line = handle; + if (typeof handle == "number") line = getLine(clipLine(handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no)) changes.push({from: no, to: no + 1}); + else return null; + return line; + } + function setLineClass(handle, className, bgClassName) { + return changeLine(handle, function(line) { + if (line.className != className || line.bgClassName != bgClassName) { + line.className = className; + line.bgClassName = bgClassName; + return true; + } + }); + } + function setLineHidden(handle, hidden) { + return changeLine(handle, function(line, no) { + if (line.hidden != hidden) { + line.hidden = hidden; + if (!options.lineWrapping) { + var l = line.text; + if (hidden && l.length == maxLine.length) { + updateMaxLine = true; + } else if (!hidden && l.length > maxLine.length) { + maxLine = l; maxWidth = null; updateMaxLine = false; + } + } + updateLineHeight(line, hidden ? 0 : 1); + var fline = sel.from.line, tline = sel.to.line; + if (hidden && (fline == no || tline == no)) { + var from = fline == no ? skipHidden({line: fline, ch: 0}, fline, 0) : sel.from; + var to = tline == no ? skipHidden({line: tline, ch: 0}, tline, 0) : sel.to; + // Can't hide the last visible line, we'd have no place to put the cursor + if (!to) return; + setSelection(from, to); + } + return (gutterDirty = true); + } + }); + } + + function lineInfo(line) { + if (typeof line == "number") { + if (!isLine(line)) return null; + var n = line; + line = getLine(line); + if (!line) return null; + } else { + var n = lineNo(line); + if (n == null) return null; + } + var marker = line.gutterMarker; + return {line: n, handle: line, text: line.text, markerText: marker && marker.text, + markerClass: marker && marker.style, lineClass: line.className, bgClass: line.bgClassName}; + } + + function stringWidth(str) { + measure.innerHTML = "
"); + html.push("x
"; + measure.firstChild.firstChild.firstChild.nodeValue = str; + return measure.firstChild.firstChild.offsetWidth || 10; + } + // These are used to go from pixel positions to character + // positions, taking varying character widths into account. + function charFromX(line, x) { + if (x <= 0) return 0; + var lineObj = getLine(line), text = lineObj.text; + function getX(len) { + return measureLine(lineObj, len).left; + } + var from = 0, fromX = 0, to = text.length, toX; + // Guess a suitable upper bound for our search. + var estimated = Math.min(to, Math.ceil(x / charWidth())); + for (;;) { + var estX = getX(estimated); + if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2)); + else {toX = estX; to = estimated; break;} + } + if (x > toX) return to; + // Try to guess a suitable lower bound as well. + estimated = Math.floor(to * 0.8); estX = getX(estimated); + if (estX < x) {from = estimated; fromX = estX;} + // Do a binary search between these bounds. + for (;;) { + if (to - from <= 1) return (toX - x > x - fromX) ? from : to; + var middle = Math.ceil((from + to) / 2), middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX;} + else {from = middle; fromX = middleX;} + } + } + + var tempId = "CodeMirror-temp-" + Math.floor(Math.random() * 0xffffff).toString(16); + function measureLine(line, ch) { + if (ch == 0) return {top: 0, left: 0}; + var wbr = options.lineWrapping && ch < line.text.length && + spanAffectsWrapping.test(line.text.slice(ch - 1, ch + 1)); + measure.innerHTML = "" + line.getHTML(makeTab, ch, tempId, wbr) + ""; + var elt = document.getElementById(tempId); + var top = elt.offsetTop, left = elt.offsetLeft; + // Older IEs report zero offsets for spans directly after a wrap + if (ie && top == 0 && left == 0) { + var backup = document.createElement("span"); + backup.innerHTML = "x"; + elt.parentNode.insertBefore(backup, elt.nextSibling); + top = backup.offsetTop; + } + return {top: top, left: left}; + } + function localCoords(pos, inLineWrap) { + var x, lh = textHeight(), y = lh * (heightAtLine(doc, pos.line) - (inLineWrap ? displayOffset : 0)); + if (pos.ch == 0) x = 0; + else { + var sp = measureLine(getLine(pos.line), pos.ch); + x = sp.left; + if (options.lineWrapping) y += Math.max(0, sp.top); + } + return {x: x, y: y, yBot: y + lh}; + } + // Coords must be lineSpace-local + function coordsChar(x, y) { + if (y < 0) y = 0; + var th = textHeight(), cw = charWidth(), heightPos = displayOffset + Math.floor(y / th); + var lineNo = lineAtHeight(doc, heightPos); + if (lineNo >= doc.size) return {line: doc.size - 1, ch: getLine(doc.size - 1).text.length}; + var lineObj = getLine(lineNo), text = lineObj.text; + var tw = options.lineWrapping, innerOff = tw ? heightPos - heightAtLine(doc, lineNo) : 0; + if (x <= 0 && innerOff == 0) return {line: lineNo, ch: 0}; + function getX(len) { + var sp = measureLine(lineObj, len); + if (tw) { + var off = Math.round(sp.top / th); + return Math.max(0, sp.left + (off - innerOff) * scroller.clientWidth); + } + return sp.left; + } + var from = 0, fromX = 0, to = text.length, toX; + // Guess a suitable upper bound for our search. + var estimated = Math.min(to, Math.ceil((x + innerOff * scroller.clientWidth * .9) / cw)); + for (;;) { + var estX = getX(estimated); + if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2)); + else {toX = estX; to = estimated; break;} + } + if (x > toX) return {line: lineNo, ch: to}; + // Try to guess a suitable lower bound as well. + estimated = Math.floor(to * 0.8); estX = getX(estimated); + if (estX < x) {from = estimated; fromX = estX;} + // Do a binary search between these bounds. + for (;;) { + if (to - from <= 1) return {line: lineNo, ch: (toX - x > x - fromX) ? from : to}; + var middle = Math.ceil((from + to) / 2), middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX;} + else {from = middle; fromX = middleX;} + } + } + function pageCoords(pos) { + var local = localCoords(pos, true), off = eltOffset(lineSpace); + return {x: off.left + local.x, y: off.top + local.y, yBot: off.top + local.yBot}; + } + + var cachedHeight, cachedHeightFor, measureText; + function textHeight() { + if (measureText == null) { + measureText = ""; + for (var i = 0; i < 49; ++i) measureText += "x"; + } + var offsetHeight = lineDiv.clientHeight; + if (offsetHeight == cachedHeightFor) return cachedHeight; + cachedHeightFor = offsetHeight; + measure.innerHTML = measureText; + cachedHeight = measure.firstChild.offsetHeight / 50 || 1; + measure.innerHTML = ""; + return cachedHeight; + } + var cachedWidth, cachedWidthFor = 0; + function charWidth() { + if (scroller.clientWidth == cachedWidthFor) return cachedWidth; + cachedWidthFor = scroller.clientWidth; + return (cachedWidth = stringWidth("x")); + } + function paddingTop() {return lineSpace.offsetTop;} + function paddingLeft() {return lineSpace.offsetLeft;} + + function posFromMouse(e, liberal) { + var offW = eltOffset(scroller, true), x, y; + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX; y = e.clientY; } catch (e) { return null; } + // This is a mess of a heuristic to try and determine whether a + // scroll-bar was clicked or not, and to return null if one was + // (and !liberal). + if (!liberal && (x - offW.left > scroller.clientWidth || y - offW.top > scroller.clientHeight)) + return null; + var offL = eltOffset(lineSpace, true); + return coordsChar(x - offL.left, y - offL.top); + } + function onContextMenu(e) { + var pos = posFromMouse(e), scrollPos = scrollbar.scrollTop; + if (!pos || opera) return; // Opera is difficult. + if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to)) + operation(setCursor)(pos.line, pos.ch); + + var oldCSS = input.style.cssText; + inputDiv.style.position = "absolute"; + input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; " + + "border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + leaveInputAlone = true; + var val = input.value = getSelection(); + focusInput(); + selectInput(input); + function rehide() { + var newVal = splitLines(input.value).join("\n"); + if (newVal != val && !options.readOnly) operation(replaceSelection)(newVal, "end"); + inputDiv.style.position = "relative"; + input.style.cssText = oldCSS; + if (ie_lt9) scrollbar.scrollTop = scrollPos; + leaveInputAlone = false; + resetInput(true); + slowPoll(); + } + + if (gecko) { + e_stop(e); + var mouseup = connect(window, "mouseup", function() { + mouseup(); + setTimeout(rehide, 20); + }, true); + } else { + setTimeout(rehide, 50); + } + } + + // Cursor-blinking + function restartBlink() { + clearInterval(blinker); + var on = true; + cursor.style.visibility = ""; + blinker = setInterval(function() { + cursor.style.visibility = (on = !on) ? "" : "hidden"; + }, 650); + } + + var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"}; + function matchBrackets(autoclear) { + var head = sel.inverted ? sel.from : sel.to, line = getLine(head.line), pos = head.ch - 1; + var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)]; + if (!match) return; + var ch = match.charAt(0), forward = match.charAt(1) == ">", d = forward ? 1 : -1, st = line.styles; + for (var off = pos + 1, i = 0, e = st.length; i < e; i+=2) + if ((off -= st[i].length) <= 0) {var style = st[i+1]; break;} + + var stack = [line.text.charAt(pos)], re = /[(){}[\]]/; + function scan(line, from, to) { + if (!line.text) return; + var st = line.styles, pos = forward ? 0 : line.text.length - 1, cur; + for (var i = forward ? 0 : st.length - 2, e = forward ? st.length : -2; i != e; i += 2*d) { + var text = st[i]; + if (st[i+1] != style) {pos += d * text.length; continue;} + for (var j = forward ? 0 : text.length - 1, te = forward ? text.length : -1; j != te; j += d, pos+=d) { + if (pos >= from && pos < to && re.test(cur = text.charAt(j))) { + var match = matching[cur]; + if (match.charAt(1) == ">" == forward) stack.push(cur); + else if (stack.pop() != match.charAt(0)) return {pos: pos, match: false}; + else if (!stack.length) return {pos: pos, match: true}; + } + } + } + } + for (var i = head.line, e = forward ? Math.min(i + 100, doc.size) : Math.max(-1, i - 100); i != e; i+=d) { + var line = getLine(i), first = i == head.line; + var found = scan(line, first && forward ? pos + 1 : 0, first && !forward ? pos : line.text.length); + if (found) break; + } + if (!found) found = {pos: null, match: false}; + var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; + var one = markText({line: head.line, ch: pos}, {line: head.line, ch: pos+1}, style), + two = found.pos != null && markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style); + var clear = operation(function(){one.clear(); two && two.clear();}); + if (autoclear) setTimeout(clear, 800); + else bracketHighlighted = clear; + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(n) { + var minindent, minline; + for (var search = n, lim = n - 40; search > lim; --search) { + if (search == 0) return 0; + var line = getLine(search-1); + if (line.stateAfter) return search; + var indented = line.indentation(options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + function getStateBefore(n) { + var start = findStartLine(n), state = start && getLine(start-1).stateAfter; + if (!state) state = startState(mode); + else state = copyState(mode, state); + doc.iter(start, n, function(line) { + line.highlight(mode, state, options.tabSize); + line.stateAfter = copyState(mode, state); + }); + if (start < n) changes.push({from: start, to: n}); + if (n < doc.size && !getLine(n).stateAfter) work.push(n); + return state; + } + function highlightLines(start, end) { + var state = getStateBefore(start); + doc.iter(start, end, function(line) { + line.highlight(mode, state, options.tabSize); + line.stateAfter = copyState(mode, state); + }); + } + function highlightWorker() { + var end = +new Date + options.workTime; + var foundWork = work.length; + while (work.length) { + if (!getLine(showingFrom).stateAfter) var task = showingFrom; + else var task = work.pop(); + if (task >= doc.size) continue; + var start = findStartLine(task), state = start && getLine(start-1).stateAfter; + if (state) state = copyState(mode, state); + else state = startState(mode); + + var unchanged = 0, compare = mode.compareStates, realChange = false, + i = start, bail = false; + doc.iter(i, doc.size, function(line) { + var hadState = line.stateAfter; + if (+new Date > end) { + work.push(i); + startWorker(options.workDelay); + if (realChange) changes.push({from: task, to: i + 1}); + return (bail = true); + } + var changed = line.highlight(mode, state, options.tabSize); + if (changed) realChange = true; + line.stateAfter = copyState(mode, state); + var done = null; + if (compare) { + var same = hadState && compare(hadState, state); + if (same != Pass) done = !!same; + } + if (done == null) { + if (changed !== false || !hadState) unchanged = 0; + else if (++unchanged > 3 && (!mode.indent || mode.indent(hadState, "") == mode.indent(state, ""))) + done = true; + } + if (done) return true; + ++i; + }); + if (bail) return; + if (realChange) changes.push({from: task, to: i + 1}); + } + if (foundWork && options.onHighlightComplete) + options.onHighlightComplete(instance); + } + function startWorker(time) { + if (!work.length) return; + highlight.set(time, operation(highlightWorker)); + } + + // Operations are used to wrap changes in such a way that each + // change won't have to update the cursor and display (which would + // be awkward, slow, and error-prone), but instead updates are + // batched and then all combined and executed at once. + function startOperation() { + updateInput = userSelChange = textChanged = null; + changes = []; selectionChanged = false; callbacks = []; + } + function endOperation() { + if (updateMaxLine) computeMaxLength(); + if (maxLineChanged && !options.lineWrapping) { + widthForcer.style.left = stringWidth(maxLine) + "px"; + maxLineChanged = false; + } + var newScrollPos, updated; + if (selectionChanged) { + var coords = calculateCursorCoords(); + newScrollPos = calculateScrollPos(coords.x, coords.y, coords.x, coords.yBot); + } + if (changes.length) updated = updateDisplay(changes, true, (newScrollPos ? newScrollPos.scrollTop : null)); + else { + if (selectionChanged) updateSelection(); + if (gutterDirty) updateGutter(); + } + if (newScrollPos) scrollCursorIntoView(); + if (selectionChanged) {scrollEditorIntoView(); restartBlink();} + + if (focused && !leaveInputAlone && + (updateInput === true || (updateInput !== false && selectionChanged))) + resetInput(userSelChange); + + if (selectionChanged && options.matchBrackets) + setTimeout(operation(function() { + if (bracketHighlighted) {bracketHighlighted(); bracketHighlighted = null;} + if (posEq(sel.from, sel.to)) matchBrackets(false); + }), 20); + var sc = selectionChanged, cbs = callbacks; // these can be reset by callbacks + if (textChanged && options.onChange && instance) + options.onChange(instance, textChanged); + if (sc && options.onCursorActivity) + options.onCursorActivity(instance); + for (var i = 0; i < cbs.length; ++i) cbs[i](instance); + if (updated && options.onUpdate) options.onUpdate(instance); + } + var nestedOperation = 0; + function operation(f) { + return function() { + if (!nestedOperation++) startOperation(); + try {var result = f.apply(this, arguments);} + finally {if (!--nestedOperation) endOperation();} + return result; + }; + } + + function compoundChange(f) { + history.startCompound(); + try { return f(); } finally { history.endCompound(); } + } + + for (var ext in extensions) + if (extensions.propertyIsEnumerable(ext) && + !instance.propertyIsEnumerable(ext)) + instance[ext] = extensions[ext]; + return instance; + } // (end of function CodeMirror) + + // The default configuration options. + CodeMirror.defaults = { + value: "", + mode: null, + theme: "default", + indentUnit: 2, + indentWithTabs: false, + smartIndent: true, + tabSize: 4, + keyMap: "default", + extraKeys: null, + electricChars: true, + autoClearEmptyLines: false, + onKeyEvent: null, + onDragEvent: null, + lineWrapping: false, + lineNumbers: false, + gutter: false, + fixedGutter: false, + firstLineNumber: 1, + readOnly: false, + dragDrop: true, + onChange: null, + onCursorActivity: null, + onGutterClick: null, + onHighlightComplete: null, + onUpdate: null, + onFocus: null, onBlur: null, onScroll: null, + matchBrackets: false, + workTime: 100, + workDelay: 200, + pollInterval: 100, + undoDepth: 40, + tabindex: null, + autofocus: null + }; + + var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); + var mac = ios || /Mac/.test(navigator.platform); + var win = /Win/.test(navigator.platform); + + // Known modes, by name and by MIME + var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + if (arguments.length > 2) { + mode.dependencies = []; + for (var i = 2; i < arguments.length; ++i) mode.dependencies.push(arguments[i]); + } + modes[name] = mode; + }; + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + CodeMirror.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) + spec = mimeModes[spec]; + else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) + return CodeMirror.resolveMode("application/xml"); + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; + }; + CodeMirror.getMode = function(options, spec) { + var spec = CodeMirror.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) return CodeMirror.getMode(options, "text/plain"); + return mfactory(options, spec); + }; + CodeMirror.listModes = function() { + var list = []; + for (var m in modes) + if (modes.propertyIsEnumerable(m)) list.push(m); + return list; + }; + CodeMirror.listMIMEs = function() { + var list = []; + for (var m in mimeModes) + if (mimeModes.propertyIsEnumerable(m)) list.push({mime: m, mode: mimeModes[m]}); + return list; + }; + + var extensions = CodeMirror.extensions = {}; + CodeMirror.defineExtension = function(name, func) { + extensions[name] = func; + }; + + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection({line: 0, ch: 0}, {line: cm.lineCount() - 1});}, + killLine: function(cm) { + var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to); + if (!sel && cm.getLine(from.line).length == from.ch) cm.replaceRange("", from, {line: from.line + 1, ch: 0}); + else cm.replaceRange("", from, sel ? to : {line: from.line}); + }, + deleteLine: function(cm) {var l = cm.getCursor().line; cm.replaceRange("", {line: l, ch: 0}, {line: l});}, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + goDocStart: function(cm) {cm.setCursor(0, 0, true);}, + goDocEnd: function(cm) {cm.setSelection({line: cm.lineCount() - 1}, null, true);}, + goLineStart: function(cm) {cm.setCursor(cm.getCursor().line, 0, true);}, + goLineStartSmart: function(cm) { + var cur = cm.getCursor(); + var text = cm.getLine(cur.line), firstNonWS = Math.max(0, text.search(/\S/)); + cm.setCursor(cur.line, cur.ch <= firstNonWS && cur.ch ? 0 : firstNonWS, true); + }, + goLineEnd: function(cm) {cm.setSelection({line: cm.getCursor().line}, null, true);}, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharLeft: function(cm) {cm.deleteH(-1, "char");}, + delCharRight: function(cm) {cm.deleteH(1, "char");}, + delWordLeft: function(cm) {cm.deleteH(-1, "word");}, + delWordRight: function(cm) {cm.deleteH(1, "word");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t", "end");}, + defaultTab: function(cm) { + if (cm.somethingSelected()) cm.indentSelection("add"); + else cm.replaceSelection("\t", "end"); + }, + transposeChars: function(cm) { + var cur = cm.getCursor(), line = cm.getLine(cur.line); + if (cur.ch > 0 && cur.ch < line.length - 1) + cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1), + {line: cur.line, ch: cur.ch - 1}, {line: cur.line, ch: cur.ch + 1}); + }, + newlineAndIndent: function(cm) { + cm.replaceSelection("\n", "end"); + cm.indentLine(cm.getCursor().line); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + var keyMap = CodeMirror.keyMap = {}; + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharRight", "Backspace": "delCharLeft", "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite" + }; + // Note that the save and find-related commands aren't defined by + // default. Unknown commands are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd", + "Ctrl-Left": "goWordLeft", "Ctrl-Right": "goWordRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delWordLeft", "Ctrl-Delete": "delWordRight", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + fallthrough: "basic" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goWordLeft", + "Alt-Right": "goWordRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delWordLeft", + "Ctrl-Alt-Backspace": "delWordRight", "Alt-Delete": "delWordRight", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageUp", "Shift-Ctrl-V": "goPageDown", "Ctrl-D": "delCharRight", "Ctrl-H": "delCharLeft", + "Alt-D": "delWordRight", "Alt-Backspace": "delWordLeft", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + + function getKeyMap(val) { + if (typeof val == "string") return keyMap[val]; + else return val; + } + function lookupKey(name, extraMap, map, handle, stop) { + function lookup(map) { + map = getKeyMap(map); + var found = map[name]; + if (found != null && handle(found)) return true; + if (map.nofallthrough) { + if (stop) stop(); + return true; + } + var fallthrough = map.fallthrough; + if (fallthrough == null) return false; + if (Object.prototype.toString.call(fallthrough) != "[object Array]") + return lookup(fallthrough); + for (var i = 0, e = fallthrough.length; i < e; ++i) { + if (lookup(fallthrough[i])) return true; + } + return false; + } + if (extraMap && lookup(extraMap)) return true; + return lookup(map); + } + function isModifierKey(event) { + var name = keyNames[e_prop(event, "keyCode")]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + } + + CodeMirror.fromTextArea = function(textarea, options) { + if (!options) options = {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabindex) + options.tabindex = textarea.tabindex; + if (options.autofocus == null && textarea.getAttribute("autofocus") != null) + options.autofocus = true; + + function save() {textarea.value = instance.getValue();} + if (textarea.form) { + // Deplorable hack to make the submit method do the right thing. + var rmSubmit = connect(textarea.form, "submit", save, true); + if (typeof textarea.form.submit == "function") { + var realSubmit = textarea.form.submit; + function wrappedSubmit() { + save(); + textarea.form.submit = realSubmit; + textarea.form.submit(); + textarea.form.submit = wrappedSubmit; + } + textarea.form.submit = wrappedSubmit; + } + } + + textarea.style.display = "none"; + var instance = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + instance.save = save; + instance.getTextArea = function() { return textarea; }; + instance.toTextArea = function() { + save(); + textarea.parentNode.removeChild(instance.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + rmSubmit(); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + return instance; + }; + + // Utility functions for working with state. Exported because modes + // sometimes need to do this. + function copyState(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + } + CodeMirror.copyState = copyState; + function startState(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + } + CodeMirror.startState = startState; + + // The character stream used by a mode's parser. + function StringStream(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + } + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == 0;}, + peek: function() {return this.string.charAt(this.pos);}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() {return countColumn(this.string, this.start, this.tabSize);}, + indentation: function() {return countColumn(this.string, null, this.tabSize);}, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + function cased(str) {return caseInsensitive ? str.toLowerCase() : str;} + if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);} + }; + CodeMirror.StringStream = StringStream; + + function MarkedText(from, to, className, marker) { + this.from = from; this.to = to; this.style = className; this.marker = marker; + } + MarkedText.prototype = { + attach: function(line) { this.marker.set.push(line); }, + detach: function(line) { + var ix = indexOf(this.marker.set, line); + if (ix > -1) this.marker.set.splice(ix, 1); + }, + split: function(pos, lenBefore) { + if (this.to <= pos && this.to != null) return null; + var from = this.from < pos || this.from == null ? null : this.from - pos + lenBefore; + var to = this.to == null ? null : this.to - pos + lenBefore; + return new MarkedText(from, to, this.style, this.marker); + }, + dup: function() { return new MarkedText(null, null, this.style, this.marker); }, + clipTo: function(fromOpen, from, toOpen, to, diff) { + if (fromOpen && to > this.from && (to < this.to || this.to == null)) + this.from = null; + else if (this.from != null && this.from >= from) + this.from = Math.max(to, this.from) + diff; + if (toOpen && (from < this.to || this.to == null) && (from > this.from || this.from == null)) + this.to = null; + else if (this.to != null && this.to > from) + this.to = to < this.to ? this.to + diff : from; + }, + isDead: function() { return this.from != null && this.to != null && this.from >= this.to; }, + sameSet: function(x) { return this.marker == x.marker; } + }; + + function Bookmark(pos) { + this.from = pos; this.to = pos; this.line = null; + } + Bookmark.prototype = { + attach: function(line) { this.line = line; }, + detach: function(line) { if (this.line == line) this.line = null; }, + split: function(pos, lenBefore) { + if (pos < this.from) { + this.from = this.to = (this.from - pos) + lenBefore; + return this; + } + }, + isDead: function() { return this.from > this.to; }, + clipTo: function(fromOpen, from, toOpen, to, diff) { + if ((fromOpen || from < this.from) && (toOpen || to > this.to)) { + this.from = 0; this.to = -1; + } else if (this.from > from) { + this.from = this.to = Math.max(to, this.from) + diff; + } + }, + sameSet: function(x) { return false; }, + find: function() { + if (!this.line || !this.line.parent) return null; + return {line: lineNo(this.line), ch: this.from}; + }, + clear: function() { + if (this.line) { + var found = indexOf(this.line.marked, this); + if (found != -1) this.line.marked.splice(found, 1); + this.line = null; + } + } + }; + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + function Line(text, styles) { + this.styles = styles || [text, null]; + this.text = text; + this.height = 1; + this.marked = this.gutterMarker = this.className = this.bgClassName = this.handlers = null; + this.stateAfter = this.parent = this.hidden = null; + } + Line.inheritMarks = function(text, orig) { + var ln = new Line(text), mk = orig && orig.marked; + if (mk) { + for (var i = 0; i < mk.length; ++i) { + if (mk[i].to == null && mk[i].style) { + var newmk = ln.marked || (ln.marked = []), mark = mk[i]; + var nmark = mark.dup(); newmk.push(nmark); nmark.attach(ln); + } + } + } + return ln; + } + Line.prototype = { + // Replace a piece of a line, keeping the styles around it intact. + replace: function(from, to_, text) { + var st = [], mk = this.marked, to = to_ == null ? this.text.length : to_; + copyStyles(0, from, this.styles, st); + if (text) st.push(text, null); + copyStyles(to, this.text.length, this.styles, st); + this.styles = st; + this.text = this.text.slice(0, from) + text + this.text.slice(to); + this.stateAfter = null; + if (mk) { + var diff = text.length - (to - from); + for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + mark.clipTo(from == null, from || 0, to_ == null, to, diff); + if (mark.isDead()) {mark.detach(this); mk.splice(i--, 1);} + } + } + }, + // Split a part off a line, keeping styles and markers intact. + split: function(pos, textBefore) { + var st = [textBefore, null], mk = this.marked; + copyStyles(pos, this.text.length, this.styles, st); + var taken = new Line(textBefore + this.text.slice(pos), st); + if (mk) { + for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + var newmark = mark.split(pos, textBefore.length); + if (newmark) { + if (!taken.marked) taken.marked = []; + taken.marked.push(newmark); newmark.attach(taken); + if (newmark == mark) mk.splice(i--, 1); + } + } + } + return taken; + }, + append: function(line) { + var mylen = this.text.length, mk = line.marked, mymk = this.marked; + this.text += line.text; + copyStyles(0, line.text.length, line.styles, this.styles); + if (mymk) { + for (var i = 0; i < mymk.length; ++i) + if (mymk[i].to == null) mymk[i].to = mylen; + } + if (mk && mk.length) { + if (!mymk) this.marked = mymk = []; + outer: for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + if (!mark.from) { + for (var j = 0; j < mymk.length; ++j) { + var mymark = mymk[j]; + if (mymark.to == mylen && mymark.sameSet(mark)) { + mymark.to = mark.to == null ? null : mark.to + mylen; + if (mymark.isDead()) { + mymark.detach(this); + mk.splice(i--, 1); + } + continue outer; + } + } + } + mymk.push(mark); + mark.attach(this); + mark.from += mylen; + if (mark.to != null) mark.to += mylen; + } + } + }, + fixMarkEnds: function(other) { + var mk = this.marked, omk = other.marked; + if (!mk) return; + for (var i = 0; i < mk.length; ++i) { + var mark = mk[i], close = mark.to == null; + if (close && omk) { + for (var j = 0; j < omk.length; ++j) + if (omk[j].sameSet(mark)) {close = false; break;} + } + if (close) mark.to = this.text.length; + } + }, + fixMarkStarts: function() { + var mk = this.marked; + if (!mk) return; + for (var i = 0; i < mk.length; ++i) + if (mk[i].from == null) mk[i].from = 0; + }, + addMark: function(mark) { + mark.attach(this); + if (this.marked == null) this.marked = []; + this.marked.push(mark); + this.marked.sort(function(a, b){return (a.from || 0) - (b.from || 0);}); + }, + // Run the given mode's parser over a line, update the styles + // array, which contains alternating fragments of text and CSS + // classes. + highlight: function(mode, state, tabSize) { + var stream = new StringStream(this.text, tabSize), st = this.styles, pos = 0; + var changed = false, curWord = st[0], prevWord; + if (this.text == "" && mode.blankLine) mode.blankLine(state); + while (!stream.eol()) { + var style = mode.token(stream, state); + var substr = this.text.slice(stream.start, stream.pos); + stream.start = stream.pos; + if (pos && st[pos-1] == style) + st[pos-2] += substr; + else if (substr) { + if (!changed && (st[pos+1] != style || (pos && st[pos-2] != prevWord))) changed = true; + st[pos++] = substr; st[pos++] = style; + prevWord = curWord; curWord = st[pos]; + } + // Give up when line is ridiculously long + if (stream.pos > 5000) { + st[pos++] = this.text.slice(stream.pos); st[pos++] = null; + break; + } + } + if (st.length != pos) {st.length = pos; changed = true;} + if (pos && st[pos-2] != prevWord) changed = true; + // Short lines with simple highlights return null, and are + // counted as changed by the driver because they are likely to + // highlight the same way in various contexts. + return changed || (st.length < 5 && this.text.length < 10 ? null : false); + }, + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(mode, state, ch) { + var txt = this.text, stream = new StringStream(txt); + while (stream.pos < ch && !stream.eol()) { + stream.start = stream.pos; + var style = mode.token(stream, state); + } + return {start: stream.start, + end: stream.pos, + string: stream.current(), + className: style || null, + state: state}; + }, + indentation: function(tabSize) {return countColumn(this.text, null, tabSize);}, + // Produces an HTML fragment for the line, taking selection, + // marking, and highlighting into account. + getHTML: function(makeTab, wrapAt, wrapId, wrapWBR) { + var html = [], first = true, col = 0; + function span_(text, style) { + if (!text) return; + // Work around a bug where, in some compat modes, IE ignores leading spaces + if (first && ie && text.charAt(0) == " ") text = "\u00a0" + text.slice(1); + first = false; + if (text.indexOf("\t") == -1) { + col += text.length; + var escaped = htmlEscape(text); + } else { + var escaped = ""; + for (var pos = 0;;) { + var idx = text.indexOf("\t", pos); + if (idx == -1) { + escaped += htmlEscape(text.slice(pos)); + col += text.length - pos; + break; + } else { + col += idx - pos; + var tab = makeTab(col); + escaped += htmlEscape(text.slice(pos, idx)) + tab.html; + col += tab.width; + pos = idx + 1; + } + } + } + if (style) html.push('', escaped, ""); + else html.push(escaped); + } + var span = span_; + if (wrapAt != null) { + var outPos = 0, open = ""; + span = function(text, style) { + var l = text.length; + if (wrapAt >= outPos && wrapAt < outPos + l) { + if (wrapAt > outPos) { + span_(text.slice(0, wrapAt - outPos), style); + // See comment at the definition of spanAffectsWrapping + if (wrapWBR) html.push("
"; + measureText += "x"); + } + html.push(open); + var cut = wrapAt - outPos; + span_(opera ? text.slice(cut, cut + 1) : text.slice(cut), style); + html.push(" "); + if (opera) span_(text.slice(cut + 1), style); + wrapAt--; + outPos += l; + } else { + outPos += l; + span_(text, style); + // Output empty wrapper when at end of line + if (outPos == wrapAt && outPos == len) html.push(open + " "); + // Stop outputting HTML when gone sufficiently far beyond measure + else if (outPos > wrapAt + 10 && /\s/.test(text)) span = function(){}; + } + } + } + + var st = this.styles, allText = this.text, marked = this.marked; + var len = allText.length; + function styleToClass(style) { + if (!style) return null; + return "cm-" + style.replace(/ +/g, " cm-"); + } + + if (!allText && wrapAt == null) { + span(" "); + } else if (!marked || !marked.length) { + for (var i = 0, ch = 0; ch < len; i+=2) { + var str = st[i], style = st[i+1], l = str.length; + if (ch + l > len) str = str.slice(0, len - ch); + ch += l; + span(str, styleToClass(style)); + } + } else { + var pos = 0, i = 0, text = "", style, sg = 0; + var nextChange = marked[0].from || 0, marks = [], markpos = 0; + function advanceMarks() { + var m; + while (markpos < marked.length && + ((m = marked[markpos]).from == pos || m.from == null)) { + if (m.style != null) marks.push(m); + ++markpos; + } + nextChange = markpos < marked.length ? marked[markpos].from : Infinity; + for (var i = 0; i < marks.length; ++i) { + var to = marks[i].to || Infinity; + if (to == pos) marks.splice(i--, 1); + else nextChange = Math.min(to, nextChange); + } + } + var m = 0; + while (pos < len) { + if (nextChange == pos) advanceMarks(); + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + var appliedStyle = style; + for (var j = 0; j < marks.length; ++j) + appliedStyle = (appliedStyle ? appliedStyle + " " : "") + marks[j].style; + span(end > upto ? text.slice(0, upto - pos) : text, appliedStyle); + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} + pos = end; + } + text = st[i++]; style = styleToClass(st[i++]); + } + } + } + return html.join(""); + }, + cleanUp: function() { + this.parent = null; + if (this.marked) + for (var i = 0, e = this.marked.length; i < e; ++i) this.marked[i].detach(this); + } + }; + // Utility used by replace and split above + function copyStyles(from, to, source, dest) { + for (var i = 0, pos = 0, state = 0; pos < to; i+=2) { + var part = source[i], end = pos + part.length; + if (state == 0) { + if (end > from) dest.push(part.slice(from - pos, Math.min(part.length, to - pos)), source[i+1]); + if (end >= from) state = 1; + } else if (state == 1) { + if (end > to) dest.push(part.slice(0, to - pos), source[i+1]); + else dest.push(part, source[i+1]); + } + pos = end; + } + } + + // Data structure that holds the sequence of lines. + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, e = lines.length, height = 0; i < e; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + remove: function(at, n, callbacks) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + line.cleanUp(); + if (line.handlers) + for (var j = 0; j < line.handlers.length; ++j) callbacks.push(line.handlers[j]); + } + this.lines.splice(at, n); + }, + collapse: function(lines) { + lines.splice.apply(lines, [lines.length, 0].concat(this.lines)); + }, + insertHeight: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0, e = lines.length; i < e; ++i) lines[i].parent = this; + }, + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0, e = children.length; i < e; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + remove: function(at, n, callbacks) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.remove(at, rm, callbacks); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + if (this.size - n < 25) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + collapse: function(lines) { + for (var i = 0, e = this.children.length; i < e; ++i) this.children[i].collapse(lines); + }, + insert: function(at, lines) { + var height = 0; + for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height; + this.insertHeight(at, lines, height); + }, + insertHeight: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0, e = this.children.length; i < e; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertHeight(at, lines, height); + if (child.lines && child.lines.length > 50) { + while (child.lines.length > 50) { + var spilled = child.lines.splice(child.lines.length - 25, 25); + var newleaf = new LeafChunk(spilled); + child.height -= newleaf.height; + this.children.splice(i + 1, 0, newleaf); + newleaf.parent = this; + } + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iter: function(from, to, op) { this.iterN(from, to - from, op); }, + iterN: function(at, n, op) { + for (var i = 0, e = this.children.length; i < e; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + function getLineAt(chunk, n) { + while (!chunk.lines) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0, e = chunk.children.length; ; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no; + } + function lineAtHeight(chunk, h) { + var n = 0; + outer: do { + for (var i = 0, e = chunk.children.length; i < e; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0, e = chunk.lines.length; i < e; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + function heightAtLine(chunk, n) { + var h = 0; + outer: do { + for (var i = 0, e = chunk.children.length; i < e; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; continue outer; } + n -= sz; + h += child.height; + } + return h; + } while (!chunk.lines); + for (var i = 0; i < n; ++i) h += chunk.lines[i].height; + return h; + } + + // The history object 'chunks' changes that are made close together + // and at almost the same time into bigger undoable units. + function History() { + this.time = 0; + this.done = []; this.undone = []; + this.compound = 0; + this.closed = false; + } + History.prototype = { + addChange: function(start, added, old) { + this.undone.length = 0; + var time = +new Date, cur = this.done[this.done.length - 1], last = cur && cur[cur.length - 1]; + var dtime = time - this.time; + + if (this.compound && cur && !this.closed) { + cur.push({start: start, added: added, old: old}); + } else if (dtime > 400 || !last || this.closed || + last.start > start + old.length || last.start + last.added < start) { + this.done.push([{start: start, added: added, old: old}]); + this.closed = false; + } else { + var startBefore = Math.max(0, last.start - start), + endAfter = Math.max(0, (start + old.length) - (last.start + last.added)); + for (var i = startBefore; i > 0; --i) last.old.unshift(old[i - 1]); + for (var i = endAfter; i > 0; --i) last.old.push(old[old.length - i]); + if (startBefore) last.start = start; + last.added += added - (old.length - startBefore - endAfter); + } + this.time = time; + }, + startCompound: function() { + if (!this.compound++) this.closed = true; + }, + endCompound: function() { + if (!--this.compound) this.closed = true; + } + }; + + function stopMethod() {e_stop(this);} + // Ensure an event has a stop method. + function addStop(event) { + if (!event.stop) event.stop = stopMethod; + return event; + } + + function e_preventDefault(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + } + function e_stopPropagation(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + } + function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} + CodeMirror.e_stop = e_stop; + CodeMirror.e_preventDefault = e_preventDefault; + CodeMirror.e_stopPropagation = e_stopPropagation; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + if (e.which) return e.which; + else if (e.button & 1) return 1; + else if (e.button & 2) return 3; + else if (e.button & 4) return 2; + } + + // Allow 3rd-party code to override event properties by adding an override + // object to an event object. + function e_prop(e, prop) { + var overridden = e.override && e.override.hasOwnProperty(prop); + return overridden ? e.override[prop] : e[prop]; + } + + // Event handler registration. If disconnect is true, it'll return a + // function that unregisters the handler. + function connect(node, type, handler, disconnect) { + if (typeof node.addEventListener == "function") { + node.addEventListener(type, handler, false); + if (disconnect) return function() {node.removeEventListener(type, handler, false);}; + } else { + var wrapHandler = function(event) {handler(event || window.event);}; + node.attachEvent("on" + type, wrapHandler); + if (disconnect) return function() {node.detachEvent("on" + type, wrapHandler);}; + } + } + CodeMirror.connect = connect; + + function Delayed() {this.id = null;} + Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}}; + + var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; + + var gecko = /gecko\/\d{7}/i.test(navigator.userAgent); + var ie = /MSIE \d/.test(navigator.userAgent); + var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent); + var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent); + var quirksMode = ie && document.documentMode == 5; + var webkit = /WebKit\//.test(navigator.userAgent); + var chrome = /Chrome\//.test(navigator.userAgent); + var opera = /Opera\//.test(navigator.userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var khtml = /KHTML\//.test(navigator.userAgent); + var mac_geLion = /Mac OS X 10\D([7-9]|\d\d)\D/.test(navigator.userAgent); + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie_lt9) return false; + var div = document.createElement('div'); + return "draggable" in div || "dragDrop" in div; + }(); + + // Feature-detect whether newlines in textareas are converted to \r\n + var lineSep = function () { + var te = document.createElement("textarea"); + te.value = "foo\nbar"; + if (te.value.indexOf("\r") > -1) return "\r\n"; + return "\n"; + }(); + + // For a reason I have yet to figure out, some browsers disallow + // word wrapping between certain characters *only* if a new inline + // element is started between them. This makes it hard to reliably + // measure the position of things, since that requires inserting an + // extra span. This terribly fragile set of regexps matches the + // character combinations that suffer from this phenomenon on the + // various browsers. + var spanAffectsWrapping = /^$/; // Won't match any two-character string + if (gecko) spanAffectsWrapping = /$'/; + else if (safari) spanAffectsWrapping = /\-[^ \-?]|\?[^ !'\"\),.\-\/:;\?\]\}]/; + else if (chrome) spanAffectsWrapping = /\-[^ \-\.?]|\?[^ \-\.?\]\}:;!'\"\),\/]|[\.!\"#&%\)*+,:;=>\]|\}~][\(\{\[<]|\$'/; + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + function countColumn(string, end, tabSize) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = 0, n = 0; i < end; ++i) { + if (string.charAt(i) == "\t") n += tabSize - (n % tabSize); + else ++n; + } + return n; + } + + function computedStyle(elt) { + if (elt.currentStyle) return elt.currentStyle; + return window.getComputedStyle(elt, null); + } + + // Find the position of an element by following the offsetParent chain. + // If screen==true, it returns screen (rather than page) coordinates. + function eltOffset(node, screen) { + var bod = node.ownerDocument.body; + var x = 0, y = 0, skipBody = false; + for (var n = node; n; n = n.offsetParent) { + var ol = n.offsetLeft, ot = n.offsetTop; + // Firefox reports weird inverted offsets when the body has a border. + if (n == bod) { x += Math.abs(ol); y += Math.abs(ot); } + else { x += ol, y += ot; } + if (screen && computedStyle(n).position == "fixed") + skipBody = true; + } + var e = screen && !skipBody ? null : bod; + for (var n = node.parentNode; n != e; n = n.parentNode) + if (n.scrollLeft != null) { x -= n.scrollLeft; y -= n.scrollTop;} + return {left: x, top: y}; + } + // Use the faster and saner getBoundingClientRect method when possible. + if (document.documentElement.getBoundingClientRect != null) eltOffset = function(node, screen) { + // Take the parts of bounding client rect that we are interested in so we are able to edit if need be, + // since the returned value cannot be changed externally (they are kept in sync as the element moves within the page) + try { var box = node.getBoundingClientRect(); box = { top: box.top, left: box.left }; } + catch(e) { box = {top: 0, left: 0}; } + if (!screen) { + // Get the toplevel scroll, working around browser differences. + if (window.pageYOffset == null) { + var t = document.documentElement || document.body.parentNode; + if (t.scrollTop == null) t = document.body; + box.top += t.scrollTop; box.left += t.scrollLeft; + } else { + box.top += window.pageYOffset; box.left += window.pageXOffset; + } + } + return box; + }; + + // Get a node's text content. + function eltText(node) { + return node.textContent || node.innerText || node.nodeValue || ""; + } + function selectInput(node) { + if (ios) { // Mobile Safari apparently has a bug where select() is broken. + node.selectionStart = 0; + node.selectionEnd = node.value.length; + } else node.select(); + } + + // Operations on {line, ch} objects. + function posEq(a, b) {return a.line == b.line && a.ch == b.ch;} + function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);} + function copyPos(x) {return {line: x.line, ch: x.ch};} + + var escapeElement = document.createElement("pre"); + function htmlEscape(str) { + escapeElement.textContent = str; + return escapeElement.innerHTML; + } + // Recent (late 2011) Opera betas insert bogus newlines at the start + // of the textContent, so we strip those. + if (htmlEscape("a") == "\na") { + htmlEscape = function(str) { + escapeElement.textContent = str; + return escapeElement.innerHTML.slice(1); + }; + // Some IEs don't preserve tabs through innerHTML + } else if (htmlEscape("\t") != "\t") { + htmlEscape = function(str) { + escapeElement.innerHTML = ""; + escapeElement.appendChild(document.createTextNode(str)); + return escapeElement.innerHTML; + }; + } + CodeMirror.htmlEscape = htmlEscape; + + // Used to position the cursor after an undo/redo by finding the + // last edited character. + function editEnd(from, to) { + if (!to) return 0; + if (!from) return to.length; + for (var i = from.length, j = to.length; i >= 0 && j >= 0; --i, --j) + if (from.charAt(i) != to.charAt(j)) break; + return j + 1; + } + + function indexOf(collection, elt) { + if (collection.indexOf) return collection.indexOf(elt); + for (var i = 0, e = collection.length; i < e; ++i) + if (collection[i] == elt) return i; + return -1; + } + function isWordChar(ch) { + return /\w/.test(ch) || ch.toUpperCase() != ch.toLowerCase(); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, nl, result = []; + while ((nl = string.indexOf("\n", pos)) > -1) { + result.push(string.slice(pos, string.charAt(nl-1) == "\r" ? nl - 1 : nl)); + pos = nl + 1; + } + result.push(string.slice(pos)); + return result; + } : function(string){return string.split(/\r?\n/);}; + CodeMirror.splitLines = splitLines; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 91: "Mod", 92: "Mod", 93: "Mod", 109: "-", 107: "=", 127: "Delete", + 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 63276: "PageUp", 63277: "PageDown", 63275: "End", 63273: "Home", + 63234: "Left", 63232: "Up", 63235: "Right", 63233: "Down", 63302: "Insert", 63272: "Delete"}; + CodeMirror.keyNames = keyNames; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + return CodeMirror; +})(); diff --git a/htdocs/themes/bootstrap/js/codemirror/lib/util/closetag.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/closetag.js new file mode 100644 index 0000000..20a43b9 --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/lib/util/closetag.js @@ -0,0 +1,146 @@ +/** + * Tag-closer extension for CodeMirror. + * + * This extension adds a "closeTag" utility function that can be used with key bindings to + * insert a matching end tag after the ">" character of a start tag has been typed. It can + * also complete "" if a matching start tag is found. It will correctly ignore signal + * characters for empty tags, comments, CDATA, etc. + * + * The function depends on internal parser state to identify tags. It is compatible with the + * following CodeMirror modes and will ignore all others: + * - htmlmixed + * - xml + * + * See demos/closetag.html for a usage example. + * + * @author Nathan Williams+ * Contributed under the same license terms as CodeMirror. + */ +(function() { + /** Option that allows tag closing behavior to be toggled. Default is true. */ + CodeMirror.defaults['closeTagEnabled'] = true; + + /** Array of tag names to add indentation after the start tag for. Default is the list of block-level html tags. */ + CodeMirror.defaults['closeTagIndent'] = ['applet', 'blockquote', 'body', 'button', 'div', 'dl', 'fieldset', 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html', 'iframe', 'layer', 'legend', 'object', 'ol', 'p', 'select', 'table', 'ul']; + + /** + * Call during key processing to close tags. Handles the key event if the tag is closed, otherwise throws CodeMirror.Pass. + * - cm: The editor instance. + * - ch: The character being processed. + * - indent: Optional. Omit or pass true to use the default indentation tag list defined in the 'closeTagIndent' option. + * Pass false to disable indentation. Pass an array to override the default list of tag names. + */ + CodeMirror.defineExtension("closeTag", function(cm, ch, indent) { + if (!cm.getOption('closeTagEnabled')) { + throw CodeMirror.Pass; + } + + var mode = cm.getOption('mode'); + + if (mode == 'text/html') { + + /* + * Relevant structure of token: + * + * htmlmixed + * className + * state + * htmlState + * type + * context + * tagName + * mode + * + * xml + * className + * state + * tagName + * type + */ + + var pos = cm.getCursor(); + var tok = cm.getTokenAt(pos); + var state = tok.state; + + if (state.mode && state.mode != 'html') { + throw CodeMirror.Pass; // With htmlmixed, we only care about the html sub-mode. + } + + if (ch == '>') { + var type = state.htmlState ? state.htmlState.type : state.type; // htmlmixed : xml + + if (tok.className == 'tag' && type == 'closeTag') { + throw CodeMirror.Pass; // Don't process the '>' at the end of an end-tag. + } + + cm.replaceSelection('>'); // Mode state won't update until we finish the tag. + pos = {line: pos.line, ch: pos.ch + 1}; + cm.setCursor(pos); + + tok = cm.getTokenAt(cm.getCursor()); + state = tok.state; + type = state.htmlState ? state.htmlState.type : state.type; // htmlmixed : xml + + if (tok.className == 'tag' && type != 'selfcloseTag') { + var tagName = state.htmlState ? state.htmlState.context.tagName : state.tagName; // htmlmixed : xml + if (tagName.length > 0) { + insertEndTag(cm, indent, pos, tagName); + } + return; + } + + // Undo the '>' insert and allow cm to handle the key instead. + cm.setSelection({line: pos.line, ch: pos.ch - 1}, pos); + cm.replaceSelection(""); + + } else if (ch == '/') { + if (tok.className == 'tag' && tok.string == '<') { + var tagName = state.htmlState ? (state.htmlState.context ? state.htmlState.context.tagName : '') : state.context.tagName; // htmlmixed : xml # extra htmlmized check is for '' edge case + if (tagName.length > 0) { + completeEndTag(cm, pos, tagName); + return; + } + } + } + + } + + throw CodeMirror.Pass; // Bubble if not handled + }); + + function insertEndTag(cm, indent, pos, tagName) { + if (shouldIndent(cm, indent, tagName)) { + cm.replaceSelection('\n\n' + tagName + '>', 'end'); + cm.indentLine(pos.line + 1); + cm.indentLine(pos.line + 2); + cm.setCursor({line: pos.line + 1, ch: cm.getLine(pos.line + 1).length}); + } else { + cm.replaceSelection('' + tagName + '>'); + cm.setCursor(pos); + } + } + + function shouldIndent(cm, indent, tagName) { + if (typeof indent == 'undefined' || indent == null || indent == true) { + indent = cm.getOption('closeTagIndent'); + } + if (!indent) { + indent = []; + } + return indexOf(indent, tagName.toLowerCase()) != -1; + } + + // C&P from codemirror.js...would be nice if this were visible to utilities. + function indexOf(collection, elt) { + if (collection.indexOf) return collection.indexOf(elt); + for (var i = 0, e = collection.length; i < e; ++i) + if (collection[i] == elt) return i; + return -1; + } + + function completeEndTag(cm, pos, tagName) { + cm.replaceSelection('/' + tagName + '>'); + cm.setCursor({line: pos.line, ch: pos.ch + tagName.length + 2 }); + } + +})(); diff --git a/htdocs/static/js/codemirror/util/dialog.css b/htdocs/themes/bootstrap/js/codemirror/lib/util/dialog.css similarity index 100% rename from htdocs/static/js/codemirror/util/dialog.css rename to htdocs/themes/bootstrap/js/codemirror/lib/util/dialog.css diff --git a/htdocs/static/js/codemirror/util/dialog.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/dialog.js similarity index 100% rename from htdocs/static/js/codemirror/util/dialog.js rename to htdocs/themes/bootstrap/js/codemirror/lib/util/dialog.js diff --git a/htdocs/themes/bootstrap/js/codemirror/lib/util/foldcode.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/foldcode.js new file mode 100644 index 0000000..02cfb50 --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/lib/util/foldcode.js @@ -0,0 +1,196 @@ +// the tagRangeFinder function is +// Copyright (C) 2011 by Daniel Glazman +// released under the MIT license (../../LICENSE) like the rest of CodeMirror +CodeMirror.tagRangeFinder = function(cm, line, hideEnd) { + var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD"; + var nameChar = nameStartChar + "\-\:\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040"; + var xmlNAMERegExp = new RegExp("^[" + nameStartChar + "][" + nameChar + "]*"); + + var lineText = cm.getLine(line); + var found = false; + var tag = null; + var pos = 0; + while (!found) { + pos = lineText.indexOf("<", pos); + if (-1 == pos) // no tag on line + return; + if (pos + 1 < lineText.length && lineText[pos + 1] == "/") { // closing tag + pos++; + continue; + } + // ok we weem to have a start tag + if (!lineText.substr(pos + 1).match(xmlNAMERegExp)) { // not a tag name... + pos++; + continue; + } + var gtPos = lineText.indexOf(">", pos + 1); + if (-1 == gtPos) { // end of start tag not in line + var l = line + 1; + var foundGt = false; + var lastLine = cm.lineCount(); + while (l < lastLine && !foundGt) { + var lt = cm.getLine(l); + var gt = lt.indexOf(">"); + if (-1 != gt) { // found a > + foundGt = true; + var slash = lt.lastIndexOf("/", gt); + if (-1 != slash && slash < gt) { + var str = lineText.substr(slash, gt - slash + 1); + if (!str.match( /\/\s*\>/ )) { // yep, that's the end of empty tag + if (hideEnd === true) l++; + return l; + } + } + } + l++; + } + found = true; + } + else { + var slashPos = lineText.lastIndexOf("/", gtPos); + if (-1 == slashPos) { // cannot be empty tag + found = true; + // don't continue + } + else { // empty tag? + // check if really empty tag + var str = lineText.substr(slashPos, gtPos - slashPos + 1); + if (!str.match( /\/\s*\>/ )) { // finally not empty + found = true; + // don't continue + } + } + } + if (found) { + var subLine = lineText.substr(pos + 1); + tag = subLine.match(xmlNAMERegExp); + if (tag) { + // we have an element name, wooohooo ! + tag = tag[0]; + // do we have the close tag on same line ??? + if (-1 != lineText.indexOf("" + tag + ">", pos)) // yep + { + found = false; + } + // we don't, so we have a candidate... + } + else + found = false; + } + if (!found) + pos++; + } + + if (found) { + var startTag = "(\\<\\/" + tag + "\\>)|(\\<" + tag + "\\>)|(\\<" + tag + "\\s)|(\\<" + tag + "$)"; + var startTagRegExp = new RegExp(startTag, "g"); + var endTag = "" + tag + ">"; + var depth = 1; + var l = line + 1; + var lastLine = cm.lineCount(); + while (l < lastLine) { + lineText = cm.getLine(l); + var match = lineText.match(startTagRegExp); + if (match) { + for (var i = 0; i < match.length; i++) { + if (match[i] == endTag) + depth--; + else + depth++; + if (!depth) { + if (hideEnd === true) l++; + return l; + } + } + } + l++; + } + return; + } +}; + +CodeMirror.braceRangeFinder = function(cm, line, hideEnd) { + var lineText = cm.getLine(line), at = lineText.length, startChar, tokenType; + for (;;) { + var found = lineText.lastIndexOf("{", at); + if (found < 0) break; + tokenType = cm.getTokenAt({line: line, ch: found}).className; + if (!/^(comment|string)/.test(tokenType)) { startChar = found; break; } + at = found - 1; + } + if (startChar == null || lineText.lastIndexOf("}") > startChar) return; + var count = 1, lastLine = cm.lineCount(), end; + outer: for (var i = line + 1; i < lastLine; ++i) { + var text = cm.getLine(i), pos = 0; + for (;;) { + var nextOpen = text.indexOf("{", pos), nextClose = text.indexOf("}", pos); + if (nextOpen < 0) nextOpen = text.length; + if (nextClose < 0) nextClose = text.length; + pos = Math.min(nextOpen, nextClose); + if (pos == text.length) break; + if (cm.getTokenAt({line: i, ch: pos + 1}).className == tokenType) { + if (pos == nextOpen) ++count; + else if (!--count) { end = i; break outer; } + } + ++pos; + } + } + if (end == null || end == line + 1) return; + if (hideEnd === true) end++; + return end; +}; + +CodeMirror.indentRangeFinder = function(cm, line) { + var tabSize = cm.getOption("tabSize"); + var myIndent = cm.getLineHandle(line).indentation(tabSize), last; + for (var i = line + 1, end = cm.lineCount(); i < end; ++i) { + var handle = cm.getLineHandle(i); + if (!/^\s*$/.test(handle.text)) { + if (handle.indentation(tabSize) <= myIndent) break; + last = i; + } + } + if (!last) return null; + return last + 1; +}; + +CodeMirror.newFoldFunction = function(rangeFinder, markText, hideEnd) { + var folded = []; + if (markText == null) markText = ' ▼%N%'; + + function isFolded(cm, n) { + for (var i = 0; i < folded.length; ++i) { + var start = cm.lineInfo(folded[i].start); + if (!start) folded.splice(i--, 1); + else if (start.line == n) return {pos: i, region: folded[i]}; + } + } + + function expand(cm, region) { + cm.clearMarker(region.start); + for (var i = 0; i < region.hidden.length; ++i) + cm.showLine(region.hidden[i]); + } + + return function(cm, line) { + cm.operation(function() { + var known = isFolded(cm, line); + if (known) { + folded.splice(known.pos, 1); + expand(cm, known.region); + } else { + var end = rangeFinder(cm, line, hideEnd); + if (end == null) return; + var hidden = []; + for (var i = line + 1; i < end; ++i) { + var handle = cm.hideLine(i); + if (handle) hidden.push(handle); + } + var first = cm.setMarker(line, markText); + var region = {start: first, hidden: hidden}; + cm.onDeleteLine(first, function() { expand(cm, region); }); + folded.push(region); + } + }); + }; +}; diff --git a/htdocs/themes/bootstrap/js/codemirror/lib/util/formatting.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/formatting.js new file mode 100644 index 0000000..3a1a987 --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/lib/util/formatting.js @@ -0,0 +1,297 @@ +// ============== Formatting extensions ============================ +// A common storage for all mode-specific formatting features +if (!CodeMirror.modeExtensions) CodeMirror.modeExtensions = {}; + +// Returns the extension of the editor's current mode +CodeMirror.defineExtension("getModeExt", function () { + var mname = CodeMirror.resolveMode(this.getOption("mode")).name; + var ext = CodeMirror.modeExtensions[mname]; + if (!ext) throw new Error("No extensions found for mode " + mname); + return ext; +}); + +// If the current mode is 'htmlmixed', returns the extension of a mode located at +// the specified position (can be htmlmixed, css or javascript). Otherwise, simply +// returns the extension of the editor's current mode. +CodeMirror.defineExtension("getModeExtAtPos", function (pos) { + var token = this.getTokenAt(pos); + if (token && token.state && token.state.mode) + return CodeMirror.modeExtensions[token.state.mode == "html" ? "htmlmixed" : token.state.mode]; + else + return this.getModeExt(); +}); + +// Comment/uncomment the specified range +CodeMirror.defineExtension("commentRange", function (isComment, from, to) { + var curMode = this.getModeExtAtPos(this.getCursor()); + if (isComment) { // Comment range + var commentedText = this.getRange(from, to); + this.replaceRange(curMode.commentStart + this.getRange(from, to) + curMode.commentEnd + , from, to); + if (from.line == to.line && from.ch == to.ch) { // An empty comment inserted - put cursor inside + this.setCursor(from.line, from.ch + curMode.commentStart.length); + } + } + else { // Uncomment range + var selText = this.getRange(from, to); + var startIndex = selText.indexOf(curMode.commentStart); + var endIndex = selText.lastIndexOf(curMode.commentEnd); + if (startIndex > -1 && endIndex > -1 && endIndex > startIndex) { + // Take string till comment start + selText = selText.substr(0, startIndex) + // From comment start till comment end + + selText.substring(startIndex + curMode.commentStart.length, endIndex) + // From comment end till string end + + selText.substr(endIndex + curMode.commentEnd.length); + } + this.replaceRange(selText, from, to); + } +}); + +// Applies automatic mode-aware indentation to the specified range +CodeMirror.defineExtension("autoIndentRange", function (from, to) { + var cmInstance = this; + this.operation(function () { + for (var i = from.line; i <= to.line; i++) { + cmInstance.indentLine(i, "smart"); + } + }); +}); + +// Applies automatic formatting to the specified range +CodeMirror.defineExtension("autoFormatRange", function (from, to) { + var absStart = this.indexFromPos(from); + var absEnd = this.indexFromPos(to); + // Insert additional line breaks where necessary according to the + // mode's syntax + var res = this.getModeExt().autoFormatLineBreaks(this.getValue(), absStart, absEnd); + var cmInstance = this; + + // Replace and auto-indent the range + this.operation(function () { + cmInstance.replaceRange(res, from, to); + var startLine = cmInstance.posFromIndex(absStart).line; + var endLine = cmInstance.posFromIndex(absStart + res.length).line; + for (var i = startLine; i <= endLine; i++) { + cmInstance.indentLine(i, "smart"); + } + }); +}); + +// Define extensions for a few modes + +CodeMirror.modeExtensions["css"] = { + commentStart: "/*", + commentEnd: "*/", + wordWrapChars: [";", "\\{", "\\}"], + autoFormatLineBreaks: function (text, startPos, endPos) { + text = text.substring(startPos, endPos); + return text.replace(new RegExp("(;|\\{|\\})([^\r\n])", "g"), "$1\n$2"); + } +}; + +CodeMirror.modeExtensions["javascript"] = { + commentStart: "/*", + commentEnd: "*/", + wordWrapChars: [";", "\\{", "\\}"], + + getNonBreakableBlocks: function (text) { + var nonBreakableRegexes = [ + new RegExp("for\\s*?\\(([\\s\\S]*?)\\)"), + new RegExp("'([\\s\\S]*?)('|$)"), + new RegExp("\"([\\s\\S]*?)(\"|$)"), + new RegExp("//.*([\r\n]|$)") + ]; + var nonBreakableBlocks = new Array(); + for (var i = 0; i < nonBreakableRegexes.length; i++) { + var curPos = 0; + while (curPos < text.length) { + var m = text.substr(curPos).match(nonBreakableRegexes[i]); + if (m != null) { + nonBreakableBlocks.push({ + start: curPos + m.index, + end: curPos + m.index + m[0].length + }); + curPos += m.index + Math.max(1, m[0].length); + } + else { // No more matches + break; + } + } + } + nonBreakableBlocks.sort(function (a, b) { + return a.start - b.start; + }); + + return nonBreakableBlocks; + }, + + autoFormatLineBreaks: function (text, startPos, endPos) { + text = text.substring(startPos, endPos); + var curPos = 0; + var reLinesSplitter = new RegExp("(;|\\{|\\})([^\r\n])", "g"); + var nonBreakableBlocks = this.getNonBreakableBlocks(text); + if (nonBreakableBlocks != null) { + var res = ""; + for (var i = 0; i < nonBreakableBlocks.length; i++) { + if (nonBreakableBlocks[i].start > curPos) { // Break lines till the block + res += text.substring(curPos, nonBreakableBlocks[i].start).replace(reLinesSplitter, "$1\n$2"); + curPos = nonBreakableBlocks[i].start; + } + if (nonBreakableBlocks[i].start <= curPos + && nonBreakableBlocks[i].end >= curPos) { // Skip non-breakable block + res += text.substring(curPos, nonBreakableBlocks[i].end); + curPos = nonBreakableBlocks[i].end; + } + } + if (curPos < text.length - 1) { + res += text.substr(curPos).replace(reLinesSplitter, "$1\n$2"); + } + return res; + } + else { + return text.replace(reLinesSplitter, "$1\n$2"); + } + } +}; + +CodeMirror.modeExtensions["xml"] = { + commentStart: "", + wordWrapChars: [">"], + + autoFormatLineBreaks: function (text, startPos, endPos) { + text = text.substring(startPos, endPos); + var lines = text.split("\n"); + var reProcessedPortion = new RegExp("(^\\s*?<|^[^<]*?)(.+)(>\\s*?$|[^>]*?$)"); + var reOpenBrackets = new RegExp("<", "g"); + var reCloseBrackets = new RegExp("(>)([^\r\n])", "g"); + for (var i = 0; i < lines.length; i++) { + var mToProcess = lines[i].match(reProcessedPortion); + if (mToProcess != null && mToProcess.length > 3) { // The line starts with whitespaces and ends with whitespaces + lines[i] = mToProcess[1] + + mToProcess[2].replace(reOpenBrackets, "\n$&").replace(reCloseBrackets, "$1\n$2") + + mToProcess[3]; + continue; + } + } + + return lines.join("\n"); + } +}; + +CodeMirror.modeExtensions["htmlmixed"] = { + commentStart: "", + wordWrapChars: [">", ";", "\\{", "\\}"], + + getModeInfos: function (text, absPos) { + var modeInfos = new Array(); + modeInfos[0] = + { + pos: 0, + modeExt: CodeMirror.modeExtensions["xml"], + modeName: "xml" + }; + + var modeMatchers = new Array(); + modeMatchers[0] = + { + regex: new RegExp("]*>|$)", "i"), + modeExt: CodeMirror.modeExtensions["css"], + modeName: "css" + }; + modeMatchers[1] = + { + regex: new RegExp("]*>|$)", "i"), + modeExt: CodeMirror.modeExtensions["javascript"], + modeName: "javascript" + }; + + var lastCharPos = (typeof (absPos) !== "undefined" ? absPos : text.length - 1); + // Detect modes for the entire text + for (var i = 0; i < modeMatchers.length; i++) { + var curPos = 0; + while (curPos <= lastCharPos) { + var m = text.substr(curPos).match(modeMatchers[i].regex); + if (m != null) { + if (m.length > 1 && m[1].length > 0) { + // Push block begin pos + var blockBegin = curPos + m.index + m[0].indexOf(m[1]); + modeInfos.push( + { + pos: blockBegin, + modeExt: modeMatchers[i].modeExt, + modeName: modeMatchers[i].modeName + }); + // Push block end pos + modeInfos.push( + { + pos: blockBegin + m[1].length, + modeExt: modeInfos[0].modeExt, + modeName: modeInfos[0].modeName + }); + curPos += m.index + m[0].length; + continue; + } + else { + curPos += m.index + Math.max(m[0].length, 1); + } + } + else { // No more matches + break; + } + } + } + // Sort mode infos + modeInfos.sort(function sortModeInfo(a, b) { + return a.pos - b.pos; + }); + + return modeInfos; + }, + + autoFormatLineBreaks: function (text, startPos, endPos) { + var modeInfos = this.getModeInfos(text); + var reBlockStartsWithNewline = new RegExp("^\\s*?\n"); + var reBlockEndsWithNewline = new RegExp("\n\\s*?$"); + var res = ""; + // Use modes info to break lines correspondingly + if (modeInfos.length > 1) { // Deal with multi-mode text + for (var i = 1; i <= modeInfos.length; i++) { + var selStart = modeInfos[i - 1].pos; + var selEnd = (i < modeInfos.length ? modeInfos[i].pos : endPos); + + if (selStart >= endPos) { // The block starts later than the needed fragment + break; + } + if (selStart < startPos) { + if (selEnd <= startPos) { // The block starts earlier than the needed fragment + continue; + } + selStart = startPos; + } + if (selEnd > endPos) { + selEnd = endPos; + } + var textPortion = text.substring(selStart, selEnd); + if (modeInfos[i - 1].modeName != "xml") { // Starting a CSS or JavaScript block + if (!reBlockStartsWithNewline.test(textPortion) + && selStart > 0) { // The block does not start with a line break + textPortion = "\n" + textPortion; + } + if (!reBlockEndsWithNewline.test(textPortion) + && selEnd < text.length - 1) { // The block does not end with a line break + textPortion += "\n"; + } + } + res += modeInfos[i - 1].modeExt.autoFormatLineBreaks(textPortion); + } + } + else { // Single-mode text + res = modeInfos[0].modeExt.autoFormatLineBreaks(text.substring(startPos, endPos)); + } + + return res; + } +}; diff --git a/htdocs/static/js/codemirror/util/javascript-hint.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/javascript-hint.js similarity index 100% rename from htdocs/static/js/codemirror/util/javascript-hint.js rename to htdocs/themes/bootstrap/js/codemirror/lib/util/javascript-hint.js diff --git a/htdocs/themes/bootstrap/js/codemirror/lib/util/loadmode.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/loadmode.js new file mode 100644 index 0000000..48d5a7a --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/lib/util/loadmode.js @@ -0,0 +1,51 @@ +(function() { + if (!CodeMirror.modeURL) CodeMirror.modeURL = "../mode/%N/%N.js"; + + var loading = {}; + function splitCallback(cont, n) { + var countDown = n; + return function() { if (--countDown == 0) cont(); } + } + function ensureDeps(mode, cont) { + var deps = CodeMirror.modes[mode].dependencies; + if (!deps) return cont(); + var missing = []; + for (var i = 0; i < deps.length; ++i) { + if (!CodeMirror.modes.hasOwnProperty(deps[i])) + missing.push(deps[i]); + } + if (!missing.length) return cont(); + var split = splitCallback(cont, missing.length); + for (var i = 0; i < missing.length; ++i) + CodeMirror.requireMode(missing[i], split); + } + + CodeMirror.requireMode = function(mode, cont) { + if (typeof mode != "string") mode = mode.name; + if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont); + if (loading.hasOwnProperty(mode)) return loading[mode].push(cont); + + var script = document.createElement("script"); + script.src = CodeMirror.modeURL.replace(/%N/g, mode); + var others = document.getElementsByTagName("script")[0]; + others.parentNode.insertBefore(script, others); + var list = loading[mode] = [cont]; + var count = 0, poll = setInterval(function() { + if (++count > 100) return clearInterval(poll); + if (CodeMirror.modes.hasOwnProperty(mode)) { + clearInterval(poll); + loading[mode] = null; + ensureDeps(mode, function() { + for (var i = 0; i < list.length; ++i) list[i](); + }); + } + }, 200); + }; + + CodeMirror.autoLoadMode = function(instance, mode) { + if (!CodeMirror.modes.hasOwnProperty(mode)) + CodeMirror.requireMode(mode, function() { + instance.setOption("mode", instance.getOption("mode")); + }); + }; +}()); diff --git a/htdocs/themes/bootstrap/js/codemirror/lib/util/match-highlighter.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/match-highlighter.js new file mode 100644 index 0000000..59098ff --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/lib/util/match-highlighter.js @@ -0,0 +1,44 @@ +// Define match-highlighter commands. Depends on searchcursor.js +// Use by attaching the following function call to the onCursorActivity event: + //myCodeMirror.matchHighlight(minChars); +// And including a special span.CodeMirror-matchhighlight css class (also optionally a separate one for .CodeMirror-focused -- see demo matchhighlighter.html) + +(function() { + var DEFAULT_MIN_CHARS = 2; + + function MatchHighlightState() { + this.marked = []; + } + function getMatchHighlightState(cm) { + return cm._matchHighlightState || (cm._matchHighlightState = new MatchHighlightState()); + } + + function clearMarks(cm) { + var state = getMatchHighlightState(cm); + for (var i = 0; i < state.marked.length; ++i) + state.marked[i].clear(); + state.marked = []; + } + + function markDocument(cm, className, minChars) { + clearMarks(cm); + minChars = (typeof minChars !== 'undefined' ? minChars : DEFAULT_MIN_CHARS); + if (cm.somethingSelected() && cm.getSelection().replace(/^\s+|\s+$/g, "").length >= minChars) { + var state = getMatchHighlightState(cm); + var query = cm.getSelection(); + cm.operation(function() { + if (cm.lineCount() < 2000) { // This is too expensive on big documents. + for (var cursor = cm.getSearchCursor(query); cursor.findNext();) { + //Only apply matchhighlight to the matches other than the one actually selected + if (!(cursor.from().line === cm.getCursor(true).line && cursor.from().ch === cm.getCursor(true).ch)) + state.marked.push(cm.markText(cursor.from(), cursor.to(), className)); + } + } + }); + } + } + + CodeMirror.defineExtension("matchHighlight", function(className, minChars) { + markDocument(this, className, minChars); + }); +})(); diff --git a/htdocs/themes/bootstrap/js/codemirror/lib/util/multiplex.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/multiplex.js new file mode 100644 index 0000000..822ee62 --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/lib/util/multiplex.js @@ -0,0 +1,72 @@ +CodeMirror.multiplexingMode = function(outer /*, others */) { + // Others should be {open, close, mode [, delimStyle]} objects + var others = Array.prototype.slice.call(arguments, 1); + var n_others = others.length; + + return { + startState: function() { + return { + outer: CodeMirror.startState(outer), + innerActive: null, + inner: null + }; + }, + + copyState: function(state) { + return { + outer: CodeMirror.copyState(outer, state.outer), + innerActive: state.innerActive, + inner: state.innerActive && CodeMirror.copyState(state.innerActive.mode, state.inner) + }; + }, + + token: function(stream, state) { + if (!state.innerActive) { + for (var i = 0; i < n_others; ++i) { + var other = others[i]; + if (stream.match(other.open)) { + state.innerActive = other; + state.inner = CodeMirror.startState(other.mode); + return other.delimStyle; + } + } + var outerToken = outer.token(stream, state.outer); + var cur = stream.current(); + for (var i = 0; i < n_others; ++i) { + var other = others[i], found = cur.indexOf(other.open); + if (found > -1) { + stream.backUp(cur.length - found); + cur = cur.slice(0, found); + } + } + return outerToken; + } else { + var curInner = state.innerActive; + if (stream.match(curInner.close)) { + state.innerActive = state.inner = null; + return curInner.delimStyle; + } + var innerToken = curInner.mode.token(stream, state.inner); + var cur = stream.current(), found = cur.indexOf(curInner.close); + if (found > -1) stream.backUp(cur.length - found); + return innerToken; + } + }, + + indent: function(state, textAfter) { + var mode = state.innerActive || outer; + if (!mode.indent) return CodeMirror.Pass; + return mode.indent(state.innerActive ? state.inner : state.outer, textAfter); + }, + + compareStates: function(a, b) { + if (a.innerActive != b.innerActive) return false; + var mode = a.innerActive || outer; + if (!mode.compareStates) return CodeMirror.Pass; + return mode.compareStates(a.innerActive ? a.inner : a.outer, + b.innerActive ? b.inner : b.outer); + }, + + electricChars: outer.electricChars + }; +}; diff --git a/htdocs/themes/bootstrap/js/codemirror/lib/util/overlay.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/overlay.js new file mode 100644 index 0000000..1d5df6c --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/lib/util/overlay.js @@ -0,0 +1,52 @@ +// Utility function that allows modes to be combined. The mode given +// as the base argument takes care of most of the normal mode +// functionality, but a second (typically simple) mode is used, which +// can override the style of text. Both modes get to parse all of the +// text, but when both assign a non-null style to a piece of code, the +// overlay wins, unless the combine argument was true, in which case +// the styles are combined. + +// overlayParser is the old, deprecated name +CodeMirror.overlayMode = CodeMirror.overlayParser = function(base, overlay, combine) { + return { + startState: function() { + return { + base: CodeMirror.startState(base), + overlay: CodeMirror.startState(overlay), + basePos: 0, baseCur: null, + overlayPos: 0, overlayCur: null + }; + }, + copyState: function(state) { + return { + base: CodeMirror.copyState(base, state.base), + overlay: CodeMirror.copyState(overlay, state.overlay), + basePos: state.basePos, baseCur: null, + overlayPos: state.overlayPos, overlayCur: null + }; + }, + + token: function(stream, state) { + if (stream.start == state.basePos) { + state.baseCur = base.token(stream, state.base); + state.basePos = stream.pos; + } + if (stream.start == state.overlayPos) { + stream.pos = stream.start; + state.overlayCur = overlay.token(stream, state.overlay); + state.overlayPos = stream.pos; + } + stream.pos = Math.min(state.basePos, state.overlayPos); + if (stream.eol()) state.basePos = state.overlayPos = 0; + + if (state.overlayCur == null) return state.baseCur; + if (state.baseCur != null && combine) return state.baseCur + " " + state.overlayCur; + else return state.overlayCur; + }, + + indent: base.indent && function(state, textAfter) { + return base.indent(state.base, textAfter); + }, + electricChars: base.electricChars + }; +}; diff --git a/htdocs/themes/bootstrap/js/codemirror/lib/util/pig-hint.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/pig-hint.js new file mode 100644 index 0000000..233b72b --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/lib/util/pig-hint.js @@ -0,0 +1,123 @@ +(function () { + function forEach(arr, f) { + for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); + } + + function arrayContains(arr, item) { + if (!Array.prototype.indexOf) { + var i = arr.length; + while (i--) { + if (arr[i] === item) { + return true; + } + } + return false; + } + return arr.indexOf(item) != -1; + } + + function scriptHint(editor, keywords, getToken) { + // Find the token at the cursor + var cur = editor.getCursor(), token = getToken(editor, cur), tprop = token; + // If it's not a 'word-style' token, ignore the token. + + if (!/^[\w$_]*$/.test(token.string)) { + token = tprop = {start: cur.ch, end: cur.ch, string: "", state: token.state, + className: token.string == ":" ? "pig-type" : null}; + } + + if (!context) var context = []; + context.push(tprop); + + completionList = getCompletions(token, context); + completionList = completionList.sort(); + //prevent autocomplete for last word, instead show dropdown with one word + if(completionList.length == 1) { + completionList.push(" "); + } + + return {list: completionList, + from: {line: cur.line, ch: token.start}, + to: {line: cur.line, ch: token.end}}; + } + + CodeMirror.pigHint = function(editor) { + return scriptHint(editor, pigKeywordsU, function (e, cur) {return e.getTokenAt(cur);}); + } + + function toTitleCase(str) { + return str.replace(/(?:^|\s)\w/g, function(match) { + return match.toUpperCase(); + }); + } + + var pigKeywords = "VOID IMPORT RETURNS DEFINE LOAD FILTER FOREACH ORDER CUBE DISTINCT COGROUP " + + "JOIN CROSS UNION SPLIT INTO IF OTHERWISE ALL AS BY USING INNER OUTER ONSCHEMA PARALLEL " + + "PARTITION GROUP AND OR NOT GENERATE FLATTEN ASC DESC IS STREAM THROUGH STORE MAPREDUCE " + + "SHIP CACHE INPUT OUTPUT STDERROR STDIN STDOUT LIMIT SAMPLE LEFT RIGHT FULL EQ GT LT GTE LTE " + + "NEQ MATCHES TRUE FALSE"; + var pigKeywordsU = pigKeywords.split(" "); + var pigKeywordsL = pigKeywords.toLowerCase().split(" "); + + var pigTypes = "BOOLEAN INT LONG FLOAT DOUBLE CHARARRAY BYTEARRAY BAG TUPLE MAP"; + var pigTypesU = pigTypes.split(" "); + var pigTypesL = pigTypes.toLowerCase().split(" "); + + var pigBuiltins = "ABS ACOS ARITY ASIN ATAN AVG BAGSIZE BINSTORAGE BLOOM BUILDBLOOM CBRT CEIL " + + "CONCAT COR COS COSH COUNT COUNT_STAR COV CONSTANTSIZE CUBEDIMENSIONS DIFF DISTINCT DOUBLEABS " + + "DOUBLEAVG DOUBLEBASE DOUBLEMAX DOUBLEMIN DOUBLEROUND DOUBLESUM EXP FLOOR FLOATABS FLOATAVG " + + "FLOATMAX FLOATMIN FLOATROUND FLOATSUM GENERICINVOKER INDEXOF INTABS INTAVG INTMAX INTMIN " + + "INTSUM INVOKEFORDOUBLE INVOKEFORFLOAT INVOKEFORINT INVOKEFORLONG INVOKEFORSTRING INVOKER " + + "ISEMPTY JSONLOADER JSONMETADATA JSONSTORAGE LAST_INDEX_OF LCFIRST LOG LOG10 LOWER LONGABS " + + "LONGAVG LONGMAX LONGMIN LONGSUM MAX MIN MAPSIZE MONITOREDUDF NONDETERMINISTIC OUTPUTSCHEMA " + + "PIGSTORAGE PIGSTREAMING RANDOM REGEX_EXTRACT REGEX_EXTRACT_ALL REPLACE ROUND SIN SINH SIZE " + + "SQRT STRSPLIT SUBSTRING SUM STRINGCONCAT STRINGMAX STRINGMIN STRINGSIZE TAN TANH TOBAG " + + "TOKENIZE TOMAP TOP TOTUPLE TRIM TEXTLOADER TUPLESIZE UCFIRST UPPER UTF8STORAGECONVERTER"; + var pigBuiltinsU = pigBuiltins.split(" ").join("() ").split(" "); + var pigBuiltinsL = pigBuiltins.toLowerCase().split(" ").join("() ").split(" "); + var pigBuiltinsC = ("BagSize BinStorage Bloom BuildBloom ConstantSize CubeDimensions DoubleAbs " + + "DoubleAvg DoubleBase DoubleMax DoubleMin DoubleRound DoubleSum FloatAbs FloatAvg FloatMax " + + "FloatMin FloatRound FloatSum GenericInvoker IntAbs IntAvg IntMax IntMin IntSum " + + "InvokeForDouble InvokeForFloat InvokeForInt InvokeForLong InvokeForString Invoker " + + "IsEmpty JsonLoader JsonMetadata JsonStorage LongAbs LongAvg LongMax LongMin LongSum MapSize " + + "MonitoredUDF Nondeterministic OutputSchema PigStorage PigStreaming StringConcat StringMax " + + "StringMin StringSize TextLoader TupleSize Utf8StorageConverter").split(" ").join("() ").split(" "); + + function getCompletions(token, context) { + var found = [], start = token.string; + function maybeAdd(str) { + if (str.indexOf(start) == 0 && !arrayContains(found, str)) found.push(str); + } + + function gatherCompletions(obj) { + if(obj == ":") { + forEach(pigTypesL, maybeAdd); + } + else { + forEach(pigBuiltinsU, maybeAdd); + forEach(pigBuiltinsL, maybeAdd); + forEach(pigBuiltinsC, maybeAdd); + forEach(pigTypesU, maybeAdd); + forEach(pigTypesL, maybeAdd); + forEach(pigKeywordsU, maybeAdd); + forEach(pigKeywordsL, maybeAdd); + } + } + + if (context) { + // If this is a property, see if it belongs to some object we can + // find in the current environment. + var obj = context.pop(), base; + + if (obj.className == "pig-word") + base = obj.string; + else if(obj.className == "pig-type") + base = ":" + obj.string; + + while (base != null && context.length) + base = base[context.pop().string]; + if (base != null) gatherCompletions(base); + } + return found; + } +})(); diff --git a/htdocs/static/js/codemirror/util/runmode.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/runmode.js similarity index 100% rename from htdocs/static/js/codemirror/util/runmode.js rename to htdocs/themes/bootstrap/js/codemirror/lib/util/runmode.js diff --git a/htdocs/themes/bootstrap/js/codemirror/lib/util/search.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/search.js new file mode 100644 index 0000000..c5a2bcc --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/lib/util/search.js @@ -0,0 +1,118 @@ +// Define search commands. Depends on dialog.js or another +// implementation of the openDialog method. + +// Replace works a little oddly -- it will do the replace on the next +// Ctrl-G (or whatever is bound to findNext) press. You prevent a +// replace by making sure the match is no longer selected when hitting +// Ctrl-G. + +(function() { + function SearchState() { + this.posFrom = this.posTo = this.query = null; + this.marked = []; + } + function getSearchState(cm) { + return cm._searchState || (cm._searchState = new SearchState()); + } + function getSearchCursor(cm, query, pos) { + // Heuristic: if the query string is all lowercase, do a case insensitive search. + return cm.getSearchCursor(query, pos, typeof query == "string" && query == query.toLowerCase()); + } + function dialog(cm, text, shortText, f) { + if (cm.openDialog) cm.openDialog(text, f); + else f(prompt(shortText, "")); + } + function confirmDialog(cm, text, shortText, fs) { + if (cm.openConfirm) cm.openConfirm(text, fs); + else if (confirm(shortText)) fs[0](); + } + function parseQuery(query) { + var isRE = query.match(/^\/(.*)\/([a-z]*)$/); + return isRE ? new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i") : query; + } + var queryDialog = + 'Search: (Use /re/ syntax for regexp search)'; + function doSearch(cm, rev) { + var state = getSearchState(cm); + if (state.query) return findNext(cm, rev); + dialog(cm, queryDialog, "Search for:", function(query) { + cm.operation(function() { + if (!query || state.query) return; + state.query = parseQuery(query); + if (cm.lineCount() < 2000) { // This is too expensive on big documents. + for (var cursor = getSearchCursor(cm, query); cursor.findNext();) + state.marked.push(cm.markText(cursor.from(), cursor.to(), "CodeMirror-searching")); + } + state.posFrom = state.posTo = cm.getCursor(); + findNext(cm, rev); + }); + }); + } + function findNext(cm, rev) {cm.operation(function() { + var state = getSearchState(cm); + var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo); + if (!cursor.find(rev)) { + cursor = getSearchCursor(cm, state.query, rev ? {line: cm.lineCount() - 1} : {line: 0, ch: 0}); + if (!cursor.find(rev)) return; + } + cm.setSelection(cursor.from(), cursor.to()); + state.posFrom = cursor.from(); state.posTo = cursor.to(); + })} + function clearSearch(cm) {cm.operation(function() { + var state = getSearchState(cm); + if (!state.query) return; + state.query = null; + for (var i = 0; i < state.marked.length; ++i) state.marked[i].clear(); + state.marked.length = 0; + })} + + var replaceQueryDialog = + 'Replace: (Use /re/ syntax for regexp search)'; + var replacementQueryDialog = 'With: '; + var doReplaceConfirm = "Replace? "; + function replace(cm, all) { + dialog(cm, replaceQueryDialog, "Replace:", function(query) { + if (!query) return; + query = parseQuery(query); + dialog(cm, replacementQueryDialog, "Replace with:", function(text) { + if (all) { + cm.compoundChange(function() { cm.operation(function() { + for (var cursor = getSearchCursor(cm, query); cursor.findNext();) { + if (typeof query != "string") { + var match = cm.getRange(cursor.from(), cursor.to()).match(query); + cursor.replace(text.replace(/\$(\d)/, function(w, i) {return match[i];})); + } else cursor.replace(text); + } + })}); + } else { + clearSearch(cm); + var cursor = getSearchCursor(cm, query, cm.getCursor()); + function advance() { + var start = cursor.from(), match; + if (!(match = cursor.findNext())) { + cursor = getSearchCursor(cm, query); + if (!(match = cursor.findNext()) || + (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return; + } + cm.setSelection(cursor.from(), cursor.to()); + confirmDialog(cm, doReplaceConfirm, "Replace?", + [function() {doReplace(match);}, advance]); + } + function doReplace(match) { + cursor.replace(typeof query == "string" ? text : + text.replace(/\$(\d)/, function(w, i) {return match[i];})); + advance(); + } + advance(); + } + }); + }); + } + + CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);}; + CodeMirror.commands.findNext = doSearch; + CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);}; + CodeMirror.commands.clearSearch = clearSearch; + CodeMirror.commands.replace = replace; + CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);}; +})(); diff --git a/htdocs/themes/bootstrap/js/codemirror/lib/util/searchcursor.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/searchcursor.js new file mode 100644 index 0000000..ec3f73c --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/lib/util/searchcursor.js @@ -0,0 +1,117 @@ +(function(){ + function SearchCursor(cm, query, pos, caseFold) { + this.atOccurrence = false; this.cm = cm; + if (caseFold == null && typeof query == "string") caseFold = false; + + pos = pos ? cm.clipPos(pos) : {line: 0, ch: 0}; + this.pos = {from: pos, to: pos}; + + // The matches method is filled in based on the type of query. + // It takes a position and a direction, and returns an object + // describing the next occurrence of the query, or null if no + // more matches were found. + if (typeof query != "string") // Regexp match + this.matches = function(reverse, pos) { + if (reverse) { + var line = cm.getLine(pos.line).slice(0, pos.ch), match = line.match(query), start = 0; + while (match) { + var ind = line.indexOf(match[0]); + start += ind; + line = line.slice(ind + 1); + var newmatch = line.match(query); + if (newmatch) match = newmatch; + else break; + start++; + } + } + else { + var line = cm.getLine(pos.line).slice(pos.ch), match = line.match(query), + start = match && pos.ch + line.indexOf(match[0]); + } + if (match) + return {from: {line: pos.line, ch: start}, + to: {line: pos.line, ch: start + match[0].length}, + match: match}; + }; + else { // String query + if (caseFold) query = query.toLowerCase(); + var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;}; + var target = query.split("\n"); + // Different methods for single-line and multi-line queries + if (target.length == 1) + this.matches = function(reverse, pos) { + var line = fold(cm.getLine(pos.line)), len = query.length, match; + if (reverse ? (pos.ch >= len && (match = line.lastIndexOf(query, pos.ch - len)) != -1) + : (match = line.indexOf(query, pos.ch)) != -1) + return {from: {line: pos.line, ch: match}, + to: {line: pos.line, ch: match + len}}; + }; + else + this.matches = function(reverse, pos) { + var ln = pos.line, idx = (reverse ? target.length - 1 : 0), match = target[idx], line = fold(cm.getLine(ln)); + var offsetA = (reverse ? line.indexOf(match) + match.length : line.lastIndexOf(match)); + if (reverse ? offsetA >= pos.ch || offsetA != match.length + : offsetA <= pos.ch || offsetA != line.length - match.length) + return; + for (;;) { + if (reverse ? !ln : ln == cm.lineCount() - 1) return; + line = fold(cm.getLine(ln += reverse ? -1 : 1)); + match = target[reverse ? --idx : ++idx]; + if (idx > 0 && idx < target.length - 1) { + if (line != match) return; + else continue; + } + var offsetB = (reverse ? line.lastIndexOf(match) : line.indexOf(match) + match.length); + if (reverse ? offsetB != line.length - match.length : offsetB != match.length) + return; + var start = {line: pos.line, ch: offsetA}, end = {line: ln, ch: offsetB}; + return {from: reverse ? end : start, to: reverse ? start : end}; + } + }; + } + } + + SearchCursor.prototype = { + findNext: function() {return this.find(false);}, + findPrevious: function() {return this.find(true);}, + + find: function(reverse) { + var self = this, pos = this.cm.clipPos(reverse ? this.pos.from : this.pos.to); + function savePosAndFail(line) { + var pos = {line: line, ch: 0}; + self.pos = {from: pos, to: pos}; + self.atOccurrence = false; + return false; + } + + for (;;) { + if (this.pos = this.matches(reverse, pos)) { + this.atOccurrence = true; + return this.pos.match || true; + } + if (reverse) { + if (!pos.line) return savePosAndFail(0); + pos = {line: pos.line-1, ch: this.cm.getLine(pos.line-1).length}; + } + else { + var maxLine = this.cm.lineCount(); + if (pos.line == maxLine - 1) return savePosAndFail(maxLine); + pos = {line: pos.line+1, ch: 0}; + } + } + }, + + from: function() {if (this.atOccurrence) return this.pos.from;}, + to: function() {if (this.atOccurrence) return this.pos.to;}, + + replace: function(newText) { + var self = this; + if (this.atOccurrence) + self.pos.to = this.cm.replaceRange(newText, self.pos.from, self.pos.to); + } + }; + + CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) { + return new SearchCursor(this, query, pos, caseFold); + }); +})(); diff --git a/htdocs/static/js/codemirror/util/simple-hint.css b/htdocs/themes/bootstrap/js/codemirror/lib/util/simple-hint.css similarity index 100% rename from htdocs/static/js/codemirror/util/simple-hint.css rename to htdocs/themes/bootstrap/js/codemirror/lib/util/simple-hint.css diff --git a/htdocs/static/js/codemirror/util/simple-hint.js b/htdocs/themes/bootstrap/js/codemirror/lib/util/simple-hint.js similarity index 100% rename from htdocs/static/js/codemirror/util/simple-hint.js rename to htdocs/themes/bootstrap/js/codemirror/lib/util/simple-hint.js diff --git a/htdocs/themes/bootstrap/js/codemirror/mode/clike/clike.js b/htdocs/themes/bootstrap/js/codemirror/mode/clike/clike.js new file mode 100644 index 0000000..0e7f48b --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/mode/clike/clike.js @@ -0,0 +1,271 @@ +CodeMirror.defineMode("clike", function(config, parserConfig) { + var indentUnit = config.indentUnit, + keywords = parserConfig.keywords || {}, + blockKeywords = parserConfig.blockKeywords || {}, + atoms = parserConfig.atoms || {}, + hooks = parserConfig.hooks || {}, + multiLineStrings = parserConfig.multiLineStrings; + var isOperatorChar = /[+\-*&%=<>!?|\/]/; + + var curPunc; + + function tokenBase(stream, state) { + var ch = stream.next(); + if (hooks[ch]) { + var result = hooks[ch](stream, state); + if (result !== false) return result; + } + if (ch == '"' || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } + if (/[\[\]{}\(\),;\:\.]/.test(ch)) { + curPunc = ch; + return null; + } + if (/\d/.test(ch)) { + stream.eatWhile(/[\w\.]/); + return "number"; + } + if (ch == "/") { + if (stream.eat("*")) { + state.tokenize = tokenComment; + return tokenComment(stream, state); + } + if (stream.eat("/")) { + stream.skipToEnd(); + return "comment"; + } + } + if (isOperatorChar.test(ch)) { + stream.eatWhile(isOperatorChar); + return "operator"; + } + stream.eatWhile(/[\w\$_]/); + var cur = stream.current(); + if (keywords.propertyIsEnumerable(cur)) { + if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement"; + return "keyword"; + } + if (atoms.propertyIsEnumerable(cur)) return "atom"; + return "word"; + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, next, end = false; + while ((next = stream.next()) != null) { + if (next == quote && !escaped) {end = true; break;} + escaped = !escaped && next == "\\"; + } + if (end || !(escaped || multiLineStrings)) + state.tokenize = null; + return "string"; + }; + } + + function tokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = null; + break; + } + maybeEnd = (ch == "*"); + } + return "comment"; + } + + function Context(indented, column, type, align, prev) { + this.indented = indented; + this.column = column; + this.type = type; + this.align = align; + this.prev = prev; + } + function pushContext(state, col, type) { + return state.context = new Context(state.indented, col, type, null, state.context); + } + function popContext(state) { + var t = state.context.type; + if (t == ")" || t == "]" || t == "}") + state.indented = state.context.indented; + return state.context = state.context.prev; + } + + // Interface + + return { + startState: function(basecolumn) { + return { + tokenize: null, + context: new Context((basecolumn || 0) - indentUnit, 0, "top", false), + indented: 0, + startOfLine: true + }; + }, + + token: function(stream, state) { + var ctx = state.context; + if (stream.sol()) { + if (ctx.align == null) ctx.align = false; + state.indented = stream.indentation(); + state.startOfLine = true; + } + if (stream.eatSpace()) return null; + curPunc = null; + var style = (state.tokenize || tokenBase)(stream, state); + if (style == "comment" || style == "meta") return style; + if (ctx.align == null) ctx.align = true; + + if ((curPunc == ";" || curPunc == ":") && ctx.type == "statement") popContext(state); + else if (curPunc == "{") pushContext(state, stream.column(), "}"); + else if (curPunc == "[") pushContext(state, stream.column(), "]"); + else if (curPunc == "(") pushContext(state, stream.column(), ")"); + else if (curPunc == "}") { + while (ctx.type == "statement") ctx = popContext(state); + if (ctx.type == "}") ctx = popContext(state); + while (ctx.type == "statement") ctx = popContext(state); + } + else if (curPunc == ctx.type) popContext(state); + else if (ctx.type == "}" || ctx.type == "top" || (ctx.type == "statement" && curPunc == "newstatement")) + pushContext(state, stream.column(), "statement"); + state.startOfLine = false; + return style; + }, + + indent: function(state, textAfter) { + if (state.tokenize != tokenBase && state.tokenize != null) return 0; + var ctx = state.context, firstChar = textAfter && textAfter.charAt(0); + if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev; + var closing = firstChar == ctx.type; + if (ctx.type == "statement") return ctx.indented + (firstChar == "{" ? 0 : indentUnit); + else if (ctx.align) return ctx.column + (closing ? 0 : 1); + else return ctx.indented + (closing ? 0 : indentUnit); + }, + + electricChars: "{}" + }; +}); + +(function() { + function words(str) { + var obj = {}, words = str.split(" "); + for (var i = 0; i < words.length; ++i) obj[words[i]] = true; + return obj; + } + var cKeywords = "auto if break int case long char register continue return default short do sizeof " + + "double static else struct entry switch extern typedef float union for unsigned " + + "goto while enum void const signed volatile"; + + function cppHook(stream, state) { + if (!state.startOfLine) return false; + stream.skipToEnd(); + return "meta"; + } + + // C#-style strings where "" escapes a quote. + function tokenAtString(stream, state) { + var next; + while ((next = stream.next()) != null) { + if (next == '"' && !stream.eat('"')) { + state.tokenize = null; + break; + } + } + return "string"; + } + + CodeMirror.defineMIME("text/x-csrc", { + name: "clike", + keywords: words(cKeywords), + blockKeywords: words("case do else for if switch while struct"), + atoms: words("null"), + hooks: {"#": cppHook} + }); + CodeMirror.defineMIME("text/x-c++src", { + name: "clike", + keywords: words(cKeywords + " asm dynamic_cast namespace reinterpret_cast try bool explicit new " + + "static_cast typeid catch operator template typename class friend private " + + "this using const_cast inline public throw virtual delete mutable protected " + + "wchar_t"), + blockKeywords: words("catch class do else finally for if struct switch try while"), + atoms: words("true false null"), + hooks: {"#": cppHook} + }); + CodeMirror.defineMIME("text/x-java", { + name: "clike", + keywords: words("abstract assert boolean break byte case catch char class const continue default " + + "do double else enum extends final finally float for goto if implements import " + + "instanceof int interface long native new package private protected public " + + "return short static strictfp super switch synchronized this throw throws transient " + + "try void volatile while"), + blockKeywords: words("catch class do else finally for if switch try while"), + atoms: words("true false null"), + hooks: { + "@": function(stream, state) { + stream.eatWhile(/[\w\$_]/); + return "meta"; + } + } + }); + CodeMirror.defineMIME("text/x-csharp", { + name: "clike", + keywords: words("abstract as base bool break byte case catch char checked class const continue decimal" + + " default delegate do double else enum event explicit extern finally fixed float for" + + " foreach goto if implicit in int interface internal is lock long namespace new object" + + " operator out override params private protected public readonly ref return sbyte sealed short" + + " sizeof stackalloc static string struct switch this throw try typeof uint ulong unchecked" + + " unsafe ushort using virtual void volatile while add alias ascending descending dynamic from get" + + " global group into join let orderby partial remove select set value var yield"), + blockKeywords: words("catch class do else finally for foreach if struct switch try while"), + atoms: words("true false null"), + hooks: { + "@": function(stream, state) { + if (stream.eat('"')) { + state.tokenize = tokenAtString; + return tokenAtString(stream, state); + } + stream.eatWhile(/[\w\$_]/); + return "meta"; + } + } + }); + CodeMirror.defineMIME("text/x-scala", { + name: "clike", + keywords: words( + + /* scala */ + "abstract case catch class def do else extends false final finally for forSome if " + + "implicit import lazy match new null object override package private protected return " + + "sealed super this throw trait try trye type val var while with yield _ : = => <- <: " + + "<% >: # @ " + + + /* package scala */ + "assert assume require print println printf readLine readBoolean readByte readShort " + + "readChar readInt readLong readFloat readDouble " + + + "AnyVal App Application Array BufferedIterator BigDecimal BigInt Char Console Either " + + "Enumeration Equiv Error Exception Fractional Function IndexedSeq Integral Iterable " + + "Iterator List Map Numeric Nil NotNull Option Ordered Ordering PartialFunction PartialOrdering " + + "Product Proxy Range Responder Seq Serializable Set Specializable Stream StringBuilder " + + "StringContext Symbol Throwable Traversable TraversableOnce Tuple Unit Vector :: #:: " + + + /* package java.lang */ + "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " + + "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " + + "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " + + "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void" + + + ), + blockKeywords: words("catch class do else finally for forSome if match switch try while"), + atoms: words("true false null"), + hooks: { + "@": function(stream, state) { + stream.eatWhile(/[\w\$_]/); + return "meta"; + } + } + }); +}()); diff --git a/htdocs/static/js/codemirror/mode/clike/index.html b/htdocs/themes/bootstrap/js/codemirror/mode/clike/index.html similarity index 100% rename from htdocs/static/js/codemirror/mode/clike/index.html rename to htdocs/themes/bootstrap/js/codemirror/mode/clike/index.html diff --git a/htdocs/themes/bootstrap/js/codemirror/mode/clike/scala.html b/htdocs/themes/bootstrap/js/codemirror/mode/clike/scala.html new file mode 100644 index 0000000..5fdd84e --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/mode/clike/scala.html @@ -0,0 +1,765 @@ + + + +CodeMirror: C-like mode + + + + + + + + + + + + + diff --git a/htdocs/static/js/codemirror/mode/clojure/clojure.js b/htdocs/themes/bootstrap/js/codemirror/mode/clojure/clojure.js similarity index 100% rename from htdocs/static/js/codemirror/mode/clojure/clojure.js rename to htdocs/themes/bootstrap/js/codemirror/mode/clojure/clojure.js diff --git a/htdocs/static/js/codemirror/mode/clojure/index.html b/htdocs/themes/bootstrap/js/codemirror/mode/clojure/index.html similarity index 100% rename from htdocs/static/js/codemirror/mode/clojure/index.html rename to htdocs/themes/bootstrap/js/codemirror/mode/clojure/index.html diff --git a/htdocs/static/js/codemirror/mode/coffeescript/LICENSE b/htdocs/themes/bootstrap/js/codemirror/mode/coffeescript/LICENSE similarity index 100% rename from htdocs/static/js/codemirror/mode/coffeescript/LICENSE rename to htdocs/themes/bootstrap/js/codemirror/mode/coffeescript/LICENSE diff --git a/htdocs/themes/bootstrap/js/codemirror/mode/coffeescript/coffeescript.js b/htdocs/themes/bootstrap/js/codemirror/mode/coffeescript/coffeescript.js new file mode 100644 index 0000000..dece5f8 --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/mode/coffeescript/coffeescript.js @@ -0,0 +1,347 @@ +/** + * Link to the project's GitHub page: + * https://github.com/pickhardt/coffeescript-codemirror-mode + */ +CodeMirror.defineMode('coffeescript', function(conf) { + var ERRORCLASS = 'error'; + + function wordRegexp(words) { + return new RegExp("^((" + words.join(")|(") + "))\\b"); + } + + var singleOperators = new RegExp("^[\\+\\-\\*/%&|\\^~<>!\?]"); + var singleDelimiters = new RegExp('^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]'); + var doubleOperators = new RegExp("^((\->)|(\=>)|(\\+\\+)|(\\+\\=)|(\\-\\-)|(\\-\\=)|(\\*\\*)|(\\*\\=)|(\\/\\/)|(\\/\\=)|(==)|(!=)|(<=)|(>=)|(<>)|(<<)|(>>)|(//))"); + var doubleDelimiters = new RegExp("^((\\.\\.)|(\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))"); + var tripleDelimiters = new RegExp("^((\\.\\.\\.)|(//=)|(>>=)|(<<=)|(\\*\\*=))"); + var identifiers = new RegExp("^[_A-Za-z$][_A-Za-z$0-9]*"); + + var wordOperators = wordRegexp(['and', 'or', 'not', + 'is', 'isnt', 'in', + 'instanceof', 'typeof']); + var indentKeywords = ['for', 'while', 'loop', 'if', 'unless', 'else', + 'switch', 'try', 'catch', 'finally', 'class']; + var commonKeywords = ['break', 'by', 'continue', 'debugger', 'delete', + 'do', 'in', 'of', 'new', 'return', 'then', + 'this', 'throw', 'when', 'until']; + + var keywords = wordRegexp(indentKeywords.concat(commonKeywords)); + + indentKeywords = wordRegexp(indentKeywords); + + + var stringPrefixes = new RegExp("^('{3}|\"{3}|['\"])"); + var regexPrefixes = new RegExp("^(/{3}|/)"); + var commonConstants = ['Infinity', 'NaN', 'undefined', 'null', 'true', 'false', 'on', 'off', 'yes', 'no']; + var constants = wordRegexp(commonConstants); + + // Tokenizers + function tokenBase(stream, state) { + // Handle scope changes + if (stream.sol()) { + var scopeOffset = state.scopes[0].offset; + if (stream.eatSpace()) { + var lineOffset = stream.indentation(); + if (lineOffset > scopeOffset) { + return 'indent'; + } else if (lineOffset < scopeOffset) { + return 'dedent'; + } + return null; + } else { + if (scopeOffset > 0) { + dedent(stream, state); + } + } + } + if (stream.eatSpace()) { + return null; + } + + var ch = stream.peek(); + + // Handle docco title comment (single line) + if (stream.match("####")) { + stream.skipToEnd(); + return 'comment'; + } + + // Handle multi line comments + if (stream.match("###")) { + state.tokenize = longComment; + return state.tokenize(stream, state); + } + + // Single line comment + if (ch === '#') { + stream.skipToEnd(); + return 'comment'; + } + + // Handle number literals + if (stream.match(/^-?[0-9\.]/, false)) { + var floatLiteral = false; + // Floats + if (stream.match(/^-?\d*\.\d+(e[\+\-]?\d+)?/i)) { + floatLiteral = true; + } + if (stream.match(/^-?\d+\.\d*/)) { + floatLiteral = true; + } + if (stream.match(/^-?\.\d+/)) { + floatLiteral = true; + } + + if (floatLiteral) { + // prevent from getting extra . on 1.. + if (stream.peek() == "."){ + stream.backUp(1); + } + return 'number'; + } + // Integers + var intLiteral = false; + // Hex + if (stream.match(/^-?0x[0-9a-f]+/i)) { + intLiteral = true; + } + // Decimal + if (stream.match(/^-?[1-9]\d*(e[\+\-]?\d+)?/)) { + intLiteral = true; + } + // Zero by itself with no other piece of number. + if (stream.match(/^-?0(?![\dx])/i)) { + intLiteral = true; + } + if (intLiteral) { + return 'number'; + } + } + + // Handle strings + if (stream.match(stringPrefixes)) { + state.tokenize = tokenFactory(stream.current(), 'string'); + return state.tokenize(stream, state); + } + // Handle regex literals + if (stream.match(regexPrefixes)) { + if (stream.current() != '/' || stream.match(/^.*\//, false)) { // prevent highlight of division + state.tokenize = tokenFactory(stream.current(), 'string-2'); + return state.tokenize(stream, state); + } else { + stream.backUp(1); + } + } + + // Handle operators and delimiters + if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) { + return 'punctuation'; + } + if (stream.match(doubleOperators) + || stream.match(singleOperators) + || stream.match(wordOperators)) { + return 'operator'; + } + if (stream.match(singleDelimiters)) { + return 'punctuation'; + } + + if (stream.match(constants)) { + return 'atom'; + } + + if (stream.match(keywords)) { + return 'keyword'; + } + + if (stream.match(identifiers)) { + return 'variable'; + } + + // Handle non-detected items + stream.next(); + return ERRORCLASS; + } + + function tokenFactory(delimiter, outclass) { + var singleline = delimiter.length == 1; + return function tokenString(stream, state) { + while (!stream.eol()) { + stream.eatWhile(/[^'"\/\\]/); + if (stream.eat('\\')) { + stream.next(); + if (singleline && stream.eol()) { + return outclass; + } + } else if (stream.match(delimiter)) { + state.tokenize = tokenBase; + return outclass; + } else { + stream.eat(/['"\/]/); + } + } + if (singleline) { + if (conf.mode.singleLineStringErrors) { + outclass = ERRORCLASS + } else { + state.tokenize = tokenBase; + } + } + return outclass; + }; + } + + function longComment(stream, state) { + while (!stream.eol()) { + stream.eatWhile(/[^#]/); + if (stream.match("###")) { + state.tokenize = tokenBase; + break; + } + stream.eatWhile("#"); + } + return "comment" + } + + function indent(stream, state, type) { + type = type || 'coffee'; + var indentUnit = 0; + if (type === 'coffee') { + for (var i = 0; i < state.scopes.length; i++) { + if (state.scopes[i].type === 'coffee') { + indentUnit = state.scopes[i].offset + conf.indentUnit; + break; + } + } + } else { + indentUnit = stream.column() + stream.current().length; + } + state.scopes.unshift({ + offset: indentUnit, + type: type + }); + } + + function dedent(stream, state) { + if (state.scopes.length == 1) return; + if (state.scopes[0].type === 'coffee') { + var _indent = stream.indentation(); + var _indent_index = -1; + for (var i = 0; i < state.scopes.length; ++i) { + if (_indent === state.scopes[i].offset) { + _indent_index = i; + break; + } + } + if (_indent_index === -1) { + return true; + } + while (state.scopes[0].offset !== _indent) { + state.scopes.shift(); + } + return false + } else { + state.scopes.shift(); + return false; + } + } + + function tokenLexer(stream, state) { + var style = state.tokenize(stream, state); + var current = stream.current(); + + // Handle '.' connected identifiers + if (current === '.') { + style = state.tokenize(stream, state); + current = stream.current(); + if (style === 'variable') { + return 'variable'; + } else { + return ERRORCLASS; + } + } + + // Handle properties + if (current === '@') { + stream.eat('@'); + return 'keyword'; + } + + // Handle scope changes. + if (current === 'return') { + state.dedent += 1; + } + if (((current === '->' || current === '=>') && + !state.lambda && + state.scopes[0].type == 'coffee' && + stream.peek() === '') + || style === 'indent') { + indent(stream, state); + } + var delimiter_index = '[({'.indexOf(current); + if (delimiter_index !== -1) { + indent(stream, state, '])}'.slice(delimiter_index, delimiter_index+1)); + } + if (indentKeywords.exec(current)){ + indent(stream, state); + } + if (current == 'then'){ + dedent(stream, state); + } + + + if (style === 'dedent') { + if (dedent(stream, state)) { + return ERRORCLASS; + } + } + delimiter_index = '])}'.indexOf(current); + if (delimiter_index !== -1) { + if (dedent(stream, state)) { + return ERRORCLASS; + } + } + if (state.dedent > 0 && stream.eol() && state.scopes[0].type == 'coffee') { + if (state.scopes.length > 1) state.scopes.shift(); + state.dedent -= 1; + } + + return style; + } + + var external = { + startState: function(basecolumn) { + return { + tokenize: tokenBase, + scopes: [{offset:basecolumn || 0, type:'coffee'}], + lastToken: null, + lambda: false, + dedent: 0 + }; + }, + + token: function(stream, state) { + var style = tokenLexer(stream, state); + + state.lastToken = {style:style, content: stream.current()}; + + if (stream.eol() && stream.lambda) { + state.lambda = false; + } + + return style; + }, + + indent: function(state, textAfter) { + if (state.tokenize != tokenBase) { + return 0; + } + + return state.scopes[0].offset; + } + + }; + return external; +}); + +CodeMirror.defineMIME('text/x-coffeescript', 'coffeescript'); diff --git a/htdocs/themes/bootstrap/js/codemirror/mode/coffeescript/index.html b/htdocs/themes/bootstrap/js/codemirror/mode/coffeescript/index.html new file mode 100644 index 0000000..98bd497 --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/mode/coffeescript/index.html @@ -0,0 +1,727 @@ + + + +CodeMirror: CoffeeScript mode + + + + + + + +CodeMirror: CoffeeScript mode
+ + + +MIME types defined:
+ +text/x-coffeescript
.The CoffeeScript mode was written by Jeff Pickhardt (license).
+ + + diff --git a/htdocs/static/js/codemirror/mode/css/css.js b/htdocs/themes/bootstrap/js/codemirror/mode/css/css.js similarity index 100% rename from htdocs/static/js/codemirror/mode/css/css.js rename to htdocs/themes/bootstrap/js/codemirror/mode/css/css.js diff --git a/htdocs/static/js/codemirror/mode/css/index.html b/htdocs/themes/bootstrap/js/codemirror/mode/css/index.html similarity index 100% rename from htdocs/static/js/codemirror/mode/css/index.html rename to htdocs/themes/bootstrap/js/codemirror/mode/css/index.html diff --git a/htdocs/themes/bootstrap/js/codemirror/mode/diff/diff.js b/htdocs/themes/bootstrap/js/codemirror/mode/diff/diff.js new file mode 100644 index 0000000..3402f3b --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/mode/diff/diff.js @@ -0,0 +1,32 @@ +CodeMirror.defineMode("diff", function() { + + var TOKEN_NAMES = { + '+': 'tag', + '-': 'string', + '@': 'meta' + }; + + return { + token: function(stream) { + var tw_pos = stream.string.search(/[\t ]+?$/); + + if (!stream.sol() || tw_pos === 0) { + stream.skipToEnd(); + return ("error " + ( + TOKEN_NAMES[stream.string.charAt(0)] || '')).replace(/ $/, ''); + } + + var token_name = TOKEN_NAMES[stream.peek()] || stream.skipToEnd(); + + if (tw_pos === -1) { + stream.skipToEnd(); + } else { + stream.pos = tw_pos; + } + + return token_name; + } + }; +}); + +CodeMirror.defineMIME("text/x-diff", "diff"); diff --git a/htdocs/themes/bootstrap/js/codemirror/mode/diff/index.html b/htdocs/themes/bootstrap/js/codemirror/mode/diff/index.html new file mode 100644 index 0000000..b102c09 --- /dev/null +++ b/htdocs/themes/bootstrap/js/codemirror/mode/diff/index.html @@ -0,0 +1,104 @@ + + + +CodeMirror: Diff mode + + + + + + + +CodeMirror: Diff mode
+ + + +MIME types defined:
+ + +text/x-diff
.