diff options
| author | Misko Hevery | 2011-11-02 21:43:56 -0700 | 
|---|---|---|
| committer | Misko Hevery | 2011-11-14 16:39:33 -0800 | 
| commit | bee6060e4b2499b385465e20f7a0dc2d11f003c0 (patch) | |
| tree | a005843abd4f741694ca88dfced03a7981d66e16 /src/service/parse.js | |
| parent | 16597e8b52bdfe34b2239a5ab86a839fa8e980d6 (diff) | |
| download | angular.js-bee6060e4b2499b385465e20f7a0dc2d11f003c0.tar.bz2 | |
move(parser): appease the History God
Diffstat (limited to 'src/service/parse.js')
| -rw-r--r-- | src/service/parse.js | 739 | 
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; +}  | 
