var OPERATORS = { 'null':function(self){return null;}, 'true':function(self){return true;}, 'false':function(self){return false;}, $undefined:noop, '+':function(self, a,b){return (isDefined(a)?a:0)+(isDefined(b)?b:0);}, '-':function(self, a,b){return (isDefined(a)?a:0)-(isDefined(b)?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;}, '=':noop, '==':function(self, a,b){return a==b;}, '!=':function(self, a,b){return a!=b;}, '<':function(self, a,b){return a':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;} }; var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; function lex(text, parseStringsForObjects){ var dateParseLength = parseStringsForObjects ? DATE_ISOSTRING_LN : -1, tokens = [], token, index = 0, json = [], ch, lastCh = ':'; // can start regexp while (index < text.length) { ch = text.charAt(index); if (is('"\'')) { readString(ch); } else if (isNumber(ch) || is('.') && isNumber(peek())) { readNumber(); } else if (isIdent(ch)) { readIdent(); // identifiers can only be if the preceding char was a { or , if (was('{,') && json[0]=='{' && (token=tokens[tokens.length-1])) { token.json = token.text.indexOf('.') == -1; } } else if (is('(){}[].,;:')) { tokens.push({ index:index, text:ch, json:(was(':[,') && is('{[')) || is('}]:,') }); if (is('{[')) json.unshift(ch); if (is('}]')) json.shift(); index++; } else if (isWhitespace(ch)) { index++; continue; } else { var ch2 = ch + peek(), fn = OPERATORS[ch], fn2 = OPERATORS[ch2]; if (fn2) { tokens.push({index:index, text:ch2, fn:fn2}); index += 2; } else if (fn) { tokens.push({index:index, text:ch, fn:fn, json: was('[,:') && is('+-')}); index += 1; } else { throwError("Unexpected next character ", index, index+1); } } lastCh = ch; } return tokens; function is(chars) { return chars.indexOf(ch) != -1; } function was(chars) { return chars.indexOf(lastCh) != -1; } function peek() { return index + 1 < text.length ? text.charAt(index + 1) : false; } function isNumber(ch) { return '0' <= ch && ch <= '9'; } function isWhitespace(ch) { return ch == ' ' || ch == '\r' || ch == '\t' || ch == '\n' || ch == '\v' || ch == '\u00A0'; // IE treats non-breaking space as \u00A0 } function isIdent(ch) { return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || '_' == ch || ch == '$'; } function isExpOperator(ch) { return ch == '-' || ch == '+' || isNumber(ch); } function throwError(error, start, end) { end = end || index; throw Error("Lexer Error: " + error + " at column" + (isDefined(start) ? "s " + start + "-" + index + " [" + text.substring(start, end) + "]" : " " + end) + " in expression [" + text + "]."); } function readNumber() { var number = ""; var start = index; while (index < text.length) { var ch = lowercase(text.charAt(index)); if (ch == '.' || isNumber(ch)) { number += ch; } else { var peekCh = peek(); if (ch == 'e' && isExpOperator(peekCh)) { number += ch; } else if (isExpOperator(ch) && peekCh && isNumber(peekCh) && number.charAt(number.length - 1) == 'e') { number += ch; } else if (isExpOperator(ch) && (!peekCh || !isNumber(peekCh)) && number.charAt(number.length - 1) == 'e') { throwError('Invalid exponent'); } else { break; } } index++; } number = 1 * number; tokens.push({index:start, text:number, json:true, fn:function(){return number;}}); } function readIdent() { var ident = ""; var start = index; var fn; while (index < text.length) { var ch = text.charAt(index); if (ch == '.' || isIdent(ch) || isNumber(ch)) { ident += ch; } else { break; } index++; } fn = OPERATORS[ident]; tokens.push({ index:start, text:ident, json: fn, fn:fn||extend(getterFn(ident), { assign:function(self, value){ return setter(self, ident, value); } }) }); } function readString(quote) { var start = index; index++; var string = ""; var rawString = quote; var escape = false; while (index < text.length) { var ch = text.charAt(index); rawString += ch; if (escape) { if (ch == 'u') { var hex = text.substring(index + 1, index + 5); if (!hex.match(/[\da-f]{4}/i)) throwError( "Invalid unicode escape [\\u" + hex + "]"); index += 4; string += String.fromCharCode(parseInt(hex, 16)); } else { var rep = ESCAPE[ch]; if (rep) { string += rep; } else { string += ch; } } escape = false; } else if (ch == '\\') { escape = true; } else if (ch == quote) { index++; tokens.push({index:start, text:rawString, string:string, json:true, fn:function(){ return (string.length == dateParseLength) ? angular['String']['toDate'](string) : string; }}); return; } else { string += ch; } index++; } throwError("Unterminated quote", start); } } ///////////////////////////////////////// function parser(text, json){ var ZERO = valueFn(0), tokens = lex(text, json), assignment = _assignment, assignable = logicalOR, functionCall = _functionCall, fieldAccess = _fieldAccess, objectIndex = _objectIndex, filterChain = _filterChain, functionIdent = _functionIdent, pipeFunction = _pipeFunction; if(json){ // The extra level of aliasing is here, just in case the lexer misses something, so that // we prevent any accidental execution in JSON. assignment = logicalOR; functionCall = fieldAccess = objectIndex = assignable = filterChain = functionIdent = pipeFunction = function (){ throwError("is not valid json", {text:text, index:0}); }; } return { assertAllConsumed: assertAllConsumed, assignable: assignable, primary: primary, statements: statements, validator: validator, formatter: formatter, filter: filter, //TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular) watch: watch }; /////////////////////////////////// function throwError(msg, token) { throw Error("Parse Error: Token '" + token.text + "' " + msg + " at column " + (token.index + 1) + " of expression [" + text + "] starting at [" + text.substring(token.index) + "]."); } function peekToken() { if (tokens.length === 0) throw Error("Unexpected end of expression: " + text); return tokens[0]; } function peek(e1, e2, e3, e4) { 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; } function expect(e1, e2, e3, e4){ var token = peek(e1, e2, e3, e4); if (token) { if (json && !token.json) { index = token.index; throwError("is not valid json", token); } tokens.shift(); this.currentToken = token; return token; } return false; } function consume(e1){ if (!expect(e1)) { throwError("is unexpected, expecting [" + e1 + "]", peek()); } } function unaryFn(fn, right) { return function(self) { return fn(self, right(self)); }; } function binaryFn(left, fn, right) { return function(self) { return fn(self, left(self), right(self)); }; } function hasTokens () { return tokens.length > 0; } function assertAllConsumed(){ if (tokens.length !== 0) { throwError("is extra token not part of expression", tokens[0]); } } function statements(){ var statements = []; while(true) { if (tokens.length > 0 && !peek('}', ')', ';', ']')) statements.push(filterChain()); if (!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; }; } } } function _filterChain(){ var left = expression(); var token; while(true) { if ((token = expect('|'))) { left = binaryFn(left, token.fn, filter()); } else { return left; } } } function filter(){ return pipeFunction(angularFilter); } function validator(){ return pipeFunction(angularValidator); } function formatter(){ var token = expect(); var formatter = angularFormatter[token.text]; var argFns = []; if (!formatter) throwError('is not a valid formatter.', token); while(true) { if ((token = expect(':'))) { argFns.push(expression()); } else { return valueFn({ format:invokeFn(formatter.format), parse:invokeFn(formatter.parse) }); } } function invokeFn(fn){ return function(self, input){ var args = [input]; for ( var i = 0; i < argFns.length; i++) { args.push(argFns[i](self)); } return fn.apply(self, args); }; } } function _pipeFunction(fnScope){ var fn = functionIdent(fnScope); var argsFn = []; var token; while(true) { if ((token = expect(':'))) { argsFn.push(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; }; } } } function expression(){ return assignment(); } function _assignment(){ var left = logicalOR(); var right; var token; if (token = expect('=')) { if (!left.assign) { throwError("implies assignment but [" + text.substring(0, token.index) + "] can not be assigned to", token); } right = logicalOR(); return function(self){ return left.assign(self, right(self)); }; } else { return left; } } function logicalOR(){ var left = logicalAND(); var token; while(true) { if ((token = expect('||'))) { left = binaryFn(left, token.fn, logicalAND()); } else { return left; } } } function logicalAND(){ var left = equality(); var token; if ((token = expect('&&'))) { left = binaryFn(left, token.fn, logicalAND()); } return left; } function equality(){ var left = relational(); var token; if ((token = expect('==','!='))) { left = binaryFn(left, token.fn, equality()); } return left; } function relational(){ var left = additive(); var token; if (token = expect('<', '>', '<=', '>=')) { left = binaryFn(left, token.fn, relational()); } return left; } function additive(){ var left = multiplicative(); var token; while(token = expect('+','-')) { left = binaryFn(left, token.fn, multiplicative()); } return left; } function multiplicative(){ var left = unary(); var token; while(token = expect('*','/','%')) { left = binaryFn(left, token.fn, unary()); } return left; } function unary(){ var token; if (expect('+')) { return primary(); } else if (token = expect('-')) { return binaryFn(ZERO, token.fn, unary()); } else if (token = expect('!')) { return unaryFn(token.fn, unary()); } else { return primary(); } } function _functionIdent(fnScope) { var token = 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) { throwError("should be a function", token); } return instance; } function primary() { var primary; if (expect('(')) { var expression = filterChain(); consume(')'); primary = expression; } else if (expect('[')) { primary = arrayDeclaration(); } else if (expect('{')) { primary = object(); } else { var token = expect(); primary = token.fn; if (!primary) { throwError("not a primary expression", token); } } var next; while (next = expect('(', '[', '.')) { if (next.text === '(') { primary = functionCall(primary); } else if (next.text === '[') { primary = objectIndex(primary); } else if (next.text === '.') { primary = fieldAccess(primary); } else { throwError("IMPOSSIBLE"); } } return primary; } function _fieldAccess(object) { var field = expect().text; var getter = getterFn(field); return extend(function (self){ return getter(object(self)); }, { assign:function(self, value){ return setter(object(self), field, value); } }); } function _objectIndex(obj) { var indexFn = expression(); consume(']'); return extend( function (self){ var o = obj(self); var i = indexFn(self); return (o) ? o[i] : undefined; }, { assign:function(self, value){ return obj(self)[indexFn(self)] = value; } }); } function _functionCall(fn) { var argsFn = []; if (peekToken().text != ')') { do { argsFn.push(expression()); } while (expect(',')); } consume(')'); return function (self){ var args = []; for ( var i = 0; i < argsFn.length; i++) { args.push(argsFn[i](self)); } var fnPtr = fn(self) || noop; // IE stupidity! return fnPtr.apply ? fnPtr.apply(self, args) : fnPtr(args[0], args[1], args[2], args[3], args[4]); }; } // This is used with json array declaration function arrayDeclaration () { var elementFns = []; if (peekToken().text != ']') { do { elementFns.push(expression()); } while (expect(',')); } consume(']'); return function (self){ var array = []; for ( var i = 0; i < elementFns.length; i++) { array.push(elementFns[i](self)); } return array; }; } function object () { var keyValues = []; if (peekToken().text != '}') { do { var token = expect(), key = token.string || token.text; consume(":"); var value = expression(); keyValues.push({key:key, value:value}); } while (expect(',')); } 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; }; } //TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular) function watch () { var decl = []; while(hasTokens()) { decl.push(watchDecl()); if (!expect(';')) { assertAllConsumed(); } } assertAllConsumed(); return function (self){ for ( var i = 0; i < decl.length; i++) { var d = decl[i](self); self.addListener(d.name, d.fn); } }; } function watchDecl () { var anchorName = expect().text; consume(":"); var expressionFn; if (peekToken().text == '{') { consume("{"); expressionFn = statements(); consume("}"); } else { expressionFn = expression(); } return function(self) { return {name:anchorName, fn:expressionFn}; }; } }