mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-04-08 22:37:20 +08:00
This gets rid of the bizarre "Usage: fooProvider();" which does not really communicate anything useful. Closes #8809
1124 lines
32 KiB
JavaScript
1124 lines
32 KiB
JavaScript
'use strict';
|
||
|
||
var $parseMinErr = minErr('$parse');
|
||
|
||
// Sandboxing Angular Expressions
|
||
// ------------------------------
|
||
// Angular expressions are generally considered safe because these expressions only have direct
|
||
// access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by
|
||
// obtaining a reference to native JS functions such as the Function constructor.
|
||
//
|
||
// As an example, consider the following Angular expression:
|
||
//
|
||
// {}.toString.constructor('alert("evil JS code")')
|
||
//
|
||
// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits
|
||
// against the expression language, but not to prevent exploits that were enabled by exposing
|
||
// sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good
|
||
// practice and therefore we are not even trying to protect against interaction with an object
|
||
// explicitly exposed in this way.
|
||
//
|
||
// In general, it is not possible to access a Window object from an angular expression unless a
|
||
// window or some DOM object that has a reference to window is published onto a Scope.
|
||
// Similarly we prevent invocations of function known to be dangerous, as well as assignments to
|
||
// native objects.
|
||
|
||
|
||
function ensureSafeMemberName(name, fullExpression) {
|
||
if (name === "__defineGetter__" || name === "__defineSetter__"
|
||
|| name === "__lookupGetter__" || name === "__lookupSetter__"
|
||
|| name === "__proto__") {
|
||
throw $parseMinErr('isecfld',
|
||
'Attempting to access a disallowed field in Angular expressions! '
|
||
+'Expression: {0}', fullExpression);
|
||
}
|
||
return name;
|
||
}
|
||
|
||
function ensureSafeObject(obj, fullExpression) {
|
||
// nifty check if obj is Function that is fast and works across iframes and other contexts
|
||
if (obj) {
|
||
if (obj.constructor === obj) {
|
||
throw $parseMinErr('isecfn',
|
||
'Referencing Function in Angular expressions is disallowed! Expression: {0}',
|
||
fullExpression);
|
||
} else if (// isWindow(obj)
|
||
obj.window === obj) {
|
||
throw $parseMinErr('isecwindow',
|
||
'Referencing the Window in Angular expressions is disallowed! Expression: {0}',
|
||
fullExpression);
|
||
} else if (// isElement(obj)
|
||
obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) {
|
||
throw $parseMinErr('isecdom',
|
||
'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}',
|
||
fullExpression);
|
||
} else if (// block Object so that we can't get hold of dangerous Object.* methods
|
||
obj === Object) {
|
||
throw $parseMinErr('isecobj',
|
||
'Referencing Object in Angular expressions is disallowed! Expression: {0}',
|
||
fullExpression);
|
||
}
|
||
}
|
||
return obj;
|
||
}
|
||
|
||
var CALL = Function.prototype.call;
|
||
var APPLY = Function.prototype.apply;
|
||
var BIND = Function.prototype.bind;
|
||
|
||
function ensureSafeFunction(obj, fullExpression) {
|
||
if (obj) {
|
||
if (obj.constructor === obj) {
|
||
throw $parseMinErr('isecfn',
|
||
'Referencing Function in Angular expressions is disallowed! Expression: {0}',
|
||
fullExpression);
|
||
} else if (obj === CALL || obj === APPLY || obj === BIND) {
|
||
throw $parseMinErr('isecff',
|
||
'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}',
|
||
fullExpression);
|
||
}
|
||
}
|
||
}
|
||
|
||
var OPERATORS = extend(createMap(), {
|
||
/* jshint bitwise : false */
|
||
'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);
|
||
if (isDefined(a)) {
|
||
if (isDefined(b)) {
|
||
return a + b;
|
||
}
|
||
return a;
|
||
}
|
||
return isDefined(b)?b:undefined;},
|
||
'-':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(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);}
|
||
});
|
||
/* jshint bitwise: true */
|
||
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
|
||
|
||
|
||
/////////////////////////////////////////
|
||
|
||
|
||
/**
|
||
* @constructor
|
||
*/
|
||
var Lexer = function (options) {
|
||
this.options = options;
|
||
};
|
||
|
||
Lexer.prototype = {
|
||
constructor: Lexer,
|
||
|
||
lex: function (text) {
|
||
this.text = text;
|
||
this.index = 0;
|
||
this.ch = undefined;
|
||
this.tokens = [];
|
||
|
||
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();
|
||
} else if (this.is('(){}[].,;:?')) {
|
||
this.tokens.push({
|
||
index: this.index,
|
||
text: this.ch
|
||
});
|
||
this.index++;
|
||
} else if (this.isWhitespace(this.ch)) {
|
||
this.index++;
|
||
} else {
|
||
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
|
||
});
|
||
this.index += 1;
|
||
} else {
|
||
this.throwError('Unexpected next character ', this.index, this.index + 1);
|
||
}
|
||
}
|
||
}
|
||
return this.tokens;
|
||
},
|
||
|
||
is: function(chars) {
|
||
return chars.indexOf(this.ch) !== -1;
|
||
},
|
||
|
||
peek: function(i) {
|
||
var num = i || 1;
|
||
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) {
|
||
// IE treats non-breaking space as \u00A0
|
||
return (ch === ' ' || ch === '\r' || ch === '\t' ||
|
||
ch === '\n' || ch === '\v' || ch === '\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 = this.peek();
|
||
if (ch == 'e' && this.isExpOperator(peekCh)) {
|
||
number += ch;
|
||
} else if (this.isExpOperator(ch) &&
|
||
peekCh && this.isNumber(peekCh) &&
|
||
number.charAt(number.length - 1) == 'e') {
|
||
number += ch;
|
||
} else if (this.isExpOperator(ch) &&
|
||
(!peekCh || !this.isNumber(peekCh)) &&
|
||
number.charAt(number.length - 1) == 'e') {
|
||
this.throwError('Invalid exponent');
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
this.index++;
|
||
}
|
||
number = 1 * number;
|
||
this.tokens.push({
|
||
index: start,
|
||
text: number,
|
||
constant: 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;
|
||
}
|
||
this.index++;
|
||
}
|
||
|
||
//check if the identifier ends with . and if so move back one char
|
||
if (lastDot && ident[ident.length - 1] === '.') {
|
||
this.index--;
|
||
ident = ident.slice(0, -1);
|
||
lastDot = ident.lastIndexOf('.');
|
||
if (lastDot === -1) {
|
||
lastDot = undefined;
|
||
}
|
||
}
|
||
|
||
//check if this is not a method invocation and if it is back out to last dot
|
||
if (lastDot) {
|
||
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);
|
||
this.index = peekIndex;
|
||
break;
|
||
}
|
||
if (this.isWhitespace(ch)) {
|
||
peekIndex++;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
var token = {
|
||
index: start,
|
||
text: ident
|
||
};
|
||
|
||
var fn = OPERATORS[ident];
|
||
|
||
if (fn) {
|
||
token.fn = fn;
|
||
token.constant = true;
|
||
} else {
|
||
var getter = getterFn(ident, this.options, this.text);
|
||
// TODO(perf): consider exposing the getter reference
|
||
token.fn = extend(function $parsePathGetter(self, locals) {
|
||
return getter(self, locals);
|
||
}, {
|
||
assign: function(self, value) {
|
||
return setter(self, ident, value, parser.text);
|
||
}
|
||
});
|
||
}
|
||
|
||
this.tokens.push(token);
|
||
|
||
if (methodName) {
|
||
this.tokens.push({
|
||
index: lastDot,
|
||
text: '.'
|
||
});
|
||
this.tokens.push({
|
||
index: lastDot + 1,
|
||
text: methodName
|
||
});
|
||
}
|
||
},
|
||
|
||
readString: function(quote) {
|
||
var start = this.index;
|
||
this.index++;
|
||
var string = '';
|
||
var rawString = quote;
|
||
var escape = false;
|
||
while (this.index < this.text.length) {
|
||
var ch = this.text.charAt(this.index);
|
||
rawString += ch;
|
||
if (escape) {
|
||
if (ch === 'u') {
|
||
var hex = this.text.substring(this.index + 1, this.index + 5);
|
||
if (!hex.match(/[\da-f]{4}/i))
|
||
this.throwError('Invalid unicode escape [\\u' + hex + ']');
|
||
this.index += 4;
|
||
string += String.fromCharCode(parseInt(hex, 16));
|
||
} else {
|
||
var rep = ESCAPE[ch];
|
||
string = string + (rep || ch);
|
||
}
|
||
escape = false;
|
||
} else if (ch === '\\') {
|
||
escape = true;
|
||
} else if (ch === quote) {
|
||
this.index++;
|
||
this.tokens.push({
|
||
index: start,
|
||
text: rawString,
|
||
string: string,
|
||
constant: true,
|
||
fn: function() { return string; }
|
||
});
|
||
return;
|
||
} else {
|
||
string += ch;
|
||
}
|
||
this.index++;
|
||
}
|
||
this.throwError('Unterminated quote', start);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* @constructor
|
||
*/
|
||
var Parser = function (lexer, $filter, options) {
|
||
this.lexer = lexer;
|
||
this.$filter = $filter;
|
||
this.options = options;
|
||
};
|
||
|
||
Parser.ZERO = extend(function () {
|
||
return 0;
|
||
}, {
|
||
constant: true
|
||
});
|
||
|
||
Parser.prototype = {
|
||
constructor: Parser,
|
||
|
||
parse: function (text) {
|
||
this.text = text;
|
||
this.tokens = this.lexer.lex(text);
|
||
|
||
var value = 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.constant) {
|
||
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 ||
|
||
(!e1 && !e2 && !e3 && !e4)) {
|
||
return token;
|
||
}
|
||
}
|
||
return false;
|
||
},
|
||
|
||
expect: function(e1, e2, e3, e4){
|
||
var token = this.peek(e1, e2, e3, e4);
|
||
if (token) {
|
||
this.tokens.shift();
|
||
return token;
|
||
}
|
||
return false;
|
||
},
|
||
|
||
consume: function(e1){
|
||
if (!this.expect(e1)) {
|
||
this.throwError('is unexpected, expecting [' + e1 + ']', this.peek());
|
||
}
|
||
},
|
||
|
||
unaryFn: function(fn, right) {
|
||
return extend(function(self, locals) {
|
||
return fn(self, locals, right);
|
||
}, {
|
||
constant:right.constant
|
||
});
|
||
},
|
||
|
||
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
|
||
});
|
||
},
|
||
|
||
binaryFn: function(left, fn, right) {
|
||
return extend(function(self, locals) {
|
||
return fn(self, locals, left, right);
|
||
}, {
|
||
constant:left.constant && right.constant
|
||
});
|
||
},
|
||
|
||
statements: function() {
|
||
var statements = [];
|
||
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;
|
||
};
|
||
}
|
||
}
|
||
},
|
||
|
||
filterChain: function() {
|
||
var left = this.expression();
|
||
var token;
|
||
while (true) {
|
||
if ((token = this.expect('|'))) {
|
||
left = this.binaryFn(left, token.fn, this.filter());
|
||
} else {
|
||
return left;
|
||
}
|
||
}
|
||
},
|
||
|
||
filter: function() {
|
||
var token = this.expect();
|
||
var fn = this.$filter(token.text);
|
||
var argsFn;
|
||
var args;
|
||
|
||
if (this.peek(':')) {
|
||
argsFn = [];
|
||
args = []; // we can safely reuse the array
|
||
while (this.expect(':')) {
|
||
argsFn.push(this.expression());
|
||
}
|
||
}
|
||
|
||
return valueFn(function $parseFilter(self, locals, input) {
|
||
if (args) {
|
||
args[0] = input;
|
||
|
||
var i = argsFn.length;
|
||
while (i--) {
|
||
args[i + 1] = argsFn[i](self, locals);
|
||
}
|
||
|
||
return fn.apply(undefined, args);
|
||
}
|
||
|
||
return fn(input);
|
||
});
|
||
},
|
||
|
||
expression: function() {
|
||
return this.assignment();
|
||
},
|
||
|
||
assignment: function() {
|
||
var left = this.ternary();
|
||
var right;
|
||
var token;
|
||
if ((token = this.expect('='))) {
|
||
if (!left.assign) {
|
||
this.throwError('implies assignment but [' +
|
||
this.text.substring(0, token.index) + '] can not be assigned to', token);
|
||
}
|
||
right = this.ternary();
|
||
return function(scope, locals) {
|
||
return left.assign(scope, right(scope, locals), locals);
|
||
};
|
||
}
|
||
return left;
|
||
},
|
||
|
||
ternary: function() {
|
||
var left = this.logicalOR();
|
||
var middle;
|
||
var token;
|
||
if ((token = this.expect('?'))) {
|
||
middle = this.assignment();
|
||
if ((token = this.expect(':'))) {
|
||
return this.ternaryFn(left, middle, this.assignment());
|
||
} else {
|
||
this.throwError('expected :', token);
|
||
}
|
||
} else {
|
||
return left;
|
||
}
|
||
},
|
||
|
||
logicalOR: function() {
|
||
var left = this.logicalAND();
|
||
var token;
|
||
while (true) {
|
||
if ((token = this.expect('||'))) {
|
||
left = this.binaryFn(left, token.fn, this.logicalAND());
|
||
} else {
|
||
return left;
|
||
}
|
||
}
|
||
},
|
||
|
||
logicalAND: function() {
|
||
var left = this.equality();
|
||
var token;
|
||
if ((token = this.expect('&&'))) {
|
||
left = this.binaryFn(left, token.fn, this.logicalAND());
|
||
}
|
||
return left;
|
||
},
|
||
|
||
equality: function() {
|
||
var left = this.relational();
|
||
var token;
|
||
if ((token = this.expect('==','!=','===','!=='))) {
|
||
left = this.binaryFn(left, token.fn, this.equality());
|
||
}
|
||
return left;
|
||
},
|
||
|
||
relational: function() {
|
||
var left = this.additive();
|
||
var token;
|
||
if ((token = this.expect('<', '>', '<=', '>='))) {
|
||
left = this.binaryFn(left, token.fn, this.relational());
|
||
}
|
||
return left;
|
||
},
|
||
|
||
additive: function() {
|
||
var left = this.multiplicative();
|
||
var token;
|
||
while ((token = this.expect('+','-'))) {
|
||
left = this.binaryFn(left, token.fn, this.multiplicative());
|
||
}
|
||
return left;
|
||
},
|
||
|
||
multiplicative: function() {
|
||
var left = this.unary();
|
||
var token;
|
||
while ((token = this.expect('*','/','%'))) {
|
||
left = this.binaryFn(left, token.fn, this.unary());
|
||
}
|
||
return left;
|
||
},
|
||
|
||
unary: function() {
|
||
var token;
|
||
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 this.primary();
|
||
}
|
||
},
|
||
|
||
fieldAccess: function(object) {
|
||
var parser = this;
|
||
var field = this.expect().text;
|
||
var getter = getterFn(field, this.options, this.text);
|
||
|
||
return extend(function $parseFieldAccess(scope, locals, self) {
|
||
return getter(self || object(scope, locals));
|
||
}, {
|
||
assign: function(scope, value, locals) {
|
||
var o = object(scope, locals);
|
||
if (!o) object.assign(scope, o = {});
|
||
return setter(o, field, value, parser.text);
|
||
}
|
||
});
|
||
},
|
||
|
||
objectIndex: function(obj) {
|
||
var parser = this;
|
||
|
||
var indexFn = this.expression();
|
||
this.consume(']');
|
||
|
||
return extend(function $parseObjectIndex(self, locals) {
|
||
var o = obj(self, locals),
|
||
i = indexFn(self, locals),
|
||
v;
|
||
|
||
ensureSafeMemberName(i, parser.text);
|
||
if (!o) return undefined;
|
||
v = ensureSafeObject(o[i], parser.text);
|
||
return v;
|
||
}, {
|
||
assign: function(self, value, locals) {
|
||
var key = ensureSafeMemberName(indexFn(self, locals), parser.text);
|
||
// prevent overwriting of Function.constructor which would break ensureSafeObject check
|
||
var o = ensureSafeObject(obj(self, locals), parser.text);
|
||
if (!o) obj.assign(self, o = {});
|
||
return o[key] = value;
|
||
}
|
||
});
|
||
},
|
||
|
||
functionCall: function(fnGetter, contextGetter) {
|
||
var argsFn = [];
|
||
if (this.peekToken().text !== ')') {
|
||
do {
|
||
argsFn.push(this.expression());
|
||
} while (this.expect(','));
|
||
}
|
||
this.consume(')');
|
||
|
||
var expressionText = this.text;
|
||
// we can safely reuse the array across invocations
|
||
var args = argsFn.length ? [] : null;
|
||
|
||
return function $parseFunctionCall(scope, locals) {
|
||
var context = contextGetter ? contextGetter(scope, locals) : scope;
|
||
var fn = fnGetter(scope, locals, context) || noop;
|
||
|
||
if (args) {
|
||
var i = argsFn.length;
|
||
while (i--) {
|
||
args[i] = argsFn[i](scope, locals);
|
||
}
|
||
}
|
||
|
||
ensureSafeObject(context, expressionText);
|
||
ensureSafeFunction(fn, expressionText);
|
||
|
||
// IE stupidity! (IE doesn't have apply for some native functions)
|
||
var v = fn.apply
|
||
? fn.apply(context, args)
|
||
: fn(args[0], args[1], args[2], args[3], args[4]);
|
||
|
||
return ensureSafeObject(v, expressionText);
|
||
};
|
||
},
|
||
|
||
// This is used with json array declaration
|
||
arrayDeclaration: function () {
|
||
var elementFns = [];
|
||
var allConstant = true;
|
||
if (this.peekToken().text !== ']') {
|
||
do {
|
||
if (this.peek(']')) {
|
||
// Support trailing commas per ES5.1.
|
||
break;
|
||
}
|
||
var elementFn = this.expression();
|
||
elementFns.push(elementFn);
|
||
if (!elementFn.constant) {
|
||
allConstant = false;
|
||
}
|
||
} while (this.expect(','));
|
||
}
|
||
this.consume(']');
|
||
|
||
return extend(function(self, locals) {
|
||
var array = [];
|
||
for (var i = 0; i < elementFns.length; i++) {
|
||
array.push(elementFns[i](self, locals));
|
||
}
|
||
return array;
|
||
}, {
|
||
literal: true,
|
||
constant: allConstant
|
||
});
|
||
},
|
||
|
||
object: function () {
|
||
var keyValues = [];
|
||
var allConstant = true;
|
||
if (this.peekToken().text !== '}') {
|
||
do {
|
||
if (this.peek('}')) {
|
||
// Support trailing commas per ES5.1.
|
||
break;
|
||
}
|
||
var token = this.expect(),
|
||
key = token.string || token.text;
|
||
this.consume(':');
|
||
var value = this.expression();
|
||
keyValues.push({key: key, value: value});
|
||
if (!value.constant) {
|
||
allConstant = false;
|
||
}
|
||
} while (this.expect(','));
|
||
}
|
||
this.consume('}');
|
||
|
||
return extend(function(self, locals) {
|
||
var object = {};
|
||
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
|
||
});
|
||
}
|
||
};
|
||
|
||
|
||
//////////////////////////////////////////////////
|
||
// Parser helper functions
|
||
//////////////////////////////////////////////////
|
||
|
||
function setter(obj, path, setValue, fullExp) {
|
||
|
||
var element = path.split('.'), key;
|
||
for (var i = 0; element.length > 1; i++) {
|
||
key = ensureSafeMemberName(element.shift(), fullExp);
|
||
var propertyObj = obj[key];
|
||
if (!propertyObj) {
|
||
propertyObj = {};
|
||
obj[key] = propertyObj;
|
||
}
|
||
obj = propertyObj;
|
||
}
|
||
key = ensureSafeMemberName(element.shift(), fullExp);
|
||
ensureSafeObject(obj, fullExp);
|
||
ensureSafeObject(obj[key], fullExp);
|
||
obj[key] = setValue;
|
||
return setValue;
|
||
}
|
||
|
||
var getterFnCache = createMap();
|
||
|
||
/**
|
||
* Implementation of the "Black Hole" variant from:
|
||
* - http://jsperf.com/angularjs-parse-getter/4
|
||
* - http://jsperf.com/path-evaluation-simplified/7
|
||
*/
|
||
function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
|
||
ensureSafeMemberName(key0, fullExp);
|
||
ensureSafeMemberName(key1, fullExp);
|
||
ensureSafeMemberName(key2, fullExp);
|
||
ensureSafeMemberName(key3, fullExp);
|
||
ensureSafeMemberName(key4, fullExp);
|
||
|
||
return function cspSafeGetter(scope, locals) {
|
||
var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope;
|
||
|
||
if (pathVal == null) return pathVal;
|
||
pathVal = pathVal[key0];
|
||
|
||
if (!key1) return pathVal;
|
||
if (pathVal == null) return undefined;
|
||
pathVal = pathVal[key1];
|
||
|
||
if (!key2) return pathVal;
|
||
if (pathVal == null) return undefined;
|
||
pathVal = pathVal[key2];
|
||
|
||
if (!key3) return pathVal;
|
||
if (pathVal == null) return undefined;
|
||
pathVal = pathVal[key3];
|
||
|
||
if (!key4) return pathVal;
|
||
if (pathVal == null) return undefined;
|
||
pathVal = pathVal[key4];
|
||
|
||
return pathVal;
|
||
};
|
||
}
|
||
|
||
function getterFn(path, options, fullExp) {
|
||
var fn = getterFnCache[path];
|
||
|
||
if (fn) return fn;
|
||
|
||
var pathKeys = path.split('.'),
|
||
pathKeysLength = pathKeys.length;
|
||
|
||
// http://jsperf.com/angularjs-parse-getter/6
|
||
if (options.csp) {
|
||
if (pathKeysLength < 6) {
|
||
fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp);
|
||
} else {
|
||
fn = function(scope, locals) {
|
||
var i = 0, val;
|
||
do {
|
||
val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++],
|
||
pathKeys[i++], fullExp)(scope, locals);
|
||
|
||
locals = undefined; // clear after first iteration
|
||
scope = val;
|
||
} while (i < pathKeysLength);
|
||
return val;
|
||
};
|
||
}
|
||
} else {
|
||
var code = '';
|
||
forEach(pathKeys, function(key, index) {
|
||
ensureSafeMemberName(key, fullExp);
|
||
code += 'if(s == null) return undefined;\n' +
|
||
's='+ (index
|
||
// we simply dereference 's' on any .dot notation
|
||
? 's'
|
||
// but if we are first then we check locals first, and if so read it first
|
||
: '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key + ';\n';
|
||
});
|
||
code += 'return s;';
|
||
|
||
/* jshint -W054 */
|
||
var evaledFnGetter = new Function('s', 'l', code); // s=scope, l=locals
|
||
/* jshint +W054 */
|
||
evaledFnGetter.toString = valueFn(code);
|
||
fn = evaledFnGetter;
|
||
}
|
||
|
||
getterFnCache[path] = fn;
|
||
return fn;
|
||
}
|
||
|
||
///////////////////////////////////
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $parse
|
||
* @kind function
|
||
*
|
||
* @description
|
||
*
|
||
* Converts Angular {@link guide/expression expression} into a function.
|
||
*
|
||
* ```js
|
||
* var getter = $parse('user.name');
|
||
* var setter = getter.assign;
|
||
* var context = {user:{name:'angular'}};
|
||
* var locals = {user:{name:'local'}};
|
||
*
|
||
* expect(getter(context)).toEqual('angular');
|
||
* setter(context, 'newValue');
|
||
* expect(context.user.name).toEqual('newValue');
|
||
* expect(getter(context, locals)).toEqual('local');
|
||
* ```
|
||
*
|
||
*
|
||
* @param {string} expression String expression to compile.
|
||
* @returns {function(context, locals)} a function which represents the compiled expression:
|
||
*
|
||
* * `context` – `{object}` – an object against which any expressions embedded in the strings
|
||
* are evaluated against (typically a scope object).
|
||
* * `locals` – `{object=}` – local variables context object, useful for overriding values in
|
||
* `context`.
|
||
*
|
||
* The returned function also has the following properties:
|
||
* * `literal` – `{boolean}` – whether the expression's top-level node is a JavaScript
|
||
* literal.
|
||
* * `constant` – `{boolean}` – whether the expression is made entirely of JavaScript
|
||
* constant literals.
|
||
* * `assign` – `{?function(context, value)}` – if the expression is assignable, this will be
|
||
* set to a function to change its value on the given context.
|
||
*
|
||
*/
|
||
|
||
|
||
/**
|
||
* @ngdoc provider
|
||
* @name $parseProvider
|
||
*
|
||
* @description
|
||
* `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse}
|
||
* service.
|
||
*/
|
||
function $ParseProvider() {
|
||
var cache = createMap();
|
||
|
||
var $parseOptions = {
|
||
csp: false
|
||
};
|
||
|
||
|
||
this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
|
||
$parseOptions.csp = $sniffer.csp;
|
||
|
||
return function $parse(exp, interceptorFn) {
|
||
var parsedExpression, oneTime, cacheKey;
|
||
|
||
switch (typeof exp) {
|
||
case 'string':
|
||
cacheKey = exp = exp.trim();
|
||
|
||
parsedExpression = cache[cacheKey];
|
||
|
||
if (!parsedExpression) {
|
||
if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
|
||
oneTime = true;
|
||
exp = exp.substring(2);
|
||
}
|
||
|
||
var lexer = new Lexer($parseOptions);
|
||
var parser = new Parser(lexer, $filter, $parseOptions);
|
||
parsedExpression = parser.parse(exp);
|
||
|
||
if (parsedExpression.constant) {
|
||
parsedExpression.$$watchDelegate = constantWatchDelegate;
|
||
} else if (oneTime) {
|
||
parsedExpression.$$watchDelegate = parsedExpression.literal ?
|
||
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
|
||
}
|
||
|
||
cache[cacheKey] = parsedExpression;
|
||
}
|
||
return addInterceptor(parsedExpression, interceptorFn);
|
||
|
||
case 'function':
|
||
return addInterceptor(exp, interceptorFn);
|
||
|
||
default:
|
||
return addInterceptor(noop, interceptorFn);
|
||
}
|
||
};
|
||
|
||
function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) {
|
||
var unwatch, lastValue;
|
||
return unwatch = scope.$watch(function oneTimeWatch(scope) {
|
||
return parsedExpression(scope);
|
||
}, function oneTimeListener(value, old, scope) {
|
||
lastValue = value;
|
||
if (isFunction(listener)) {
|
||
listener.apply(this, arguments);
|
||
}
|
||
if (isDefined(value)) {
|
||
scope.$$postDigest(function () {
|
||
if (isDefined(lastValue)) {
|
||
unwatch();
|
||
}
|
||
});
|
||
}
|
||
}, objectEquality);
|
||
}
|
||
|
||
function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) {
|
||
var unwatch;
|
||
return unwatch = scope.$watch(function oneTimeWatch(scope) {
|
||
return parsedExpression(scope);
|
||
}, function oneTimeListener(value, old, scope) {
|
||
if (isFunction(listener)) {
|
||
listener.call(this, value, old, scope);
|
||
}
|
||
if (isAllDefined(value)) {
|
||
scope.$$postDigest(function () {
|
||
if(isAllDefined(value)) unwatch();
|
||
});
|
||
}
|
||
}, objectEquality);
|
||
|
||
function isAllDefined(value) {
|
||
var allDefined = true;
|
||
forEach(value, function (val) {
|
||
if (!isDefined(val)) allDefined = false;
|
||
});
|
||
return allDefined;
|
||
}
|
||
}
|
||
|
||
function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) {
|
||
var unwatch;
|
||
return unwatch = scope.$watch(function constantWatch(scope) {
|
||
return parsedExpression(scope);
|
||
}, function constantListener(value, old, scope) {
|
||
if (isFunction(listener)) {
|
||
listener.apply(this, arguments);
|
||
}
|
||
unwatch();
|
||
}, objectEquality);
|
||
}
|
||
|
||
function addInterceptor(parsedExpression, interceptorFn) {
|
||
if (!interceptorFn) return parsedExpression;
|
||
|
||
var fn = function interceptedExpression(scope, locals) {
|
||
var value = parsedExpression(scope, locals);
|
||
var result = interceptorFn(value, scope, locals);
|
||
// we only return the interceptor's result if the
|
||
// initial value is defined (for bind-once)
|
||
return isDefined(value) ? result : value;
|
||
};
|
||
fn.$$watchDelegate = parsedExpression.$$watchDelegate;
|
||
return fn;
|
||
}
|
||
}];
|
||
}
|