diff options
| author | Adam Abrons | 2010-01-05 16:36:58 -0800 |
|---|---|---|
| committer | Adam Abrons | 2010-01-05 16:36:58 -0800 |
| commit | c9c176a53b1632ca2b1c6ed27382ab72ac21d45d (patch) | |
| tree | b5f719a095c03ee9f8b2721ffdaf1e5ff8c11b41 /src/Parser.js | |
| download | angular.js-c9c176a53b1632ca2b1c6ed27382ab72ac21d45d.tar.bz2 | |
angular.js
Diffstat (limited to 'src/Parser.js')
| -rw-r--r-- | src/Parser.js | 741 |
1 files changed, 741 insertions, 0 deletions
diff --git a/src/Parser.js b/src/Parser.js new file mode 100644 index 00000000..3d72bebf --- /dev/null +++ b/src/Parser.js @@ -0,0 +1,741 @@ +nglr.Lexer = function(text, parsStrings){ + this.text = text; + // UTC dates have 20 characters, we send them through parser + this.dateParseLength = parsStrings ? 20 : -1; + this.tokens = []; + this.index = 0; +}; + +nglr.Lexer.OPERATORS = { + 'null':function(self){return null;}, + 'true':function(self){return true;}, + 'false':function(self){return false;}, + '+':function(self, a,b){return (a||0)+(b||0);}, + '-':function(self, a,b){return (a||0)-(b||0);}, + '*':function(self, a,b){return a*b;}, + '/':function(self, a,b){return a/b;}, + '%':function(self, a,b){return a%b;}, + '^':function(self, a,b){return a^b;}, + '=':function(self, a,b){return self.scope.set(a, b);}, + '==':function(self, a,b){return a==b;}, + '!=':function(self, a,b){return a!=b;}, + '<':function(self, a,b){return a<b;}, + '>':function(self, a,b){return a>b;}, + '<=':function(self, a,b){return a<=b;}, + '>=':function(self, a,b){return a>=b;}, + '&&':function(self, a,b){return a&&b;}, + '||':function(self, a,b){return a||b;}, + '&':function(self, a,b){return a&b;}, +// '|':function(self, a,b){return a|b;}, + '|':function(self, a,b){return b(self, a);}, + '!':function(self, a){return !a;} +}; + +nglr.Lexer.prototype.peek = function() { + if (this.index + 1 < this.text.length) { + return this.text.charAt(this.index + 1); + } else { + return false; + } +}; + +nglr.Lexer.prototype.parse = function() { + var tokens = this.tokens; + var OPERATORS = nglr.Lexer.OPERATORS; + var canStartRegExp = true; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + if (ch == '"' || ch == "'") { + this.readString(ch); + canStartRegExp = true; + } else if (ch == '(' || ch == '[') { + tokens.push({index:this.index, text:ch}); + this.index++; + } else if (ch == '{' ) { + var peekCh = this.peek(); + if (peekCh == ':' || peekCh == '(') { + tokens.push({index:this.index, text:ch + peekCh}); + this.index++; + } else { + tokens.push({index:this.index, text:ch}); + } + this.index++; + canStartRegExp = true; + } else if (ch == ')' || ch == ']' || ch == '}' ) { + tokens.push({index:this.index, text:ch}); + this.index++; + canStartRegExp = false; + } else if ( ch == ':' || ch == '.' || ch == ',' || ch == ';') { + tokens.push({index:this.index, text:ch}); + this.index++; + canStartRegExp = true; + } else if ( canStartRegExp && ch == '/' ) { + this.readRegexp(); + canStartRegExp = false; + } else if ( this.isNumber(ch) ) { + this.readNumber(); + canStartRegExp = false; + } else if (this.isIdent(ch)) { + this.readIdent(); + canStartRegExp = false; + } else if (this.isWhitespace(ch)) { + this.index++; + } else { + var ch2 = ch + this.peek(); + var fn = OPERATORS[ch]; + var fn2 = OPERATORS[ch2]; + if (fn2) { + tokens.push({index:this.index, text:ch2, fn:fn2}); + this.index += 2; + } else if (fn) { + tokens.push({index:this.index, text:ch, fn:fn}); + this.index += 1; + } else { + throw "Lexer Error: Unexpected next character [" + + this.text.substring(this.index) + + "] in expression '" + this.text + + "' at column '" + (this.index+1) + "'."; + } + canStartRegExp = true; + } + } + return tokens; +}; + +nglr.Lexer.prototype.isNumber = function(ch) { + return '0' <= ch && ch <= '9'; +}; + +nglr.Lexer.prototype.isWhitespace = function(ch) { + return ch == ' ' || ch == '\r' || ch == '\t' || + ch == '\n' || ch == '\v'; +}; + +nglr.Lexer.prototype.isIdent = function(ch) { + return 'a' <= ch && ch <= 'z' || + 'A' <= ch && ch <= 'Z' || + '_' == ch || ch == '$'; +}; + +nglr.Lexer.prototype.readNumber = function() { + var number = ""; + var start = this.index; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + if (ch == '.' || this.isNumber(ch)) { + number += ch; + } else { + break; + } + this.index++; + } + number = 1 * number; + this.tokens.push({index:start, text:number, + fn:function(){return number;}}); +}; + +nglr.Lexer.prototype.readIdent = function() { + var ident = ""; + var start = this.index; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + if (ch == '.' || this.isIdent(ch) || this.isNumber(ch)) { + ident += ch; + } else { + break; + } + this.index++; + } + var fn = nglr.Lexer.OPERATORS[ident]; + if (!fn) { + fn = function(self){ + return self.scope.get(ident); + }; + fn.isAssignable = ident; + } + this.tokens.push({index:start, text:ident, fn:fn}); +}; +nglr.Lexer.ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; +nglr.Lexer.prototype.readString = function(quote) { + var start = this.index; + var dateParseLength = this.dateParseLength; + this.index++; + var string = ""; + var escape = false; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + if (escape) { + if (ch == 'u') { + var hex = this.text.substring(this.index + 1, this.index + 5); + this.index += 4; + string += String.fromCharCode(parseInt(hex, 16)); + } else { + var rep = nglr.Lexer.ESCAPE[ch]; + if (rep) { + string += rep; + } else { + string += ch; + } + } + escape = false; + } else if (ch == '\\') { + escape = true; + } else if (ch == quote) { + this.index++; + this.tokens.push({index:start, text:string, + fn:function(){ + return (string.length == dateParseLength) ? + angular.String.toDate(string) : string; + }}); + return; + } else { + string += ch; + } + this.index++; + } + throw "Lexer Error: Unterminated quote [" + + this.text.substring(start) + "] starting at column '" + + (start+1) + "' in expression '" + this.text + "'."; +}; + +nglr.Lexer.prototype.readRegexp = function(quote) { + var start = this.index; + this.index++; + var regexp = ""; + var escape = false; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + if (escape) { + regexp += ch; + escape = false; + } else if (ch === '\\') { + regexp += ch; + escape = true; + } else if (ch === '/') { + this.index++; + var flags = ""; + if (this.isIdent(this.text.charAt(this.index))) { + this.readIdent(); + flags = this.tokens.pop().text; + } + var compiledRegexp = new RegExp(regexp, flags); + this.tokens.push({index:start, text:regexp, flags:flags, + fn:function(){return compiledRegexp;}}); + return; + } else { + regexp += ch; + } + this.index++; + } + throw "Lexer Error: Unterminated RegExp [" + + this.text.substring(start) + "] starting at column '" + + (start+1) + "' in expression '" + this.text + "'."; +}; + + +nglr.Parser = function(text, parseStrings){ + this.text = text; + this.tokens = new nglr.Lexer(text, parseStrings).parse(); + this.index = 0; +}; + +nglr.Parser.ZERO = function(){ + return 0; +}; + +nglr.Parser.prototype.error = function(msg, token) { + throw "Token '" + token.text + + "' is " + msg + " at column='" + + (token.index + 1) + "' of expression '" + + this.text + "' starting at '" + this.text.substring(token.index) + "'."; +}; + +nglr.Parser.prototype.peekToken = function() { + if (this.tokens.length === 0) + throw "Unexpected end of expression: " + this.text; + return this.tokens[0]; +}; + +nglr.Parser.prototype.peek = function(e1, e2, e3, e4) { + var tokens = this.tokens; + if (tokens.length > 0) { + var token = tokens[0]; + var t = token.text; + if (t==e1 || t==e2 || t==e3 || t==e4 || + (!e1 && !e2 && !e3 && !e4)) { + return token; + } + } + return false; +}; + +nglr.Parser.prototype.expect = function(e1, e2, e3, e4){ + var token = this.peek(e1, e2, e3, e4); + if (token) { + this.tokens.shift(); + this.currentToken = token; + return token; + } + return false; +}; + +nglr.Parser.prototype.consume = function(e1){ + if (!this.expect(e1)) { + var token = this.peek(); + throw "Expecting '" + e1 + "' at column '" + + (token.index+1) + "' in '" + + this.text + "' got '" + + this.text.substring(token.index) + "'."; + } +}; + +nglr.Parser.prototype._unary = function(fn, parse) { + var right = parse.apply(this); + return function(self) { + return fn(self, right(self)); + }; +}; + +nglr.Parser.prototype._binary = function(left, fn, parse) { + var right = parse.apply(this); + return function(self) { + return fn(self, left(self), right(self)); + }; +}; + +nglr.Parser.prototype.hasTokens = function () { + return this.tokens.length > 0; +}; + +nglr.Parser.prototype.assertAllConsumed = function(){ + if (this.tokens.length !== 0) { + throw "Did not understand '" + this.text.substring(this.tokens[0].index) + + "' while evaluating '" + this.text + "'."; + } +}; + +nglr.Parser.prototype.statements = function(){ + var statements = []; + while(true) { + if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) + statements.push(this.filterChain()); + if (!this.expect(';')) { + return function (self){ + var value; + for ( var i = 0; i < statements.length; i++) { + var statement = statements[i]; + if (statement) + value = statement(self); + } + return value; + }; + } + } +}; + +nglr.Parser.prototype.filterChain = function(){ + var left = this.expression(); + var token; + while(true) { + if ((token = this.expect('|'))) { + left = this._binary(left, token.fn, this.filter); + } else { + return left; + } + } +}; + +nglr.Parser.prototype.filter = function(){ + return this._pipeFunction(angular.filter); +}; + +nglr.Parser.prototype.validator = function(){ + return this._pipeFunction(angular.validator); +}; + +nglr.Parser.prototype._pipeFunction = function(fnScope){ + var fn = this.functionIdent(fnScope); + var argsFn = []; + var token; + while(true) { + if ((token = this.expect(':'))) { + argsFn.push(this.expression()); + } else { + var fnInvoke = function(self, input){ + var args = [input]; + for ( var i = 0; i < argsFn.length; i++) { + args.push(argsFn[i](self)); + } + return fn.apply(self, args); + }; + return function(){ + return fnInvoke; + }; + } + } +}; + +nglr.Parser.prototype.expression = function(){ + return this.throwStmt(); +}; + +nglr.Parser.prototype.throwStmt = function(){ + if (this.expect('throw')) { + var throwExp = this.assignment(); + return function (self) { + throw throwExp(self); + }; + } else { + return this.assignment(); + } +}; + +nglr.Parser.prototype.assignment = function(){ + var left = this.logicalOR(); + var token; + if (token = this.expect('=')) { + if (!left.isAssignable) { + throw "Left hand side '" + + this.text.substring(0, token.index) + "' of assignment '" + + this.text.substring(token.index) + "' is not assignable."; + } + var ident = function(){return left.isAssignable;}; + return this._binary(ident, token.fn, this.logicalOR); + } else { + return left; + } +}; + +nglr.Parser.prototype.logicalOR = function(){ + var left = this.logicalAND(); + var token; + while(true) { + if ((token = this.expect('||'))) { + left = this._binary(left, token.fn, this.logicalAND); + } else { + return left; + } + } +}; + +nglr.Parser.prototype.logicalAND = function(){ + var left = this.negated(); + var token; + while(true) { + if ((token = this.expect('&&'))) { + left = this._binary(left, token.fn, this.negated); + } else { + return left; + } + } +}; + +nglr.Parser.prototype.negated = function(){ + var token; + if (token = this.expect('!')) { + return this._unary(token.fn, this.equality); + } else { + return this.equality(); + } +}; + +nglr.Parser.prototype.equality = function(){ + var left = this.relational(); + var token; + while(true) { + if ((token = this.expect('==','!='))) { + left = this._binary(left, token.fn, this.relational); + } else { + return left; + } + } +}; + +nglr.Parser.prototype.relational = function(){ + var left = this.additive(); + var token; + while(true) { + if ((token = this.expect('<', '>', '<=', '>='))) { + left = this._binary(left, token.fn, this.additive); + } else { + return left; + } + } +}; + +nglr.Parser.prototype.additive = function(){ + var left = this.multiplicative(); + var token; + while(token = this.expect('+','-')) { + left = this._binary(left, token.fn, this.multiplicative); + } + return left; +}; + +nglr.Parser.prototype.multiplicative = function(){ + var left = this.unary(); + var token; + while(token = this.expect('*','/','%')) { + left = this._binary(left, token.fn, this.unary); + } + return left; +}; + +nglr.Parser.prototype.unary = function(){ + var token; + if (this.expect('+')) { + return this.primary(); + } else if (token = this.expect('-')) { + return this._binary(nglr.Parser.ZERO, token.fn, this.multiplicative); + } else { + return this.primary(); + } +}; + +nglr.Parser.prototype.functionIdent = function(fnScope) { + var token = this.expect(); + var element = token.text.split('.'); + var instance = fnScope; + var key; + for ( var i = 0; i < element.length; i++) { + key = element[i]; + if (instance) + instance = instance[key]; + } + if (typeof instance != 'function') { + throw "Function '" + token.text + "' at column '" + + (token.index+1) + "' in '" + this.text + "' is not defined."; + } + return instance; +}; + +nglr.Parser.prototype.primary = function() { + var primary; + if (this.expect('(')) { + var expression = this.filterChain(); + this.consume(')'); + primary = expression; + } else if (this.expect('[')) { + primary = this.arrayDeclaration(); + } else if (this.expect('{')) { + primary = this.object(); + } else if (this.expect('{:')) { + primary = this.closure(false); + } else if (this.expect('{(')) { + primary = this.closure(true); + } else { + var token = this.expect(); + primary = token.fn; + if (!primary) { + this.error("not a primary expression", token); + } + } + var next; + while (next = this.expect('(', '[', '.')) { + if (next.text === '(') { + primary = this.functionCall(primary); + } else if (next.text === '[') { + primary = this.objectIndex(primary); + } else if (next.text === '.') { + primary = this.fieldAccess(primary); + } else { + throw "IMPOSSIBLE"; + } + } + return primary; +}; + +nglr.Parser.prototype.closure = function(hasArgs) { + var args = []; + if (hasArgs) { + if (!this.expect(')')) { + args.push(this.expect().text); + while(this.expect(',')) { + args.push(this.expect().text); + } + this.consume(')'); + } + this.consume(":"); + } + var statements = this.statements(); + this.consume("}"); + return function(self){ + return function($){ + var scope = new nglr.Scope(self.scope.state); + scope.set('$', $); + for ( var i = 0; i < args.length; i++) { + scope.set(args[i], arguments[i]); + } + return statements({scope:scope}); + }; + }; +}; + +nglr.Parser.prototype.fieldAccess = function(object) { + var field = this.expect().text; + var fn = function (self){ + return nglr.Scope.getter(object(self), field); + }; + fn.isAssignable = field; + return fn; +}; + +nglr.Parser.prototype.objectIndex = function(obj) { + var indexFn = this.expression(); + this.consume(']'); + if (this.expect('=')) { + var rhs = this.expression(); + return function (self){ + return obj(self)[indexFn(self)] = rhs(self); + }; + } else { + return function (self){ + var o = obj(self); + var i = indexFn(self); + return (o) ? o[i] : undefined; + }; + } +}; + +nglr.Parser.prototype.functionCall = function(fn) { + var argsFn = []; + if (this.peekToken().text != ')') { + do { + argsFn.push(this.expression()); + } while (this.expect(',')); + } + this.consume(')'); + return function (self){ + var args = []; + for ( var i = 0; i < argsFn.length; i++) { + args.push(argsFn[i](self)); + } + var fnPtr = fn(self); + if (typeof fnPtr === 'function') { + return fnPtr.apply(self, args); + } else { + throw "Expression '" + fn.isAssignable + "' is not a function."; + } + }; +}; + +// This is used with json array declaration +nglr.Parser.prototype.arrayDeclaration = function () { + var elementFns = []; + if (this.peekToken().text != ']') { + do { + elementFns.push(this.expression()); + } while (this.expect(',')); + } + this.consume(']'); + return function (self){ + var array = []; + for ( var i = 0; i < elementFns.length; i++) { + array.push(elementFns[i](self)); + } + return array; + }; +}; + +nglr.Parser.prototype.object = function () { + var keyValues = []; + if (this.peekToken().text != '}') { + do { + var key = this.expect().text; + this.consume(":"); + var value = this.expression(); + keyValues.push({key:key, value:value}); + } while (this.expect(',')); + } + this.consume('}'); + return function (self){ + var object = {}; + for ( var i = 0; i < keyValues.length; i++) { + var keyValue = keyValues[i]; + var value = keyValue.value(self); + object[keyValue.key] = value; + } + return object; + }; +}; + +nglr.Parser.prototype.entityDeclaration = function () { + var decl = []; + while(this.hasTokens()) { + decl.push(this.entityDecl()); + if (!this.expect(';')) { + this.assertAllConsumed(); + } + } + return function (self){ + var code = ""; + for ( var i = 0; i < decl.length; i++) { + code += decl[i](self); + } + return code; + }; +}; + +nglr.Parser.prototype.entityDecl = function () { + var entity = this.expect().text; + var instance; + var defaults; + if (this.expect('=')) { + instance = entity; + entity = this.expect().text; + } + if (this.expect(':')) { + defaults = this.primary()(null); + } + return function(self) { + var datastore = self.scope.get('$datastore'); + var Entity = datastore.entity(entity, defaults); + self.scope.set(entity, Entity); + if (instance) { + var document = Entity(); + document.$$anchor = instance; + self.scope.set(instance, document); + return "$anchor." + instance + ":{" + + instance + "=" + entity + ".load($anchor." + instance + ");" + + instance + ".$$anchor=" + angular.String.quote(instance) + ";" + + "};"; + } else { + return ""; + } + }; +}; + +nglr.Parser.prototype.watch = function () { + var decl = []; + while(this.hasTokens()) { + decl.push(this.watchDecl()); + if (!this.expect(';')) { + this.assertAllConsumed(); + } + } + this.assertAllConsumed(); + return function (self){ + for ( var i = 0; i < decl.length; i++) { + var d = decl[i](self); + self.addListener(d.name, d.fn); + } + }; +}; + +nglr.Parser.prototype.watchDecl = function () { + var anchorName = this.expect().text; + this.consume(":"); + var expression; + if (this.peekToken().text == '{') { + this.consume("{"); + expression = this.statements(); + this.consume("}"); + } else { + expression = this.expression(); + } + return function(self) { + return {name:anchorName, fn:expression}; + }; +}; + + |
