diff options
| author | jankuca | 2013-08-20 17:19:49 -0700 | 
|---|---|---|
| committer | Igor Minar | 2013-10-04 14:15:56 -0700 | 
| commit | 49e06eace58d871972f0cf3ec92aa28e19c4b02b (patch) | |
| tree | e32ab386133b57cbe5e4f99c40eaaf398e65a504 /src/ng/parse.js | |
| parent | 948e8ca3250f2e806e6c822fbaee64f8e54c2a53 (diff) | |
| download | angular.js-49e06eace58d871972f0cf3ec92aa28e19c4b02b.tar.bz2 | |
chore($parse): convert parser() and lex() to prototype-based code
This reduces memory consumption of parsed angular expressions and
speeds up parsing.
This JSPerf case demonstrates the performance boost:
http://jsperf.com/closure-vs-prototype-ngparser
Chrome: 1.5–2x boost
FF: slightly slower (I would love to know why)
IE: 4x boost
To be clear, this doesn't have any impact on runtime performance
of expressions as demostrated in this JSPerf:
http://jsperf.com/angular-parser-changes
Closes #3681
Diffstat (limited to 'src/ng/parse.js')
| -rw-r--r-- | src/ng/parse.js | 920 | 
1 files changed, 491 insertions, 429 deletions
| diff --git a/src/ng/parse.js b/src/ng/parse.js index 8f8c0f87..ae22f0e8 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -42,7 +42,6 @@ function ensureSafeObject(obj, fullExpression) {    if (obj && obj.constructor === obj) {      throw $parseMinErr('isecfn',          'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression); -  //     } else if (// isWindow(obj)        obj && obj.document && obj.location && obj.alert && obj.setInterval) {      throw $parseMinErr('isecwindow', @@ -93,156 +92,191 @@ var OPERATORS = {  };  var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; -function lex(text, csp){ -  var 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(), -          ch3 = ch2 + peek(2), -          fn = OPERATORS[ch], -          fn2 = OPERATORS[ch2], -          fn3 = OPERATORS[ch3]; -      if (fn3) { -        tokens.push({index:index, text:ch3, fn:fn3}); -        index += 3; -      } else 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; + +///////////////////////////////////////// + + +/** + * @constructor + */ +var Lexer = function (csp) { +  this.csp = csp; +}; + +Lexer.prototype = { +  constructor: Lexer, + +  lex: function (text) { +    this.text = text; +    this.index = 0; +    this.ch = undefined; +    this.lastCh = ':'; // can start regexp + +    this.tokens = []; + +    var token; +    var json = []; + +    while (this.index < this.text.length) { +      this.ch = this.text.charAt(this.index); +      if (this.is('"\'')) { +        this.readString(this.ch); +      } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) { +        this.readNumber(); +      } else if (this.isIdent(this.ch)) { +        this.readIdent(); +        // identifiers can only be if the preceding char was a { or , +        if (this.was('{,') && json[0] === '{' && +            (token = this.tokens[this.tokens.length - 1])) { +          token.json = token.text.indexOf('.') === -1; +        } +      } else if (this.is('(){}[].,;:?')) { +        this.tokens.push({ +          index: this.index, +          text: this.ch, +          json: (this.was(':[,') && this.is('{[')) || this.is('}]:,') +        }); +        if (this.is('{[')) json.unshift(this.ch); +        if (this.is('}]')) json.shift(); +        this.index++; +      } else if (this.isWhitespace(this.ch)) { +        this.index++; +        continue;        } else { -        throwError("Unexpected next character ", index, index+1); +        var ch2 = this.ch + this.peek(); +        var ch3 = ch2 + this.peek(2); +        var fn = OPERATORS[this.ch]; +        var fn2 = OPERATORS[ch2]; +        var fn3 = OPERATORS[ch3]; +        if (fn3) { +          this.tokens.push({index: this.index, text: ch3, fn: fn3}); +          this.index += 3; +        } else if (fn2) { +          this.tokens.push({index: this.index, text: ch2, fn: fn2}); +          this.index += 2; +        } else if (fn) { +          this.tokens.push({ +            index: this.index, +            text: this.ch, +            fn: fn, +            json: (this.was('[,:') && this.is('+-')) +          }); +          this.index += 1; +        } else { +          this.throwError('Unexpected next character ', this.index, this.index + 1); +        }        } +      this.lastCh = this.ch;      } -    lastCh = ch; -  } -  return tokens; +    return this.tokens; +  }, -  function is(chars) { -    return chars.indexOf(ch) != -1; -  } +  is: function(chars) { +    return chars.indexOf(this.ch) !== -1; +  }, -  function was(chars) { -    return chars.indexOf(lastCh) != -1; -  } +  was: function(chars) { +    return chars.indexOf(this.lastCh) !== -1; +  }, -  function peek(i) { +  peek: function(i) {      var num = i || 1; -    return index + num < text.length ? text.charAt(index + num) : 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; -    var colStr = (isDefined(start) ? -        "s " + start +  "-" + index + " [" + text.substring(start, end) + "]" -        : " " + end); -    throw $parseMinErr('lexerr', "Lexer Error: {0} at column{1} in expression [{2}].", -        error, colStr, text); -  } - -  function readNumber() { -    var number = ""; -    var start = index; -    while (index < text.length) { -      var ch = lowercase(text.charAt(index)); -      if (ch == '.' || isNumber(ch)) { +    return (this.index + num < this.text.length) ? this.text.charAt(this.index + num) : false; +  }, + +  isNumber: function(ch) { +    return ('0' <= ch && ch <= '9'); +  }, + +  isWhitespace: function(ch) { +    return (ch === ' ' || ch === '\r' || ch === '\t' || +            ch === '\n' || ch === '\v' || ch === '\u00A0'); // IE treats non-breaking space as \u00A0 +  }, + +  isIdent: function(ch) { +    return ('a' <= ch && ch <= 'z' || +            'A' <= ch && ch <= 'Z' || +            '_' === ch || ch === '$'); +  }, + +  isExpOperator: function(ch) { +    return (ch === '-' || ch === '+' || this.isNumber(ch)); +  }, + +  throwError: function(error, start, end) { +    end = end || this.index; +    var colStr = (isDefined(start) +            ? 's ' + start +  '-' + this.index + ' [' + this.text.substring(start, end) + ']' +            : ' ' + end); +    throw $parseMinErr('lexerr', 'Lexer Error: {0} at column{1} in expression [{2}].', +        error, colStr, this.text); +  }, + +  readNumber: function() { +    var number = ''; +    var start = this.index; +    while (this.index < this.text.length) { +      var ch = lowercase(this.text.charAt(this.index)); +      if (ch == '.' || this.isNumber(ch)) {          number += ch;        } else { -        var peekCh = peek(); -        if (ch == 'e' && isExpOperator(peekCh)) { +        var peekCh = this.peek(); +        if (ch == 'e' && this.isExpOperator(peekCh)) {            number += ch; -        } else if (isExpOperator(ch) && -            peekCh && isNumber(peekCh) && +        } else if (this.isExpOperator(ch) && +            peekCh && this.isNumber(peekCh) &&              number.charAt(number.length - 1) == 'e') {            number += ch; -        } else if (isExpOperator(ch) && -            (!peekCh || !isNumber(peekCh)) && +        } else if (this.isExpOperator(ch) && +            (!peekCh || !this.isNumber(peekCh)) &&              number.charAt(number.length - 1) == 'e') { -          throwError('Invalid exponent'); +          this.throwError('Invalid exponent');          } else {            break;          }        } -      index++; +      this.index++;      }      number = 1 * number; -    tokens.push({index:start, text:number, json:true, -      fn:function() {return number;}}); -  } -  function readIdent() { -    var ident = "", -        start = index, -        lastDot, peekIndex, methodName, ch; - -    while (index < text.length) { -      ch = text.charAt(index); -      if (ch == '.' || isIdent(ch) || isNumber(ch)) { -        if (ch == '.') lastDot = index; +    this.tokens.push({ +      index: start, +      text: number, +      json: true, +      fn: function() { return number; } +    }); +  }, + +  readIdent: function() { +    var parser = this; + +    var ident = ''; +    var start = this.index; + +    var lastDot, peekIndex, methodName, ch; + +    while (this.index < this.text.length) { +      ch = this.text.charAt(this.index); +      if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) { +        if (ch === '.') lastDot = this.index;          ident += ch;        } else {          break;        } -      index++; +      this.index++;      }      //check if this is not a method invocation and if it is back out to last dot      if (lastDot) { -      peekIndex = index; -      while(peekIndex < text.length) { -        ch = text.charAt(peekIndex); -        if (ch == '(') { +      peekIndex = this.index; +      while (peekIndex < this.text.length) { +        ch = this.text.charAt(peekIndex); +        if (ch === '(') {            methodName = ident.substr(lastDot - start + 1);            ident = ident.substr(0, lastDot - start); -          index = peekIndex; +          this.index = peekIndex;            break;          } -        if(isWhitespace(ch)) { +        if (this.isWhitespace(ch)) {            peekIndex++;          } else {            break; @@ -252,54 +286,55 @@ function lex(text, csp){      var token = { -      index:start, -      text:ident +      index: start, +      text: ident      };      if (OPERATORS.hasOwnProperty(ident)) { -      token.fn = token.json = OPERATORS[ident]; +      token.fn = OPERATORS[ident]; +      token.json = OPERATORS[ident];      } else { -      var getter = getterFn(ident, csp, text); +      var getter = getterFn(ident, this.csp, this.text);        token.fn = extend(function(self, locals) {          return (getter(self, locals));        }, {          assign: function(self, value) { -          return setter(self, ident, value, text); +          return setter(self, ident, value, parser.text);          }        });      } -    tokens.push(token); +    this.tokens.push(token);      if (methodName) { -      tokens.push({ +      this.tokens.push({          index:lastDot,          text: '.',          json: false        }); -      tokens.push({ +      this.tokens.push({          index: lastDot + 1,          text: methodName,          json: false        });      } -  } +  }, -  function readString(quote) { -    var start = index; -    index++; -    var string = ""; +  readString: function(quote) { +    var start = this.index; +    this.index++; +    var string = '';      var rawString = quote;      var escape = false; -    while (index < text.length) { -      var ch = text.charAt(index); +    while (this.index < this.text.length) { +      var ch = this.text.charAt(this.index);        rawString += ch;        if (escape) { -        if (ch == 'u') { -          var hex = text.substring(index + 1, index + 5); +        if (ch === 'u') { +          var hex = this.text.substring(this.index + 1, this.index + 5);            if (!hex.match(/[\da-f]{4}/i)) -            throwError( "Invalid unicode escape [\\u" + hex + "]"); -          index += 4; +            this.throwError('Invalid unicode escape [\\u' + hex + ']'); +          this.index += 4;            string += String.fromCharCode(parseInt(hex, 16));          } else {            var rep = ESCAPE[ch]; @@ -310,172 +345,227 @@ function lex(text, csp){            }          }          escape = false; -      } else if (ch == '\\') { +      } else if (ch === '\\') {          escape = true; -      } else if (ch == quote) { -        index++; -        tokens.push({ -          index:start, -          text:rawString, -          string:string, -          json:true, -          fn:function() { return string; } +      } else if (ch === quote) { +        this.index++; +        this.tokens.push({ +          index: start, +          text: rawString, +          string: string, +          json: true, +          fn: function() { return string; }          });          return;        } else {          string += ch;        } -      index++; +      this.index++;      } -    throwError("Unterminated quote", start); +    this.throwError('Unterminated quote', start);    } -} +}; -///////////////////////////////////////// -function parser(text, json, $filter, csp){ -  var ZERO = valueFn(0), -      value, -      tokens = lex(text, csp), -      assignment = _assignment, -      functionCall = _functionCall, -      fieldAccess = _fieldAccess, -      objectIndex = _objectIndex, -      filterChain = _filterChain; - -  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 = -      filterChain = -        function() { throwError("is not valid json", {text:text, index:0}); }; -    value = primary(); -  } else { -    value = statements(); -  } -  if (tokens.length !== 0) { -    throwError("is an unexpected token", tokens[0]); -  } -  value.literal = !!value.literal; -  value.constant = !!value.constant; -  return value; +/** + * @constructor + */ +var Parser = function (lexer, $filter, csp) { +  this.lexer = lexer; +  this.$filter = $filter; +  this.csp = csp; +}; -  /////////////////////////////////// -  function throwError(msg, token) { -    throw $parseMinErr('syntax', -        "Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].", -        token.text, msg, (token.index + 1), text, text.substring(token.index)); -  } +Parser.ZERO = function () { return 0; }; -  function peekToken() { -    if (tokens.length === 0) -      throw $parseMinErr('ueoe', "Unexpected end of expression: {0}", text); -    return tokens[0]; -  } +Parser.prototype = { +  constructor: Parser, -  function peek(e1, e2, e3, e4) { -    if (tokens.length > 0) { -      var token = tokens[0]; +  parse: function (text, json) { +    this.text = text; + +    //TODO(i): strip all the obsolte json stuff from this file +    this.json = json; + +    this.tokens = this.lexer.lex(text, this.csp); + +    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. +      this.assignment = this.logicalOR; + +      this.functionCall = +      this.fieldAccess = +      this.objectIndex = +      this.filterChain = function() { +        this.throwError('is not valid json', {text: text, index: 0}); +      }; +    } + +    var value = json ? this.primary() : this.statements(); + +    if (this.tokens.length !== 0) { +      this.throwError('is an unexpected token', this.tokens[0]); +    } + +    value.literal = !!value.literal; +    value.constant = !!value.constant; + +    return value; +  }, + +  primary: function () { +    var primary; +    if (this.expect('(')) { +      primary = this.filterChain(); +      this.consume(')'); +    } else if (this.expect('[')) { +      primary = this.arrayDeclaration(); +    } else if (this.expect('{')) { +      primary = this.object(); +    } else { +      var token = this.expect(); +      primary = token.fn; +      if (!primary) { +        this.throwError('not a primary expression', token); +      } +      if (token.json) { +        primary.constant = true; +        primary.literal = true; +      } +    } + +    var next, context; +    while ((next = this.expect('(', '[', '.'))) { +      if (next.text === '(') { +        primary = this.functionCall(primary, context); +        context = null; +      } else if (next.text === '[') { +        context = primary; +        primary = this.objectIndex(primary); +      } else if (next.text === '.') { +        context = primary; +        primary = this.fieldAccess(primary); +      } else { +        this.throwError('IMPOSSIBLE'); +      } +    } +    return primary; +  }, + +  throwError: function(msg, token) { +    throw $parseMinErr('syntax', +        'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', +          token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); +  }, + +  peekToken: function() { +    if (this.tokens.length === 0) +      throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); +    return this.tokens[0]; +  }, + +  peek: function(e1, e2, e3, e4) { +    if (this.tokens.length > 0) { +      var token = this.tokens[0];        var t = token.text; -      if (t==e1 || t==e2 || t==e3 || t==e4 || +      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); +  expect: function(e1, e2, e3, e4){ +    var token = this.peek(e1, e2, e3, e4);      if (token) { -      if (json && !token.json) { -        throwError("is not valid json", token); +      if (this.json && !token.json) { +        this.throwError('is not valid json', token);        } -      tokens.shift(); +      this.tokens.shift();        return token;      }      return false; -  } +  }, -  function consume(e1){ -    if (!expect(e1)) { -      throwError("is unexpected, expecting [" + e1 + "]", peek()); +  consume: function(e1){ +    if (!this.expect(e1)) { +      this.throwError('is unexpected, expecting [' + e1 + ']', this.peek());      } -  } +  }, -  function unaryFn(fn, right) { +  unaryFn: function(fn, right) {      return extend(function(self, locals) {        return fn(self, locals, right);      }, {        constant:right.constant      }); -  } +  }, -  function ternaryFn(left, middle, right){ +  ternaryFn: function(left, middle, right){      return extend(function(self, locals){        return left(self, locals) ? middle(self, locals) : right(self, locals);      }, {        constant: left.constant && middle.constant && right.constant      }); -  } +  }, -  function binaryFn(left, fn, right) { +  binaryFn: function(left, fn, right) {      return extend(function(self, locals) {        return fn(self, locals, left, right);      }, {        constant:left.constant && right.constant      }); -  } +  }, -  function statements() { +  statements: function() {      var statements = []; -    while(true) { -      if (tokens.length > 0 && !peek('}', ')', ';', ']')) -        statements.push(filterChain()); -      if (!expect(';')) { +    while (true) { +      if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) +        statements.push(this.filterChain()); +      if (!this.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, locals){ -            var value; -            for ( var i = 0; i < statements.length; i++) { -              var statement = statements[i]; -              if (statement) -                value = statement(self, locals); -            } -            return value; -          }; +        return (statements.length === 1) +            ? statements[0] +            : function(self, locals) { +                var value; +                for (var i = 0; i < statements.length; i++) { +                  var statement = statements[i]; +                  if (statement) { +                    value = statement(self, locals); +                  } +                } +                return value; +              };        }      } -  } +  }, -  function _filterChain() { -    var left = expression(); +  filterChain: function() { +    var left = this.expression();      var token; -    while(true) { -      if ((token = expect('|'))) { -        left = binaryFn(left, token.fn, filter()); +    while (true) { +      if ((token = this.expect('|'))) { +        left = this.binaryFn(left, token.fn, this.filter());        } else {          return left;        }      } -  } +  }, -  function filter() { -    var token = expect(); -    var fn = $filter(token.text); +  filter: function() { +    var token = this.expect(); +    var fn = this.$filter(token.text);      var argsFn = []; -    while(true) { -      if ((token = expect(':'))) { -        argsFn.push(expression()); +    while (true) { +      if ((token = this.expect(':'))) { +        argsFn.push(this.expression());        } else { -        var fnInvoke = function(self, locals, input){ +        var fnInvoke = function(self, locals, input) {            var args = [input]; -          for ( var i = 0; i < argsFn.length; i++) { +          for (var i = 0; i < argsFn.length; i++) {              args.push(argsFn[i](self, locals));            }            return fn.apply(self, args); @@ -485,224 +575,187 @@ function parser(text, json, $filter, csp){          };        }      } -  } +  }, -  function expression() { -    return assignment(); -  } +  expression: function() { +    return this.assignment(); +  }, -  function _assignment() { -    var left = ternary(); +  assignment: function() { +    var left = this.ternary();      var right;      var token; -    if ((token = expect('='))) { +    if ((token = this.expect('='))) {        if (!left.assign) { -        throwError("implies assignment but [" + -          text.substring(0, token.index) + "] can not be assigned to", token); +        this.throwError('implies assignment but [' + +            this.text.substring(0, token.index) + '] can not be assigned to', token);        } -      right = ternary(); -      return function(scope, locals){ +      right = this.ternary(); +      return function(scope, locals) {          return left.assign(scope, right(scope, locals), locals);        }; -    } else { -      return left;      } -  } +    return left; +  }, -  function ternary() { -    var left = logicalOR(); +  ternary: function() { +    var left = this.logicalOR();      var middle;      var token; -    if((token = expect('?'))){ -      middle = ternary(); -      if((token = expect(':'))){ -        return ternaryFn(left, middle, ternary()); -      } -      else { -        throwError('expected :', token); +    if ((token = this.expect('?'))) { +      middle = this.ternary(); +      if ((token = this.expect(':'))) { +        return this.ternaryFn(left, middle, this.ternary()); +      } else { +        this.throwError('expected :', token);        } -    } -    else { +    } else {        return left;      } -  } +  }, -  function logicalOR() { -    var left = logicalAND(); +  logicalOR: function() { +    var left = this.logicalAND();      var token; -    while(true) { -      if ((token = expect('||'))) { -        left = binaryFn(left, token.fn, logicalAND()); +    while (true) { +      if ((token = this.expect('||'))) { +        left = this.binaryFn(left, token.fn, this.logicalAND());        } else {          return left;        }      } -  } +  }, -  function logicalAND() { -    var left = equality(); +  logicalAND: function() { +    var left = this.equality();      var token; -    if ((token = expect('&&'))) { -      left = binaryFn(left, token.fn, logicalAND()); +    if ((token = this.expect('&&'))) { +      left = this.binaryFn(left, token.fn, this.logicalAND());      }      return left; -  } +  }, -  function equality() { -    var left = relational(); +  equality: function() { +    var left = this.relational();      var token; -    if ((token = expect('==','!=','===','!=='))) { -      left = binaryFn(left, token.fn, equality()); +    if ((token = this.expect('==','!=','===','!=='))) { +      left = this.binaryFn(left, token.fn, this.equality());      }      return left; -  } +  }, -  function relational() { -    var left = additive(); +  relational: function() { +    var left = this.additive();      var token; -    if ((token = expect('<', '>', '<=', '>='))) { -      left = binaryFn(left, token.fn, relational()); +    if ((token = this.expect('<', '>', '<=', '>='))) { +      left = this.binaryFn(left, token.fn, this.relational());      }      return left; -  } +  }, -  function additive() { -    var left = multiplicative(); +  additive: function() { +    var left = this.multiplicative();      var token; -    while ((token = expect('+','-'))) { -      left = binaryFn(left, token.fn, multiplicative()); +    while ((token = this.expect('+','-'))) { +      left = this.binaryFn(left, token.fn, this.multiplicative());      }      return left; -  } +  }, -  function multiplicative() { -    var left = unary(); +  multiplicative: function() { +    var left = this.unary();      var token; -    while ((token = expect('*','/','%'))) { -      left = binaryFn(left, token.fn, unary()); +    while ((token = this.expect('*','/','%'))) { +      left = this.binaryFn(left, token.fn, this.unary());      }      return left; -  } +  }, -  function unary() { +  unary: function() {      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()); +    if (this.expect('+')) { +      return this.primary(); +    } else if ((token = this.expect('-'))) { +      return this.binaryFn(Parser.ZERO, token.fn, this.unary()); +    } else if ((token = this.expect('!'))) { +      return this.unaryFn(token.fn, this.unary());      } else { -      return primary(); +      return this.primary();      } -  } +  }, +  fieldAccess: function(object) { +    var parser = this; +    var field = this.expect().text; +    var getter = getterFn(field, this.csp, this.text); -  function primary() { -    var primary; -    if (expect('(')) { -      primary = filterChain(); -      consume(')'); -    } 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); -      } -      if (token.json) { -        primary.constant = primary.literal = true; +    return extend(function(scope, locals, self) { +      return getter(self || object(scope, locals), locals); +    }, { +      assign: function(scope, value, locals) { +        return setter(object(scope, locals), field, value, parser.text);        } -    } +    }); +  }, -    var next, context; -    while ((next = expect('(', '[', '.'))) { -      if (next.text === '(') { -        primary = functionCall(primary, context); -        context = null; -      } else if (next.text === '[') { -        context = primary; -        primary = objectIndex(primary); -      } else if (next.text === '.') { -        context = primary; -        primary = fieldAccess(primary); -      } else { -        throwError("IMPOSSIBLE"); -      } -    } -    return primary; -  } +  objectIndex: function(obj) { +    var parser = this; -  function _fieldAccess(object) { -    var field = expect().text; -    var getter = getterFn(field, csp, text); -    return extend( -        function(scope, locals, self) { -          return getter(self || object(scope, locals), locals); -        }, -        { -          assign:function(scope, value, locals) { -            return setter(object(scope, locals), field, value, text); -          } -        } -    ); -  } +    var indexFn = this.expression(); +    this.consume(']'); -  function _objectIndex(obj) { -    var indexFn = expression(); -    consume(']'); -    return extend( -      function(self, locals){ -        var o = obj(self, locals), -            i = indexFn(self, locals), -            v, p; - -        if (!o) return undefined; -        v = ensureSafeObject(o[i], text); -        if (v && v.then) { -          p = v; -          if (!('$$v' in v)) { -            p.$$v = undefined; -            p.then(function(val) { p.$$v = val; }); -          } -          v = v.$$v; -        } -        return v; -      }, { -        assign:function(self, value, locals){ -          var key = indexFn(self, locals); -          // prevent overwriting of Function.constructor which would break ensureSafeObject check -          return ensureSafeObject(obj(self, locals), text)[key] = value; +    return extend(function(self, locals) { +      var o = obj(self, locals), +          i = indexFn(self, locals), +          v, p; + +      if (!o) return undefined; +      v = ensureSafeObject(o[i], parser.text); +      if (v && v.then) { +        p = v; +        if (!('$$v' in v)) { +          p.$$v = undefined; +          p.then(function(val) { p.$$v = val; });          } -      }); -  } +        v = v.$$v; +      } +      return v; +    }, { +      assign: function(self, value, locals) { +        var key = indexFn(self, locals); +        // prevent overwriting of Function.constructor which would break ensureSafeObject check +        var safe = ensureSafeObject(obj(self, locals), parser.text); +        return safe[key] = value; +      } +    }); +  }, -  function _functionCall(fn, contextGetter) { +  functionCall: function(fn, contextGetter) {      var argsFn = []; -    if (peekToken().text != ')') { +    if (this.peekToken().text !== ')') {        do { -        argsFn.push(expression()); -      } while (expect(',')); +        argsFn.push(this.expression()); +      } while (this.expect(','));      } -    consume(')'); -    return function(scope, locals){ -      var args = [], -          context = contextGetter ? contextGetter(scope, locals) : scope; +    this.consume(')'); + +    var parser = this; -      for ( var i = 0; i < argsFn.length; i++) { +    return function(scope, locals) { +      var args = []; +      var context = contextGetter ? contextGetter(scope, locals) : scope; + +      for (var i = 0; i < argsFn.length; i++) {          args.push(argsFn[i](scope, locals));        }        var fnPtr = fn(scope, locals, context) || noop; -      ensureSafeObject(fnPtr, text); +      ensureSafeObject(fnPtr, parser.text); -      // IE stupidity! +      // IE stupidity! (IE doesn't have apply for some native functions)        var v = fnPtr.apply -          ? fnPtr.apply(context, args) -          : fnPtr(args[0], args[1], args[2], args[3], args[4]); +            ? fnPtr.apply(context, args) +            : fnPtr(args[0], args[1], args[2], args[3], args[4]);        // Check for promise        if (v && v.then) { @@ -714,65 +767,68 @@ function parser(text, json, $filter, csp){          v = v.$$v;        } -      return ensureSafeObject(v, text); +      return ensureSafeObject(v, parser.text);      }; -  } +  },    // This is used with json array declaration -  function arrayDeclaration () { +  arrayDeclaration: function () {      var elementFns = [];      var allConstant = true; -    if (peekToken().text != ']') { +    if (this.peekToken().text !== ']') {        do { -        var elementFn = expression(); +        var elementFn = this.expression();          elementFns.push(elementFn);          if (!elementFn.constant) {            allConstant = false;          } -      } while (expect(',')); +      } while (this.expect(','));      } -    consume(']'); -    return extend(function(self, locals){ +    this.consume(']'); + +    return extend(function(self, locals) {        var array = []; -      for ( var i = 0; i < elementFns.length; i++) { +      for (var i = 0; i < elementFns.length; i++) {          array.push(elementFns[i](self, locals));        }        return array;      }, { -      literal:true, -      constant:allConstant +      literal: true, +      constant: allConstant      }); -  } +  }, -  function object () { +  object: function () {      var keyValues = [];      var allConstant = true; -    if (peekToken().text != '}') { +    if (this.peekToken().text !== '}') {        do { -        var token = expect(), +        var token = this.expect(),          key = token.string || token.text; -        consume(":"); -        var value = expression(); -        keyValues.push({key:key, value:value}); +        this.consume(':'); +        var value = this.expression(); +        keyValues.push({key: key, value: value});          if (!value.constant) {            allConstant = false;          } -      } while (expect(',')); +      } while (this.expect(','));      } -    consume('}'); -    return extend(function(self, locals){ +    this.consume('}'); + +    return extend(function(self, locals) {        var object = {}; -      for ( var i = 0; i < keyValues.length; i++) { +      for (var i = 0; i < keyValues.length; i++) {          var keyValue = keyValues[i];          object[keyValue.key] = keyValue.value(self, locals);        }        return object;      }, { -      literal:true, -      constant:allConstant +      literal: true, +      constant: allConstant      }); -  } -} +  }, +}; +  //////////////////////////////////////////////////  // Parser helper functions @@ -978,13 +1034,19 @@ function $ParseProvider() {    var cache = {};    this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {      return function(exp) { -      switch(typeof exp) { +      switch (typeof exp) {          case 'string': -          return cache.hasOwnProperty(exp) -            ? cache[exp] -            : cache[exp] =  parser(exp, false, $filter, $sniffer.csp); +          if (cache.hasOwnProperty(exp)) { +            return cache[exp]; +          } + +          var lexer = new Lexer($sniffer.csp); +          var parser = new Parser(lexer, $filter, $sniffer.csp); +          return cache[exp] = parser.parse(exp, false); +          case 'function':            return exp; +          default:            return noop;        } | 
