/** * Constructor for command storage. * It uses localStorage if available. Otherwise fallback to normal JS array. */ function CommandStorage() { this.previousCommands = []; var previousCommandOffset = 0; var hasLocalStorage = typeof window.localStorage !== 'undefined'; var STORAGE_KEY = "web_console_previous_commands"; var MAX_STORAGE = 100; if (hasLocalStorage) { this.previousCommands = JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; previousCommandOffset = this.previousCommands.length; } this.addCommand = function(command) { previousCommandOffset = this.previousCommands.push(command); if (previousCommandOffset > MAX_STORAGE) { this.previousCommands.splice(0, 1); previousCommandOffset = MAX_STORAGE; } if (hasLocalStorage) { localStorage.setItem(STORAGE_KEY, JSON.stringify(this.previousCommands)); } }; this.navigate = function(offset) { previousCommandOffset += offset; if (previousCommandOffset < 0) { previousCommandOffset = -1; return null; } if (previousCommandOffset >= this.previousCommands.length) { previousCommandOffset = this.previousCommands.length; return null; } return this.previousCommands[previousCommandOffset]; } } function Autocomplete(_words, prefix) { this.words = prepareWords(_words); this.current = -1; this.left = 0; // [left, right) this.right = this.words.length; this.confirmed = false; function createSpan(label, className) { var el = document.createElement('span'); addClass(el, className); el.innerText = label; return el; } function prepareWords(words) { // convert into an object with priority and element var res = new Array(words.length); for (var i = 0, ind = 0; i < words.length; ++i) { res[i] = new Array(words[i].length); for (var j = 0; j < words[i].length; ++j) { res[i][j] = { word: words[i][j], priority: i, element: createSpan(words[i][j], 'trimmed keyword') }; } } // flatten and sort by alphabetical order to refine incrementally res = flatten(res); res.sort(function(a, b) { return a.word == b.word ? 0 : (a.word < b.word ? -1 : 1); }); for (var i = 0; i < res.length; ++i) res[i].element.dataset.index = i; return res; } this.view = document.createElement('pre'); addClass(this.view, 'auto-complete console-message'); this.view.appendChild(this.prefix = createSpan('...', 'trimmed keyword')); this.view.appendChild(this.stage = document.createElement('span')); this.elements = this.stage.children; this.view.appendChild(this.suffix = createSpan('...', 'trimmed keyword')); this.refine(prefix || ''); } Autocomplete.prototype.getSelectedWord = function() { return this.lastSelected && this.lastSelected.innerText; }; Autocomplete.prototype.onFinished = function(callback) { this.onFinishedCallback = callback; if (this.confirmed) callback(this.confirmed); }; Autocomplete.prototype.onKeyDown = function(ev) { var self = this; if (!this.elements.length) return; function move(nextCurrent) { if (self.lastSelected) removeClass(self.lastSelected, 'selected'); addClass(self.lastSelected = self.elements[nextCurrent], 'selected'); self.trim(self.current, true); self.trim(nextCurrent, false); self.current = nextCurrent; } switch (ev.keyCode) { case 69: if (ev.ctrlKey) { move(this.current + 1 >= this.elements.length ? 0 : this.current + 1); return true; } return false; case 9: // Tab if (ev.shiftKey) { // move back move(this.current - 1 < 0 ? this.elements.length - 1 : this.current - 1); } else { // move next move(this.current + 1 >= this.elements.length ? 0 : this.current + 1); } return true; case 13: // Enter this.finish(); return true; case 27: // Esc this.cancel(); return true; case 37: case 38: case 39: case 40: // disable using arrow keys on completing return true; } return false; }; Autocomplete.prototype.trim = function(from, needToTrim) { var self = this; var num = 5; if (this.elements.length > num) { (0 < from ? removeClass : addClass)(this.prefix, 'trimmed'); (from + num < this.elements.length ? removeClass : addClass)(this.suffix, 'trimmed'); } else { addClass(this.prefix, 'trimmed'); addClass(this.suffix, 'trimmed'); } function iterate(x) { for (var i = 0; i < num; ++i, ++x) if (0 <= x && x < self.elements.length) { toggleClass(self.elements[x], 'trimmed'); } } var toggleClass = needToTrim ? addClass : removeClass; if (from < 0) { iterate(0); } else if (from + num - 1 >= this.elements.length) { iterate(this.elements.length - num); } else { iterate(from); } }; Autocomplete.prototype.refine = function(prefix) { if (this.confirmed) return; var inc = !this.prev || (prefix.length >= this.prev.length); this.prev = prefix; var self = this; function remove(parent, child) { if (parent == child.parentNode) parent.removeChild(child); } function toggle(el) { return inc ? remove(self.stage, el) : self.stage.appendChild(el); } function startsWith(str, prefix) { return !prefix || str.substr(0, prefix.length) === prefix; } function moveRight(l, r) { while (l < r && inc !== startsWith(self.words[l].word, prefix)) toggle(self.words[l++].element); return l; } function moveLeft(l, r) { while (l < r - 1 && inc !== startsWith(self.words[r-1].word, prefix)) toggle(self.words[--r].element); return r; } self.trim(self.current, true); // reset trimming // Refine the range of words having same prefix if (inc) { self.left = moveRight(self.left, self.right); self.right = moveLeft(self.left, self.right); } else { self.left = moveLeft(-1, self.left); self.right = moveRight(self.right, self.words.length); } // Render elements with sorting by scope groups var words = this.words.slice(this.left, this.right); words.sort(function(a, b) { return a.priority == b.priority ? (a.word < b.word ? -1 : 1) : (a.priority < b.priority ? -1 : 1); }); removeAllChildren(this.elements); for (var i = 0; i < words.length; ++i) { this.stage.appendChild(words[i].element); } // Keep a previous selected element if the refined range includes the element if (this.lastSelected && this.left <= this.lastSelected.dataset.index && this.lastSelected.dataset.index < this.right) { this.current = Array.prototype.indexOf.call(this.elements, this.lastSelected); this.trim(this.current, false); } else { if (this.lastSelected) removeClass(this.lastSelected, 'selected'); this.lastSelected = null; this.current = -1; this.trim(0, false); } if (self.left + 1 == self.right) { self.current = 0; self.finish(); } else if (self.left == self.right) { self.cancel(); } }; Autocomplete.prototype.finish = function() { if (0 <= this.current && this.current < this.elements.length) { this.confirmed = this.elements[this.current].innerText; if (this.onFinishedCallback) this.onFinishedCallback(this.confirmed); this.removeView(); } else { this.cancel(); } }; Autocomplete.prototype.cancel = function() { if (this.onFinishedCallback) this.onFinishedCallback(); this.removeView(); }; Autocomplete.prototype.removeView = function() { if (this.view.parentNode) this.view.parentNode.removeChild(this.view); removeAllChildren(this.view); } // HTML strings for dynamic elements. var consoleInnerHtml = <%= render_inlined_string '_inner_console_markup' %>; var promptBoxHtml = <%= render_inlined_string '_prompt_box_markup' %>; // CSS var consoleStyleCss = <%= render_inlined_string 'style' %>; // Insert a style element with the unique ID var styleElementId = 'sr02459pvbvrmhco'; // Nonce to use for CSP var styleElementNonce = '<%= @nonce %>'; // REPLConsole Constructor function REPLConsole(config) { function getConfig(key, defaultValue) { return config && config[key] || defaultValue; } this.commandStorage = new CommandStorage(); this.prompt = getConfig('promptLabel', ' >>'); this.mountPoint = getConfig('mountPoint'); this.sessionId = getConfig('sessionId'); this.autocomplete = false; } REPLConsole.prototype.getSessionUrl = function(path) { var parts = [ this.mountPoint, 'repl_sessions', this.sessionId ]; if (path) { parts.push(path); } // Join and remove duplicate slashes. return parts.join('/').replace(/([^:]\/)\/+/g, '$1'); }; REPLConsole.prototype.contextRequest = function(keyword, callback) { putRequest(this.getSessionUrl(), 'context=' + getContext(keyword), function(xhr) { if (xhr.status == 200) { callback(null, JSON.parse(xhr.responseText)); } else { callback(xhr.statusText); } }); }; REPLConsole.prototype.commandHandle = function(line, callback) { var self = this; var params = 'input=' + encodeURIComponent(line); callback = callback || function() {}; function isSuccess(status) { return status >= 200 && status < 300 || status === 304; } function parseJSON(text) { try { return JSON.parse(text); } catch (e) { return null; } } function getErrorText(xhr) { if (!xhr.status) { return "Connection Refused"; } else { return xhr.status + ' ' + xhr.statusText; } } putRequest(self.getSessionUrl(), params, function(xhr) { var response = parseJSON(xhr.responseText); var result = isSuccess(xhr.status); if (result) { self.writeOutput(response.output); } else { if (response && response.output) { self.writeError(response.output); } else { self.writeError(getErrorText(xhr)); } } callback(result, response); }); }; REPLConsole.prototype.uninstall = function() { this.container.parentNode.removeChild(this.container); }; REPLConsole.prototype.install = function(container) { var _this = this; document.onkeydown = function(ev) { if (_this.focused) { _this.onKeyDown(ev); } }; document.onkeypress = function(ev) { if (_this.focused) { _this.onKeyPress(ev); } }; document.addEventListener('mousedown', function(ev) { var el = ev.target || ev.srcElement; if (el) { do { if (el === container) { _this.focus(); return; } } while (el = el.parentNode); _this.blur(); } }); // Render the console. container.innerHTML = consoleInnerHtml; var consoleOuter = findChild(container, 'console-outer'); var consoleActions = findChild(consoleOuter, 'console-actions'); addClass(container, 'console'); addClass(container.getElementsByClassName('layer'), 'pos-absolute border-box'); addClass(container.getElementsByClassName('button'), 'border-box'); addClass(consoleActions, 'pos-fixed pos-right'); // Make the console resizable. function resizeContainer(ev) { var startY = ev.clientY; var startHeight = parseInt(document.defaultView.getComputedStyle(container).height, 10); var scrollTopStart = consoleOuter.scrollTop; var clientHeightStart = consoleOuter.clientHeight; var doDrag = function(e) { var height = startHeight + startY - e.clientY; consoleOuter.scrollTop = scrollTopStart + (clientHeightStart - consoleOuter.clientHeight); if (height > document.documentElement.clientHeight) { container.style.height = document.documentElement.clientHeight; } else { container.style.height = height + 'px'; } shiftConsoleActions(); }; var stopDrag = function(e) { document.documentElement.removeEventListener('mousemove', doDrag, false); document.documentElement.removeEventListener('mouseup', stopDrag, false); }; document.documentElement.addEventListener('mousemove', doDrag, false); document.documentElement.addEventListener('mouseup', stopDrag, false); } function closeContainer(ev) { container.parentNode.removeChild(container); } var shifted = false; function shiftConsoleActions() { if (consoleOuter.scrollHeight > consoleOuter.clientHeight) { var widthDiff = document.documentElement.clientWidth - consoleOuter.clientWidth; if (shifted || ! widthDiff) return; shifted = true; consoleActions.style.marginRight = widthDiff + 'px'; } else if (shifted) { shifted = false; consoleActions.style.marginRight = '0px'; } } var observer = new MutationObserver(function(mutationsList) { for (let mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { shiftConsoleActions(); } } }); // Initialize this.container = container; this.outer = consoleOuter; this.inner = findChild(this.outer, 'console-inner'); this.clipboard = findChild(container, 'clipboard'); this.suggestWait = 1500; this.newPromptBox(); this.insertCss(); findChild(container, 'resizer').addEventListener('mousedown', resizeContainer); findChild(consoleActions, 'close-button').addEventListener('click', closeContainer); observer.observe(consoleOuter, { childList: true, subtree: true }); REPLConsole.currentSession = this; }; // Add CSS styles dynamically. This probably doesnt work for IE <8. REPLConsole.prototype.insertCss = function() { if (document.getElementById(styleElementId)) { return; // already inserted } var style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = consoleStyleCss; style.id = styleElementId; if (styleElementNonce.length > 0) { style.nonce = styleElementNonce; } document.getElementsByTagName('head')[0].appendChild(style); }; REPLConsole.prototype.focus = function() { if (! this.focused) { this.focused = true; if (! hasClass(this.inner, "console-focus")) { addClass(this.inner, "console-focus"); } this.scrollToBottom(); } }; REPLConsole.prototype.blur = function() { this.focused = false; removeClass(this.inner, "console-focus"); }; /** * Add a new empty prompt box to the console. */ REPLConsole.prototype.newPromptBox = function() { // Remove the caret from previous prompt display if any. if (this.promptDisplay) { this.removeCaretFromPrompt(); } var promptBox = document.createElement('div'); promptBox.className = "console-prompt-box"; promptBox.innerHTML = promptBoxHtml; this.promptLabel = promptBox.getElementsByClassName('console-prompt-label')[0]; this.promptDisplay = promptBox.getElementsByClassName('console-prompt-display')[0]; // Render the prompt box this.setInput(""); this.promptLabel.innerHTML = this.prompt; this.inner.appendChild(promptBox); this.scrollToBottom(); }; /** * Remove the caret from the prompt box, * mainly before adding a new prompt box. * For simplicity, just re-render the prompt box * with caret position -1. */ REPLConsole.prototype.removeCaretFromPrompt = function() { this.setInput(this._input, -1); }; REPLConsole.prototype.getSuggestion = function(keyword) { var self = this; function show(found) { if (!found) return; var hint = self.promptDisplay.childNodes[1]; hint.className = 'console-hint'; hint.dataset.keyword = found; hint.innerText = found.substr(self.suggestKeyword.length); // clear hinting information after timeout in a few time if (self.suggestTimeout) clearTimeout(self.suggestTimeout); self.suggestTimeout = setTimeout(function() { self.renderInput() }, self.suggestWait); } function find(context) { var k = self.suggestKeyword; for (var i = 0; i < context.length; ++i) if (context[i].substr(0, k.length) === k) { if (context[i] === k) return; return context[i]; } } function request(keyword, callback) { self.contextRequest(keyword, function(err, res) { if (err) throw new Error(err); var c = flatten(res['context']); c.sort(); callback(c); }); } self.suggestKeyword = keyword; var input = getContext(keyword); if (keyword.length - input.length < 3) return; if (self.suggestInput !== input) { self.suggestInput = input; request(keyword, function(c) { show(find(self.suggestContext = c)); }); } else if (self.suggestContext) { show(find(self.suggestContext)); } }; REPLConsole.prototype.getHintKeyword = function() { var hint = this.promptDisplay.childNodes[1]; return hint.className === 'console-hint' && hint.dataset.keyword; }; REPLConsole.prototype.setInput = function(input, caretPos) { if (input == null) return; // keep value if input is undefined this._caretPos = caretPos === undefined ? input.length : caretPos; this._input = input; if (this.autocomplete) this.autocomplete.refine(this.getCurrentWord()); this.renderInput(); if (!this.autocomplete && input.length == this._caretPos) this.getSuggestion(this.getCurrentWord()); }; /** * Add some text to the existing input. */ REPLConsole.prototype.addToInput = function(val, caretPos) { caretPos = caretPos || this._caretPos; var before = this._input.substring(0, caretPos); var after = this._input.substring(caretPos, this._input.length); var newInput = before + val + after; this.setInput(newInput, caretPos + val.length); }; /** * Render the input prompt. This is called whenever * the user input changes, sometimes not very efficient. */ REPLConsole.prototype.renderInput = function() { // Clear the current input. removeAllChildren(this.promptDisplay); var before, current, after; var center = document.createElement('span'); if (this._caretPos < 0) { before = this._input; current = after = ""; } else if (this._caretPos === this._input.length) { before = this._input; current = "\u00A0"; after = ""; } else { before = this._input.substring(0, this._caretPos); current = this._input.charAt(this._caretPos); after = this._input.substring(this._caretPos + 1, this._input.length); } this.promptDisplay.appendChild(document.createTextNode(before)); this.promptDisplay.appendChild(center); this.promptDisplay.appendChild(document.createTextNode(after)); var hint = this.autocomplete && this.autocomplete.getSelectedWord(); addClass(center, hint ? 'console-hint' : 'console-cursor'); center.appendChild(document.createTextNode(hint ? hint.substr(this.getCurrentWord().length) : current)); }; REPLConsole.prototype.writeOutput = function(output) { var consoleMessage = document.createElement('pre'); consoleMessage.className = "console-message"; consoleMessage.innerHTML = escapeHTML(output); this.inner.appendChild(consoleMessage); this.newPromptBox(); return consoleMessage; }; REPLConsole.prototype.writeError = function(output) { var consoleMessage = this.writeOutput(output); addClass(consoleMessage, "error-message"); return consoleMessage; }; REPLConsole.prototype.writeNotification = function(output) { var consoleMessage = this.writeOutput(output); addClass(consoleMessage, "notification-message"); return consoleMessage; }; REPLConsole.prototype.onEnterKey = function() { var input = this._input; if(input != "" && input !== undefined) { this.commandStorage.addCommand(input); } this.commandHandle(input); }; REPLConsole.prototype.onTabKey = function() { var self = this; var hintKeyword; if (hintKeyword = self.getHintKeyword()) { self.swapCurrentWord(hintKeyword); return; } if (self.autocomplete) return; self.autocomplete = new Autocomplete([]); self.contextRequest(self.getCurrentWord(), function(err, obj) { if (err) return self.autocomplete = false; self.autocomplete = new Autocomplete(obj['context'], self.getCurrentWord()); self.inner.appendChild(self.autocomplete.view); self.autocomplete.onFinished(function(word) { self.swapCurrentWord(word); self.autocomplete = false; }); self.scrollToBottom(); }); }; REPLConsole.prototype.onNavigateHistory = function(offset) { var command = this.commandStorage.navigate(offset) || ""; this.setInput(command); }; /** * Handle control keys like up, down, left, right. */ REPLConsole.prototype.onKeyDown = function(ev) { if (this.autocomplete && this.autocomplete.onKeyDown(ev)) { this.renderInput(); ev.preventDefault(); ev.stopPropagation(); return; } switch (ev.keyCode) { case 65: // Ctrl-A if (ev.ctrlKey) { this.setInput(this._input, 0); ev.preventDefault(); } break; case 69: // Ctrl-E if (ev.ctrlKey) { this.onTabKey(); ev.preventDefault(); } break; case 87: // Ctrl-W if (ev.ctrlKey) { this.deleteWord(); ev.preventDefault(); } break; case 85: // Ctrl-U if (ev.ctrlKey) { this.deleteLine(); ev.preventDefault(); } break; case 69: // Ctrl-E if (ev.ctrlKey) { this.onTabKey(); ev.preventDefault(); } break; case 80: // Ctrl-P if (! ev.ctrlKey) break; case 78: // Ctrl-N if (! ev.ctrlKey) break; case 9: // Tab this.onTabKey(); ev.preventDefault(); break; case 13: // Enter key this.onEnterKey(); ev.preventDefault(); break; case 38: // Up arrow this.onNavigateHistory(-1); ev.preventDefault(); break; case 40: // Down arrow this.onNavigateHistory(1); ev.preventDefault(); break; case 37: // Left arrow var caretPos = this._caretPos > 0 ? this._caretPos - 1 : this._caretPos; this.setInput(this._input, caretPos); ev.preventDefault(); break; case 39: // Right arrow var length = this._input.length; var caretPos = this._caretPos < length ? this._caretPos + 1 : this._caretPos; this.setInput(this._input, caretPos); ev.preventDefault(); break; case 8: // Delete this.deleteAtCurrent(); ev.preventDefault(); break; default: break; } if (ev.ctrlKey || ev.metaKey) { if (ev.keyCode == 86) { // Set focus to our clipboard when they hit the "v" key this.clipboard.focus(); // Pasting to clipboard doesn't happen immediately, // so we have to wait for a while to get the pasted text. var _this = this; setTimeout(function() { _this.addToInput(_this.clipboard.value); _this.clipboard.value = ""; _this.clipboard.blur(); }, 100); } } ev.stopPropagation(); }; /** * Handle input key press. */ REPLConsole.prototype.onKeyPress = function(ev) { // Only write to the console if it's a single key press. if (ev.ctrlKey && !ev.altKey || ev.metaKey) { return; } var keyCode = ev.keyCode || ev.which; this.insertAtCurrent(String.fromCharCode(keyCode)); ev.stopPropagation(); ev.preventDefault(); }; /** * Delete a character at the current position. */ REPLConsole.prototype.deleteAtCurrent = function() { if (this._caretPos > 0) { var caretPos = this._caretPos - 1; var before = this._input.substring(0, caretPos); var after = this._input.substring(this._caretPos, this._input.length); this.setInput(before + after, caretPos); if (!this._input) { this.autocomplete && this.autocomplete.cancel(); this.autocomplete = false; } } }; /** * Deletes the current line. */ REPLConsole.prototype.deleteLine = function() { if (this._caretPos > 0) { this.setInput("", 0); if (!this._input) { this.autocomplete && this.autocomplete.cancel(); this.autocomplete = false; } } }; /** * Deletes the current word. */ REPLConsole.prototype.deleteWord = function() { if (this._caretPos > 0) { var i = 1, current = this._caretPos; while (this._input[current - i++] == " "); var deleteIndex = 0; for (; current - i > 0; i++) { if (this._input[current - i] == " ") { deleteIndex = current - i; break; } } var before = this._input.substring(0, deleteIndex); var after = this._input.substring(current, this._input.length); this.setInput(before + after, deleteIndex); if (!this._input) { this.autocomplete && this.autocomplete.cancel(); this.autocomplete = false; } } }; /** * Insert a character at the current position. */ REPLConsole.prototype.insertAtCurrent = function(char) { var before = this._input.substring(0, this._caretPos); var after = this._input.substring(this._caretPos, this._input.length); this.setInput(before + char + after, this._caretPos + 1); }; REPLConsole.prototype.swapCurrentWord = function(next) { function right(s, pos) { var x = s.indexOf(' ', pos); return x === -1 ? s.length : x; } function swap(s, pos) { return s.substr(0, s.lastIndexOf(' ', pos) + 1) + next + s.substr(right(s, pos)) } if (!next) return; var swapped = swap(this._input, this._caretPos); this.setInput(swapped, this._caretPos + swapped.length - this._input.length); }; REPLConsole.prototype.getCurrentWord = function() { return (function(s, pos) { var left = s.lastIndexOf(' ', pos); if (left === -1) left = 0; var right = s.indexOf(' ', pos) if (right === -1) right = s.length - 1; return s.substr(left, right - left + 1).replace(/^\s+|\s+$/g,''); })(this._input, this._caretPos); }; REPLConsole.prototype.scrollToBottom = function() { this.outer.scrollTop = this.outer.scrollHeight; }; // Change the binding of the console. REPLConsole.prototype.switchBindingTo = function(frameId, exceptionObjectId, callback) { var url = this.getSessionUrl('trace'); var params = "frame_id=" + encodeURIComponent(frameId); if (exceptionObjectId) { params = params + "&exception_object_id=" + encodeURIComponent(exceptionObjectId); } var _this = this; postRequest(url, params, function() { var text = "Context has changed to: " + callback(); _this.writeNotification(text); }); }; /** * Install the console into the element with a specific ID. * Example: REPLConsole.installInto("target-id") */ REPLConsole.installInto = function(id, options) { var consoleElement = document.getElementById(id); options = options || {}; for (var prop in consoleElement.dataset) { options[prop] = options[prop] || consoleElement.dataset[prop]; } var replConsole = new REPLConsole(options); replConsole.install(consoleElement); return replConsole; }; // This is to store the latest single session, and the stored session // is updated by the REPLConsole#install() method. // It allows to operate the current session from the other scripts. REPLConsole.currentSession = null; // This line is for the Firefox Add-on, because it doesn't have XMLHttpRequest as default. // And so we need to require a module compatible with XMLHttpRequest from SDK. REPLConsole.XMLHttpRequest = typeof XMLHttpRequest === 'undefined' ? null : XMLHttpRequest; REPLConsole.request = function request(method, url, params, callback) { var xhr = new REPLConsole.XMLHttpRequest(); xhr.open(method, url, true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.send(params); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { callback(xhr); } }; }; // DOM helpers function hasClass(el, className) { var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g'); return el.className && el.className.match(regex); } function isNodeList(el) { return typeof el.length === 'number' && typeof el.item === 'function'; } function addClass(el, className) { if (isNodeList(el)) { for (var i = 0; i < el.length; ++ i) { addClass(el[i], className); } } else if (!hasClass(el, className)) { el.className += " " + className; } } function removeClass(el, className) { var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g'); el.className = el.className.replace(regex, ''); } function removeAllChildren(el) { while (el.firstChild) { el.removeChild(el.firstChild); } } function findChild(el, className) { for (var i = 0; i < el.childNodes.length; ++ i) { if (hasClass(el.childNodes[i], className)) { return el.childNodes[i]; } } } function escapeHTML(html) { return html .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/`/g, '`'); } // XHR helpers function postRequest() { REPLConsole.request.apply(this, ["POST"].concat([].slice.call(arguments))); } function putRequest() { REPLConsole.request.apply(this, ["PUT"].concat([].slice.call(arguments))); } if (typeof exports === 'object') { exports.REPLConsole = REPLConsole; } else { window.REPLConsole = REPLConsole; } // Split string by module operators of ruby function getContext(s) { var methodOp = s.lastIndexOf('.'); var moduleOp = s.lastIndexOf('::'); var x = methodOp > moduleOp ? methodOp : moduleOp; return x !== -1 ? s.substr(0, x) : ''; } function flatten(arrays) { return Array.prototype.concat.apply([], arrays); }