aboutsummaryrefslogtreecommitdiffstats
path: root/src/service
diff options
context:
space:
mode:
Diffstat (limited to 'src/service')
-rw-r--r--src/service/parse.js739
1 files changed, 739 insertions, 0 deletions
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)<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(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;
+}