From bee6060e4b2499b385465e20f7a0dc2d11f003c0 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Wed, 2 Nov 2011 21:43:56 -0700 Subject: move(parser): appease the History God --- angularFiles.js | 2 +- src/parser.js | 739 ---------------------------------------------- src/service/parse.js | 739 ++++++++++++++++++++++++++++++++++++++++++++++ test/ParserSpec.js | 424 -------------------------- test/service/parseSpec.js | 424 ++++++++++++++++++++++++++ 5 files changed, 1164 insertions(+), 1164 deletions(-) delete mode 100644 src/parser.js create mode 100644 src/service/parse.js delete mode 100644 test/ParserSpec.js create mode 100644 test/service/parseSpec.js diff --git a/angularFiles.js b/angularFiles.js index 936f9b7d..f7d84231 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -3,7 +3,6 @@ angularFiles = { 'src/Angular.js', 'src/JSON.js', 'src/Injector.js', - 'src/parser.js', 'src/Resource.js', 'src/Browser.js', 'src/sanitizer.js', @@ -20,6 +19,7 @@ angularFiles = { 'src/service/location.js', 'src/service/log.js', 'src/service/resource.js', + 'src/service/parser.js', 'src/service/route.js', 'src/service/routeParams.js', 'src/service/scope.js', diff --git a/src/parser.js b/src/parser.js deleted file mode 100644 index 41fff7d5..00000000 --- a/src/parser.js +++ /dev/null @@ -1,739 +0,0 @@ -'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) - }; - - 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): 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; -} diff --git a/src/service/parse.js b/src/service/parse.js new file mode 100644 index 00000000..41fff7d5 --- /dev/null +++ b/src/service/parse.js @@ -0,0 +1,739 @@ +'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) + }; + + 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): 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; +} diff --git a/test/ParserSpec.js b/test/ParserSpec.js deleted file mode 100644 index ce3b22ca..00000000 --- a/test/ParserSpec.js +++ /dev/null @@ -1,424 +0,0 @@ -'use strict'; - -describe('parser', function() { - describe('lexer', function() { - it('should tokenize a string', function() { - var tokens = lex("a.bc[22]+1.3|f:'a\\\'c':\"d\\\"e\""); - var i = 0; - expect(tokens[i].index).toEqual(0); - expect(tokens[i].text).toEqual('a.bc'); - - i++; - expect(tokens[i].index).toEqual(4); - expect(tokens[i].text).toEqual('['); - - i++; - expect(tokens[i].index).toEqual(5); - expect(tokens[i].text).toEqual(22); - - i++; - expect(tokens[i].index).toEqual(7); - expect(tokens[i].text).toEqual(']'); - - i++; - expect(tokens[i].index).toEqual(8); - expect(tokens[i].text).toEqual('+'); - - i++; - expect(tokens[i].index).toEqual(9); - expect(tokens[i].text).toEqual(1.3); - - i++; - expect(tokens[i].index).toEqual(12); - expect(tokens[i].text).toEqual('|'); - - i++; - expect(tokens[i].index).toEqual(13); - expect(tokens[i].text).toEqual('f'); - - i++; - expect(tokens[i].index).toEqual(14); - expect(tokens[i].text).toEqual(':'); - - i++; - expect(tokens[i].index).toEqual(15); - expect(tokens[i].string).toEqual("a'c"); - - i++; - expect(tokens[i].index).toEqual(21); - expect(tokens[i].text).toEqual(':'); - - i++; - expect(tokens[i].index).toEqual(22); - expect(tokens[i].string).toEqual('d"e'); - }); - - it('should tokenize undefined', function() { - var tokens = lex("undefined"); - var i = 0; - expect(tokens[i].index).toEqual(0); - expect(tokens[i].text).toEqual('undefined'); - expect(undefined).toEqual(tokens[i].fn()); - }); - - it('should tokenize quoted string', function() { - var str = "['\\'', \"\\\"\"]"; - var tokens = lex(str); - - expect(tokens[1].index).toEqual(1); - expect(tokens[1].string).toEqual("'"); - - expect(tokens[3].index).toEqual(7); - expect(tokens[3].string).toEqual('"'); - }); - - it('should tokenize escaped quoted string', function() { - var str = '"\\"\\n\\f\\r\\t\\v\\u00A0"'; - var tokens = lex(str); - - expect(tokens[0].string).toEqual('"\n\f\r\t\v\u00A0'); - }); - - it('should tokenize unicode', function() { - var tokens = lex('"\\u00A0"'); - expect(tokens.length).toEqual(1); - expect(tokens[0].string).toEqual('\u00a0'); - }); - - it('should ignore whitespace', function() { - var tokens = lex("a \t \n \r b"); - expect(tokens[0].text).toEqual('a'); - expect(tokens[1].text).toEqual('b'); - }); - - it('should tokenize relation', function() { - var tokens = lex("! == != < > <= >="); - expect(tokens[0].text).toEqual('!'); - expect(tokens[1].text).toEqual('=='); - expect(tokens[2].text).toEqual('!='); - expect(tokens[3].text).toEqual('<'); - expect(tokens[4].text).toEqual('>'); - expect(tokens[5].text).toEqual('<='); - expect(tokens[6].text).toEqual('>='); - }); - - it('should tokenize statements', function() { - var tokens = lex("a;b;"); - expect(tokens[0].text).toEqual('a'); - expect(tokens[1].text).toEqual(';'); - expect(tokens[2].text).toEqual('b'); - expect(tokens[3].text).toEqual(';'); - }); - - it('should tokenize number', function() { - var tokens = lex("0.5"); - expect(tokens[0].text).toEqual(0.5); - }); - - it('should tokenize negative number', inject(function($rootScope) { - var value = $rootScope.$eval("-0.5"); - expect(value).toEqual(-0.5); - - value = $rootScope.$eval("{a:-0.5}"); - expect(value).toEqual({a:-0.5}); - })); - - it('should tokenize number with exponent', inject(function($rootScope) { - var tokens = lex("0.5E-10"); - expect(tokens[0].text).toEqual(0.5E-10); - expect($rootScope.$eval("0.5E-10")).toEqual(0.5E-10); - - tokens = lex("0.5E+10"); - expect(tokens[0].text).toEqual(0.5E+10); - })); - - it('should throws exception for invalid exponent', function() { - expect(function() { - lex("0.5E-"); - }).toThrow(new Error('Lexer Error: Invalid exponent at column 4 in expression [0.5E-].')); - - expect(function() { - lex("0.5E-A"); - }).toThrow(new Error('Lexer Error: Invalid exponent at column 4 in expression [0.5E-A].')); - }); - - it('should tokenize number starting with a dot', function() { - var tokens = lex(".5"); - expect(tokens[0].text).toEqual(0.5); - }); - - it('should throw error on invalid unicode', function() { - expect(function() { - lex("'\\u1''bla'"); - }).toThrow(new Error("Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla'].")); - }); - }); - - var scope; - beforeEach(inject(function ($rootScope) { - scope = $rootScope; - })); - - it('should parse expressions', function() { - expect(scope.$eval("-1")).toEqual(-1); - expect(scope.$eval("1 + 2.5")).toEqual(3.5); - expect(scope.$eval("1 + -2.5")).toEqual(-1.5); - expect(scope.$eval("1+2*3/4")).toEqual(1+2*3/4); - expect(scope.$eval("0--1+1.5")).toEqual(0- -1 + 1.5); - expect(scope.$eval("-0--1++2*-3/-4")).toEqual(-0- -1+ +2*-3/-4); - expect(scope.$eval("1/2*3")).toEqual(1/2*3); - }); - - it('should parse comparison', function() { - expect(scope.$eval("false")).toBeFalsy(); - expect(scope.$eval("!true")).toBeFalsy(); - expect(scope.$eval("1==1")).toBeTruthy(); - expect(scope.$eval("1!=2")).toBeTruthy(); - expect(scope.$eval("1<2")).toBeTruthy(); - expect(scope.$eval("1<=1")).toBeTruthy(); - expect(scope.$eval("1>2")).toEqual(1>2); - expect(scope.$eval("2>=1")).toEqual(2>=1); - expect(scope.$eval("true==2<3")).toEqual(true === 2<3); - }); - - it('should parse logical', function() { - expect(scope.$eval("0&&2")).toEqual(0&&2); - expect(scope.$eval("0||2")).toEqual(0||2); - expect(scope.$eval("0||1&&2")).toEqual(0||1&&2); - }); - - it('should parse string', function() { - expect(scope.$eval("'a' + 'b c'")).toEqual("ab c"); - }); - - it('should parse filters', function() { - angular.filter.substring = function(input, start, end) { - return input.substring(start, end); - }; - - angular.filter.upper = {_case: function(input) { - return input.toUpperCase(); - }}; - - expect(function() { - scope.$eval("1|nonExistant"); - }).toThrow(new Error("Syntax Error: Token 'nonExistant' should be a function at column 3 of the expression [1|nonExistant] starting at [nonExistant].")); - - scope.offset = 3; - expect(scope.$eval("'abcd'|upper._case")).toEqual("ABCD"); - expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc"); - expect(scope.$eval("'abcd'|substring:1:3|upper._case")).toEqual("BC"); - }); - - it('should access scope', function() { - scope.a = 123; - scope.b = {c: 456}; - expect(scope.$eval("a", scope)).toEqual(123); - expect(scope.$eval("b.c", scope)).toEqual(456); - expect(scope.$eval("x.y.z", scope)).not.toBeDefined(); - }); - - it('should evaluate grouped expressions', function() { - expect(scope.$eval("(1+2)*3")).toEqual((1+2)*3); - }); - - it('should evaluate assignments', function() { - expect(scope.$eval("a=12")).toEqual(12); - expect(scope.a).toEqual(12); - - expect(scope.$eval("x.y.z=123;")).toEqual(123); - expect(scope.x.y.z).toEqual(123); - - expect(scope.$eval("a=123; b=234")).toEqual(234); - expect(scope.a).toEqual(123); - expect(scope.b).toEqual(234); - }); - - it('should evaluate function call without arguments', function() { - scope['const'] = function(a,b){return 123;}; - expect(scope.$eval("const()")).toEqual(123); - }); - - it('should evaluate function call with arguments', function() { - scope.add = function(a,b) { - return a+b; - }; - expect(scope.$eval("add(1,2)")).toEqual(3); - }); - - it('should evaluate multiplication and division', function() { - scope.taxRate = 8; - scope.subTotal = 100; - expect(scope.$eval("taxRate / 100 * subTotal")).toEqual(8); - expect(scope.$eval("subTotal * taxRate / 100")).toEqual(8); - }); - - it('should evaluate array', function() { - expect(scope.$eval("[]").length).toEqual(0); - expect(scope.$eval("[1, 2]").length).toEqual(2); - expect(scope.$eval("[1, 2]")[0]).toEqual(1); - expect(scope.$eval("[1, 2]")[1]).toEqual(2); - }); - - it('should evaluate array access', function() { - expect(scope.$eval("[1][0]")).toEqual(1); - expect(scope.$eval("[[1]][0][0]")).toEqual(1); - expect(scope.$eval("[].length")).toEqual(0); - expect(scope.$eval("[1, 2].length")).toEqual(2); - }); - - it('should evaluate object', function() { - expect(toJson(scope.$eval("{}"))).toEqual("{}"); - expect(toJson(scope.$eval("{a:'b'}"))).toEqual('{"a":"b"}'); - expect(toJson(scope.$eval("{'a':'b'}"))).toEqual('{"a":"b"}'); - expect(toJson(scope.$eval("{\"a\":'b'}"))).toEqual('{"a":"b"}'); - }); - - it('should evaluate object access', function() { - expect(scope.$eval("{false:'WC', true:'CC'}[false]")).toEqual("WC"); - }); - - it('should evaluate JSON', function() { - expect(toJson(scope.$eval("[{}]"))).toEqual("[{}]"); - expect(toJson(scope.$eval("[{a:[]}, {b:1}]"))).toEqual('[{"a":[]},{"b":1}]'); - }); - - it('should evaluate multipple statements', function() { - expect(scope.$eval("a=1;b=3;a+b")).toEqual(4); - expect(scope.$eval(";;1;;")).toEqual(1); - }); - - it('should evaluate object methods in correct context (this)', function() { - var C = function () { - this.a = 123; - }; - C.prototype.getA = function() { - return this.a; - }; - - scope.obj = new C(); - expect(scope.$eval("obj.getA()")).toEqual(123); - }); - - it('should evaluate methods in correct context (this) in argument', function() { - var C = function () { - this.a = 123; - }; - C.prototype.sum = function(value) { - return this.a + value; - }; - C.prototype.getA = function() { - return this.a; - }; - - scope.obj = new C(); - expect(scope.$eval("obj.sum(obj.getA())")).toEqual(246); - }); - - it('should evaluate objects on scope context', function() { - scope.a = "abc"; - expect(scope.$eval("{a:a}").a).toEqual("abc"); - }); - - it('should evaluate field access on function call result', function() { - scope.a = function() { - return {name:'misko'}; - }; - expect(scope.$eval("a().name")).toEqual("misko"); - }); - - it('should evaluate field access after array access', function () { - scope.items = [{}, {name:'misko'}]; - expect(scope.$eval('items[1].name')).toEqual("misko"); - }); - - it('should evaluate array assignment', function() { - scope.items = []; - - expect(scope.$eval('items[1] = "abc"')).toEqual("abc"); - expect(scope.$eval('items[1]')).toEqual("abc"); -// Dont know how to make this work.... -// expect(scope.$eval('books[1] = "moby"')).toEqual("moby"); -// expect(scope.$eval('books[1]')).toEqual("moby"); - }); - - it('should evaluate grouped filters', function() { - scope.name = 'MISKO'; - expect(scope.$eval('n = (name|lowercase)')).toEqual('misko'); - expect(scope.$eval('n')).toEqual('misko'); - }); - - it('should evaluate remainder', function() { - expect(scope.$eval('1%2')).toEqual(1); - }); - - it('should evaluate sum with undefined', function() { - expect(scope.$eval('1+undefined')).toEqual(1); - expect(scope.$eval('undefined+1')).toEqual(1); - }); - - it('should throw exception on non-closed bracket', function() { - expect(function() { - scope.$eval('[].count('); - }).toThrow('Unexpected end of expression: [].count('); - }); - - it('should evaluate double negation', function() { - expect(scope.$eval('true')).toBeTruthy(); - expect(scope.$eval('!true')).toBeFalsy(); - expect(scope.$eval('!!true')).toBeTruthy(); - expect(scope.$eval('{true:"a", false:"b"}[!!true]')).toEqual('a'); - }); - - it('should evaluate negation', function() { - expect(scope.$eval("!false || true")).toEqual(!false || true); - expect(scope.$eval("!11 == 10")).toEqual(!11 == 10); - expect(scope.$eval("12/6/2")).toEqual(12/6/2); - }); - - it('should evaluate exclamation mark', function() { - expect(scope.$eval('suffix = "!"')).toEqual('!'); - }); - - it('should evaluate minus', function() { - expect(scope.$eval("{a:'-'}")).toEqual({a: "-"}); - }); - - it('should evaluate undefined', function() { - expect(scope.$eval("undefined")).not.toBeDefined(); - expect(scope.$eval("a=undefined")).not.toBeDefined(); - expect(scope.a).not.toBeDefined(); - }); - - it('should allow assignment after array dereference', function() { - scope.obj = [{}]; - scope.$eval('obj[0].name=1'); - expect(scope.obj.name).toBeUndefined(); - expect(scope.obj[0].name).toEqual(1); - }); - - it('should short-circuit AND operator', function() { - scope.run = function() { - throw "IT SHOULD NOT HAVE RUN"; - }; - expect(scope.$eval('false && run()')).toBe(false); - }); - - it('should short-circuit OR operator', function() { - scope.run = function() { - throw "IT SHOULD NOT HAVE RUN"; - }; - expect(scope.$eval('true || run()')).toBe(true); - }); - - - describe('assignable', function() { - it('should expose assignment function', function() { - var fn = parser('a').assignable(); - expect(fn.assign).toBeTruthy(); - var scope = {}; - fn.assign(scope, 123); - expect(scope).toEqual({a:123}); - }); - }); -}); diff --git a/test/service/parseSpec.js b/test/service/parseSpec.js new file mode 100644 index 00000000..ce3b22ca --- /dev/null +++ b/test/service/parseSpec.js @@ -0,0 +1,424 @@ +'use strict'; + +describe('parser', function() { + describe('lexer', function() { + it('should tokenize a string', function() { + var tokens = lex("a.bc[22]+1.3|f:'a\\\'c':\"d\\\"e\""); + var i = 0; + expect(tokens[i].index).toEqual(0); + expect(tokens[i].text).toEqual('a.bc'); + + i++; + expect(tokens[i].index).toEqual(4); + expect(tokens[i].text).toEqual('['); + + i++; + expect(tokens[i].index).toEqual(5); + expect(tokens[i].text).toEqual(22); + + i++; + expect(tokens[i].index).toEqual(7); + expect(tokens[i].text).toEqual(']'); + + i++; + expect(tokens[i].index).toEqual(8); + expect(tokens[i].text).toEqual('+'); + + i++; + expect(tokens[i].index).toEqual(9); + expect(tokens[i].text).toEqual(1.3); + + i++; + expect(tokens[i].index).toEqual(12); + expect(tokens[i].text).toEqual('|'); + + i++; + expect(tokens[i].index).toEqual(13); + expect(tokens[i].text).toEqual('f'); + + i++; + expect(tokens[i].index).toEqual(14); + expect(tokens[i].text).toEqual(':'); + + i++; + expect(tokens[i].index).toEqual(15); + expect(tokens[i].string).toEqual("a'c"); + + i++; + expect(tokens[i].index).toEqual(21); + expect(tokens[i].text).toEqual(':'); + + i++; + expect(tokens[i].index).toEqual(22); + expect(tokens[i].string).toEqual('d"e'); + }); + + it('should tokenize undefined', function() { + var tokens = lex("undefined"); + var i = 0; + expect(tokens[i].index).toEqual(0); + expect(tokens[i].text).toEqual('undefined'); + expect(undefined).toEqual(tokens[i].fn()); + }); + + it('should tokenize quoted string', function() { + var str = "['\\'', \"\\\"\"]"; + var tokens = lex(str); + + expect(tokens[1].index).toEqual(1); + expect(tokens[1].string).toEqual("'"); + + expect(tokens[3].index).toEqual(7); + expect(tokens[3].string).toEqual('"'); + }); + + it('should tokenize escaped quoted string', function() { + var str = '"\\"\\n\\f\\r\\t\\v\\u00A0"'; + var tokens = lex(str); + + expect(tokens[0].string).toEqual('"\n\f\r\t\v\u00A0'); + }); + + it('should tokenize unicode', function() { + var tokens = lex('"\\u00A0"'); + expect(tokens.length).toEqual(1); + expect(tokens[0].string).toEqual('\u00a0'); + }); + + it('should ignore whitespace', function() { + var tokens = lex("a \t \n \r b"); + expect(tokens[0].text).toEqual('a'); + expect(tokens[1].text).toEqual('b'); + }); + + it('should tokenize relation', function() { + var tokens = lex("! == != < > <= >="); + expect(tokens[0].text).toEqual('!'); + expect(tokens[1].text).toEqual('=='); + expect(tokens[2].text).toEqual('!='); + expect(tokens[3].text).toEqual('<'); + expect(tokens[4].text).toEqual('>'); + expect(tokens[5].text).toEqual('<='); + expect(tokens[6].text).toEqual('>='); + }); + + it('should tokenize statements', function() { + var tokens = lex("a;b;"); + expect(tokens[0].text).toEqual('a'); + expect(tokens[1].text).toEqual(';'); + expect(tokens[2].text).toEqual('b'); + expect(tokens[3].text).toEqual(';'); + }); + + it('should tokenize number', function() { + var tokens = lex("0.5"); + expect(tokens[0].text).toEqual(0.5); + }); + + it('should tokenize negative number', inject(function($rootScope) { + var value = $rootScope.$eval("-0.5"); + expect(value).toEqual(-0.5); + + value = $rootScope.$eval("{a:-0.5}"); + expect(value).toEqual({a:-0.5}); + })); + + it('should tokenize number with exponent', inject(function($rootScope) { + var tokens = lex("0.5E-10"); + expect(tokens[0].text).toEqual(0.5E-10); + expect($rootScope.$eval("0.5E-10")).toEqual(0.5E-10); + + tokens = lex("0.5E+10"); + expect(tokens[0].text).toEqual(0.5E+10); + })); + + it('should throws exception for invalid exponent', function() { + expect(function() { + lex("0.5E-"); + }).toThrow(new Error('Lexer Error: Invalid exponent at column 4 in expression [0.5E-].')); + + expect(function() { + lex("0.5E-A"); + }).toThrow(new Error('Lexer Error: Invalid exponent at column 4 in expression [0.5E-A].')); + }); + + it('should tokenize number starting with a dot', function() { + var tokens = lex(".5"); + expect(tokens[0].text).toEqual(0.5); + }); + + it('should throw error on invalid unicode', function() { + expect(function() { + lex("'\\u1''bla'"); + }).toThrow(new Error("Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla'].")); + }); + }); + + var scope; + beforeEach(inject(function ($rootScope) { + scope = $rootScope; + })); + + it('should parse expressions', function() { + expect(scope.$eval("-1")).toEqual(-1); + expect(scope.$eval("1 + 2.5")).toEqual(3.5); + expect(scope.$eval("1 + -2.5")).toEqual(-1.5); + expect(scope.$eval("1+2*3/4")).toEqual(1+2*3/4); + expect(scope.$eval("0--1+1.5")).toEqual(0- -1 + 1.5); + expect(scope.$eval("-0--1++2*-3/-4")).toEqual(-0- -1+ +2*-3/-4); + expect(scope.$eval("1/2*3")).toEqual(1/2*3); + }); + + it('should parse comparison', function() { + expect(scope.$eval("false")).toBeFalsy(); + expect(scope.$eval("!true")).toBeFalsy(); + expect(scope.$eval("1==1")).toBeTruthy(); + expect(scope.$eval("1!=2")).toBeTruthy(); + expect(scope.$eval("1<2")).toBeTruthy(); + expect(scope.$eval("1<=1")).toBeTruthy(); + expect(scope.$eval("1>2")).toEqual(1>2); + expect(scope.$eval("2>=1")).toEqual(2>=1); + expect(scope.$eval("true==2<3")).toEqual(true === 2<3); + }); + + it('should parse logical', function() { + expect(scope.$eval("0&&2")).toEqual(0&&2); + expect(scope.$eval("0||2")).toEqual(0||2); + expect(scope.$eval("0||1&&2")).toEqual(0||1&&2); + }); + + it('should parse string', function() { + expect(scope.$eval("'a' + 'b c'")).toEqual("ab c"); + }); + + it('should parse filters', function() { + angular.filter.substring = function(input, start, end) { + return input.substring(start, end); + }; + + angular.filter.upper = {_case: function(input) { + return input.toUpperCase(); + }}; + + expect(function() { + scope.$eval("1|nonExistant"); + }).toThrow(new Error("Syntax Error: Token 'nonExistant' should be a function at column 3 of the expression [1|nonExistant] starting at [nonExistant].")); + + scope.offset = 3; + expect(scope.$eval("'abcd'|upper._case")).toEqual("ABCD"); + expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc"); + expect(scope.$eval("'abcd'|substring:1:3|upper._case")).toEqual("BC"); + }); + + it('should access scope', function() { + scope.a = 123; + scope.b = {c: 456}; + expect(scope.$eval("a", scope)).toEqual(123); + expect(scope.$eval("b.c", scope)).toEqual(456); + expect(scope.$eval("x.y.z", scope)).not.toBeDefined(); + }); + + it('should evaluate grouped expressions', function() { + expect(scope.$eval("(1+2)*3")).toEqual((1+2)*3); + }); + + it('should evaluate assignments', function() { + expect(scope.$eval("a=12")).toEqual(12); + expect(scope.a).toEqual(12); + + expect(scope.$eval("x.y.z=123;")).toEqual(123); + expect(scope.x.y.z).toEqual(123); + + expect(scope.$eval("a=123; b=234")).toEqual(234); + expect(scope.a).toEqual(123); + expect(scope.b).toEqual(234); + }); + + it('should evaluate function call without arguments', function() { + scope['const'] = function(a,b){return 123;}; + expect(scope.$eval("const()")).toEqual(123); + }); + + it('should evaluate function call with arguments', function() { + scope.add = function(a,b) { + return a+b; + }; + expect(scope.$eval("add(1,2)")).toEqual(3); + }); + + it('should evaluate multiplication and division', function() { + scope.taxRate = 8; + scope.subTotal = 100; + expect(scope.$eval("taxRate / 100 * subTotal")).toEqual(8); + expect(scope.$eval("subTotal * taxRate / 100")).toEqual(8); + }); + + it('should evaluate array', function() { + expect(scope.$eval("[]").length).toEqual(0); + expect(scope.$eval("[1, 2]").length).toEqual(2); + expect(scope.$eval("[1, 2]")[0]).toEqual(1); + expect(scope.$eval("[1, 2]")[1]).toEqual(2); + }); + + it('should evaluate array access', function() { + expect(scope.$eval("[1][0]")).toEqual(1); + expect(scope.$eval("[[1]][0][0]")).toEqual(1); + expect(scope.$eval("[].length")).toEqual(0); + expect(scope.$eval("[1, 2].length")).toEqual(2); + }); + + it('should evaluate object', function() { + expect(toJson(scope.$eval("{}"))).toEqual("{}"); + expect(toJson(scope.$eval("{a:'b'}"))).toEqual('{"a":"b"}'); + expect(toJson(scope.$eval("{'a':'b'}"))).toEqual('{"a":"b"}'); + expect(toJson(scope.$eval("{\"a\":'b'}"))).toEqual('{"a":"b"}'); + }); + + it('should evaluate object access', function() { + expect(scope.$eval("{false:'WC', true:'CC'}[false]")).toEqual("WC"); + }); + + it('should evaluate JSON', function() { + expect(toJson(scope.$eval("[{}]"))).toEqual("[{}]"); + expect(toJson(scope.$eval("[{a:[]}, {b:1}]"))).toEqual('[{"a":[]},{"b":1}]'); + }); + + it('should evaluate multipple statements', function() { + expect(scope.$eval("a=1;b=3;a+b")).toEqual(4); + expect(scope.$eval(";;1;;")).toEqual(1); + }); + + it('should evaluate object methods in correct context (this)', function() { + var C = function () { + this.a = 123; + }; + C.prototype.getA = function() { + return this.a; + }; + + scope.obj = new C(); + expect(scope.$eval("obj.getA()")).toEqual(123); + }); + + it('should evaluate methods in correct context (this) in argument', function() { + var C = function () { + this.a = 123; + }; + C.prototype.sum = function(value) { + return this.a + value; + }; + C.prototype.getA = function() { + return this.a; + }; + + scope.obj = new C(); + expect(scope.$eval("obj.sum(obj.getA())")).toEqual(246); + }); + + it('should evaluate objects on scope context', function() { + scope.a = "abc"; + expect(scope.$eval("{a:a}").a).toEqual("abc"); + }); + + it('should evaluate field access on function call result', function() { + scope.a = function() { + return {name:'misko'}; + }; + expect(scope.$eval("a().name")).toEqual("misko"); + }); + + it('should evaluate field access after array access', function () { + scope.items = [{}, {name:'misko'}]; + expect(scope.$eval('items[1].name')).toEqual("misko"); + }); + + it('should evaluate array assignment', function() { + scope.items = []; + + expect(scope.$eval('items[1] = "abc"')).toEqual("abc"); + expect(scope.$eval('items[1]')).toEqual("abc"); +// Dont know how to make this work.... +// expect(scope.$eval('books[1] = "moby"')).toEqual("moby"); +// expect(scope.$eval('books[1]')).toEqual("moby"); + }); + + it('should evaluate grouped filters', function() { + scope.name = 'MISKO'; + expect(scope.$eval('n = (name|lowercase)')).toEqual('misko'); + expect(scope.$eval('n')).toEqual('misko'); + }); + + it('should evaluate remainder', function() { + expect(scope.$eval('1%2')).toEqual(1); + }); + + it('should evaluate sum with undefined', function() { + expect(scope.$eval('1+undefined')).toEqual(1); + expect(scope.$eval('undefined+1')).toEqual(1); + }); + + it('should throw exception on non-closed bracket', function() { + expect(function() { + scope.$eval('[].count('); + }).toThrow('Unexpected end of expression: [].count('); + }); + + it('should evaluate double negation', function() { + expect(scope.$eval('true')).toBeTruthy(); + expect(scope.$eval('!true')).toBeFalsy(); + expect(scope.$eval('!!true')).toBeTruthy(); + expect(scope.$eval('{true:"a", false:"b"}[!!true]')).toEqual('a'); + }); + + it('should evaluate negation', function() { + expect(scope.$eval("!false || true")).toEqual(!false || true); + expect(scope.$eval("!11 == 10")).toEqual(!11 == 10); + expect(scope.$eval("12/6/2")).toEqual(12/6/2); + }); + + it('should evaluate exclamation mark', function() { + expect(scope.$eval('suffix = "!"')).toEqual('!'); + }); + + it('should evaluate minus', function() { + expect(scope.$eval("{a:'-'}")).toEqual({a: "-"}); + }); + + it('should evaluate undefined', function() { + expect(scope.$eval("undefined")).not.toBeDefined(); + expect(scope.$eval("a=undefined")).not.toBeDefined(); + expect(scope.a).not.toBeDefined(); + }); + + it('should allow assignment after array dereference', function() { + scope.obj = [{}]; + scope.$eval('obj[0].name=1'); + expect(scope.obj.name).toBeUndefined(); + expect(scope.obj[0].name).toEqual(1); + }); + + it('should short-circuit AND operator', function() { + scope.run = function() { + throw "IT SHOULD NOT HAVE RUN"; + }; + expect(scope.$eval('false && run()')).toBe(false); + }); + + it('should short-circuit OR operator', function() { + scope.run = function() { + throw "IT SHOULD NOT HAVE RUN"; + }; + expect(scope.$eval('true || run()')).toBe(true); + }); + + + describe('assignable', function() { + it('should expose assignment function', function() { + var fn = parser('a').assignable(); + expect(fn.assign).toBeTruthy(); + var scope = {}; + fn.assign(scope, 123); + expect(scope).toEqual({a:123}); + }); + }); +}); -- cgit v1.2.3