add swank-js

This commit is contained in:
Sami Samhuri 2011-06-26 15:54:22 -07:00
parent d2665ae0c5
commit 22be8e220f
18 changed files with 3917 additions and 0 deletions

48
emacs.d/swank-js/LICENSE Normal file
View file

@ -0,0 +1,48 @@
Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Client file serving code was adapted from socket.io:
Copyright (c) 2010 LearnBoost <dev@learnboost.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

262
emacs.d/swank-js/README.md Normal file
View file

@ -0,0 +1,262 @@
swank-js
========
swank-js provides [SLIME](http://common-lisp.net/project/slime/) REPL
and other development tools for in-browser JavaScript and
[Node.JS](http://nodejs.org). It consists of SWANK backend and
accompanying SLIME contrib. [Socket.IO](http://socket.io/) is used to
communicate with wide range of web browsers.
Motivation
----------
From my experience an ability to use REPL for JavaScript debugging and
interactive development is very helpful when developing Web
applications. Previously I was using a heavily patched
[MozRepl](https://github.com/bard/mozrepl/wiki/) version that was
adapted for in-browser JavaScript. Primary downsides of that approach
were extreme instability of communication between Emacs and the
browser, the lack of cross-browser support and the lack of good RPC
between Emacs and JS that can be used to develop some handy
extensions.
I knew there exists [slime-proxy](https://github.com/3b/slime-proxy)
project that provides such functionality for
[Parenscript](http://common-lisp.net/project/parenscript/). The
problem is that most of us including myself can't use Lisp all the
time and a lot of code needs to be developed using languages like
plain JavaScript (as opposed to Parenscript), Python and so on. My
first thought was to adapt slime-proxy for use with plain JS, but then
I decided to roll my own SWANK backend using Node.JS. I wanted to find
out what this buzz around Node.JS is about and perhaps steal an
idea or two from there for use in my Lisp projects. Another reason was
availability of [Socket.IO](http://socket.io/) and an example of
[tiny http server proxy](http://www.catonmat.net/http-proxy-in-nodejs).
Some people may prefer Firebug or built-in browser development tools
to Emacs-based development, but for example in case of mobile browsers
you don't have much choice. At some point I did try swank-js with an
colleague's iPhone and it worked, which is not too unexpected given
that Socket.IO has good cross-browser support.
Status
------
As of now swank-js provides REPL with an ability to work with multiple
browser connections, supports dynamic updates of JavaScript code using
C-c C-c / C-M-x, provides debug output function and an ability to
reload web pages in the browser or refresh their CSS using Emacs
commands.
Many aspects of full-fledged SWANK backends aren't implemented yet,
there's no debugger/completion/autodoc and so on, but as I plan to use
swank-js a lot in future there's a good chance many of these features
will be eventually added.
Installation
------------
1. Install [Node.JS](http://nodejs.org), [npm](http://npmjs.org/) and
then [Socket.IO](http://socket.io/):
npm install socket.io
2. Get recent [SLIME](http://common-lisp.net/project/slime/) from its CVS
or the [git mirror](http://git.boinkor.net/gitweb/slime.git).
3. Make sure you have latest [js2-mode](http://code.google.com/p/js2-mode/).
Add it to your .emacs:
(add-to-list 'load-path "/path/to/js2-mode/directory")
(autoload 'js2-mode "js2-mode" nil t)
(add-to-list 'auto-mode-alist '("\\.js$" . js2-mode))
3. Create symbolic link to slime-js.el in the contrib subdirectory of
SLIME project.
4. In your .emacs, add the following lines (you may use other key for
slime-js-reload; also, if you're already using SLIME, just add slime-js
to the list of contribs, otherwise adjust the load-path item):
(add-to-list 'load-path "/path/to/slime/installation")
(require 'slime)
(slime-setup '(slime-repl slime-js))
(global-set-key [f5] 'slime-js-reload)
(add-hook 'js2-mode-hook
(lambda ()
(slime-js-minor-mode 1)))
5. If you're using CSS mode, you may want to add the following lines too:
(add-hook 'css-mode-hook
(lambda ()
(define-key css-mode-map "\M-\C-x" 'slime-js-refresh-css)))
Usage
-----
Start swank-js with the following command in the project directory:
node swank.js
Make SLIME connect to the backend using M-x slime-connect and
specifying localhost and port 4005. You will see REPL buffer with the
following prompt:
NODE>
This means that you're currently talking to Node.JS. You may play
around with it by running some JavaScript expressions.
If you get warning about SLIME version mismatch, you may make it
disappear until the next SLIME upgrade by typing *,js-slime-version*
at the REPL and entering your SLIME version (e.g. 2010-11-13).
Point your web browser to
http://localhost:8009/swank-js/test.html
You will see the following message appear in the REPL (browser name
and version may differ):
Remote attached: (browser) Firefox3.6:127.0.0.1
This means that the browser is now connected. Several browsers can
connect simultaneously and you can switch between them and Node.JS
REPL using *,select-remote* REPL shortcut. To use it, press ','
(comma) and type *select-remote* (completion is supported). You will
see "Remote:" prompt. Press TAB to see completions. Select your
browser in the list by typing its name or clicking on the
completion. The following message will appear:
NODE>
Remote selected: (browser) Firefox3.6:127.0.0.1
FIREFOX-3.6>
After that, you can interactively evaluate expressions in your
browser. To go back to Node.JS repl, switch back to node.js/direct
remote.
FIREFOX-3.6> document.body.nodeName
BODY
FIREFOX-3.6> alert("test!")
FIREFOX-3.6>
When working with browser, you may use F5 to reload the page. swank-js
connection with browser is lost in this case, but to solve this
problem you may use *,sticky-select-remote* instead of
*,select-remote*. This way swank-js will remember your selection and
automagically attach to the browser whenever it connects. If you press
F5 after using *,sticky-select-remote*, you will see that browser
briefly disconnects but then connects again:
Remote detached: (browser) Firefox3.6:127.0.0.1
FIREFOX-3.6>
Remote selected (auto): (direct) node.js
Remote attached: (browser) Firefox3.6:127.0.0.1
NODE>
Remote selected (auto): (browser) Firefox3.6:127.0.0.1
FIREFOX-3.6>
The sticky remote selection is saved in the config file, ~/.swankjsrc,
so you don't need to do *,sticky-select-remote* after restarting the
browser.
Now, let's try to make it work with an actual site. swank-js acts as a
proxy between your browser and the site so it can inject necessary
script tags into HTML pages and avoid cross-domain HTTP request
problems. Let's point it to [reddit](http://www.reddit.com). Type
*,target-url* and then *http://www.reddit.com* (www. part is
important, otherwise it will redirect to www.reddit.com skipping the
proxy). Point your browser to http://localhost:8009, you'll see reddit
frontpage load. If you didn't do *,select-remote* or
*,sticky-select-remote* yet do it now and select your browser from the
list of remotes. Now you can execute JavaScript in the context of
reddit:
FIREFOX-3.6> $(".sitetable a.title").map(function(n) { return (n + 1) + ". " + $(this).text(); }).get().join("\n")
1. Wikileaks currently under a mass DDOS attack
2. Munich University - Jealous
...
Let's make a function from it. Create a file test.js somewhere and
make sure it uses js2-mode (if it doesn't, switch it to js2-mode using
M-x js2-mode). Type the following there:
function listRedditTitles () {
$(".sitetable a.title").map(
function (n) {
SwankJS.output((n + 1) + ". " + $(this).text() + "\n");
}).get().join("\n");
}
Note SwankJS.output() function being used there. It allows you to send
debug print to SLIME REPL.
Move the point somewhere into the middle of the listRedditTitles() function
and press C-M-x. Now you may try it out in the REPL:
FIREFOX-3.6> listRedditTitles()
1. Wikileaks currently under a mass DDOS attack
2. Munich University - Jealous
...
You may edit the function definition and update it using C-M-x any
number of times.
Now let's try some CSS hacking. Create a directory named zzz and start
a Web server in it from your command prompt:
$ mkdir zzz && cd zzz && python -mSimpleHTTPServer
Create a file named a.css there and make sure it uses css-mode (like
with js2-mode, you can force it with M-x css-mode). Add some CSS rules
to this file:
body {
background: green;
}
Now let's add the stylesheet to the reddit page:
FIREFOX-3.6> $('head').append('<link rel="stylesheet" href="http://localhost:8000/a.css" type="text/css" />');
[object Object]
You will see some parts of the page become green. Now, change green to
blue in the CSS file and press C-M-x (it will save the file
automatically):
body {
background: blue;
}
You will see the page become blue, maybe after some flickering as all
CSS used on the page is reloaded. This way you may update CSS in an
AJAX application without reloading the page, which is often rather
handy. Unlike editing CSS in Firebug in case when you're editing CSS
of your own application changes will not disappear upon page reload
(with reddit page you'll have to readd the stylesheet).
Troubleshooting
---------------
I've noticed that flashsocket Socket.IO transport does exhibit some
instability. You may want to try other transports by changing the
socketio cookie, e.g.:
document.cookie = "socketio=xhr-polling"
Be careful not to lose connection to the browser though especially in
case of REPL-less browser like IE6/7 or you'll have to type something
like
javascript:void(document.cookie = "socketio=flashsocket")
in the address bar.
In case of IE, increasing the maximum number of HTTP connections may
help with non-Flash transports, although I didn't try it yet. To do it
add DWORD value MaxConnectionsPer1_0Server to the following registry
key:
HKEY\_CURRENT\_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings
License
-------
swank-js is distributed under X11-style license. See LICENSE file.

View file

@ -0,0 +1,483 @@
/*
http://www.JSON.org/json2.js
2010-11-17
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
This file creates a global JSON object containing two methods: stringify
and parse.
JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array of strings.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or '&nbsp;'),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the value
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array of strings, then it will be
used to select the members to be serialized. It filters the results
such that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = JSON.stringify(['e', {pluribus: 'unum'}]);
// text is '["e",{"pluribus":"unum"}]'
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
text = JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
// text is '["Date(---current time---)"]'
JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
// Parse the text. Values that look like ISO date strings will
// be converted to Date objects.
myData = JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
*/
/*jslint evil: true, strict: false, regexp: false */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
if (!this.JSON) {
this.JSON = {};
}
(function () {
"use strict";
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return isFinite(this.valueOf()) ?
this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z' : null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;
return escapable.test(string) ?
'"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string' ? c :
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' :
'"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === '[object Array]') {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0 ? '[]' :
gap ? '[\n' + gap +
partial.join(',\n' + gap) + '\n' +
mind + ']' :
'[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
k = rep[i];
if (typeof k === 'string') {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0 ? '{}' :
gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
mind + '}' : '{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== 'function') {
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== 'function') {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function' ?
walk({'': j}, '') : j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse');
};
}
}());

View file

@ -0,0 +1 @@
SwankJS.setup();

View file

@ -0,0 +1,356 @@
// Domain Public by Eric Wendelin http://eriwen.com/ (2008)
// Luke Smith http://lucassmith.name/ (2008)
// Loic Dachary <loic@dachary.org> (2008)
// Johan Euphrosine <proppy@aminche.com> (2008)
// Øyvind Sean Kinsey http://kinsey.no/blog (2010)
//
// Information and discussions
// http://jspoker.pokersource.info/skin/test-printstacktrace.html
// http://eriwen.com/javascript/js-stack-trace/
// http://eriwen.com/javascript/stacktrace-update/
// http://pastie.org/253058
//
// guessFunctionNameFromLines comes from firebug
//
// Software License Agreement (BSD License)
//
// Copyright (c) 2007, Parakey Inc.
// All rights reserved.
//
// Redistribution and use of this software in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above
// copyright notice, this list of conditions and the
// following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// * Neither the name of Parakey Inc. nor the names of its
// contributors may be used to endorse or promote products
// derived from this software without specific prior
// written permission of Parakey Inc.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/**
* Main function giving a function stack trace with a forced or passed in Error
*
* @cfg {Error} e The error to create a stacktrace from (optional)
* @cfg {Boolean} guess If we should try to resolve the names of anonymous functions
* @return {Array} of Strings with functions, lines, files, and arguments where possible
*/
function swank_printStackTrace(options) {
var ex = (options && options.e) ? options.e : null;
var guess = options ? !!options.guess : true;
var p = new swank_printStackTrace.implementation();
var result = p.run(ex);
return (guess) ? p.guessFunctions(result) : result;
}
swank_printStackTrace.implementation = function() {};
swank_printStackTrace.implementation.prototype = {
run: function(ex) {
// Use either the stored mode, or resolve it
var mode = this._mode || this.mode();
if (mode === 'other') {
return this.other(arguments.callee);
} else {
ex = ex ||
(function() {
try {
var _err = __undef__ << 1;
} catch (e) {
return e;
}
})();
return this[mode](ex);
}
},
/**
* @return {String} mode of operation for the environment in question.
*/
mode: function() {
try {
var _err = __undef__ << 1;
} catch (e) {
if (e['arguments']) {
return (this._mode = 'chrome');
} else if (window.opera && e.stacktrace) {
return (this._mode = 'opera10');
} else if (e.stack) {
return (this._mode = 'firefox');
} else if (window.opera && !('stacktrace' in e)) { //Opera 9-
return (this._mode = 'opera');
}
}
return (this._mode = 'other');
},
/**
* Given a context, function name, and callback function, overwrite it so that it calls
* swank_printStackTrace() first with a callback and then runs the rest of the body.
*
* @param {Object} context of execution (e.g. window)
* @param {String} functionName to instrument
* @param {Function} function to call with a stack trace on invocation
*/
instrumentFunction: function(context, functionName, callback) {
context = context || window;
context['_old' + functionName] = context[functionName];
context[functionName] = function() {
callback.call(this, swank_printStackTrace());
return context['_old' + functionName].apply(this, arguments);
};
context[functionName]._instrumented = true;
},
/**
* Given a context and function name of a function that has been
* instrumented, revert the function to it's original (non-instrumented)
* state.
*
* @param {Object} context of execution (e.g. window)
* @param {String} functionName to de-instrument
*/
deinstrumentFunction: function(context, functionName) {
if (context[functionName].constructor === Function &&
context[functionName]._instrumented &&
context['_old' + functionName].constructor === Function) {
context[functionName] = context['_old' + functionName];
}
},
/**
* Given an Error object, return a formatted Array based on Chrome's stack string.
*
* @param e - Error object to inspect
* @return Array<String> of function calls, files and line numbers
*/
chrome: function(e) {
return e.stack.replace(/^[^\n]*\n/, '').replace(/^[^\n]*\n/, '').replace(/^[^\(]+?[\n$]/gm, '').replace(/^\s+at\s+/gm, '').replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@').split('\n');
},
/**
* Given an Error object, return a formatted Array based on Firefox's stack string.
*
* @param e - Error object to inspect
* @return Array<String> of function calls, files and line numbers
*/
firefox: function(e) {
return e.stack ? e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anonymous}(').split('\n') : [];
},
/**
* Given an Error object, return a formatted Array based on Opera 10's stacktrace string.
*
* @param e - Error object to inspect
* @return Array<String> of function calls, files and line numbers
*/
opera10: function(e) {
var stack = e.stacktrace;
var lines = stack.split('\n'), ANON = '{anonymous}',
lineRE = /.*line (\d+), column (\d+) in ((<anonymous function\:?\s*(\S+))|([^\(]+)\([^\)]*\))(?: in )?(.*)\s*$/i, i, j, len;
for (i = 2, j = 0, len = lines.length; i < len - 2; i++) {
if (lineRE.test(lines[i])) {
var location = RegExp.$6 + ':' + RegExp.$1 + ':' + RegExp.$2;
var fnName = RegExp.$3;
fnName = fnName.replace(/<anonymous function\s?(\S+)?>/g, ANON);
lines[j++] = fnName + '@' + location;
}
}
lines.splice(j, lines.length - j);
return lines;
},
// Opera 7.x-9.x only!
opera: function(e) {
var lines = e.message.split('\n'), ANON = '{anonymous}',
lineRE = /Line\s+(\d+).*script\s+(http\S+)(?:.*in\s+function\s+(\S+))?/i,
i, j, len;
for (i = 4, j = 0, len = lines.length; i < len; i += 2) {
//TODO: RegExp.exec() would probably be cleaner here
if (lineRE.test(lines[i])) {
lines[j++] = (RegExp.$3 ? RegExp.$3 + '()@' + RegExp.$2 + RegExp.$1 : ANON + '()@' + RegExp.$2 + ':' + RegExp.$1) + ' -- ' + lines[i + 1].replace(/^\s+/, '');
}
}
lines.splice(j, lines.length - j);
return lines;
},
// Safari, IE, and others
other: function(curr) {
var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i,
stack = [], j = 0, fn, args;
var maxStackSize = 10;
while (curr && stack.length < maxStackSize) {
fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON;
args = Array.prototype.slice.call(curr['arguments']);
stack[j++] = fn + '(' + this.stringifyArguments(args) + ')';
curr = curr.caller;
}
return stack;
},
/**
* Given arguments array as a String, subsituting type names for non-string types.
*
* @param {Arguments} object
* @return {Array} of Strings with stringified arguments
*/
stringifyArguments: function(args) {
for (var i = 0; i < args.length; ++i) {
var arg = args[i];
if (arg === undefined) {
args[i] = 'undefined';
} else if (arg === null) {
args[i] = 'null';
} else if (arg.constructor) {
if (arg.constructor === Array) {
if (arg.length < 3) {
args[i] = '[' + this.stringifyArguments(arg) + ']';
} else {
args[i] = '[' + this.stringifyArguments(Array.prototype.slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(Array.prototype.slice.call(arg, -1)) + ']';
}
} else if (arg.constructor === Object) {
args[i] = '#object';
} else if (arg.constructor === Function) {
args[i] = '#function';
} else if (arg.constructor === String) {
args[i] = '"' + arg + '"';
}
}
}
return args.join(',');
},
sourceCache: {},
/**
* @return the text from a given URL.
*/
ajax: function(url) {
var req = this.createXMLHTTPObject();
if (!req) {
return;
}
req.open('GET', url, false);
req.setRequestHeader('User-Agent', 'XMLHTTP/1.0');
req.send('');
return req.responseText;
},
/**
* Try XHR methods in order and store XHR factory.
*
* @return <Function> XHR function or equivalent
*/
createXMLHTTPObject: function() {
var xmlhttp, XMLHttpFactories = [
function() {
return new XMLHttpRequest();
}, function() {
return new ActiveXObject('Msxml2.XMLHTTP');
}, function() {
return new ActiveXObject('Msxml3.XMLHTTP');
}, function() {
return new ActiveXObject('Microsoft.XMLHTTP');
}
];
for (var i = 0; i < XMLHttpFactories.length; i++) {
try {
xmlhttp = XMLHttpFactories[i]();
// Use memoization to cache the factory
this.createXMLHTTPObject = XMLHttpFactories[i];
return xmlhttp;
} catch (e) {}
}
},
/**
* Given a URL, check if it is in the same domain (so we can get the source
* via Ajax).
*
* @param url <String> source url
* @return False if we need a cross-domain request
*/
isSameDomain: function(url) {
return url.indexOf(location.hostname) !== -1;
},
/**
* Get source code from given URL if in the same domain.
*
* @param url <String> JS source URL
* @return <String> Source code
*/
getSource: function(url) {
if (!(url in this.sourceCache)) {
this.sourceCache[url] = this.ajax(url).split('\n');
}
return this.sourceCache[url];
},
guessFunctions: function(stack) {
for (var i = 0; i < stack.length; ++i) {
var reStack = /\{anonymous\}\(.*\)@(\w+:\/\/([\-\w\.]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/;
var frame = stack[i], m = reStack.exec(frame);
if (m) {
var file = m[1], lineno = m[4]; //m[7] is character position in Chrome
if (file && this.isSameDomain(file) && lineno) {
var functionName = this.guessFunctionName(file, lineno);
stack[i] = frame.replace('{anonymous}', functionName);
}
}
}
return stack;
},
guessFunctionName: function(url, lineNo) {
try {
return this.guessFunctionNameFromLines(lineNo, this.getSource(url));
} catch (e) {
return 'getSource failed with url: ' + url + ', exception: ' + e.toString();
}
},
guessFunctionNameFromLines: function(lineNo, source) {
var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/;
var reGuessFunction = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/;
// Walk backwards from the first line in the function until we find the line which
// matches the pattern above, which is the function definition
var line = "", maxLines = 10;
for (var i = 0; i < maxLines; ++i) {
line = source[lineNo - i] + line;
if (line !== undefined) {
var m = reGuessFunction.exec(line);
if (m && m[1]) {
return m[1];
} else {
m = reFunctionArgNames.exec(line);
if (m && m[1]) {
return m[1];
}
}
}
}
return '(?)';
}
};

View file

@ -0,0 +1,139 @@
//
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
var SwankJS = { socket: null, connected: false, bufferedOutput: [] };
// TBD: check message contents
// TBD: exception handling
// TBD: trim stack trace excluding everything starting from swankjs_evaluate line
SwankJS.debug = function debug () {
if (!window.console)
return;
var debug = console.debug || console.log;
if (!debug)
return;
var args = [];
for (var i = 0; i < arguments.length; ++i)
args.push(arguments[i]);
debug.apply(console, args);
};
SwankJS.setup = function setup () {
try {
if (parent.window && parent.window.document !== document && parent.window.SwankJS)
return;
} catch (e) {}
var self = this;
// TBD: swank-js should proxy all requests to autoadd its scripts
// (this way, the dynamic script loading stuff isn't necessary)
// and to make flashsocket swf load from the same url as the
// web app itself.
// Don't forget about 'Host: ' header though!
this.socket = new io.Socket();
this.socket.on(
"connect",
function() {
self.connected = true;
self.debug("connected");
self.socket.send({ op: "handshake", userAgent: navigator.userAgent });
if (self.bufferedOutput.length > 0) {
for (var i = 0; i < self.bufferedOutput.length; ++i)
self.output(self.bufferedOutput[i]);
self.bufferedOutput = [];
}
});
this.socket.on(
"message", function swankjs_evaluate (m) {
self.debug("eval: %o", m);
// var m = JSON.parse(message);
try {
var r = window.eval(m.code);
} catch (e) {
var message = String(e);
if (message == "[object Error]") {
try {
message = "ERROR: " + e.message;
} catch(e1) {}
}
self.debug("error = %s", message);
self.socket.send({ op: "result", id: m.id,
error: message + "\n" + swank_printStackTrace({ e: e }).join("\n") });
return;
}
self.debug("result = %s", String(r));
self.socket.send({ op: "result", id: m.id, error: null, values: r === undefined ? [] : [String(r)] }); });
this.socket.on(
"disconnect", function() {
self.debug("connected");
});
this.socket.connect();
};
// useful functions for the REPL / web apps
SwankJS.output = function output (str) {
if (this.socket && this.connected)
this.socket.send({ op: "output", str: str });
else
this.bufferedOutput.push(str);
};
SwankJS.reload = function reload () {
document.location.reload(true);
};
SwankJS.refreshCSS = function refreshCSS () {
// FIXME: this doesn't work in IE yet
// FIXME: support refresh of individual CSS files
var links = document.getElementsByTagName('link');
for (var i = 0; i < links.length; i++) {
var link = links[i];
if (link.rel.toLowerCase().indexOf('stylesheet') >=0 && link.href) {
var h = link.href.replace(/(&|\\?)forceReload=\d+/, "");
link.href = h + (h.indexOf('?') >= 0 ? '&' : '?') + 'forceReload=' + Date.now();
}
}
};
/*
// we may need this later
SwankJS.makeScriptElement = function makeScriptElement (src, content) {
var script = document.createElement("script");
script.type = "text/javascript";
if (src)
script.src = src;
else {
var text = document.createTextNode(content);
script.appendChild(text);
}
return script;
};
*/
SwankJS.setup();

View file

@ -0,0 +1,10 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<script type="text/javascript" src="/swank-js/json2.js"></script>
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script type="text/javascript" src="/swank-js/stacktrace.js"></script>
<script type="text/javascript" src="/swank-js/swank-js.js"></script>
</head>
<body></body>
</html>

103
emacs.d/swank-js/config.js Normal file
View file

@ -0,0 +1,103 @@
// -*- mode: js2 -*-
//
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
var path = require("path"), fs = require("fs");
function Config (fileName) {
this.fileName = fileName;
if (/^~\//.test(this.fileName))
this.fileName = path.join(process.env.HOME || "/", this.fileName.substring(2));
this.config = null;
}
Config.prototype.loadConfig = function loadConfig (cont) {
var self = this;
if (!this.config) {
fs.readFile(
self.fileName, "utf-8", function (err, data) {
self.config = {};
if (!err) {
try {
self.config = JSON.parse(data);
} catch (e) {}
}
cont(self.config);
});
} else
cont(this.config);
};
Config.prototype.saveConfig = function saveConfig (cont) {
if (!this.config)
return;
var self = this;
fs.writeFile(
this.fileName, JSON.stringify(this.config), "utf8",
function (err) {
if (err)
console.warn("error writing config file %s: %s", self.fileName, err);
cont();
});
};
Config.prototype.get = function get (name, cont) {
this.loadConfig(
function (cfg) {
cont(cfg.hasOwnProperty(name) ? cfg[name] : undefined);
});
};
Config.prototype.set = function set (name, value, cont) {
var self = this;
cont = cont || function () {};
this.loadConfig(
function (cfg) {
cfg[name] = value;
self.saveConfig(cont);
});
};
function FakeConfig (values) {
this.config = values || {};
}
FakeConfig.prototype.getNow = function getNow (name) {
return this.config.hasOwnProperty(name) ? this.config[name] : undefined;
};
FakeConfig.prototype.get = function get (name, cont) {
cont(this.config.hasOwnProperty(name) ? this.config[name] : undefined);
};
FakeConfig.prototype.set = function set (name, value, cont) {
this.config[name] = value;
if (cont) cont();
};
exports.Config = Config;
exports.FakeConfig = FakeConfig;

View file

@ -0,0 +1,273 @@
// -*- mode: js2; js-run: t -*-
//
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
var lisp = require("./lisp");
var assert = require("assert");
var S = lisp.S, cons = lisp.cons, consp = lisp.consp, car = lisp.car, cdr = lisp.cdr,
nil = lisp.nil, nullp = lisp.nullp, listp = lisp.listp, list = lisp.list,
reverse = lisp.reverse, append = lisp.append, repr = lisp.repr,
StringInputStream = lisp.StringInputStream,
readFromString = lisp.readFromString,
fromLisp = lisp.fromLisp,
toLisp = lisp.toLisp;
assert.equal(S("zzz"), S("zzz"));
assert.deepEqual(cons(1, cons(2, cons(3, nil))), list(1, 2, 3));
assert.equal("abc", car(cons("abc", "def")));
assert.equal("def", cdr(cons("abc", "def")));
assert.equal(nil, list());
assert.ok(consp(cons(1, 2)));
assert.ok(!consp(nil));
assert.ok(listp(cons(1, 2)));
assert.ok(listp(list(1, 2)));
assert.ok(listp(nil));
assert.ok(nullp(nil));
assert.ok(!nullp(cons(1, 2)));
assert.ok(!nullp(1));
assert.deepEqual(list(), reverse(list()));
assert.deepEqual(list(1), reverse(list(1)));
assert.deepEqual(list(3, 2, 1), reverse(list(1, 2, 3)));
assert.deepEqual(nil, append(nil, nil));
assert.deepEqual(list(1), append(list(1), nil));
assert.deepEqual(list(1), append(nil, list(1)));
assert.deepEqual(list(1, 2, 3), append(list(1, 2), list(3)));
assert.deepEqual(list(1, 2, 3, 4), append(list(1, 2), list(3, 4)));
var s = new StringInputStream("abc");
assert.equal(0, s.pos());
assert.equal("a", s.getc());
assert.equal(1, s.pos());
assert.equal("b", s.readchar());
assert.equal(2, s.pos());
assert.equal("c", s.readchar());
assert.equal(3, s.pos());
assert.equal(null, s.getc());
assert.equal(3, s.pos());
assert["throws"](function () { s.readchar(); });
assert.equal(3, s.pos());
s.ungetc("c");
assert.equal(2, s.pos());
assert.equal("c", s.getc());
assert.equal(3, s.pos());
assert["throws"](function () { s.ungetc("z"); });
assert.equal(3, s.pos());
s.ungetc("c");
s.ungetc("b");
assert.equal(1, s.pos());
assert.equal("b", s.getc());
assert.equal("c", s.getc());
assert.equal(3, s.pos());
s.ungetc("c");
s.ungetc("b");
s.ungetc("a");
assert.equal(0, s.pos());
assert["throws"](function () { s.ungetc("z"); });
assert["throws"](function () { s.ungetc(""); });
assert.equal(0, s.pos());
assert.equal("a", s.readchar());
assert.equal("b", s.readchar());
assert.equal("c", s.readchar());
assert.equal(3, s.pos());
s = new StringInputStream("");
assert.equal(0, s.pos());
assert["throws"](function () { s.ungetc("z"); });
assert["throws"](function () { s.ungetc(""); });
assert.equal(null, s.getc());
assert["throws"](function () { s.readchar(); });
assert.equal(0, s.pos());
function test_read (str, o) {
assert.equal(str, repr(o));
var rs = readFromString(str);
assert.deepEqual(o, rs);
assert.equal(str, repr(rs));
};
test_read("zzz", S("zzz"));
test_read("'zzz", list(S("quote"), S("zzz")));
test_read('"zzz"', "zzz");
test_read('"zz\nz"', "zz\nz");
test_read('\'"zzz"', list(S("quote"), "zzz"));
test_read('"z\\"z\\\\z"', "z\"z\\z");
test_read("nil", nil);
test_read("(1)", list(1));
test_read("(1 2)", list(1, 2));
test_read("(1 2 4.25)", list(1, 2, 4.25));
test_read("(1 2 eprst)", list(1, 2, S("eprst")));
test_read('(1 2 eprst ("abra" "kodabra"))',
list(1, 2, S("eprst"), list("abra", "kodabra")));
test_read('(1 2 eprst ("abra" . "kodabra"))',
list(1, 2, S("eprst"), cons("abra", "kodabra")));
test_read('(1 2 eprst ("abra" "kodabra" .schwabbra))',
list(1, 2, S("eprst"), list("abra", "kodabra", S(".schwabbra"))));
test_read('(1 2 eprst ("abra" "kodabra" .schwabbra . QQQ))',
list(1, 2, S("eprst"),
cons("abra",cons("kodabra", cons(S(".schwabbra"), S("QQQ"))))));
test_read('(1 2 eprst \'("abra" "kodabra" .schwabbra . QQQ))',
list(1, 2, S("eprst"),
list(S("quote"),
cons("abra", cons("kodabra", cons(S(".schwabbra"), S("QQQ")))))));
test_read("(1 2 3)", list(1, 2, 3));
test_read("(1 2 3 (4 5 6))", list(1, 2, 3, list(4, 5, 6)));
test_read("((4 5 6) . 7)", cons(list(4, 5, 6), 7));
test_read("((4 5 6) 7 8 . :eprst)",
cons(list(4, 5, 6), cons(7, cons(8, S(":eprst")))));
test_read("((4 5 6) 7 8 . swank:connection-info)",
cons(list(4, 5, 6), cons(7, cons(8, S("swank:connection-info")))));
var CONV_ERROR = {};
function test_conversion(spec, source, expectedResult, reconverted) {
var r, l = readFromString(source);
try {
r = spec === null ? fromLisp(l) : fromLisp(l, spec);
} catch (e) {
if (e instanceof TypeError && /^error converting/.test(e.message))
r = CONV_ERROR;
else
throw e;
}
assert.deepEqual(expectedResult, r);
if (r !== CONV_ERROR)
assert.equal(reconverted || source, repr(spec === null ? toLisp(r) : toLisp(r, spec)));
}
test_conversion("N", "1", 1);
test_conversion("K", ":abc", "abc");
test_conversion("K", "nil", null);
test_conversion("B", "t", true);
test_conversion("B", "nil", false);
test_conversion("B", "123", true, "t");
test_conversion("B", ":zzz", true, "t");
test_conversion("@", '(test nil () 123 "456" :zzz (1 2 3) (4 . 5))',
["test", null, null, 123, "456", ":zzz", [1, 2, 3], [4, 5]],
'("test" nil nil 123 "456" ":zzz" (1 2 3) (4 5))');
test_conversion(null, '(test nil () 123 "456" :zzz (1 2 3) (4 . 5))',
["test", null, null, 123, "456", ":zzz", [1, 2, 3], [4, 5]],
'("test" nil nil 123 "456" ":zzz" (1 2 3) (4 5))');
test_conversion(["N:one"], "(1)", { one: 1 });
test_conversion(["N:one", "N:two", "N:three"], "(1 2 3)", { one: 1, two: 2, three: 3 });
test_conversion(["N:one", "N:two", "N:three"], "(1 2)", CONV_ERROR);
test_conversion(["N:one", "N:two", "s:zzz"], '(1 2 "qqqq")', { one: 1, two: 2, zzz: "qqqq" });
test_conversion(["N:one", "N:two", "s:zzz"], '(1 2 3)', CONV_ERROR);
test_conversion(["N:one", "N:two", "s:zzz"], '(1 2 :RRR)', CONV_ERROR);
test_conversion(["S:op", "_:form", "_:packageName", "_:threadId", "N:id"],
'(:emacs-rex (swank:connection-info) "COMMON-LISP-USER" t 1)',
{ op: ":emacs-rex",
form: list(S("swank:connection-info")),
packageName: "COMMON-LISP-USER",
threadId: S("t"),
id: 1 });
test_conversion(["@:x"], "(test)", { x: "test" }, '("test")');
test_conversion(["@:x"], '((test 123 "456" :zzz (1 2 3) (4 . 5)))',
{ x: ["test", 123, "456", ":zzz", [1, 2, 3], [4, 5]] },
'(("test" 123 "456" ":zzz" (1 2 3) (4 5)))');
test_conversion(["S:name", "R:args"], '(test)',
{ name: "test",
args: [] });
test_conversion(["S:name", "R:args"], '(test :abc :def "QQQ" 123)',
{ name: "test",
args: [":abc", ":def", "QQQ", 123] },
'(test ":abc" ":def" "QQQ" 123)');
test_conversion(["S:name", "R*:args"], '(test)',
{ name: "test",
args: [] });
test_conversion(["S:name", "R*:args"], '(test :abc :def (123 . 456))',
{ name: "test",
args: [S(":abc"), S(":def"), cons(123, 456)] });
test_conversion(["N:n", "D:dict"], '(42.25 ())',
{ n: 42.25, dict: {} },
'(42.25 nil)');
test_conversion(["N:n", "D:dict"], '(42.25 (:x 3))',
{ n: 42.25, dict: { x: 3 } });
test_conversion(["N:n", "D:dict"], '(42.25 (:x))', CONV_ERROR);
test_conversion(["N:n", "D:dict"], '(42.25 (:x :y :z))', CONV_ERROR);
test_conversion(["N:n", "D:dict"], '(42.25 (:x 3 :abc "fff" :zzz qwerty))',
{ n: 42.25, dict: { x: 3, abc: "fff", zzz: "qwerty" }},
'(42.25 (:abc "fff" :x 3 :zzz "qwerty"))');
test_conversion(["N:n", "D*:dict"], '(42.25 ())',
{ n: 42.25, dict: {} },
'(42.25 nil)');
test_conversion(["N:n", "D*:dict"], '(42.25 (:x 3))',
{ n: 42.25, dict: { x: 3 } });
test_conversion(["N:n", "D*:dict"], '(42.25 (:x))', CONV_ERROR);
test_conversion(["N:n", "D*:dict"], '(42.25 (:x :y :z))', CONV_ERROR);
test_conversion(["N:n", "D*:dict"], '(42.25 (:x 3 :abc "fff" :zzz qwerty))',
{ n: 42.25, dict: { x: 3, abc: "fff", zzz: S("qwerty") } },
'(42.25 (:abc "fff" :x 3 :zzz qwerty))');
test_conversion(["N:n", "RD:dict"], '(42.25)',
{ n: 42.25, dict: {} });
test_conversion(["N:n", "RD:dict"], '(42.25 :x 3)',
{ n: 42.25, dict: { x: 3 } });
test_conversion(["N:n", "RD:dict"], '(42.25 :x)', CONV_ERROR);
test_conversion(["N:n", "RD:dict"], '(42.25 :x 3 :abc "fff" :zzz qwerty)',
{ n: 42.25, dict: { x: 3, abc: "fff", zzz: "qwerty" }},
'(42.25 :abc "fff" :x 3 :zzz "qwerty")');
test_conversion(["N:n", "RD*:dict"], '(42.25)',
{ n: 42.25, dict: {} });
test_conversion(["N:n", "RD*:dict"], '(42.25 :x 3)',
{ n: 42.25, dict: { x: 3 } });
test_conversion(["N:n", "RD*:dict"], '(42.25 :x)', CONV_ERROR);
test_conversion(["N:n", "RD*:dict"], '(42.25 :x 3 :abc "fff" :zzz qwerty)',
{ n: 42.25, dict: { x: 3, abc: "fff", zzz: S("qwerty") } },
'(42.25 :abc "fff" :x 3 :zzz qwerty)');
test_conversion({ x: "N", "abc-def": "D:abcDef", name: "S", rrr: "_:r1", qqq: "_" },
'(:abc-def (:x 3 :y 9) :x 42 :name :abcd :rrr "whatever" :unused 99)',
{ x: 42, abcDef: { x: 3, y: 9 }, name: ":abcd", r1: "whatever" },
'(:abc-def (:x 3 :y 9) :name :abcd :rrr "whatever" :x 42)');
// > and >* tell arrayValue to consume the next argument as type value
test_conversion(["S:name", ">:dict", { x: "N", y: "S" }],
'(:somename (:x 32 :y :zzz))',
{ name: ":somename", dict: { x: 32, y: ":zzz" } });
test_conversion(["S:name", ">*:dict", { x: "N", y: "S" }],
'(:somename :x 32 :y :zzz)',
{ name: ":somename", dict: { x: 32, y: ":zzz" } });
test_conversion({ x: "N", l: { name: "theList", spec: ["S:name", "N:n", "K:keyword"] },
d: { name: "dict1", spec: { a: "N", b: "N" } },
d2: { spec: { a: "N", b: "N" } }},
'(:x 99 :l (zzz 42 :eprst) :d (:a 11 :b 12) :d2 (:a 1 :b 2))',
{ x: 99,
theList: { name: "zzz", n: 42, keyword: "eprst" },
dict1: { a: 11, b: 12 },
d2: { a: 1, b : 2 } },
'(:d (:a 11 :b 12) :d2 (:a 1 :b 2) :l (zzz 42 :eprst) :x 99)');
assert.equal("(:abc 12 :def 4242)", repr(toLisp({ abc: 12, def: 4242 }, "@")));
assert.equal("(:abc 19)", repr(toLisp({ x: 19 }, [S(":abc"), "N:x"])));
assert.equal("(abc 19 :def)", repr(toLisp({ x: 19 }, [S("abc"), "N:x", S(":def")])));
assert.equal("nil", repr(toLisp(null, "@")));
assert.equal("nil", repr(toLisp(null, "_")));
// TBD: toLisp should use "@" as spec by default

600
emacs.d/swank-js/lisp.js Normal file
View file

@ -0,0 +1,600 @@
// -*- mode: js2; js-run: "lisp-tests.js" -*-
//
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
var util = require("util");
var assert = process.assert;
var I = {};
function _symbol (name) {
this.name = name;
}
_symbol.prototype.toString = function toString () {
return this.name;
};
function S(name) {
if (I.hasOwnProperty(name))
return I[name];
return I[name] = new _symbol(name);
};
function symbolp (o) {
return o instanceof _symbol;
};
var nil = S("nil");
function nullp (o) {
return o === nil;
};
function _cons (car, cdr) {
this.car = car;
this.cdr = cdr;
}
_cons.prototype.toString = function toString () {
var result = [];
if (this.car == S("quote") && consp(this.cdr) && nullp(this.cdr.cdr))
return "'" + repr(this.cdr.car);
for (var c = this;; c = c.cdr) {
if (consp(c))
result.push(repr(c.car));
else {
if (!nullp(c))
result.push(". " + repr(c));
break;
}
}
return '(' + result.join(" ") + ')';
};
function consp (o) {
return o instanceof _cons;
}
function cons (car, cdr) {
return new _cons(car, cdr);
}
function car (o) {
return o.car;
}
function cdr (o) {
return o.cdr;
}
function repr (x) {
if (typeof(x) == "string")
return '"' + x.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
return String(x);
};
function list () {
var tail = nil;
for (var i = arguments.length - 1; i >= 0; --i)
tail = cons(arguments[i], tail);
return tail;
}
function listp (o) {
return nullp(o) || consp(o);
}
function reverse (l) {
var r = nil;
for (; !nullp(l); l = cdr(l))
r = cons(car(l), r);
return r;
}
function append (l1, l2) {
if (l1 === nil)
return l2;
var r = cons(car(l1), nil), tail = r;
while ((l1 = cdr(l1)) !== nil) {
tail.cdr = cons(car(l1));
tail = tail.cdr;
}
tail.cdr = l2;
return r;
}
function StringInputStream (string) {
this._string = string;
this._pos = 0;
this._max = this._string.length;
}
StringInputStream.prototype.pos = function pos () {
return this._pos;
};
StringInputStream.prototype.getc = function getc () {
if (this._pos == this._max)
return null;
return this._string.charAt(this._pos++);
};
StringInputStream.prototype.readchar = function readchar () {
var c = this.getc();
if (c === null)
throw new Error("StringInputStream.readchar(): EOF reached");
return c;
};
StringInputStream.prototype.ungetc = function ungetc (c) {
if (this._pos > 0 && this._string[this._pos - 1] == c)
--this._pos;
else { /* FIXME: { is just to make nodejs repl happy */
throw new Error("StringInputStream.ungetc(): invalid argument");
}
};
function LispReader (s) {
this.s = s;
}
LispReader.prototype.readNumberOrSymbol = function readNumberOrSymbol () {
var token = this.readToken();
if (token == "")
throw new Error("LispReader.readNumberOrSymbol(): EOF reached");
if (/^[-+]?[0-9]+$/.test(token))
return parseInt(token);
if (/^[-+]?[0-9]*\.?[0-9]+(?:[dDeE][-+]?[0-9]+)?/.test(token))
return parseFloat(token.replace(/d/g, "e"));
return S(token);
};
LispReader.prototype.read = function read () {
this.skipWhitespace();
var c = this.s.getc();
switch (c) {
case "(":
return this.readList();
case '"':
return this.readString();
case "'":
return this.readQuote();
case null:
throw new Error("LispReader.read(): EOF reached");
default:
this.s.ungetc(c);
return this.readNumberOrSymbol();
}
};
LispReader.prototype.readList = function readList () {
var l = nil;
var head = nil;
while (true) {
this.skipWhitespace();
var c = this.s.readchar();
switch (c) {
case ")":
return l;
case ".":
var c1 = this.s.readchar();
if (" \n\t".indexOf(c1) < 0)
this.s.ungetc(c1); // process the default case
else {
if (nullp(l))
throw new Error("LispReader.readList(): invalid placement of the dot");
head.cdr = this.read();
return l;
}
default:
this.s.ungetc(c);
if (nullp(l)) {
l = list(this.read());
head = l;
} else {
head.cdr = list(this.read());
head = head.cdr;
}
}
}
return null; /* never get there */
};
LispReader.prototype.readString = function readString () {
var r = [];
while (true) {
var c = this.s.readchar();
switch (c) {
case '"':
return r.join("");
case "\\":
c = this.s.readchar();
if (c != "\\" && c != '"')
throw new Error("Invalid escape char " + c);
}
r.push(c);
}
return null; /* never get there */
};
LispReader.prototype.readQuote = function readQuote () {
return list(S("quote"), this.read());
};
LispReader.prototype.readToken = function readToken () {
var c, token = [];
while ((c = this.s.getc()) != null) {
if (this.isTerminating(c)) {
this.s.ungetc(c);
break;
}
token.push(c);
}
return token.join("");
};
LispReader.prototype.skipWhitespace = function skipWhitespace () {
while (true) {
var c = this.s.getc();
switch (c) {
case " ":
case "\n":
case "\t":
continue;
case null:
return;
default:
this.s.ungetc(c);
return;
}
}
};
LispReader.prototype.isTerminating = function isTerminating (c) {
return " \n\t()\"'".indexOf(c) >= 0;
};
function readFromString (str) {
return new LispReader(new StringInputStream(str)).read();
}
function _conversionError (value, spec) {
return new TypeError(
"error converting " + util.inspect(value) + " using spec " + util.inspect(spec));
};
function naturalValue (v) {
if (typeof(v) == "number" || typeof(v) == "string")
return v;
else if (symbolp(v))
return v === nil ? null : v.name;
else if (consp(v)) {
var result = [];
for (; v !== nil; v = cdr(v)) {
if (consp(v))
result.push(naturalValue(car(v)));
else {
result.push(naturalValue(v));
break;
}
}
return result;
} else
return v;
};
function plistValue (l, useNatural, map) {
assert(!map || !useNatural);
var origList = l;
var result = {};
for (; l !== nil; l = cdr(cdr(l))) {
if (!consp(l) || !consp(cdr(l)))
throw _conversionError(origList, "<plist>");
var nameSym = car(l);
if (!symbolp(nameSym))
throw _conversionError(origList, "<plist>");
var value = car(cdr(l));
var targetName = nameSym.name.replace(/^.*:/, "").toLowerCase();
if (useNatural)
result[targetName] = naturalValue(value);
else if (map) {
if (!map.hasOwnProperty(targetName))
continue;
var mapSpec = map[targetName];
if (typeof(mapSpec) == "object") {
assert(mapSpec.spec);
result[mapSpec.hasOwnProperty("name") ? mapSpec.name : targetName] =
fromLisp(value, mapSpec.spec);
} else {
var arg = mapSpec.split(/:/);
if (arg.length > 1)
result[arg[1]] = fromLisp(value, arg[0]);
else
result[targetName] = fromLisp(value, arg[0]);
}
} else
result[targetName] = value;
}
return result;
};
function plainList (l, spec) {
var result = {};
var origList = l;
for (var i = 0; i < spec.length; ++i, l = cdr(l)) {
if (l !== nil && !consp(l))
throw _conversionError(origList, spec);
var arg = spec[i].split(/:/);
var type = arg[0];
var name = arg[1];
if (type == ">") {
assert(i < spec.length - 1);
type = spec[++i];
}
if (type == ">*") {
assert(i < spec.length - 1);
result[name] = fromLisp(l, spec[++i]);
l = nil;
break;
}
if (type == "R" || type == "R*") {
result[name] = [];
for (; l !== nil; l = cdr(l))
result[name].push(type == "R*" ? car(l) : naturalValue(car(l)));
break;
}
if (type == "RD" || type == "RD*") {
result[name] = plistValue(l, type == "RD");
l = nil;
break;
}
if (l === nil)
throw _conversionError(origList, spec);
result[name] = fromLisp(car(l), type);
}
if (l !== nil)
throw _conversionError(origList, spec);
return result;
};
function fromLisp (o, spec) {
spec = spec || "@";
if (typeof(spec) == "string") {
switch (spec) {
case 'B':
return naturalValue(o) !== null;
case 'S':
if (!symbolp(o))
throw _conversionError(o, spec);
return nullp(o) ? null : o.name;
case 'K':
if (!symbolp(o) || (!nullp(o) && !/:/.test(o.name)))
throw _conversionError(o, spec);
return nullp(o) ? null : o.name.replace(/^:/, "");
case 's':
if (typeof(o) != "string")
throw _conversionError(o, spec);
return o;
case 'N':
if (typeof(o) != "number")
throw _conversionError(o, spec);
return o;
case 'D':
case 'D*':
return plistValue(o, spec == "D");
case "@":
return naturalValue(o);
case '_':
return o;
}
} else if (spec instanceof Array)
return plainList(o, spec);
else if (typeof(spec) == "object")
return plistValue(o, false, spec);
throw new Error("invalid destructuring type spec");
}
function naturalValueToLisp (v) {
if (v === null)
return nil;
if (typeof(v) == "number" || typeof(v) == "string" || symbolp(v) || consp(v))
return v;
if (v instanceof Array) {
var r = nil;
for (var i = 0; i < v.length; ++i)
r = cons(naturalValueToLisp(v[i]), r);
return reverse(r);
}
if (typeof(v) != "object")
throw _conversionError(v, "<natural>");
var keys = [];
for (var k in v) {
if (v.hasOwnProperty(k))
keys.push(k);
}
keys.sort();
var r = nil;
for (var i = 0; i < keys.length; ++i) {
var keyNameSym = /:/.test(keys[i]) ? S(keys[i]) : S(":" + keys[i]);
r = cons(naturalValueToLisp(v[keys[i]]), cons(keyNameSym, r));
}
return reverse(r);
};
function plistValueToLisp (o, useNatural) {
var r = nil;
var keys = [];
for (var k in o) {
if (o.hasOwnProperty(k))
keys.push(k);
}
keys.sort();
for (var i = 0; i < keys.length; ++i) {
var v = o[keys[i]];
if (useNatural)
v = naturalValueToLisp(v);
var keyNameSym = /:/.test(keys[i]) ? S(keys[i]) : S(":" + keys[i]);
r = cons(v, cons(keyNameSym, r));
}
return reverse(r);
}
function mappedPlistValueToLisp (o, map) {
var items = [];
for (var k in map) {
if (!map.hasOwnProperty(k))
continue;
var mapSpec = map[k];
if (typeof(mapSpec) == "object") {
assert(mapSpec.spec);
items.push({ jsName: mapSpec.hasOwnProperty("name") ? mapSpec.name : k,
lispName: k, spec: mapSpec.spec });
} else {
var arg = mapSpec.split(/:/);
items.push({ jsName: arg.length > 1 ? arg[1] : k,
lispName: k, spec: arg[0] });
}
}
items.sort(function (a, b) {
if (a.lispName < b.lispName)
return -1;
else if (a.lispName > b.lispName)
return 1;
return 0;
});
var r = nil;
for (var i = 0; i < items.length; ++i) {
if (!o.hasOwnProperty(items[i].jsName))
continue;
var v = toLisp(o[items[i].jsName], items[i].spec);
var lispName = items[i].lispName;
var keyNameSym = /:/.test(lispName) ? S(lispName) : S(":" + lispName);
r = cons(v, cons(keyNameSym, r));
}
return reverse(r);
};
function plainListToLisp (o, spec) {
var r = nil;
if (typeof(o) != "object")
throw _conversionError(o, spec);
for (var i = 0; i < spec.length; ++i) {
if (symbolp(spec[i])) {
r = cons(spec[i], r);
continue;
}
var arg = spec[i].split(/:/);
var type = arg[0];
var name = arg[1];
if (!o.hasOwnProperty(name))
throw _conversionError(o, spec);
var v = o[name];
if (type == ">") {
assert(i < spec.length - 1);
type = spec[++i];
}
switch (type) {
case ">*":
assert(i < spec.length - 1);
return append(reverse(r), toLisp(v, spec[++i]));
case "R":
return append(reverse(r), naturalValueToLisp(v));
case "R*":
return append(reverse(r), list.apply(null, v));
case "RD":
case "RD*":
return append(reverse(r), plistValueToLisp(v, type == "RD"));
default:
r = cons(toLisp(v, type), r);
}
}
return reverse(r);
};
function toLisp (o, spec) {
spec = spec || "@";
if (typeof(spec) == "string") {
switch (spec) {
case 'B':
return !o || o === nil ? nil : S("t");
case 'S':
case 'K':
if (symbolp(o))
return o;
if (o === null)
return nil;
if (typeof(o) != "string")
throw _conversionError(o, spec);
return S(spec == "S" ? o : ":" + o);
case 's':
if (typeof(o) != "string")
throw _conversionError(o, spec);
return o;
case 'N':
if (typeof(o) != "number")
throw _conversionError(o, spec);
return o;
case 'D':
case 'D*':
return plistValueToLisp(o, spec == "D");
case "@":
return naturalValueToLisp(o);
case '_':
return o === null ? nil : o;
}
} else if (spec instanceof Array)
return plainListToLisp(o, spec);
else if (typeof(spec) == "object")
return mappedPlistValueToLisp(o, spec);
throw new Error("invalid destructuring type spec");
}
exports.S = S;
exports.cons = cons;
exports.consp = consp;
exports.car = car;
exports.cdr = cdr;
exports.nil = nil;
exports.nullp = nullp;
exports.listp = listp;
exports.list = list;
exports.reverse = reverse;
exports.append = append;
exports.repr = repr;
exports.StringInputStream = StringInputStream;
exports.readFromString = readFromString;
exports.fromLisp = fromLisp;
exports.toLisp = toLisp;

View file

@ -0,0 +1,203 @@
;;;
;;; Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
;;;
;;; Redistribution and use in source and binary forms, with or without
;;; modification, are permitted provided that the following conditions
;;; are met:
;;;
;;; * Redistributions of source code must retain the above copyright
;;; notice, this list of conditions and the following disclaimer.
;;;
;;; * Redistributions in binary form must reproduce the above
;;; copyright notice, this list of conditions and the following
;;; disclaimer in the documentation and/or other materials
;;; provided with the distribution.
;;;
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
(define-slime-contrib slime-js
"Emacs-side support for Swank-JS."
(:authors "Ivan Shvedunov")
(:license "X11-style")
(:on-load
(add-hook 'slime-event-hooks 'slime-js-event-hook-function))
(:on-unload
(remove-hook 'slime-event-hooks 'slime-js-event-hook-function)))
(defun slime-js-repl-update-package ()
(let ((name (slime-current-package)))
(with-current-buffer (slime-output-buffer)
(let ((previouse-point (- (point) slime-repl-input-start-mark)))
(setf (slime-lisp-package) name
(slime-lisp-package-prompt-string) name
slime-buffer-package name)
(slime-repl-insert-prompt)
(when (plusp previouse-point)
(goto-char (+ previouse-point slime-repl-input-start-mark)))))))
(defun slime-js-event-hook-function (event)
(when (equal "JS" (slime-lisp-implementation-type))
(destructure-case event
((:new-package package prompt)
(let ((buffer (slime-connection-output-buffer)))
(setf (slime-lisp-package) package)
(setf (slime-lisp-package-prompt-string) prompt)
(when (buffer-live-p buffer)
(with-current-buffer buffer
(setq slime-buffer-package package)
(slime-js-repl-update-package)
(save-excursion
(goto-char (marker-position slime-repl-prompt-start-mark))
(slime-mark-output-start))))
t))
(t nil))))
(defvar slime-js-remote-history nil
"History list for JS remote names.")
(defun slime-js-read-remote-index (&optional prompt)
(let* ((completion-ignore-case nil)
(remotes (slime-eval '(js:list-remotes)))
(remote-names
(loop for remote in remotes
collect (concat (third remote)
"/"
(replace-regexp-in-string
"^:" ""(symbol-name (second remote))))))
(prompt (or prompt "Remote: "))
(p (or (position
(completing-read prompt (slime-bogus-completion-alist remote-names)
nil nil nil
'slime-remote-history nil)
remote-names :test #'equal)
(error "bad remote name"))))
(first (elt remotes p))))
(defun slime-js-select-remote (n)
"Select JS remote by number"
(interactive (list (slime-js-read-remote-index)))
(slime-eval-async `(js:select-remote ,n nil)))
(defslime-repl-shortcut slime-repl-js-select-remote ("select-remote")
(:handler 'slime-js-select-remote)
(:one-liner "Select JS remote."))
(defun slime-js-sticky-select-remote (n)
"Select JS remote by number in sticky mode"
(interactive (list (slime-js-read-remote-index)))
(slime-eval-async `(js:select-remote ,n t)))
(defslime-repl-shortcut slime-repl-js-sticky-select-remote ("sticky-select-remote")
(:handler 'slime-js-sticky-select-remote)
(:one-liner "Select JS remote in sticky mode."))
(defun slime-js-set-target-url (url)
"Set target URL for the proxy"
(interactive "sTarget URL: ")
(slime-eval-async `(js:set-target-url ,url)))
(defslime-repl-shortcut slime-repl-js-set-target-url ("target-url")
(:handler 'slime-js-set-target-url)
(:one-liner "Select target URL for the swank-js proxy"))
(defun slime-js-set-slime-version (url)
"Set SLIME version for swank-js"
(interactive "sVersion: ")
(slime-eval-async `(js:set-slime-version ,url)))
(defslime-repl-shortcut slime-repl-js-set-slime-version ("js-slime-version")
(:handler 'slime-js-set-slime-version)
(:one-liner "Set SLIME version for swank-js"))
;; FIXME: should add an rpc command for browser-only eval
(defun slime-js-eval (str &optional cont)
(slime-eval-async `(swank:interactive-eval ,str) cont))
(defun slime-js-reload ()
(interactive)
(slime-js-eval "SwankJS.reload()"
#'(lambda (v)
(message "Reloading the page"))))
(defun slime-js-refresh-css ()
(interactive)
(slime-js-eval "SwankJS.refreshCSS()"
#'(lambda (v)
(message "Refreshing CSS"))))
(defun slime-js-start-of-toplevel-form ()
(interactive)
(when js2-mode-buffer-dirty-p
(js2-mode-wait-for-parse #'slime-js-start-of-toplevel-form))
(js2-forward-sws)
(if (= (point) (point-max))
(js2-mode-forward-sexp -1)
(let ((node (js2-node-at-point)))
(when (or (null node)
(js2-ast-root-p node))
(error "cannot locate any toplevel form"))
(while (and (js2-node-parent node)
(not (js2-ast-root-p (js2-node-parent node))))
(setf node (js2-node-parent node)))
(goto-char (js2-node-abs-pos node))
(js2-forward-sws)))
(point))
(defun slime-js-end-of-toplevel-form ()
(interactive)
(js2-forward-sws)
(let ((node (js2-node-at-point)))
(unless (or (null node) (js2-ast-root-p node))
(while (and (js2-node-parent node)
(not (js2-ast-root-p (js2-node-parent node))))
(setf node (js2-node-parent node)))
(goto-char (js2-node-abs-end node)))
(point)))
;; FIXME: this breaks if // comment directly precedes the function
(defun slime-js-send-defun ()
(interactive)
(save-excursion
(lexical-let ((start (slime-js-start-of-toplevel-form))
(end (slime-js-end-of-toplevel-form)))
;; FIXME: use slime-eval-region
(slime-js-eval
(buffer-substring-no-properties start end)
#'(lambda (v)
(save-excursion
(goto-char start)
(let ((sent-func "<...>"))
(when (looking-at "[ \t]*\\([^ \t\n{}][^\n{}]*\\)")
(setf sent-func (match-string 1)))
(message "Sent: %s" sent-func))))))))
(define-minor-mode slime-js-minor-mode
"Toggle slime-js minor mode
With no argument, this command toggles the mode.
Non-null prefix argument turns on the mode.
Null prefix argument turns off the mode."
nil
" slime-js"
'(("\C-\M-x" . slime-js-send-defun)
("\C-c\C-c" . slime-js-send-defun)
;; ("\C-c\C-r" . slime-eval-region)
("\C-c\C-z" . slime-switch-to-output-buffer)))
;; TBD: dabbrev in repl:
;; DABBREV--GOTO-START-OF-ABBREV function skips over REPL prompt
;; because it has property 'intangible' and (forward-char -1) doesn't do
;; what is expected at the propmpt edge. Must redefine this function
;; or define and advice for it.
;; TBD: lost continuations (pipelined request ...) - maybe when closing page
(provide 'slime-js)

View file

@ -0,0 +1,292 @@
// -*- mode: js2; js-run: t -*-
//
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
var swh = require("./swank-handler");
var readFromString = require("./lisp").readFromString;
var config = require("./config");
var util = require("util");
var assert = require("assert");
var cfg = new config.FakeConfig();
var expected = [];
var executive = new swh.Executive({ config: cfg, pid: 4242 });
var handler = new swh.Handler(executive);
handler.on(
"response", function (response) {
// console.log("=> %s", response);
assert.ok(expected.length > 0);
assert.ok(typeof(response) == "string");
// console.log("response: %s", response);
var expectedResponse = expected.shift();
if (expectedResponse instanceof RegExp)
assert.ok(expectedResponse.test(response));
else
assert.equal(expectedResponse, response,
"got response " + response + " instead of " + expectedResponse);
});
function expect () {
for (var i = 0; i < arguments.length; ++i)
expected.push(arguments[i]);
}
function verifyExpectations () {
// console.log("expected: %s\n", expected.map(JSON.stringify).join("\n"));
assert.equal(0, expected.length);
}
function request (str) {
for (var i = 1; i < arguments.length; ++i)
expected.push(arguments[i]);
handler.receive(readFromString(str));
verifyExpectations();
}
request('(:emacs-rex (swank:connection-info) "COMMON-LISP-USER" t 1)',
'(:return (:ok (:encoding (:coding-system "utf-8" :external-format "UTF-8") ' +
':lisp-implementation (:name "JS" :type "JS" :version "1.5") ' +
':package (:name "NODE" :prompt "NODE") ' +
':pid 4242 :version "2010-11-13")) ' +
'1)');
// currently we just ignore swank-require
request('(:emacs-rex (swank:swank-require \'(swank-listener-hooks swank-indentation)) "COMMON-LISP-USER" t 2)',
'(:return (:ok nil) 2)');
request('(:emacs-rex (swank:create-repl nil) "COMMON-LISP-USER" t 3)',
'(:return (:ok ("NODE" "NODE")) 3)');
request('(:emacs-rex (swank:listener-eval "3 * 10\n") "NODE" :repl-thread 4)',
'(:return (:ok (:values "30")) 4)');
request('(:emacs-rex (swank:listener-eval "undefined") "NODE" :repl-thread 5)',
'(:return (:ok nil) 5)');
request('(:emacs-rex (swank:autodoc \'("zzzz" swank::%cursor-marker%) :print-right-margin 236)' +
' "COMMON-LISP-USER" :repl-thread 6)',
'(:return (:ok :not-available) 6)');
request('(:emacs-rex (swank:listener-eval "_swank.output(\'hello world\\\\n\')") "NODE" :repl-thread 7)',
'(:write-string "hello world\n")',
'(:return (:ok nil) 7)');
request('(:emacs-rex (swank:listener-eval "_swank.output(1234)") "NODE" :repl-thread 8)',
'(:write-string "1234")',
'(:return (:ok nil) 8)');
request('(:emacs-rex (swank:listener-eval "zzz") "NODE" :repl-thread 9)',
/^\(:write-string "ReferenceError: zzz is not defined(.|\n)*"\)$/,
'(:return (:ok nil) 9)');
// TBD: debugger
function FakeRemote (name) {
this.name = name;
};
util.inherits(FakeRemote, swh.Remote);
FakeRemote.prototype.prompt = function prompt () {
return "FAKE";
};
FakeRemote.prototype.kind = function kind () {
return "test";
};
FakeRemote.prototype.id = function id () {
return this.name;
};
FakeRemote.prototype.evaluate = function evaluate (id, str) {
this.sendResult(id, [ "R:" + this.name + ":" + str.replace(/^\s*|\s*$/g, "") ]);
};
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 10)',
'(:return (:ok ((1 :direct "node.js" t))) 10)');
expect('(:write-string "Remote attached: (test) test/localhost:8080\n")');
var r1 = new FakeRemote("test/localhost:8080");
executive.attachRemote(r1);
verifyExpectations();
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 11)',
'(:return (:ok ((1 :direct "node.js" t) (2 :test "test/localhost:8080" nil))) 11)');
expect('(:write-string "Remote attached: (test) test/localhost:9999\n")');
var r2 = new FakeRemote("test/localhost:9999");
executive.attachRemote(r2);
verifyExpectations();
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 12)',
'(:return (:ok ((1 :direct "node.js" t) ' +
'(2 :test "test/localhost:8080" nil) ' +
'(3 :test "test/localhost:9999" nil))) 12)');
request('(:emacs-rex (swank:listener-eval "3 * 10\n") "NODE" :repl-thread 13)',
'(:return (:ok (:values "30")) 13)');
request('(:emacs-rex (js:select-remote 2 nil) "NODE" :repl-thread 14)',
'(:new-package "FAKE" "FAKE")',
'(:write-string "Remote selected: (test) test/localhost:8080\n")',
'(:return (:ok nil) 14)');
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 15)',
'(:return (:ok ((1 :direct "node.js" nil) ' +
'(2 :test "test/localhost:8080" t) ' +
'(3 :test "test/localhost:9999" nil))) 15)');
request('(:emacs-rex (swank:listener-eval "3 * 10\n") "NODE" :repl-thread 16)',
'(:return (:ok (:values "R:test/localhost:8080:3 * 10")) 16)');
expect('(:write-string "Remote detached: (test) test/localhost:8080\n")',
'(:new-package "NODE" "NODE")',
'(:write-string "Remote selected (auto): (direct) node.js\n")');
r1.disconnect();
verifyExpectations();
request('(:emacs-rex (swank:listener-eval "3 * 10\n") "NODE" :repl-thread 17)',
'(:return (:ok (:values "30")) 17)');
// TBD: add higher-level functions for testing remotes
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 18)',
'(:return (:ok ((1 :direct "node.js" t) ' +
'(3 :test "test/localhost:9999" nil))) 18)');
expect('(:write-string "Remote detached: (test) test/localhost:9999\n")');
r2.disconnect();
verifyExpectations();
request('(:emacs-rex (swank:listener-eval "3 * 10\n") "NODE" :repl-thread 19)',
'(:return (:ok (:values "30")) 19)');
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 20)',
'(:return (:ok ((1 :direct "node.js" t))) 20)');
request('(:emacs-rex (js:select-remote 2 nil) "NODE" :repl-thread 21)',
'(:write-string "WARNING: bad remote index\n")',
'(:return (:ok nil) 21)');
request('(:emacs-rex (swank:listener-eval "3 * 10\n") "NODE" :repl-thread 22)',
'(:return (:ok (:values "30")) 22)');
request('(:emacs-rex (js:select-remote 1 nil) "NODE" :repl-thread 23)',
'(:write-string "WARNING: remote already selected: (direct) node.js\n")',
'(:return (:ok nil) 23)');
assert.equal(null, cfg.getNow("stickyRemote"));
// test sticky remote selection
expect('(:write-string "Remote attached: (test) test/localhost:8001\n")');
var r3 = new FakeRemote("test/localhost:8001");
executive.attachRemote(r3);
verifyExpectations();
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 24)',
'(:return (:ok ((1 :direct "node.js" t) (4 :test "test/localhost:8001" nil))) 24)');
request('(:emacs-rex (js:select-remote 4 t) "NODE" :repl-thread 25)',
'(:new-package "FAKE" "FAKE")',
'(:write-string "Remote selected (sticky): (test) test/localhost:8001\n")',
'(:return (:ok nil) 25)');
assert.equal("(test) test/localhost:8001", cfg.getNow("stickyRemote"));
expect('(:write-string "Remote detached: (test) test/localhost:8001\n")',
'(:new-package "NODE" "NODE")',
'(:write-string "Remote selected (auto): (direct) node.js\n")');
r3.disconnect();
verifyExpectations();
expect('(:write-string "Remote attached: (test) test/localhost:8001\n")',
'(:new-package "FAKE" "FAKE")',
'(:write-string "Remote selected (auto): (test) test/localhost:8001\n")');
var r5 = new FakeRemote("test/localhost:8001");
executive.attachRemote(r5);
verifyExpectations();
assert.equal("(test) test/localhost:8001", cfg.getNow("stickyRemote"));
request('(:emacs-rex (js:select-remote 1 nil) "NODE" :repl-thread 26)',
'(:new-package "NODE" "NODE")',
'(:write-string "Remote selected: (direct) node.js\n")',
'(:return (:ok nil) 26)');
request('(:emacs-rex (js:select-remote 5 nil) "NODE" :repl-thread 27)',
'(:new-package "FAKE" "FAKE")',
'(:write-string "Remote selected: (test) test/localhost:8001\n")',
'(:return (:ok nil) 27)');
assert.equal(null, cfg.getNow("stickyRemote"));
expect('(:write-string "Remote detached: (test) test/localhost:8001\n")',
'(:new-package "NODE" "NODE")',
'(:write-string "Remote selected (auto): (direct) node.js\n")');
r5.disconnect();
verifyExpectations();
expect('(:write-string "Remote attached: (test) test/localhost:8001\n")');
var r6 = new FakeRemote("test/localhost:8001");
executive.attachRemote(r6);
verifyExpectations();
assert.equal(null, cfg.getNow("stickyRemote"));
request('(:emacs-rex (js:set-target-url "http://localhost:1234/") "NODE" :repl-thread 28)',
'(:return (:ok nil) 28)');
assert.equal("http://localhost:1234/", cfg.getNow("targetUrl"));
request('(:emacs-rex (js:set-target-url "zzz") "NODE" :repl-thread 29)',
'(:write-string "WARNING: the URL must contain host and port\n")',
'(:return (:ok nil) 29)');
assert.equal("http://localhost:1234/", cfg.getNow("targetUrl"));
assert.equal(null, cfg.getNow("slimeVersion"));
request('(:emacs-rex (js:set-slime-version "2010-11-28") "NODE" :repl-thread 30)',
'(:return (:ok nil) 30)');
assert.equal("2010-11-28", cfg.getNow("slimeVersion"));
// TBD: use ## instead of numbers in the tests above (request() should take care of it)
// TBD: test output from an inactive remote
// TBD: are out-of-order results for :emacs-rex ok? look at slime sources
/*
list/select remotes along the lines of
catching errors on the client: window.onerror
http://stackoverflow.com/questions/951791/javascript-global-error-handling
*/
// TBD: add \n to messages from remotes / executive

View file

@ -0,0 +1,416 @@
// -*- mode: js2; js-run: "swank-handler-tests.js" -*-
//
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
var EventEmitter = require("events").EventEmitter;
var Script = process.binding('evals').Script;
var evalcx = Script.runInContext;
var util = require("util");
var url = require("url");
var assert = process.assert;
var lisp = require("./lisp");
var S = lisp.S, list = lisp.list, consp = lisp.consp, car = lisp.car, cdr = lisp.cdr,
repr = lisp.repr, fromLisp = lisp.fromLisp, toLisp = lisp.toLisp;
var DEFAULT_SLIME_VERSION = "2010-11-13";
function Handler (executive) {
this.executive = executive;
var self = this;
this.executive.on("output", function (str) { self.output(str); });
this.executive.on("newPackage", function (name) { self.newPackage(name); });
};
util.inherits(Handler, EventEmitter);
Handler.prototype.receive = function receive (message) {
// FIXME: error handling
console.log("Handler.prototype.receive(): %s", repr(message).replace(/\n/, "\\n"));
if (!consp(message) || car(message) != S(":emacs-rex")) {
console.log("bad message: %s", message);
return;
}
var d, expr;
try {
d = fromLisp(message, ["S:op", ">:form",
["S:name", "R*:args"],
"_:package", "_:threadId", "N:id"]);
} catch (e) {
if (e instanceof TypeError) {
console.log("failed to parse %s: %s", message, e);
return; // FIXME
}
throw e;
}
var r = { status: ":ok", result: null };
var self = this;
var cont = function cont () {
self.sendResponse({ r: r, id: d.id },
[S(":return"), ">:r", ["S:status", "_:result"], "N:id"]);
};
switch (d.form.name) {
case "swank:connection-info":
this.executive.connectionInfo(
function (info) {
console.log("info = %j", info);
r.result = toLisp(
info,
{ "pid": "N:pid",
"encoding": { name: "encoding", spec: { "coding-system": "s:codingSystem",
"external-format": "s:externalFormat" } },
"package": { name: "packageSpec", spec: { name: "s", prompt: "s" } },
"lisp-implementation": {
name: "implementation",
spec: { type: "s", name: "s", version: "s" } },
"version": "s:version" });
cont();
});
return;
case "swank:create-repl":
r.result = toLisp(this.executive.createRepl(), ["s:packageName", "s:prompt"]);
break;
case "swank:autodoc":
r.result = S(":not-available");
break;
case "js:list-remotes":
// FIXME: support 'list of similar elements' type spec
r.result = toLisp(
this.executive.listRemotes().map(
function (item) {
return toLisp(item, ["N:index", "K:kind", "s:id", "B:isActive"]);
}, this));
break;
case "js:select-remote":
if (d.form.args.length != 2) {
console.log("bad args len for SWANK:SELECT-REMOTE -- %s", d.form.args.length);
return; // FIXME
}
// FIXME: get rid of spaghetti
var remoteIndex, sticky;
try {
// FIXME: args should be a cons / NIL
remoteIndex = fromLisp(d.form.args[0], "N");
sticky = fromLisp(d.form.args[1]);
} catch (e) {
if (e instanceof TypeError) {
console.log("can't parse arg -- %s", d.form.args[0]);
return; // FIXME
}
throw e;
}
this.executive.selectRemote(remoteIndex, sticky);
break;
case "js:set-target-url":
case "js:set-slime-version":
if (d.form.args.length != 1) {
console.log("bad args len for JS:SET-TARGET-URL -- %s", d.form.args.length);
return; // FIXME
}
try {
expr = fromLisp(d.form.args[0], "s");
} catch (e) {
if (e instanceof TypeError) {
console.log("can't parse arg -- %s", d.form.args[0]);
return; // FIXME
}
throw e;
}
this.executive[d.form.name == "js:set-target-url" ? "setTargetUrl" : "setSlimeVersion"](expr);
break;
case "swank:interactive-eval":
case "swank:listener-eval":
if (d.form.args.length != 1) {
console.log("bad args len for SWANK:LISTENER-EVAL -- %s", d.form.args.length);
return; // FIXME
}
try {
expr = fromLisp(d.form.args[0], "s");
} catch (e) {
if (e instanceof TypeError) {
console.log("can't parse arg -- %s", d.form.args[0]);
return; // FIXME
}
throw e;
}
this.executive.listenerEval(
expr, function (values) {
if (values.length)
r.result = toLisp({ values: values }, [S(":values"), "R:values"]);
cont();
});
return;
default:
// FIXME: handle unknown commands
}
cont();
};
Handler.prototype.output = function output (str) {
this.sendResponse([S(":write-string"), str]);
};
Handler.prototype.newPackage = function newPackage (name) {
this.sendResponse([S(":new-package"), name, name]);
};
Handler.prototype.sendResponse = function sendResponse(response, spec)
{
this.emit("response", repr(toLisp(response, spec || "@")));
};
function Remote () {};
util.inherits(Remote, EventEmitter);
Remote.prototype.prompt = function prompt () {
return "JS";
};
Remote.prototype.kind = function kind () {
throw new Error("must override Remote.prototype.kind()");
};
Remote.prototype.id = function id () {
throw new Error("must override Remote.prototype.id()");
};
Remote.prototype.evaluate = function evaluate (id, str) {
throw new Error("must override Remote.prototype.evaluate()");
};
Remote.prototype.fullName = function fullName () {
return "(" + this.kind() + ") " + this.id();
};
Remote.prototype.disconnect = function disconnect () {
this.emit("disconnect");
};
Remote.prototype.detachSelf = function detachSelf () {
this.removeAllListeners("output");
this.removeAllListeners("disconnect");
this.removeAllListeners("result");
};
Remote.prototype.output = function output (str) {
this.emit("output", String(str));
};
Remote.prototype.setIndex = function setIndex (n) {
this._index = n;
};
Remote.prototype.index = function index () {
return this._index;
};
Remote.prototype.sendResult = function sendResult (id, values) {
this.emit("result", id, values);
};
function DefaultRemote () {
this.context = Script.createContext();
for (var i in global) this.context[i] = global[i];
this.context.module = module;
this.context.require = require;
var self = this;
this.context._swank = {
output: function output (arg) {
self.output(arg);
}
};
}
util.inherits(DefaultRemote, Remote);
DefaultRemote.prototype.prompt = function prompt () {
return "NODE";
};
DefaultRemote.prototype.kind = function kind () {
return "direct";
};
DefaultRemote.prototype.id = function id () {
return "node.js";
};
DefaultRemote.prototype.evaluate = function evaluate (id, str) {
var r;
try {
r = evalcx(str, this.context, "repl");
} catch (e) {
r = undefined;
this.output(e.stack);
}
this.sendResult(id, r === undefined ? [] : [util.inspect(r)]);
};
// TBD: rename Executive to Dispatcher
function Executive (options) {
options = options || {};
assert(options.hasOwnProperty("config") && options.config);
this.config = options.config;
this.pid = options.hasOwnProperty("pid") ? options.pid : null;
this.remotes = [];
this.attachRemote(new DefaultRemote());
this.activeRemote = this.remotes[0];
this.pendingRequests = {};
};
util.inherits(Executive, EventEmitter);
Executive.nextId = 1; // request id counter is global in order to avoid inter-connection conflicts
Executive.nextRemoteIndex = 1;
Executive.prototype.attachRemote = function attachRemote (remote) {
assert(this.remotes.indexOf(remote) < 0);
remote.setIndex(Executive.nextRemoteIndex++);
var self = this;
remote.on(
"output", function (str) {
if (remote == self.activeRemote)
self.emit("output", str);
});
remote.on(
"disconnect", function (str) {
self.handleDisconnectRemote(remote);
});
remote.on(
"result", function (id, values) {
if (!self.pendingRequests[id]) {
self.emit("output", "WARNING: received late result from " + remote.fullName() + "\n");
return;
}
try {
self.pendingRequests[id](values);
} finally {
delete self.pendingRequests[id];
}
});
this.remotes.push(remote);
this.emit("output", "Remote attached: " + remote.fullName() + "\n");
this.config.get(
"stickyRemote",
function (stickyRemote) {
if (stickyRemote !== null &&
(!self.activeRemote || self.activeRemote.fullName() != stickyRemote) &&
remote.fullName() == stickyRemote)
self.selectRemote(remote.index(), true, true);
});
};
Executive.prototype.handleDisconnectRemote = function handleDisconnectRemote (remote) {
remote.detachSelf();
var index = this.remotes.indexOf(remote);
if (index < 0) {
this.emit("output", "WARNING: disconnectRemote() called for an unknown remote: " + remote.fullName() + "\n");
return;
}
this.remotes.splice(index, 1);
this.emit("output", "Remote detached: " + remote.fullName() + "\n");
if (remote == this.activeRemote)
this.selectRemote(this.remotes[0].index(), false, true);
};
Executive.prototype.connectionInfo = function connectionInfo (cont) {
var self = this;
var prompt = this.activeRemote.prompt();
this.config.get(
"slimeVersion",
function (slimeVersion) {
cont({ pid: self.pid === null ? process.pid : self.pid,
encoding: { codingSystem: "utf-8", externalFormat: "UTF-8" },
packageSpec: { name: prompt, prompt: prompt },
implementation: { type: "JS", name: "JS", version: "1.5" },
version: slimeVersion || DEFAULT_SLIME_VERSION });
});
};
Executive.prototype.createRepl = function createRepl () {
var prompt = this.activeRemote.prompt();
return { packageName: prompt, prompt: prompt };
};
Executive.prototype.listenerEval = function listenerEval (str, cont) {
var id = Executive.nextId++;
this.pendingRequests[id] = cont;
this.activeRemote.evaluate(id, str);
};
Executive.prototype.listRemotes = function listRemotes () {
return this.remotes.map(
function (remote) {
return { index: remote.index(), kind: remote.kind(), id: remote.id(),
isActive: remote === this.activeRemote };
}, this);
};
Executive.prototype.selectRemote = function selectRemote (index, sticky, auto) {
// TBD: sticky support (should autoselect the remote with message upon attachment)
for (var i = 0; i < this.remotes.length; ++i) {
var remote = this.remotes[i];
if (remote.index() == index) {
if (remote == this.activeRemote) {
this.emit("output", "WARNING: remote already selected: " + remote.fullName() + "\n");
return;
}
this.activeRemote = remote;
if (!auto)
this.config.set("stickyRemote", sticky ? remote.fullName() : null);
this.emit("newPackage", remote.prompt());
this.emit("output", "Remote selected" + (auto ? " (auto)" : sticky ? " (sticky)" : "") +
": " + remote.fullName() + "\n");
return;
}
}
this.emit("output", "WARNING: bad remote index\n");
};
Executive.prototype.setTargetUrl = function setTargetUrl (targetUrl) {
var parsedUrl = null;
try {
parsedUrl = url.parse(targetUrl);
} catch (e) {}
if (parsedUrl && parsedUrl.hostname)
this.config.set("targetUrl", targetUrl);
else
this.emit("output", "WARNING: the URL must contain host and port\n");
};
Executive.prototype.setSlimeVersion = function setSlimeVersion (slimeVersion) {
this.config.set("slimeVersion", slimeVersion);
};
exports.Handler = Handler;
exports.Remote = Remote;
exports.Executive = Executive;

View file

@ -0,0 +1,76 @@
// -*- mode: js2; js-run: t -*-
//
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
var swp = require("./swank-protocol");
var lisp = require("./lisp");
var Buffer = require('buffer').Buffer;
var assert = require("assert");
var S = lisp.S, list = lisp.list, nil = lisp.nil;
var expected = [];
var parser = new swp.SwankParser(
function onMessage (message) {
assert.ok(expected.length > 0);
var expectedMessage = expected.shift();
assert.deepEqual(expectedMessage, message);
});
function feed (text) {
for (var i = 1; i < arguments.length; ++i)
expected.push(arguments[i]);
parser.execute(text);
assert.equal(0, expected.length);
}
// dispatch: see dispatch-event in swank.lisp
feed("000");
feed("03b");
feed("(:emacs-rex (swank:connection-info) \"COMMON-LISP-USER\" t 1)",
list(S(":emacs-rex"), list(S("swank:connection-info")),
"COMMON-LISP-USER", S("t"), 1));
feed("0");
feed("0");
feed("0");
feed("03b(:emacs-rex (swank:connection-info)");
feed(" \"COMMON-LISP-USER\" t 1)000",
list(S(":emacs-rex"), list(S("swank:connection-info")),
"COMMON-LISP-USER", S("t"), 1));
feed("03b(:emacs-rex (swank:connection-info)");
feed(" \"COMMON-LISP-USER\" t 1");
feed(")",
list(S(":emacs-rex"), list(S("swank:connection-info")),
"COMMON-LISP-USER", S("t"), 1));
assert.equal(
"000015(:return (:ok nil) 1)",
swp.buildMessage(list(S(":return"), list(S(":ok"), nil), 1)));
// TBD: check unicode string handling (use \uxxxx notation)

View file

@ -0,0 +1,87 @@
// -*- mode: js2; js-run: "swank-protocol-tests.js" -*-
//
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
var readFromString = require("./lisp").readFromString;
const HEADER_LEN = 6;
const DUMMY_HEADER = "000000";
const MAX_MESSAGE_SIZE = 10 * 1024 * 1024;
function SwankParser (onMessage) {
this.needChars = HEADER_LEN;
this.handleData = this.handleHeader;
this.stash = "";
this.onMessage = onMessage;
};
// FIXME: proper error handling (handle both packet parsing and reader errors)
SwankParser.prototype.execute = function execute (text) {
var offset = 0;
while (offset < text.length)
offset += this.handleContent(text, offset);
};
SwankParser.prototype.handleContent = function handleContent (text, offset) {
var stashLen = this.stash.length;
var avail = Math.min(this.needChars, text.length + stashLen - offset);
var message = this.stash + text.substring(offset, offset + avail - stashLen);
if (avail < this.needChars)
this.stash = message;
else {
this.stash = "";
this.handleData(message);
}
return message.length - stashLen;
};
SwankParser.prototype.handleHeader = function handleHeader (str) {
var count = parseInt(str, 16) || 0;
if (count > 0 && count < MAX_MESSAGE_SIZE) {
this.needChars = count;
this.handleData = this.handleMessage;
} else
this.needChars = HEADER_LEN; // FIXME: handle errors
};
SwankParser.prototype.handleMessage = function handleMessage (str) {
this.onMessage(readFromString(str)); // FIXME: handle errors
this.needChars = HEADER_LEN;
this.handleData = this.handleHeader;
};
function buildMessage (obj) {
var str = obj.toString();
var lenStr = "" + str.length.toString(16);
while (lenStr.length < HEADER_LEN)
lenStr = "0" + lenStr;
return lenStr + str;
};
exports.SwankParser = SwankParser;
exports.buildMessage = buildMessage;

425
emacs.d/swank-js/swank.js Normal file
View file

@ -0,0 +1,425 @@
// -*- mode: js2 -*-
//
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
var net = require("net"), http = require('http'), io = require('socket.io'), util = require("util"),
url = require('url'), fs = require('fs');
var swh = require("./swank-handler");
var swp = require("./swank-protocol");
var ua = require("./user-agent");
var config = require("./config");
var DEFAULT_TARGET_HOST = "localhost";
var DEFAULT_TARGET_PORT = 8080;
var CONFIG_FILE_NAME = "~/.swankjsrc";
var cfg = new config.Config(CONFIG_FILE_NAME);
var executive = new swh.Executive({ config: cfg });
var swankServer = net.createServer(
function (stream) {
stream.setEncoding("utf-8");
var handler = new swh.Handler(executive);
var parser = new swp.SwankParser(
function onMessage (message) {
handler.receive(message);
});
handler.on(
"response", function (response) {
var responseText = swp.buildMessage(response);
console.log("response: %s", responseText);
stream.write(responseText);
});
stream.on(
"data", function (data) {
parser.execute(data);
});
stream.on(
"end", function () {
// FIXME: notify handler -> executive
// TBD: destroy the handler
handler.removeAllListeners("response");
});
});
swankServer.listen(process.argv[2] || 4005, process.argv[3] || "localhost");
function BrowserRemote (clientInfo, client) {
var userAgent = ua.recognize(clientInfo.userAgent);
this.name = userAgent.replace(/ /g, "") + (clientInfo.address ? (":" + clientInfo.address) : "");
this._prompt = userAgent.toUpperCase().replace(/ /g, '-');
this.client = client;
this.client.on(
"message", function(m) {
// TBD: handle parse errors
// TBD: validate incoming message (id, etc.)
console.log("message from browser: %s", JSON.stringify(m));
switch(m.op) {
case "output":
this.output(m.str);
break;
case "result":
if (m.error) {
this.output(m.error + "\n");
this.sendResult(m.id, []);
break;
}
this.sendResult(m.id, m.values);
break;
default:
console.log("WARNING: cannot interpret the client message");
}
}.bind(this));
this.client.on(
"disconnect", function() {
console.log("client disconnected: %s", this.id());
this.disconnect();
}.bind(this));
}
util.inherits(BrowserRemote, swh.Remote);
BrowserRemote.prototype.prompt = function prompt () {
return this._prompt;
};
BrowserRemote.prototype.kind = function kind () {
return "browser";
};
BrowserRemote.prototype.id = function id () {
return this.name;
};
BrowserRemote.prototype.evaluate = function evaluate (id, str) {
this.client.send({ id: id, code: str });
};
// proxy code from http://www.catonmat.net/http-proxy-in-nodejs
function HttpListener (cfg) {
this.config = cfg;
}
HttpListener.prototype.clientVersion = "0.1";
HttpListener.prototype.cachedFiles = {};
HttpListener.prototype.clientFiles = {
'json2.js': 'json2.js',
'stacktrace.js': 'stacktrace.js',
'swank-js.js': 'swank-js.js',
'load.js': 'load.js',
'test.html': 'test.html'
};
HttpListener.prototype.types = {
html: "text/html; charset=utf-8",
js: "text/javascript; charset=utf-8"
};
HttpListener.prototype.scriptBlock =
new Buffer(
'<script type="text/javascript" src="/swank-js/json2.js"></script>' +
'<script type="text/javascript" src="/socket.io/socket.io.js"></script>' +
'<script type="text/javascript" src="/swank-js/stacktrace.js"></script>' +
'<script type="text/javascript" src="/swank-js/swank-js.js"></script>');
HttpListener.prototype.findClosingTag = function findClosingTag (buffer, name) {
// note: this function is suitable for <head> and <body> tags,
// because they don't contain any repeating letters, but
// it will not work for tags that have such letters
var chars = [];
var endChar = ">".charCodeAt(0);
name = "</" + name.toLowerCase();
for (var i = 0; i < name.length; ++i)
chars.push(name.charCodeAt(i));
var A_CODE = "A".charCodeAt(0), Z_CODE = "Z".charCodeAt(0), CODE_INC = "a".charCodeAt(0) - A_CODE;
function codeToLower (x) {
return x >= A_CODE && x <= Z_CODE ? x + CODE_INC : x;
}
for (i = 0; i < buffer.length - chars.length - 1;) {
var found = true;
if (buffer[i++] != chars[0]) // note: no lowercasing for matching against '<'
continue;
for (var j = 1; j < chars.length; ++j, ++i) {
if (codeToLower(buffer[i]) != chars[j]) {
found = false;
break;
}
}
if (found) {
for (var k = i; k < buffer.length; ++k) {
if (buffer[k] == endChar)// note: no lowercasing for matching against '>'
return i - chars.length;
}
}
}
return -1;
};
HttpListener.prototype.injectScripts = function injectScripts (buffer, url) {
var p = this.findClosingTag(buffer, "head");
if (p < 0) {
p = this.findClosingTag(buffer, "body");
if (p < 0) {
// html blocks without head / body tags aren't that uncommon
// console.log("WARNING: unable to inject script block: %s", url);
return buffer;
}
}
var newBuf = new Buffer(buffer.length + this.scriptBlock.length);
buffer.copy(newBuf, 0, 0, p);
this.scriptBlock.copy(newBuf, p, 0);
buffer.copy(newBuf, p + this.scriptBlock.length, p);
return newBuf;
};
HttpListener.prototype.proxyRequest = function proxyRequest (request, response) {
var self = this;
this.config.get(
"targetUrl",
function (targetUrl) {
self.doProxyRequest(targetUrl, request, response);
});
};
HttpListener.prototype.doProxyRequest = function doProxyRequest (targetUrl, request, response) {
var self = this;
var headersSent = false;
var done = false;
var hostname = DEFAULT_TARGET_HOST;
var port = DEFAULT_TARGET_PORT;
var parsedUrl = null;
try {
parsedUrl = url.parse(targetUrl);
} catch (e) {}
if (parsedUrl && parsedUrl.hostname) {
hostname = parsedUrl.hostname;
port = parsedUrl.port ? parsedUrl.port - 0 : 80;
}
request.headers["host"] = hostname + (port == 80 ? "" : ":" + port);
delete request.headers["accept-encoding"]; // we don't want gzipped pages, do we?
// note on http client error handling:
// http://rentzsch.tumblr.com/post/664884799/node-js-handling-refused-http-client-connections
var proxy = http.createClient(port, hostname);
proxy.addListener(
'error', function handleError (e) {
console.log("proxy error: %s", e);
if (done)
return;
if (headersSent) {
response.end();
return;
}
response.writeHead(502, {'Content-Type': 'text/plain; charset=utf-8'});
response.end("swank-js: unable to forward the request");
});
console.log("PROXY: %s %s", request.method, request.url);
var proxyRequest = proxy.request(request.method, request.url, request.headers);
proxyRequest.addListener(
'response', function (proxyResponse) {
var contentType = proxyResponse.headers["content-type"];
var statusCode = proxyResponse.statusCode;
console.log("==> status %s", statusCode);
var headers = {};
for (k in proxyResponse.headers) {
if (proxyResponse.headers.hasOwnProperty(k))
headers[k] = proxyResponse.headers[k];
}
var chunks = proxyResponse.statusCode == 200 && contentType && /^text\/html\b|^application\/xhtml\+xml/.test(contentType) ?
[] : null;
if (chunks === null) {
// FIXME: without this, there were problems with redirects.
// I don't quite understand why...
response.writeHead(statusCode, headers);
headersSent = true;
}
proxyResponse.addListener(
'data', function (chunk) {
if (chunks !== null) {
chunks.push(chunk);
return;
}
if (!headersSent) {
response.writeHead(statusCode, headers);
headersSent = true;
}
response.write(chunk, 'binary');
});
proxyResponse.addListener(
'end', function() {
if (chunks !== null) {
console.log("^^MOD: %s %s", request.method, request.url);
var buf = new Buffer(chunks.reduce(function (s, chunk) { return s += chunk.length; }, 0));
var p = 0;
chunks.forEach(
function (chunk) {
chunk.copy(buf, p, 0);
p += chunk.length;
});
buf = self.injectScripts(buf, request.url);
headers["content-length"] = buf.length;
response.writeHead(statusCode, headers);
headersSent = true;
response.write(buf, 'binary');
} else if (!headersSent) {
response.writeHead(statusCode, headers);
headersSent = true;
}
response.end();
done = true;
});
});
request.addListener(
'data', function(chunk) {
proxyRequest.write(chunk, 'binary');
});
request.addListener(
'end', function() {
proxyRequest.end();
});
};
HttpListener.prototype.sendCachedFile = function sendCachedFile (req, res, path) {
if (req.headers['if-none-match'] == this.clientVersion) {
res.writeHead(304);
res.end();
} else {
res.writeHead(200, this.cachedFiles[path].headers);
res.end(this.cachedFiles[path].content, this.cachedFiles[path].encoding);
}
};
HttpListener.prototype.notFound = function notFound (res) {
res.writeHead(404, {'Content-Type': 'text/plain; charset=utf-8'});
res.end("file not found");
};
HttpListener.prototype.serveClient = function serveClient(req, res) {
var self = this;
var path = url.parse(req.url).pathname, parts, cn;
// console.log("%s %s", req.method, req.url);
if (path && path.indexOf("/swank-js/") != 0) {
// console.log("--> proxy");
this.proxyRequest(req, res);
return;
}
var file = path.substr(1).split('/').slice(1);
var localPath = this.clientFiles[file];
if (req.method == 'GET' && localPath !== undefined){
// TBD: reenable caching, check datetime of the file
// if (path in this.cachedFiles){
// this.sendCachedFile(req, res, path);
// return;
// }
fs.readFile(
__dirname + '/client/' + localPath, function(err, data) {
if (err) {
console.log("error: %s", err);
self.notFound(res);
} else {
var ext = localPath.split('.').pop();
self.cachedFiles[localPath] = {
headers: {
'Content-Length': data.length,
'Content-Type': self.types[ext],
'ETag': self.clientVersion
},
content: data,
encoding: ext == 'swf' ? 'binary' : 'utf8'
};
self.sendCachedFile(req, res, localPath);
}
});
} else {
console.log("bad request for /swank-js/ path");
this.notFound(res);
}
};
var httpListener = new HttpListener(cfg);
var httpServer = http.createServer(httpListener.serveClient.bind(httpListener));
httpServer.listen(8009);
var socket = io.listen(httpServer);
socket.on(
"connection", function (client) {
// new client is here!
console.log("client connected");
function handleHandshake (message) {
client.removeListener("message", handleHandshake);
if (!message.hasOwnProperty("op") || !message.op == "handshake")
console.warn("WARNING: bad handshake message: %j", message);
else {
var address = null;
if (client.connection && client.connection.remoteAddress)
address = client.connection.remoteAddress;
var remote = new BrowserRemote({ address: address, userAgent: message.userAgent }, client);
executive.attachRemote(remote);
console.log("added remote: %s", remote.fullName());
}
};
client.on("message", handleHandshake);
});
// TBD: handle reader errors
// function location determination:
// for code loaded from scripts: direct (if possible)
// for 'compiled' code: load the code by adding <script> tag loaded from the swank-js' webserver, its name should encode the real path and line offset
// for code entered via REPL: none
// PREPROCESS STACK TRACES!!!
// https://github.com/emwendelin/Javascript-Stacktrace
// ALSO: http://blog.yoursway.com/2009/07/3-painful-ways-to-obtain-stack-trace-in.html -- onerror in ie gives the innermost frame
// it should be also possible to 'soft-trace' functions so that they extend Exception objects with caller info as it passes through them
// TBD: unix domain sockets, normal slime startup
// TBD: http request logging (for specific remote)
// TBD: sudden disconnections (flashsocket), sometimes after lots of output (?) --
// Error: You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround.
// TBD: autoreconnect + connection error handling
// ALSO: are htmlfile, jsonp-polling modes etc supposed to disconnect after each message?
// TBD: add SwankJS scripts to all passing html pages (into <head> or <body>)
// TBD: it should be possible to serve local files instead of proxying
// (maybe using https://github.com/felixge/node-paperboy )
// TBD: handle edge case: new sticky remote connects, old sticky remote disconnects
// (late disconnect) - as of now, swank-js switches to node.js, but it should
// instead upon remote detachment see whether another remote with the same name
// is available
// TBD: handle/add X-Forwarded-For headers
// TBD: fix all assert calls: we need (actual, expected) not (expected, actual)
// TBD: invoke SwankJS.setup() only when DOM is ready (at least in IE)
// TBD: timeouts for browser requests

View file

@ -0,0 +1,89 @@
// -*- mode: js2; js-run: t -*-
//
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
var assert = require("assert");
var useragent = require("./user-agent");
// for reference: http://www.pgts.com.au/cgi-bin/psql?agent_main
var USER_AGENT_TESTS = [
[ "Firefox 0.9", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7) Gecko/20040707 Firefox/0.9.2 StumbleUpon/1.998" ],
[ "Firefox 1.5", "Mozilla/5.0 (Windows NT 5.1; U; SV1; MEGAUPLOAD 1.0; ru; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 Opera 9.23" ],
[ "Firefox 2.0", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.13) Gecko/20081108 Firefox/2.0.0.13" ],
[ "Firefox 3.6", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3" ],
[ "Firefox 3.5", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5" ],
[ "Firefox 3.0", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.0.6) Gecko/2009011913 Firefox/3.0.6" ],
[ "Firefox 4.0", "Mozilla/5.0 (Windows NT 5.1; rv:2.0b6) Gecko/20100101 Firefox/4.0b6" ],
[ "Firefox", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; ToolKit; rv:1.9.0.8) Gecko/2009032609 Firefox MRA 5.5 (build 02842);" ],
[ "Opera 11.00", "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.6.37 Version/11.00" ],
[ "Opera 10.62", "Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.6.30 Version/10.62" ],
[ "Opera 9.50", "Opera/9.50 (Windows NT 6.0; U; MRA 5.5 (build 02842); ru)" ],
[ "Opera 8.01", "Opera/8.01 (J2ME/MIDP; Opera Mini/3.1.9427/1724; ru; U; ssr)" ],
[ "Opera 7.0", "Mozilla/4.0 (compatible; MSIE 6.0; MSIE 5.5; Windows NT 4.0) Opera 7.0 [en]" ],
[ "Opera 6.0", "Mozilla/4.0 (compatible; MSIE 5.0; Windows 2000) Opera 6.0 [en]" ],
[ "Opera 5.11", "Mozilla/4.0 (compatible; MSIE 5.0; Windows ME) Opera 5.11 [en]" ],
[ "Chrome 6.0", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3" ],
[ "ChromeFrame 6.0", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0) chromeframe/6.0.472.63" ],
[ "Safari 4.0", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; ru-ru) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10" ],
[ "iPhone 3.1.3", "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_1_3 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Mobile/7E18" ],
[ "iPad 3.2.2", "Mozilla/5.0 (iPad; U; CPU OS 3_2_2 like Mac OS X; ru-ru) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B500 Safari/531.21.10" ],
[ "iPod", "Mozilla/5.0 (iPod; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3A101a Safari/419.3" ],
[ "QtWeb 4.7", "Mozilla/5.0 (X11; U; Linux x86_64; C) AppleWebKit/533.3 (KHTML, like Gecko) Qt/4.7.0 Safari/533.3" ],
[ "WebKit", "Mozilla/5.0 (X11; U; Linux x86_64; C) AppleWebKit/533.3 (KHTML, like Gecko) Safari/533.3" ], // made up
[ "WebKit", "Mozilla/5.0 (X11; U; Linux x86_64; C) AppleWebKit/533.3 (KHTML, like Gecko)" ], // made up
[ "WebKit", "Midori/0.2 (X11; Linux; U; en-us) WebKit/531.2+" ],
[ "Conkeror 0.9", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.10) Gecko/20100915 Conkeror/0.9.2" ],
[ "Gecko", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.4b) Gecko/20030516 Mozilla Firebird/0.6" ],
[ "SeaMonkey 2.0", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.4) Gecko/20091028 SeaMonkey/2.0" ],
[ "Android 2.2", "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Droid Build/FRG22D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"],
[ "Android 1.6", "Mozilla/5.0 (Linux; U; Android 1.6; en-us; T-Mobile G1 Build/DMD64) AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1"],
[ "MSIE 9.0", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)" ],
[ "MSIE 8.0", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; GTB5; MRSPUTNIK 2, 3, 0, 104; MRA 5.6 (build 03392); .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)" ],
[ "MSIE 7.0", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; InfoPath.2; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)" ],
[ "MSIE 6.0", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; InfoPath.2; .NET CLR 2.0.50727)" ],
[ "MSIE 5.01", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)" ],
[ "MSIE 5.5", "Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)" ],
[ "MSIE 4.01", "Mozilla/4.0 (compatible; MSIE 4.01; Windows 95)" ],
[ "MSIE 4.01", "Mozilla/4.0 (compatible; MSIE 4.01; Digital AlphaServer 1000A 4/233; Windows NT; Powered By 64-Bit Alpha Processor)" ],
[ "MSIE 3.0", "Mozilla/2.0 (compatible; MSIE 3.0B; Windows NT)" ],
[ "MSIE 3.0", "Mozilla/2.0 (compatible; MSIE 3.0B; Windows NT)" ],
[ "MSIE 3.02", "Mozilla/2.0 (compatible; MSIE 3.02; Windows CE; 240x320)" ],
[ "MSIE 3.01", "Mozilla/2.0 (compatible; MSIE 3.01; Windows 98)" ],
[ "MSIE 2.0", "Mozilla/1.22 (compatible; MSIE 2.0d; Windows NT)" ],
[ "MSIE 1.5", "Mozilla/1.22 (compatible; MSIE 1.5; Windows NT)" ],
[ "MSIE 1.0", "Mozilla/1.0 (compatible; MSIE 1.0; Windows 3.11)" ], // made up
[ "unknown", "don't know" ],
[ "unknown", "" ]
];
USER_AGENT_TESTS.forEach(
function (item) {
var actual = useragent.recognize(item[1]);
assert.equal(
item[0], actual,
"got " + actual + " instead of " + item[0] + " for " + item[1]);
});

View file

@ -0,0 +1,54 @@
// -*- mode: js2; js-run: "user-agent-tests.js" -*-
//
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
var RX_TABLE = [
[ /^.*(Firefox|Chrome|chromeframe|Conkeror|SeaMonkey)\/(\d+\.\d+).*$/, "$1 $2"],
[ /^.*Firefox.*$/, "Firefox" ],
[ /^.*Opera.*Version\/(\d+\.\d+).*$/, "Opera $1" ],
[ /^.*Opera[/ ](\d+\.\d+).*$/, "Opera $1" ],
[ /^.*Android (\d+\.\d+).*$/, "Android $1" ],
[ /^.*iPhone;.*OS (\d+(?:_\d+)*).*$/, "iPhone $1" ],
[ /^.*iPad;.*OS (\d+(?:_\d+)*).*$/, "iPad $1" ],
[ /^.*iPod;.*$/, "iPod" ],
[ /^.*Version\/(\d+\.\d+).*Safari.*$/, "Safari $1" ],
[ /^.*Qt\/(\d+\.\d+).*$/, "QtWeb $1" ],
[ /^.*WebKit.*$/, "WebKit" ],
[ /^.*Gecko.*$/, "Gecko" ],
[ /^.*MSIE (\d+\.\d+).*$/, "MSIE $1" ]
];
exports.recognize = function recognize (name) {
// console.log("name=%s", name);
for (var i = 0; i < RX_TABLE.length; ++i) {
var r = name.replace(RX_TABLE[i][0], RX_TABLE[i][1]);
// console.log("m=%s r=%s", RX_TABLE[i][0], r);
if (r != name)
return r.replace(/chromeframe/, "ChromeFrame").replace(/_/g, ".");
}
return "unknown";
};