mirror of
https://github.com/samsonjs/elisp.js.git
synced 2026-03-25 09:15:49 +00:00
260 lines
6.7 KiB
JavaScript
260 lines
6.7 KiB
JavaScript
////
|
|
// Emacs Lisp implementation in JavaScript.
|
|
//
|
|
// Copyright (c) 2009 Sami Samhuri - sami.samhuri@gmail.com
|
|
//
|
|
// Released under the terms of the MIT license. See the included file
|
|
// LICENSE.
|
|
|
|
|
|
EL.Parser = function(data) {
|
|
this.data = data || '';
|
|
};
|
|
|
|
EL.Parser.Error = function(name, message) {
|
|
this.parserError = true;
|
|
this.name = name;
|
|
this.message = message;
|
|
};
|
|
|
|
EL.Parser.Error.messages = {
|
|
'eof': "no more input"
|
|
};
|
|
|
|
EL.Parser.prototype.error = function(name) {
|
|
throw(new EL.Parser.Error(name, EL.Parser.Error.messages[name]));
|
|
};
|
|
|
|
EL.Parser.prototype.peek = function() {
|
|
return this.data[this.pos];
|
|
};
|
|
|
|
EL.Parser.prototype.consumeChar = function() {
|
|
if (this.pos >= this.data.length) this.error('eof');
|
|
return this.data[this.pos++];
|
|
};
|
|
|
|
EL.Parser.prototype.consumeWhitespace = function() {
|
|
var c;
|
|
while ((c = this.peek()) && c.match(/[\s\n]/)) {
|
|
this.consumeChar();
|
|
}
|
|
};
|
|
|
|
EL.Parser.prototype.rewind = function() {
|
|
this.pos = 0;
|
|
};
|
|
|
|
EL.Parser.prototype.rest = function() {
|
|
return this.data.substring(this.pos);
|
|
};
|
|
|
|
EL.Parser.prototype.moreInput = function() {
|
|
return (this.pos < this.data.length);
|
|
};
|
|
|
|
EL.Parser.prototype.parse = function(string) {
|
|
if (string) this.data = string;
|
|
this.rewind();
|
|
var exprs = [];
|
|
while (this.moreInput()) {
|
|
try {
|
|
exprs.push(this.parseExpression());
|
|
} catch (e) {
|
|
if (e.parserError && e.name == 'eof') {
|
|
print("error: " + e.message);
|
|
break;
|
|
}
|
|
else {
|
|
throw(e);
|
|
}
|
|
}
|
|
}
|
|
this.expressions = exprs;
|
|
// print('');
|
|
// EL.Util.pp(exprs);
|
|
// print('');
|
|
return exprs;
|
|
};
|
|
|
|
EL.Parser.prototype.parseOne = function(string) {
|
|
return this.parse(string)[0];
|
|
};
|
|
|
|
EL.Parser.prototype.parseUntil = function(regex, initial, next, consumeTerminator) {
|
|
var c,
|
|
token = initial,
|
|
condition = function(c){ return c.match(regex) == null; };
|
|
while ((c = this.peek()) && condition(c)) {
|
|
token = next(token, this.consumeChar());
|
|
}
|
|
if (consumeTerminator && this.peek()) this.consumeChar();
|
|
return token;
|
|
};
|
|
|
|
EL.Parser.prototype.parseList = function() {
|
|
var list = [],
|
|
expr;
|
|
// consume initial paren '('
|
|
this.consumeChar();
|
|
while ((expr = this.parseExpression()) && expr != ')') {
|
|
list.push(expr);
|
|
}
|
|
return list;
|
|
};
|
|
|
|
EL.Parser.prototype.parseCons = function() {
|
|
var cons = [],
|
|
expr;
|
|
// consume initial paren '('
|
|
this.consumeChar();
|
|
cons.push(this.parseExpression());
|
|
// ignore .
|
|
this.parseExpression();
|
|
cons.push(this.parseExpression());
|
|
return cons;
|
|
};
|
|
|
|
EL.Parser.prototype.parseString = function() {
|
|
// consume initial quotation mark
|
|
this.consumeChar();
|
|
var self = this;
|
|
return this.parseUntil(/"/, '', function(s,c){
|
|
if (c == '\\') {
|
|
c = self.consumeChar();
|
|
}
|
|
return s + c;
|
|
}, true /* consume terminator */);
|
|
};
|
|
|
|
EL.Parser.prototype.parseSymbol = function() {
|
|
var symbol = this.parseUntil(/[\s()]/, '', function(t,c){return t + c;});
|
|
return symbol;
|
|
};
|
|
|
|
// Probably easy to break
|
|
EL.Parser.prototype.parseRegex = function() {
|
|
// consume initial slash
|
|
this.consumeChar();
|
|
var self = this;
|
|
return new RegExp(this.parseUntil(/\//, '', function(s,c){
|
|
if (c == '\\') {
|
|
c = self.consumeChar();
|
|
}
|
|
return s + c;
|
|
}, true /* consume terminator */));
|
|
};
|
|
|
|
|
|
// In Emacs Lisp a trailing . is allowed on integers.
|
|
// Valid kinds of numbers we parse here are:
|
|
//
|
|
// * Integers of the form 42, +17, -300, 7300. (trailing .), +1. and
|
|
// -1.
|
|
//
|
|
// * Floating point numbers of the form -4.5, 0.0, and +933825.3450133492
|
|
//
|
|
// * Exponential notation for floats, e.g. 1.5e2 (150.0) or 420e-1 (42.0)
|
|
// (There is no trailing . allowed anywhere in exponent notation)
|
|
EL.Parser.prototype.parseNumber = function() {
|
|
var value = this.parseIntOrFloat(),
|
|
exponentAllowed = value === parseInt(value),
|
|
exp;
|
|
|
|
// now check for an exponent
|
|
if (this.exponentAllowed && (this.peek() == 'e' || this.peek() == 'E')) {
|
|
this.consumeChar();
|
|
|
|
// Technically this is an error as a float is not allowed for exponents
|
|
// but the regex is strict enough to keep us from trying to do that.
|
|
exp = this.parseIntOrFloat();
|
|
|
|
value *= Math.pow(10, exp);
|
|
}
|
|
|
|
return value;
|
|
};
|
|
|
|
// Pack int and float parsing together for simplicity's sake.
|
|
EL.Parser.prototype.parseIntOrFloat = function() {
|
|
this.exponentAllowed = true;
|
|
var sign = this.peek() == '-' || this.peek() == '+' ? this.consumeChar() : '+',
|
|
value = this.parseUntil(/[^\d]/, 0, function(n,c) {
|
|
return n*10 + parseInt(c);
|
|
});
|
|
|
|
// if we see a . there might be a float to parse
|
|
if (this.peek() == '.') {
|
|
this.exponentAllowed = false;
|
|
this.consumeChar();
|
|
if (this.peek() && this.peek().match(/\d/)) {
|
|
var decimal = this.parseUntil(/[^\d]/, '', function(s,c) {return s + c;});
|
|
value = parseFloat('' + value + '.' + decimal);
|
|
}
|
|
}
|
|
|
|
return sign == '-' ? -1*value : value;
|
|
};
|
|
|
|
// These regexes matches all the inputs specified above parseNumber.
|
|
EL.Parser.prototype.lookingAtNumber = function() {
|
|
var pos = this.pos,
|
|
rest = this.rest(),
|
|
match = rest.match(/^[+-]?\d+(\.\d*)?[)\s\n]/)
|
|
|| rest.match(/^[+-]?\d+(\.\d*)?$/)
|
|
|| rest.match(/^[+-]?\d+(\.\d+)?([eE][+-]?\d+)?[)\s\n]/)
|
|
|| rest.match(/^[+-]?\d+(\.\d+)?([eE][+-]?\d+)?$/);
|
|
return (match != null);
|
|
};
|
|
|
|
EL.Parser.prototype.lookingAtCons = function() {
|
|
var orig_pos = this.pos,
|
|
_ = this.consumeChar(),
|
|
_ = this.parseExpression(),
|
|
cdr = this.parseExpression();
|
|
this.pos = orig_pos; // rewind, like it never happened.
|
|
return EL.typeOf(cdr) == 'array' && EL.isSymbol(cdr) && EL.val(cdr) == '.';
|
|
};
|
|
|
|
EL.Parser.prototype.parseExpression = function() {
|
|
var value,
|
|
c = this.peek();
|
|
if (c == '(' && this.lookingAtCons()) {
|
|
value = EL.consPair(this.parseCons());
|
|
}
|
|
else if (c == '(') {
|
|
var list = this.parseList();
|
|
value = (list.length > 0) ? EL.list(list) : EL.nil;
|
|
}
|
|
else if (c == ')') {
|
|
return this.consumeChar();
|
|
}
|
|
else if (c == "'") {
|
|
this.consumeChar();
|
|
value = EL.cons(EL.symbol('quote'), this.parseExpression());
|
|
}
|
|
else if (c == '"') {
|
|
value = EL.string(this.parseString());
|
|
}
|
|
else if (this.lookingAtNumber()) {
|
|
value = EL.number(this.parseNumber());
|
|
}
|
|
else if (c) {
|
|
value = EL.symbol(this.parseSymbol());
|
|
}
|
|
else {
|
|
if (this.pos == this.data.length) {
|
|
print('[error] no more input. unterminated string or list? (continuing anyway)');
|
|
}
|
|
print('[warning] in EL.Parser.parseExpression: unrecognized char "' + c + '"');
|
|
print('this.pos = ' + this.pos);
|
|
print('this.data.length = ' + this.data.length);
|
|
print('this.rest = ' + this.rest());
|
|
}
|
|
this.consumeWhitespace();
|
|
return value;
|
|
};
|
|
|
|
/****************************************************************
|
|
****************************************************************/
|
|
|