/** * @fileOverview Live browser interaction with Emacs * @version 1.4 */ /** * Connects to Emacs and waits for a request. After handling the * request it sends back the results and queues itself for another * request. * @namespace Holds all of Skewer's functionality. */ function skewer() { function callback(request) { var result = skewer.fn[request.type](request); if (result) { result = skewer.extend({ id: request.id, type: request.type, status: 'success', value: '' }, result); skewer.postJSON(skewer.host + "/skewer/post", result, callback); } else { skewer.getJSON(skewer.host + "/skewer/get", callback); } }; skewer.getJSON(skewer.host + "/skewer/get", callback); } /** * Get a JSON-encoded object from a server. * @param {String} url The location of the remote server * @param {Function} [callback] The callback to receive a response object */ skewer.getJSON = function(url, callback) { var XHR = window.skewerNativeXHR || XMLHttpRequest; var xhr = new XHR(); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { callback(JSON.parse(xhr.responseText)); } }; xhr.open('GET', url, true); xhr.send(); }; /** * Send a JSON-encoded object to a server. * @param {String} url The location of the remote server * @param {Object} object The object to transmit to the server * @param {Function} [callback] The callback to receive a response object */ skewer.postJSON = function(url, object, callback) { var XHR = window.skewerNativeXHR || XMLHttpRequest; var xhr = new XHR(); xhr.onreadystatechange = function() { if (callback && xhr.readyState === 4 && xhr.status === 200) { callback(JSON.parse(xhr.responseText)); } }; xhr.open('POST', url, true); xhr.setRequestHeader("Content-Type", "text/plain"); // CORS xhr.send(JSON.stringify(object)); }; /** * Add the properties other objects to a target object (jQuery.extend). * @param {Object} target The object to receive new properties * @param {...Object} objects Source objects for properties * @returns The target object */ skewer.extend = function(target) { for (var i = 1; i < arguments.length; i++) { var object = arguments[i]; for (var key in object) { if (object.hasOwnProperty(key)) { target[key] = object[key]; } } } return target; }; /** * Globally evaluate an expression and return the result. This * only works when the implementation's indirect eval performs * a global eval. If not, there's no alternative, since a return value * is essential. * * @see http://perfectionkills.com/global-eval-what-are-the-options/ * * @param expression A string containing an expression to evaluate * @returns The result of the evaluation */ skewer.globalEval = (function() { var eval0 = (function(original, Object) { try { return [eval][0]('Object') === original; } catch (e) { return false; } }(Object, false)); if (eval0) { return function(expression) { return [eval][0](expression); }; } else { return function(expression) { // Safari return eval.call(window, expression); }; } }()); /** * Same as Date.now(), supplied for pre-ES5 JS (<=IE8). * @returns {number} The epoch time in milliseconds */ skewer.now = function() { return new Date().valueOf(); }; /** * Handlers accept a request object from Emacs and return either a * logical false (no response) or an object to return to Emacs. * @namespace Request handlers. */ skewer.fn = {}; /** * Handles an code evaluation request from Emacs. * @param request The request object sent by Emacs * @returns The result object to be returned to Emacs */ skewer.fn.eval = function(request) { var result = { strict: request.strict }; var start = skewer.now(); try { var prefix = request.strict ? '"use strict";\n' : ""; var value = skewer.globalEval(prefix + request.eval); result.value = skewer.safeStringify(value, request.verbose); } catch (error) { result = skewer.errorResult(error, result, request); } result.time = (skewer.now() - start) / 1000; return result; }; /** * Load a hosted script named by the request. * @param request The request object sent by Emacs * @returns The result object to be returned to Emacs */ skewer.fn.script = function(request) { var script = document.createElement('script'); script.src = skewer.host + request.eval; document.body.appendChild(script); return {value: JSON.stringify(request.eval)}; }; /** * A keep-alive and connecton testing handler. * @param request The request object sent by Emacs * @returns The result object to be returned to Emacs */ skewer.fn.ping = function(request) { return { type: 'pong', date: skewer.now() / 1000, value: request.eval }; }; /** * Establish a new stylesheet with the provided value. */ skewer.fn.css = function(request) { var style = document.createElement('style'); style.type = 'text/css'; style.className = 'skewer'; if (style.styleSheet) { // < IE9 style.styleSheet.cssText = request.eval; } else { style.appendChild(document.createTextNode(request.eval)); } document.body.appendChild(style); return {}; }; /** * Remove all of Skewer's style tags from the document. */ skewer.fn.cssClearAll = function(request) { var styles = document.body.querySelectorAll('style.skewer'); for (var i = 0; i < styles.length; i++) { styles[i].parentNode.removeChild(styles[i]); } return {}; }; /** * HTML evaluator, appends or replaces a selection with given HTML. */ skewer.fn.html = function(request) { function buildSelector(ancestry) { return ancestry.map(function(tag) { return tag[0] + ':nth-of-type(' + tag[1] + ')'; }).join(' > '); } function query(ancestry) { return document.querySelector(buildSelector(ancestry)); } function htmlToNode(html) { var wrapper = document.createElement('div'); wrapper.innerHTML = html; return wrapper.firstChild; } var target = query(request.ancestry); if (target == null) { /* Determine missing part of the ancestry. */ var path = request.ancestry.slice(0); // copy var missing = []; while (query(path) == null) { missing.push(path.pop()); } /* Build up the missing elements. */ target = query(path); while (missing.length > 0) { var tag = missing.pop(), name = tag[0], nth = tag[1]; var empty = null; var count = target.querySelectorAll(name).length; for (; count < nth; count++) { empty = document.createElement(tag[0]); target.appendChild(empty); } target = empty; } } target.parentNode.replaceChild(htmlToNode(request.eval), target); return {}; }; /** * Fetch the HTML contents of selector. */ skewer.fn.fetchselector = function(request) { var element = document.querySelector(request.eval); return { value: element.innerHTML }; }; /** * Return a list of completions for an object. */ skewer.fn.completions = function(request) { var object = skewer.globalEval(request.eval); var keys = new Set(); var regex = new RegExp(request.regexp); for (var key in object) { if (regex.test(key)) { keys.add(key); } } var props = object != null ? Object.getOwnPropertyNames(object) : []; for (var i = 0; i < props.length; i++) { if (regex.test(props[i])) { keys.add(props[i]); } } return { value: Array.from(keys).sort() }; }; /** * Host of the skewer script (CORS support). * @type string */ (function() { var script = document.querySelector('script[src$="/skewer"]'); if (script) { skewer.host = script.src.match(/\w+:\/\/[^/]+/)[0]; } else { skewer.host = ''; // default to the current host } }()); /** * Stringify a potentially circular object without throwing an exception. * @param object The object to be printed. * @param {boolean} verbose Enable more verbose output. * @returns {string} The printed object. */ skewer.safeStringify = function (object, verbose) { "use strict"; var circular = "#"; var seen = []; var stringify = function(obj) { if (obj === true) { return "true"; } else if (obj === false) { return "false"; } else if (obj === undefined) { return "undefined"; } else if (obj === null) { return "null"; } else if (typeof obj === "number") { return obj.toString(); } else if (obj instanceof Array) { if (seen.indexOf(obj) >= 0) { return circular; } else { seen.push(obj); return "[" + obj.map(function(e) { return stringify(e); }).join(", ") + "]"; } } else if (typeof obj === "string") { return JSON.stringify(obj); } else if (window.Node != null && obj instanceof Node) { return obj.toString(); // DOM elements can't stringify } else if (typeof obj === "function") { if (verbose) return obj.toString(); else return "Function"; } else if (Object.prototype.toString.call(obj) === '[object Date]') { if (verbose) return JSON.stringify(obj); else return obj.toString(); } else { if (verbose) { if (seen.indexOf(obj) >= 0) return circular; else seen.push(obj); var pairs = []; for (var key in obj) { if (obj.hasOwnProperty(key)) { var pair = JSON.stringify(key) + ":"; pair += stringify(obj[key]); pairs.push(pair); } } return "{" + pairs.join(',') + "}"; } else { try { return obj.toString(); } catch (error) { return ({}).toString(); } } } }; try { return stringify(object); } catch (error) { return skewer.safeStringify(object, false); } }; /** * Log an object to the Skewer REPL in Emacs (console.log). * @param message The object to be logged. */ skewer.log = function() { "use strict"; for (var i = 0; i < arguments.length; i++) { var log = { type: "log", value: skewer.safeStringify(arguments[i], true) }; skewer.postJSON(skewer.host + "/skewer/post", log); } }; /** * Report an error event to the REPL. * @param event An error event object. */ skewer.error = function(event) { "use strict"; var log = { type: "error", value: event.message, filename: event.filename, line: event.lineno, column: event.column }; skewer.postJSON(skewer.host + "/skewer/post", log); }; /** * Prepare a result when an error occurs evaluating Javascript code. * @param error The error object given by catch. * @param result The resutl object to return to Emacs. * @param request The request object from Emacs. * @return The result object to send back to Emacs. */ skewer.errorResult = function(error, result, request) { "use strict"; return skewer.extend({}, result, { value: error.toString(), status: 'error', error: { name: error.name, stack: error.stack, type: error.type, message: error.message, eval: request.eval } }); }; if (window.addEventListener) { window.addEventListener('error', skewer.error); if (document.readyState === 'complete') { skewer(); } else { window.addEventListener('load', skewer); } } else { // < IE9 window.attachEvent('onerror', skewer.error); if (document.readyState === 'complete') { skewer(); } else { window.attachEvent('onload', skewer); } }