diff options
Diffstat (limited to 'src/ng/parse.js')
| -rw-r--r-- | src/ng/parse.js | 760 | 
1 files changed, 760 insertions, 0 deletions
| diff --git a/src/ng/parse.js b/src/ng/parse.js new file mode 100644 index 00000000..47c5188e --- /dev/null +++ b/src/ng/parse.js @@ -0,0 +1,760 @@ +'use strict'; + +var OPERATORS = { +    'null':function(){return null;}, +    'true':function(){return true;}, +    'false':function(){return false;}, +    undefined:noop, +    '+':function(self, locals, a,b){a=a(self, locals); b=b(self, locals); return (isDefined(a)?a:0)+(isDefined(b)?b:0);}, +    '-':function(self, locals, a,b){a=a(self, locals); b=b(self, locals); return (isDefined(a)?a:0)-(isDefined(b)?b:0);}, +    '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);}, +    '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);}, +    '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);}, +    '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);}, +    '=':noop, +    '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);}, +    '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);}, +    '<':function(self, locals, a,b){return a(self, locals)<b(self, locals);}, +    '>':function(self, locals, a,b){return a(self, locals)>b(self, locals);}, +    '<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);}, +    '>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);}, +    '&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);}, +    '||':function(self, locals, a,b){return a(self, locals)||b(self, locals);}, +    '&':function(self, locals, a,b){return a(self, locals)&b(self, locals);}, +//    '|':function(self, locals, a,b){return a|b;}, +    '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));}, +    '!':function(self, locals, a){return !a(self, locals);} +}; +var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; + +function lex(text){ +  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(), +          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 = "", +        start = index, +        lastDot, peekIndex, methodName; + +    while (index < text.length) { +      var ch = text.charAt(index); +      if (ch == '.' || isIdent(ch) || isNumber(ch)) { +        if (ch == '.') lastDot = index; +        ident += ch; +      } else { +        break; +      } +      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) { +        var ch = text.charAt(peekIndex); +        if (ch == '(') { +          methodName = ident.substr(lastDot - start + 1); +          ident = ident.substr(0, lastDot - start); +          index = peekIndex; +          break; +        } +        if(isWhitespace(ch)) { +          peekIndex++; +        } else { +          break; +        } +      } +    } + + +    var token = { +      index:start, +      text:ident +    }; + +    if (OPERATORS.hasOwnProperty(ident)) { +      token.fn = token.json = OPERATORS[ident]; +    } else { +      var getter = getterFn(ident); +      token.fn = extend(function(self, locals) { +        return (getter(self, locals)); +      }, { +        assign: function(self, value) { +          return setter(self, ident, value); +        } +      }); +    } + +    tokens.push(token); + +    if (methodName) { +      tokens.push({ +        index:lastDot, +        text: '.', +        json: false +      }); +      tokens.push({ +        index: lastDot + 1, +        text: methodName, +        json: false +      }); +    } +  } + +  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; } +        }); +        return; +      } else { +        string += ch; +      } +      index++; +    } +    throwError("Unterminated quote", start); +  } +} + +///////////////////////////////////////// + +function parser(text, json, $filter){ +  var ZERO = valueFn(0), +      value, +      tokens = lex(text), +      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]); +  } +  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, locals) { +      return fn(self, locals, right); +    }; +  } + +  function binaryFn(left, fn, right) { +    return function(self, locals) { +      return fn(self, locals, 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, 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(); +    var token; +    while(true) { +      if ((token = expect('|'))) { +        left = binaryFn(left, token.fn, filter()); +      } else { +        return left; +      } +    } +  } + +  function filter() { +    var token = expect(); +    var fn = $filter(token.text); +    var argsFn = []; +    while(true) { +      if ((token = expect(':'))) { +        argsFn.push(expression()); +      } else { +        var fnInvoke = function(self, locals, input){ +          var args = [input]; +          for ( var i = 0; i < argsFn.length; i++) { +            args.push(argsFn[i](self, locals)); +          } +          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, locals){ +        return left.assign(self, right(self, locals), locals); +      }; +    } 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('(')) { +      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); +      } +    } + +    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; +  } + +  function _fieldAccess(object) { +    var field = expect().text; +    var getter = getterFn(field); +    return extend( +        function(self, locals) { +          return getter(object(self, locals), locals); +        }, +        { +          assign:function(self, value, locals) { +            return setter(object(self, locals), field, value); +          } +        } +    ); +  } + +  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 = o[i]; +        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){ +          return obj(self, locals)[indexFn(self, locals)] = value; +        } +      }); +  } + +  function _functionCall(fn, contextGetter) { +    var argsFn = []; +    if (peekToken().text != ')') { +      do { +        argsFn.push(expression()); +      } while (expect(',')); +    } +    consume(')'); +    return function(self, locals){ +      var args = [], +          context = contextGetter ? contextGetter(self, locals) : self; + +      for ( var i = 0; i < argsFn.length; i++) { +        args.push(argsFn[i](self, locals)); +      } +      var fnPtr = fn(self, locals) || noop; +      // IE stupidity! +      return fnPtr.apply +          ? fnPtr.apply(context, 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, locals){ +      var array = []; +      for ( var i = 0; i < elementFns.length; i++) { +        array.push(elementFns[i](self, locals)); +      } +      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, locals){ +      var object = {}; +      for ( var i = 0; i < keyValues.length; i++) { +        var keyValue = keyValues[i]; +        var value = keyValue.value(self, locals); +        object[keyValue.key] = value; +      } +      return object; +    }; +  } +} + +////////////////////////////////////////////////// +// 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 + */ +//TODO(misko): this function needs to be removed +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 (!bindFnToScope && isFunction(obj)) { +    return bind(lastInstance, obj); +  } +  return obj; +} + +var getterFnCache = {}; + +function getterFn(path) { +  if (getterFnCache.hasOwnProperty(path)) { +    return getterFnCache[path]; +  } + +  var fn, code = 'var l, fn, p;\n'; +  forEach(path.split('.'), function(key, index) { +    code += 'if(!s) return s;\n' + +            'l=s;\n' + +            's='+ (index +                    // we simply direference 's' on any .dot notation +                    ? 's' +                    // but if we are first then we check locals firs, and if so read it first +                    : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + +            'if (s && s.then) {\n' + +              ' if (!("$$v" in s)) {\n' + +                ' p=s;\n' + +                ' p.$$v = undefined;\n' + +                ' p.then(function(v) {p.$$v=v;});\n' + +                '}\n' + +              ' s=s.$$v\n' + +            '}\n'; +  }); +  code += 'return s;'; +  fn = Function('s', 'k', code); +  fn.toString = function() { return code; }; + +  return getterFnCache[path] = fn; +} + +/////////////////////////////////// + +function $ParseProvider() { +  var cache = {}; +  this.$get = ['$filter', function($filter) { +    return function(exp) { +      switch(typeof exp) { +        case 'string': +          return cache.hasOwnProperty(exp) +            ? cache[exp] +            : cache[exp] =  parser(exp, false, $filter); +        case 'function': +          return exp; +        default: +          return noop; +      } +    }; +  }]; +} + + +// This is a special access for JSON parser which bypasses the injector +var parseJson = function(json) { +  return parser(json, true); +}; | 
