Files
angular.js/src/ng/parse.js
thorn0 5b2a386b1f docs(jsdoc): remove @kind function for providers
This gets rid of the bizarre "Usage: fooProvider();" which does not really communicate anything useful.

Closes #8809
2014-09-03 08:32:15 -04:00

1124 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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;
}
}];
}