mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-01-12 22:45:52 +08:00
Expressions that start with `::` will be binded once. The rule
that binding follows is that the binding will take the first
not-undefined value at the end of a $digest cycle.
Watchers from $watch, $watchCollection and $watchGroup will
automatically stop watching when the expression(s) are bind-once
and fulfill.
Watchers from text and attributes interpolations will
automatically stop watching when the expressions are fulfill.
All directives that use $parse for expressions will automatically
work with bind-once expressions. E.g.
<div ng-bind="::foo"></div>
<li ng-repeat="item in ::items">{{::item.name}};</li>
Paired with: Caitlin and Igor
Design doc: https://docs.google.com/document/d/1fTqaaQYD2QE1rz-OywvRKFSpZirbWUPsnfaZaMq8fWI/edit#
Closes #7486
Closes #5408
1086 lines
31 KiB
JavaScript
1086 lines
31 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"))
|
||
//
|
||
// We want to prevent this type of access. For the sake of performance, during the lexing phase we
|
||
// disallow any "dotted" access to any member named "constructor".
|
||
//
|
||
// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor
|
||
// while evaluating the expression, which is a stronger but more expensive test. Since reflective
|
||
// calls are expensive anyway, this is not such a big deal compared to static dereferencing.
|
||
//
|
||
// 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.
|
||
//
|
||
// A developer could foil the name check by aliasing the Function constructor under a different
|
||
// name on the scope.
|
||
//
|
||
// 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.
|
||
|
||
function ensureSafeMemberName(name, fullExpression) {
|
||
if (name === "constructor") {
|
||
throw $parseMinErr('isecfld',
|
||
'Referencing "constructor" field in Angular expressions is disallowed! 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.document && obj.location && obj.alert && obj.setInterval) {
|
||
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);
|
||
}
|
||
}
|
||
return obj;
|
||
}
|
||
|
||
var OPERATORS = {
|
||
/* 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.lastCh = ':'; // can start regexp
|
||
|
||
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++;
|
||
continue;
|
||
} 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);
|
||
}
|
||
}
|
||
this.lastCh = this.ch;
|
||
}
|
||
return this.tokens;
|
||
},
|
||
|
||
is: function(chars) {
|
||
return chars.indexOf(this.ch) !== -1;
|
||
},
|
||
|
||
was: function(chars) {
|
||
return chars.indexOf(this.lastCh) !== -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,
|
||
literal: true,
|
||
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 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
|
||
};
|
||
|
||
// OPERATORS is our own object so we don't need to use special hasOwnPropertyFn
|
||
if (OPERATORS.hasOwnProperty(ident)) {
|
||
token.fn = OPERATORS[ident];
|
||
token.literal = true;
|
||
token.constant = true;
|
||
} else {
|
||
var getter = getterFn(ident, this.options, this.text);
|
||
token.fn = extend(function(self, locals) {
|
||
return (getter(self, locals));
|
||
}, {
|
||
assign: function(self, value) {
|
||
return setter(self, ident, value, parser.text, parser.options);
|
||
}
|
||
});
|
||
}
|
||
|
||
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];
|
||
if (rep) {
|
||
string += rep;
|
||
} else {
|
||
string += ch;
|
||
}
|
||
}
|
||
escape = false;
|
||
} else if (ch === '\\') {
|
||
escape = true;
|
||
} else if (ch === quote) {
|
||
this.index++;
|
||
this.tokens.push({
|
||
index: start,
|
||
text: rawString,
|
||
string: string,
|
||
literal: true,
|
||
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);
|
||
}
|
||
primary.literal = !!token.literal;
|
||
primary.constant = !!token.constant;
|
||
}
|
||
|
||
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 = [];
|
||
while (true) {
|
||
if ((token = this.expect(':'))) {
|
||
argsFn.push(this.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;
|
||
};
|
||
}
|
||
}
|
||
},
|
||
|
||
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.ternary();
|
||
if ((token = this.expect(':'))) {
|
||
return this.ternaryFn(left, middle, this.ternary());
|
||
} 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(scope, locals, self) {
|
||
return getter(self || object(scope, locals));
|
||
}, {
|
||
assign: function(scope, value, locals) {
|
||
return setter(object(scope, locals), field, value, parser.text, parser.options);
|
||
}
|
||
});
|
||
},
|
||
|
||
objectIndex: function(obj) {
|
||
var parser = this;
|
||
|
||
var indexFn = this.expression();
|
||
this.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], parser.text);
|
||
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;
|
||
}
|
||
});
|
||
},
|
||
|
||
functionCall: function(fn, contextGetter) {
|
||
var argsFn = [];
|
||
if (this.peekToken().text !== ')') {
|
||
do {
|
||
argsFn.push(this.expression());
|
||
} while (this.expect(','));
|
||
}
|
||
this.consume(')');
|
||
|
||
var parser = this;
|
||
|
||
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(context, parser.text);
|
||
ensureSafeObject(fnPtr, parser.text);
|
||
|
||
// 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]);
|
||
|
||
return ensureSafeObject(v, parser.text);
|
||
};
|
||
},
|
||
|
||
// 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, options) {
|
||
//needed?
|
||
options = options || {};
|
||
|
||
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);
|
||
obj[key] = setValue;
|
||
return setValue;
|
||
}
|
||
|
||
var getterFnCache = {};
|
||
|
||
/**
|
||
* 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, options) {
|
||
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 simpleGetterFn1(key0, fullExp) {
|
||
ensureSafeMemberName(key0, fullExp);
|
||
|
||
return function simpleGetterFn1(scope, locals) {
|
||
if (scope == null) return undefined;
|
||
return ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0];
|
||
};
|
||
}
|
||
|
||
function simpleGetterFn2(key0, key1, fullExp) {
|
||
ensureSafeMemberName(key0, fullExp);
|
||
ensureSafeMemberName(key1, fullExp);
|
||
|
||
return function simpleGetterFn2(scope, locals) {
|
||
if (scope == null) return undefined;
|
||
scope = ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0];
|
||
return scope == null ? undefined : scope[key1];
|
||
};
|
||
}
|
||
|
||
function getterFn(path, options, fullExp) {
|
||
// Check whether the cache has this getter already.
|
||
// We can use hasOwnProperty directly on the cache because we ensure,
|
||
// see below, that the cache never stores a path called 'hasOwnProperty'
|
||
if (getterFnCache.hasOwnProperty(path)) {
|
||
return getterFnCache[path];
|
||
}
|
||
|
||
var pathKeys = path.split('.'),
|
||
pathKeysLength = pathKeys.length,
|
||
fn;
|
||
|
||
// When we have only 1 or 2 tokens, use optimized special case closures.
|
||
// http://jsperf.com/angularjs-parse-getter/6
|
||
if (pathKeysLength === 1) {
|
||
fn = simpleGetterFn1(pathKeys[0], fullExp);
|
||
} else if (pathKeysLength === 2) {
|
||
fn = simpleGetterFn2(pathKeys[0], pathKeys[1], fullExp);
|
||
} else if (options.csp) {
|
||
if (pathKeysLength < 6) {
|
||
fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp,
|
||
options);
|
||
} else {
|
||
fn = function(scope, locals) {
|
||
var i = 0, val;
|
||
do {
|
||
val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++],
|
||
pathKeys[i++], fullExp, options)(scope, locals);
|
||
|
||
locals = undefined; // clear after first iteration
|
||
scope = val;
|
||
} while (i < pathKeysLength);
|
||
return val;
|
||
};
|
||
}
|
||
} else {
|
||
var code = 'var p;\n';
|
||
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
|
||
: '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n';
|
||
});
|
||
code += 'return s;';
|
||
|
||
/* jshint -W054 */
|
||
var evaledFnGetter = new Function('s', 'k', code); // s=scope, k=locals
|
||
/* jshint +W054 */
|
||
evaledFnGetter.toString = valueFn(code);
|
||
fn = evaledFnGetter;
|
||
}
|
||
|
||
// Only cache the value if it's not going to mess up the cache object
|
||
// This is more performant that using Object.prototype.hasOwnProperty.call
|
||
if (path !== 'hasOwnProperty') {
|
||
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
|
||
* @function
|
||
*
|
||
* @description
|
||
* `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse}
|
||
* service.
|
||
*/
|
||
function $ParseProvider() {
|
||
var cache = {};
|
||
|
||
var $parseOptions = {
|
||
csp: false
|
||
};
|
||
|
||
|
||
this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) {
|
||
$parseOptions.csp = $sniffer.csp;
|
||
|
||
return function(exp) {
|
||
var parsedExpression,
|
||
oneTime;
|
||
|
||
switch (typeof exp) {
|
||
case 'string':
|
||
|
||
if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
|
||
oneTime = true;
|
||
exp = exp.substring(2);
|
||
}
|
||
|
||
if (cache.hasOwnProperty(exp)) {
|
||
return oneTime ? oneTimeWrapper(cache[exp]) : cache[exp];
|
||
}
|
||
|
||
var lexer = new Lexer($parseOptions);
|
||
var parser = new Parser(lexer, $filter, $parseOptions);
|
||
parsedExpression = parser.parse(exp);
|
||
|
||
if (exp !== 'hasOwnProperty') {
|
||
// Only cache the value if it's not going to mess up the cache object
|
||
// This is more performant that using Object.prototype.hasOwnProperty.call
|
||
cache[exp] = parsedExpression;
|
||
}
|
||
|
||
if (parsedExpression.constant) {
|
||
parsedExpression.$$unwatch = true;
|
||
}
|
||
|
||
return oneTime ? oneTimeWrapper(parsedExpression) : parsedExpression;
|
||
|
||
case 'function':
|
||
return exp;
|
||
|
||
default:
|
||
return noop;
|
||
}
|
||
|
||
function oneTimeWrapper(expression) {
|
||
var stable = false,
|
||
lastValue;
|
||
oneTimeParseFn.literal = expression.literal;
|
||
oneTimeParseFn.constant = expression.constant;
|
||
oneTimeParseFn.assign = expression.assign;
|
||
return oneTimeParseFn;
|
||
|
||
function oneTimeParseFn(self, locals) {
|
||
if (!stable) {
|
||
lastValue = expression(self, locals);
|
||
oneTimeParseFn.$$unwatch = isDefined(lastValue);
|
||
if (oneTimeParseFn.$$unwatch && self && self.$$postDigestQueue) {
|
||
self.$$postDigestQueue.push(function () {
|
||
// create a copy if the value is defined and it is not a $sce value
|
||
if ((stable = isDefined(lastValue)) && !lastValue.$$unwrapTrustedValue) {
|
||
lastValue = copy(lastValue);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
return lastValue;
|
||
}
|
||
}
|
||
};
|
||
}];
|
||
}
|