'use strict'; var OPERATORS = { 'null':function(self){return null;}, 'true':function(self){return true;}, 'false':function(self){return false;}, $undefined:noop, '+':function(self, a,b){a=a(self); b=b(self); return (isDefined(a)?a:0)+(isDefined(b)?b:0);}, '-':function(self, a,b){a=a(self); b=b(self); return (isDefined(a)?a:0)-(isDefined(b)?b:0);}, '*':function(self, a,b){return a(self)*b(self);}, '/':function(self, a,b){return a(self)/b(self);}, '%':function(self, a,b){return a(self)%b(self);}, '^':function(self, a,b){return a(self)^b(self);}, '=':noop, '==':function(self, a,b){return a(self)==b(self);}, '!=':function(self, a,b){return a(self)!=b(self);}, '<':function(self, a,b){return a(self)':function(self, a,b){return a(self)>b(self);}, '<=':function(self, a,b){return a(self)<=b(self);}, '>=':function(self, a,b){return a(self)>=b(self);}, '&&':function(self, a,b){return a(self)&&b(self);}, '||':function(self, a,b){return a(self)||b(self);}, '&':function(self, a,b){return a(self)&b(self);}, // '|':function(self, a,b){return a|b;}, '|':function(self, a,b){return b(self)(self, a(self));}, '!':function(self, a){return !a(self);} }; 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}); }; } //TODO: Shouldn't all of the public methods have assertAllConsumed? //TODO: I think these should be public as part of the parser api instead of scope.$eval(). return { assignable: assertConsumed(assignable), primary: assertConsumed(primary), statements: assertConsumed(statements), filter: assertConsumed(filter) }; function assertConsumed(fn) { return function(){ var value = fn(); if (tokens.length !== 0) { throwError("is an unexpected token", tokens[0]); } return value; }; } /////////////////////////////////// function throwError(msg, token) { throw Error("Syntax Error: Token '" + token.text + "' " + msg + " at column " + (token.index + 1) + " of the 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) { throwError("is not valid json", token); } tokens.shift(); 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); }; } function binaryFn(left, fn, right) { return function(self) { return fn(self, left, right); }; } function hasTokens () { return tokens.length > 0; } function statements(){ var statements = []; while(true) { if (tokens.length > 0 && !peek('}', ')', ';', ']')) statements.push(filterChain()); if (!expect(';')) { // optimize for the common case where there is only one statement. // TODO(size): maybe we should not support multiple statements? return statements.length == 1 ? statements[0] : 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 _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 (!isFunction(instance)) { 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; }; } 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}; }; } } ////////////////////////////////////////////////// // Parser helper functions ////////////////////////////////////////////////// function setter(obj, path, setValue) { var element = path.split('.'); for (var i = 0; element.length > 1; i++) { var key = element.shift(); var propertyObj = obj[key]; if (!propertyObj) { propertyObj = {}; obj[key] = propertyObj; } obj = propertyObj; } obj[element.shift()] = setValue; return setValue; } /** * Return the value accesible from the object by path. Any undefined traversals are ignored * @param {Object} obj starting object * @param {string} path path to traverse * @param {boolean=true} bindFnToScope * @returns value as accesbile by path */ function getter(obj, path, bindFnToScope) { if (!path) return obj; var keys = path.split('.'); var key; var lastInstance = obj; var len = keys.length; for (var i = 0; i < len; i++) { key = keys[i]; if (obj) { obj = (lastInstance = obj)[key]; } if (isUndefined(obj) && key.charAt(0) == '$') { var type = angularGlobal.typeOf(lastInstance); type = angular[type.charAt(0).toUpperCase()+type.substring(1)]; var fn = type ? type[[key.substring(1)]] : _undefined; if (fn) { return obj = bind(lastInstance, fn, lastInstance); } } } if (!bindFnToScope && isFunction(obj)) { return bind(lastInstance, obj); } return obj; } var getterFnCache = {}, compileCache = {}, JS_KEYWORDS = {}; forEach( ("abstract,boolean,break,byte,case,catch,char,class,const,continue,debugger,default," + "delete,do,double,else,enum,export,extends,false,final,finally,float,for,function,goto," + "if,implements,import,ininstanceof,intinterface,long,native,new,null,package,private," + "protected,public,return,short,static,super,switch,synchronized,this,throw,throws," + "transient,true,try,typeof,var,volatile,void,undefined,while,with").split(/,/), function(key){ JS_KEYWORDS[key] = true;} ); function getterFn(path) { var fn = getterFnCache[path]; if (fn) return fn; var code = 'var l, fn, t;\n'; forEach(path.split('.'), function(key) { key = (JS_KEYWORDS[key]) ? '["' + key + '"]' : '.' + key; code += 'if(!s) return s;\n' + 'l=s;\n' + 's=s' + key + ';\n' + 'if(typeof s=="function" && !(s instanceof RegExp)) {\n' + ' fn=function(){ return l' + key + '.apply(l, arguments); };\n' + ' fn.$unboundFn=s;\n' + ' s=fn;\n' + '}\n'; if (key.charAt(1) == '$') { // special code for super-imposed functions var name = key.substr(2); code += 'if(!s) {\n' + ' t = angular.Global.typeOf(l);\n' + ' fn = (angular[t.charAt(0).toUpperCase() + t.substring(1)]||{})["' + name + '"];\n' + ' if (fn) ' + 's = function(){ return fn.apply(l, [l].concat(Array.prototype.slice.call(arguments, 0))); };\n' + '}\n'; } }); code += 'return s;'; fn = Function('s', code); fn["toString"] = function(){ return code; }; return getterFnCache[path] = fn; } /////////////////////////////////// // TODO(misko): Should this function be public? function compileExpr(expr) { return parser(expr).statements(); } // TODO(misko): Deprecate? Remove! // I think that compilation should be a service. function expressionCompile(exp) { if (isFunction(exp)) return exp; var fn = compileCache[exp]; if (!fn) { fn = compileCache[exp] = parser(exp).statements(); } return fn; }