Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

436 lines
12 KiB

  1. /**
  2. * @fileOverview Live browser interaction with Emacs
  3. * @version 1.4
  4. */
  5. /**
  6. * Connects to Emacs and waits for a request. After handling the
  7. * request it sends back the results and queues itself for another
  8. * request.
  9. * @namespace Holds all of Skewer's functionality.
  10. */
  11. function skewer() {
  12. function callback(request) {
  13. var result = skewer.fn[request.type](request);
  14. if (result) {
  15. result = skewer.extend({
  16. id: request.id,
  17. type: request.type,
  18. status: 'success',
  19. value: ''
  20. }, result);
  21. skewer.postJSON(skewer.host + "/skewer/post", result, callback);
  22. } else {
  23. skewer.getJSON(skewer.host + "/skewer/get", callback);
  24. }
  25. };
  26. skewer.getJSON(skewer.host + "/skewer/get", callback);
  27. }
  28. /**
  29. * Get a JSON-encoded object from a server.
  30. * @param {String} url The location of the remote server
  31. * @param {Function} [callback] The callback to receive a response object
  32. */
  33. skewer.getJSON = function(url, callback) {
  34. var XHR = window.skewerNativeXHR || XMLHttpRequest;
  35. var xhr = new XHR();
  36. xhr.onreadystatechange = function() {
  37. if (xhr.readyState === 4 && xhr.status === 200) {
  38. callback(JSON.parse(xhr.responseText));
  39. }
  40. };
  41. xhr.open('GET', url, true);
  42. xhr.send();
  43. };
  44. /**
  45. * Send a JSON-encoded object to a server.
  46. * @param {String} url The location of the remote server
  47. * @param {Object} object The object to transmit to the server
  48. * @param {Function} [callback] The callback to receive a response object
  49. */
  50. skewer.postJSON = function(url, object, callback) {
  51. var XHR = window.skewerNativeXHR || XMLHttpRequest;
  52. var xhr = new XHR();
  53. xhr.onreadystatechange = function() {
  54. if (callback && xhr.readyState === 4 && xhr.status === 200) {
  55. callback(JSON.parse(xhr.responseText));
  56. }
  57. };
  58. xhr.open('POST', url, true);
  59. xhr.setRequestHeader("Content-Type", "text/plain"); // CORS
  60. xhr.send(JSON.stringify(object));
  61. };
  62. /**
  63. * Add the properties other objects to a target object (jQuery.extend).
  64. * @param {Object} target The object to receive new properties
  65. * @param {...Object} objects Source objects for properties
  66. * @returns The target object
  67. */
  68. skewer.extend = function(target) {
  69. for (var i = 1; i < arguments.length; i++) {
  70. var object = arguments[i];
  71. for (var key in object) {
  72. if (object.hasOwnProperty(key)) {
  73. target[key] = object[key];
  74. }
  75. }
  76. }
  77. return target;
  78. };
  79. /**
  80. * Globally evaluate an expression and return the result. This
  81. * <i>only</i> works when the implementation's indirect eval performs
  82. * a global eval. If not, there's no alternative, since a return value
  83. * is essential.
  84. *
  85. * @see http://perfectionkills.com/global-eval-what-are-the-options/
  86. *
  87. * @param expression A string containing an expression to evaluate
  88. * @returns The result of the evaluation
  89. */
  90. skewer.globalEval = (function() {
  91. var eval0 = (function(original, Object) {
  92. try {
  93. return [eval][0]('Object') === original;
  94. } catch (e) {
  95. return false;
  96. }
  97. }(Object, false));
  98. if (eval0) {
  99. return function(expression) {
  100. return [eval][0](expression);
  101. };
  102. } else {
  103. return function(expression) { // Safari
  104. return eval.call(window, expression);
  105. };
  106. }
  107. }());
  108. /**
  109. * Same as Date.now(), supplied for pre-ES5 JS (<=IE8).
  110. * @returns {number} The epoch time in milliseconds
  111. */
  112. skewer.now = function() {
  113. return new Date().valueOf();
  114. };
  115. /**
  116. * Handlers accept a request object from Emacs and return either a
  117. * logical false (no response) or an object to return to Emacs.
  118. * @namespace Request handlers.
  119. */
  120. skewer.fn = {};
  121. /**
  122. * Handles an code evaluation request from Emacs.
  123. * @param request The request object sent by Emacs
  124. * @returns The result object to be returned to Emacs
  125. */
  126. skewer.fn.eval = function(request) {
  127. var result = {
  128. strict: request.strict
  129. };
  130. var start = skewer.now();
  131. try {
  132. var prefix = request.strict ? '"use strict";\n' : "";
  133. var value = skewer.globalEval(prefix + request.eval);
  134. result.value = skewer.safeStringify(value, request.verbose);
  135. } catch (error) {
  136. result = skewer.errorResult(error, result, request);
  137. }
  138. result.time = (skewer.now() - start) / 1000;
  139. return result;
  140. };
  141. /**
  142. * Load a hosted script named by the request.
  143. * @param request The request object sent by Emacs
  144. * @returns The result object to be returned to Emacs
  145. */
  146. skewer.fn.script = function(request) {
  147. var script = document.createElement('script');
  148. script.src = skewer.host + request.eval;
  149. document.body.appendChild(script);
  150. return {value: JSON.stringify(request.eval)};
  151. };
  152. /**
  153. * A keep-alive and connecton testing handler.
  154. * @param request The request object sent by Emacs
  155. * @returns The result object to be returned to Emacs
  156. */
  157. skewer.fn.ping = function(request) {
  158. return {
  159. type: 'pong',
  160. date: skewer.now() / 1000,
  161. value: request.eval
  162. };
  163. };
  164. /**
  165. * Establish a new stylesheet with the provided value.
  166. */
  167. skewer.fn.css = function(request) {
  168. var style = document.createElement('style');
  169. style.type = 'text/css';
  170. style.className = 'skewer';
  171. if (style.styleSheet) { // < IE9
  172. style.styleSheet.cssText = request.eval;
  173. } else {
  174. style.appendChild(document.createTextNode(request.eval));
  175. }
  176. document.body.appendChild(style);
  177. return {};
  178. };
  179. /**
  180. * Remove all of Skewer's style tags from the document.
  181. */
  182. skewer.fn.cssClearAll = function(request) {
  183. var styles = document.body.querySelectorAll('style.skewer');
  184. for (var i = 0; i < styles.length; i++) {
  185. styles[i].parentNode.removeChild(styles[i]);
  186. }
  187. return {};
  188. };
  189. /**
  190. * HTML evaluator, appends or replaces a selection with given HTML.
  191. */
  192. skewer.fn.html = function(request) {
  193. function buildSelector(ancestry) {
  194. return ancestry.map(function(tag) {
  195. return tag[0] + ':nth-of-type(' + tag[1] + ')';
  196. }).join(' > ');
  197. }
  198. function query(ancestry) {
  199. return document.querySelector(buildSelector(ancestry));
  200. }
  201. function htmlToNode(html) {
  202. var wrapper = document.createElement('div');
  203. wrapper.innerHTML = html;
  204. return wrapper.firstChild;
  205. }
  206. var target = query(request.ancestry);
  207. if (target == null) {
  208. /* Determine missing part of the ancestry. */
  209. var path = request.ancestry.slice(0); // copy
  210. var missing = [];
  211. while (query(path) == null) {
  212. missing.push(path.pop());
  213. }
  214. /* Build up the missing elements. */
  215. target = query(path);
  216. while (missing.length > 0) {
  217. var tag = missing.pop(),
  218. name = tag[0],
  219. nth = tag[1];
  220. var empty = null;
  221. var count = target.querySelectorAll(name).length;
  222. for (; count < nth; count++) {
  223. empty = document.createElement(tag[0]);
  224. target.appendChild(empty);
  225. }
  226. target = empty;
  227. }
  228. }
  229. target.parentNode.replaceChild(htmlToNode(request.eval), target);
  230. return {};
  231. };
  232. /**
  233. * Fetch the HTML contents of selector.
  234. */
  235. skewer.fn.fetchselector = function(request) {
  236. var element = document.querySelector(request.eval);
  237. return { value: element.innerHTML };
  238. };
  239. /**
  240. * Return a list of completions for an object.
  241. */
  242. skewer.fn.completions = function(request) {
  243. var object = skewer.globalEval(request.eval);
  244. var keys = new Set();
  245. var regex = new RegExp(request.regexp);
  246. for (var key in object) {
  247. if (regex.test(key)) {
  248. keys.add(key);
  249. }
  250. }
  251. var props = object != null ? Object.getOwnPropertyNames(object) : [];
  252. for (var i = 0; i < props.length; i++) {
  253. if (regex.test(props[i])) {
  254. keys.add(props[i]);
  255. }
  256. }
  257. return { value: Array.from(keys).sort() };
  258. };
  259. /**
  260. * Host of the skewer script (CORS support).
  261. * @type string
  262. */
  263. (function() {
  264. var script = document.querySelector('script[src$="/skewer"]');
  265. if (script) {
  266. skewer.host = script.src.match(/\w+:\/\/[^/]+/)[0];
  267. } else {
  268. skewer.host = ''; // default to the current host
  269. }
  270. }());
  271. /**
  272. * Stringify a potentially circular object without throwing an exception.
  273. * @param object The object to be printed.
  274. * @param {boolean} verbose Enable more verbose output.
  275. * @returns {string} The printed object.
  276. */
  277. skewer.safeStringify = function (object, verbose) {
  278. "use strict";
  279. var circular = "#<Circular>";
  280. var seen = [];
  281. var stringify = function(obj) {
  282. if (obj === true) {
  283. return "true";
  284. } else if (obj === false) {
  285. return "false";
  286. } else if (obj === undefined) {
  287. return "undefined";
  288. } else if (obj === null) {
  289. return "null";
  290. } else if (typeof obj === "number") {
  291. return obj.toString();
  292. } else if (obj instanceof Array) {
  293. if (seen.indexOf(obj) >= 0) {
  294. return circular;
  295. } else {
  296. seen.push(obj);
  297. return "[" + obj.map(function(e) {
  298. return stringify(e);
  299. }).join(", ") + "]";
  300. }
  301. } else if (typeof obj === "string") {
  302. return JSON.stringify(obj);
  303. } else if (window.Node != null && obj instanceof Node) {
  304. return obj.toString(); // DOM elements can't stringify
  305. } else if (typeof obj === "function") {
  306. if (verbose)
  307. return obj.toString();
  308. else
  309. return "Function";
  310. } else if (Object.prototype.toString.call(obj) === '[object Date]') {
  311. if (verbose)
  312. return JSON.stringify(obj);
  313. else
  314. return obj.toString();
  315. } else {
  316. if (verbose) {
  317. if (seen.indexOf(obj) >= 0)
  318. return circular;
  319. else
  320. seen.push(obj);
  321. var pairs = [];
  322. for (var key in obj) {
  323. if (obj.hasOwnProperty(key)) {
  324. var pair = JSON.stringify(key) + ":";
  325. pair += stringify(obj[key]);
  326. pairs.push(pair);
  327. }
  328. }
  329. return "{" + pairs.join(',') + "}";
  330. } else {
  331. try {
  332. return obj.toString();
  333. } catch (error) {
  334. return ({}).toString();
  335. }
  336. }
  337. }
  338. };
  339. try {
  340. return stringify(object);
  341. } catch (error) {
  342. return skewer.safeStringify(object, false);
  343. }
  344. };
  345. /**
  346. * Log an object to the Skewer REPL in Emacs (console.log).
  347. * @param message The object to be logged.
  348. */
  349. skewer.log = function() {
  350. "use strict";
  351. for (var i = 0; i < arguments.length; i++) {
  352. var log = {
  353. type: "log",
  354. value: skewer.safeStringify(arguments[i], true)
  355. };
  356. skewer.postJSON(skewer.host + "/skewer/post", log);
  357. }
  358. };
  359. /**
  360. * Report an error event to the REPL.
  361. * @param event An error event object.
  362. */
  363. skewer.error = function(event) {
  364. "use strict";
  365. var log = {
  366. type: "error",
  367. value: event.message,
  368. filename: event.filename,
  369. line: event.lineno,
  370. column: event.column
  371. };
  372. skewer.postJSON(skewer.host + "/skewer/post", log);
  373. };
  374. /**
  375. * Prepare a result when an error occurs evaluating Javascript code.
  376. * @param error The error object given by catch.
  377. * @param result The resutl object to return to Emacs.
  378. * @param request The request object from Emacs.
  379. * @return The result object to send back to Emacs.
  380. */
  381. skewer.errorResult = function(error, result, request) {
  382. "use strict";
  383. return skewer.extend({}, result, {
  384. value: error.toString(),
  385. status: 'error',
  386. error: {
  387. name: error.name,
  388. stack: error.stack,
  389. type: error.type,
  390. message: error.message,
  391. eval: request.eval
  392. }
  393. });
  394. };
  395. if (window.addEventListener) {
  396. window.addEventListener('error', skewer.error);
  397. if (document.readyState === 'complete') {
  398. skewer();
  399. } else {
  400. window.addEventListener('load', skewer);
  401. }
  402. } else { // < IE9
  403. window.attachEvent('onerror', skewer.error);
  404. if (document.readyState === 'complete') {
  405. skewer();
  406. } else {
  407. window.attachEvent('onload', skewer);
  408. }
  409. }