src/ng/parse.js
'use strict';
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Any commits to this file should be reviewed with security in mind. *
* Changes to this file can potentially create security vulnerabilities. *
* An approval from 2 Core members with history of modifying *
* this file is required. *
* *
* Does the change somehow allow for arbitrary javascript to be executed? *
* Or allows for someone to change the prototype of built-in objects? *
* Or gives undesired access to variables likes document or window? *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
var $parseMinErr = minErr('$parse');
var objectValueOf = {}.constructor.prototype.valueOf;
// Sandboxing AngularJS Expressions
// ------------------------------
// AngularJS expressions are no longer sandboxed. So it is now even easier to access arbitrary JS code by
// various means such as obtaining a reference to native JS functions like the Function constructor.
//
// As an example, consider the following AngularJS expression:
//
// {}.toString.constructor('alert("evil JS code")')
//
// It is important to realize that if you create an expression from a string that contains user provided
// content then it is possible that your application contains a security vulnerability to an XSS style attack.
//
// See https://docs.angularjs.org/guide/security
function getStringValue(name) {
// Property names must be strings. This means that non-string objects cannot be used
// as keys in an object. Any non-string object, including a number, is typecasted
// into a string via the toString method.
// -- MDN, https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Property_accessors#Property_names
//
// So, to ensure that we are checking the same `name` that JavaScript would use, we cast it
// to a string. It's not always possible. If `name` is an object and its `toString` method is
// 'broken' (doesn't return a string, isn't a function, etc.), an error will be thrown:
//
// TypeError: Cannot convert object to primitive value
//
// For performance reasons, we don't catch this error here and allow it to propagate up the call
// stack. Note that you'll get the same error in JavaScript if you try to access a property using
// such a 'broken' object as a key.
return name + '';
}
var OPERATORS = createMap();
forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; });
var ESCAPE = {'n':'\n', 'f':'\f', 'r':'\r', 't':'\t', 'v':'\v', '\'':'\'', '"':'"'};
/////////////////////////////////////////
/**
* @constructor
*/
var Lexer = function Lexer(options) {
this.options = options;
};
Lexer.prototype = {
constructor: Lexer,
lex: function(text) {
this.text = text;
this.index = 0;
this.tokens = [];
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
if (ch === '"' || ch === '\'') {
this.readString(ch);
} else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) {
this.readNumber();
} else if (this.isIdentifierStart(this.peekMultichar())) {
this.readIdent();
} else if (this.is(ch, '(){}[].,;:?')) {
this.tokens.push({index: this.index, text: ch});
this.index++;
} else if (this.isWhitespace(ch)) {
this.index++;
} else {
var ch2 = ch + this.peek();
var ch3 = ch2 + this.peek(2);
var op1 = OPERATORS[ch];
var op2 = OPERATORS[ch2];
var op3 = OPERATORS[ch3];
if (op1 || op2 || op3) {
var token = op3 ? ch3 : (op2 ? ch2 : ch);
this.tokens.push({index: this.index, text: token, operator: true});
this.index += token.length;
} else {
this.throwError('Unexpected next character ', this.index, this.index + 1);
}
}
}
return this.tokens;
},
is: function(ch, chars) {
return chars.indexOf(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') && typeof ch === 'string';
},
isWhitespace: function(ch) {
// IE treats non-breaking space as \u00A0
return (ch === ' ' || ch === '\r' || ch === '\t' ||
ch === '\n' || ch === '\v' || ch === '\u00A0');
},
isIdentifierStart: function(ch) {
return this.options.isIdentifierStart ?
this.options.isIdentifierStart(ch, this.codePointAt(ch)) :
this.isValidIdentifierStart(ch);
},
isValidIdentifierStart: function(ch) {
return ('a' <= ch && ch <= 'z' ||
'A' <= ch && ch <= 'Z' ||
'_' === ch || ch === '$');
},
isIdentifierContinue: function(ch) {
return this.options.isIdentifierContinue ?
this.options.isIdentifierContinue(ch, this.codePointAt(ch)) :
this.isValidIdentifierContinue(ch);
},
isValidIdentifierContinue: function(ch, cp) {
return this.isValidIdentifierStart(ch, cp) || this.isNumber(ch);
},
codePointAt: function(ch) {
if (ch.length === 1) return ch.charCodeAt(0);
// eslint-disable-next-line no-bitwise
return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00;
},
peekMultichar: function() {
var ch = this.text.charAt(this.index);
var peek = this.peek();
if (!peek) {
return ch;
}
var cp1 = ch.charCodeAt(0);
var cp2 = peek.charCodeAt(0);
if (cp1 >= 0xD800 && cp1 <= 0xDBFF && cp2 >= 0xDC00 && cp2 <= 0xDFFF) {
return ch + peek;
}
return 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++;
}
this.tokens.push({
index: start,
text: number,
constant: true,
value: Number(number)
});
},
readIdent: function() {
var start = this.index;
this.index += this.peekMultichar().length;
while (this.index < this.text.length) {
var ch = this.peekMultichar();
if (!this.isIdentifierContinue(ch)) {
break;
}
this.index += ch.length;
}
this.tokens.push({
index: start,
text: this.text.slice(start, this.index),
identifier: true
});
},
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,
constant: true,
value: string
});
return;
} else {
string += ch;
}
this.index++;
}
this.throwError('Unterminated quote', start);
}
};
var AST = function AST(lexer, options) {
this.lexer = lexer;
this.options = options;
};
AST.Program = 'Program';
AST.ExpressionStatement = 'ExpressionStatement';
AST.AssignmentExpression = 'AssignmentExpression';
AST.ConditionalExpression = 'ConditionalExpression';
AST.LogicalExpression = 'LogicalExpression';
AST.BinaryExpression = 'BinaryExpression';
AST.UnaryExpression = 'UnaryExpression';
AST.CallExpression = 'CallExpression';
AST.MemberExpression = 'MemberExpression';
AST.Identifier = 'Identifier';
AST.Literal = 'Literal';
AST.ArrayExpression = 'ArrayExpression';
AST.Property = 'Property';
AST.ObjectExpression = 'ObjectExpression';
AST.ThisExpression = 'ThisExpression';
AST.LocalsExpression = 'LocalsExpression';
// Internal use only
AST.NGValueParameter = 'NGValueParameter';
AST.prototype = {
ast: function(text) {
this.text = text;
this.tokens = this.lexer.lex(text);
var value = this.program();
if (this.tokens.length !== 0) {
this.throwError('is an unexpected token', this.tokens[0]);
}
return value;
},
program: function() {
var body = [];
while (true) {
if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']'))
body.push(this.expressionStatement());
if (!this.expect(';')) {
return { type: AST.Program, body: body};
}
}
},
expressionStatement: function() {
return { type: AST.ExpressionStatement, expression: this.filterChain() };
},
filterChain: function() {
var left = this.expression();
while (this.expect('|')) {
left = this.filter(left);
}
return left;
},
expression: function() {
return this.assignment();
},
assignment: function() {
var result = this.ternary();
if (this.expect('=')) {
if (!isAssignable(result)) {
throw $parseMinErr('lval', 'Trying to assign a value to a non l-value');
}
result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='};
}
return result;
},
ternary: function() {
var test = this.logicalOR();
var alternate;
var consequent;
if (this.expect('?')) {
alternate = this.expression();
if (this.consume(':')) {
consequent = this.expression();
return { type: AST.ConditionalExpression, test: test, alternate: alternate, consequent: consequent};
}
}
return test;
},
logicalOR: function() {
var left = this.logicalAND();
while (this.expect('||')) {
left = { type: AST.LogicalExpression, operator: '||', left: left, right: this.logicalAND() };
}
return left;
},
logicalAND: function() {
var left = this.equality();
while (this.expect('&&')) {
left = { type: AST.LogicalExpression, operator: '&&', left: left, right: this.equality()};
}
return left;
},
equality: function() {
var left = this.relational();
var token;
while ((token = this.expect('==','!=','===','!=='))) {
left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.relational() };
}
return left;
},
relational: function() {
var left = this.additive();
var token;
while ((token = this.expect('<', '>', '<=', '>='))) {
left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.additive() };
}
return left;
},
additive: function() {
var left = this.multiplicative();
var token;
while ((token = this.expect('+','-'))) {
left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.multiplicative() };
}
return left;
},
multiplicative: function() {
var left = this.unary();
var token;
while ((token = this.expect('*','/','%'))) {
left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.unary() };
}
return left;
},
unary: function() {
var token;
if ((token = this.expect('+', '-', '!'))) {
return { type: AST.UnaryExpression, operator: token.text, prefix: true, argument: this.unary() };
} else {
return this.primary();
}
},
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 if (this.selfReferential.hasOwnProperty(this.peek().text)) {
primary = copy(this.selfReferential[this.consume().text]);
} else if (this.options.literals.hasOwnProperty(this.peek().text)) {
primary = { type: AST.Literal, value: this.options.literals[this.consume().text]};
} else if (this.peek().identifier) {
primary = this.identifier();
} else if (this.peek().constant) {
primary = this.constant();
} else {
this.throwError('not a primary expression', this.peek());
}
var next;
while ((next = this.expect('(', '[', '.'))) {
if (next.text === '(') {
primary = {type: AST.CallExpression, callee: primary, arguments: this.parseArguments() };
this.consume(')');
} else if (next.text === '[') {
primary = { type: AST.MemberExpression, object: primary, property: this.expression(), computed: true };
this.consume(']');
} else if (next.text === '.') {
primary = { type: AST.MemberExpression, object: primary, property: this.identifier(), computed: false };
} else {
this.throwError('IMPOSSIBLE');
}
}
return primary;
},
filter: function(baseExpression) {
var args = [baseExpression];
var result = {type: AST.CallExpression, callee: this.identifier(), arguments: args, filter: true};
while (this.expect(':')) {
args.push(this.expression());
}
return result;
},
parseArguments: function() {
var args = [];
if (this.peekToken().text !== ')') {
do {
args.push(this.filterChain());
} while (this.expect(','));
}
return args;
},
identifier: function() {
var token = this.consume();
if (!token.identifier) {
this.throwError('is not a valid identifier', token);
}
return { type: AST.Identifier, name: token.text };
},
constant: function() {
// TODO check that it is a constant
return { type: AST.Literal, value: this.consume().value };
},
arrayDeclaration: function() {
var elements = [];
if (this.peekToken().text !== ']') {
do {
if (this.peek(']')) {
// Support trailing commas per ES5.1.
break;
}
elements.push(this.expression());
} while (this.expect(','));
}
this.consume(']');
return { type: AST.ArrayExpression, elements: elements };
},
object: function() {
var properties = [], property;
if (this.peekToken().text !== '}') {
do {
if (this.peek('}')) {
// Support trailing commas per ES5.1.
break;
}
property = {type: AST.Property, kind: 'init'};
if (this.peek().constant) {
property.key = this.constant();
property.computed = false;
this.consume(':');
property.value = this.expression();
} else if (this.peek().identifier) {
property.key = this.identifier();
property.computed = false;
if (this.peek(':')) {
this.consume(':');
property.value = this.expression();
} else {
property.value = property.key;
}
} else if (this.peek('[')) {
this.consume('[');
property.key = this.expression();
this.consume(']');
property.computed = true;
this.consume(':');
property.value = this.expression();
} else {
this.throwError('invalid key', this.peek());
}
properties.push(property);
} while (this.expect(','));
}
this.consume('}');
return {type: AST.ObjectExpression, properties: properties };
},
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));
},
consume: function(e1) {
if (this.tokens.length === 0) {
throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text);
}
var token = this.expect(e1);
if (!token) {
this.throwError('is unexpected, expecting [' + e1 + ']', this.peek());
}
return token;
},
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) {
return this.peekAhead(0, e1, e2, e3, e4);
},
peekAhead: function(i, e1, e2, e3, e4) {
if (this.tokens.length > i) {
var token = this.tokens[i];
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;
},
selfReferential: {
'this': {type: AST.ThisExpression },
'$locals': {type: AST.LocalsExpression }
}
};
function ifDefined(v, d) {
return typeof v !== 'undefined' ? v : d;
}
function plusFn(l, r) {
if (typeof l === 'undefined') return r;
if (typeof r === 'undefined') return l;
return l + r;
}
function isStateless($filter, filterName) {
var fn = $filter(filterName);
return !fn.$stateful;
}
var PURITY_ABSOLUTE = 1;
var PURITY_RELATIVE = 2;
// Detect nodes which could depend on non-shallow state of objects
function isPure(node, parentIsPure) {
switch (node.type) {
// Computed members might invoke a stateful toString()
case AST.MemberExpression:
if (node.computed) {
return false;
}
break;
// Unary always convert to primative
case AST.UnaryExpression:
return PURITY_ABSOLUTE;
// The binary + operator can invoke a stateful toString().
case AST.BinaryExpression:
return node.operator !== '+' ? PURITY_ABSOLUTE : false;
// Functions / filters probably read state from within objects
case AST.CallExpression:
return false;
}
return (undefined === parentIsPure) ? PURITY_RELATIVE : parentIsPure;
}
function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
var allConstants;
var argsToWatch;
var isStatelessFilter;
var astIsPure = ast.isPure = isPure(ast, parentIsPure);
switch (ast.type) {
case AST.Program:
allConstants = true;
forEach(ast.body, function(expr) {
findConstantAndWatchExpressions(expr.expression, $filter, astIsPure);
allConstants = allConstants && expr.expression.constant;
});
ast.constant = allConstants;
break;
case AST.Literal:
ast.constant = true;
ast.toWatch = [];
break;
case AST.UnaryExpression:
findConstantAndWatchExpressions(ast.argument, $filter, astIsPure);
ast.constant = ast.argument.constant;
ast.toWatch = ast.argument.toWatch;
break;
case AST.BinaryExpression:
findConstantAndWatchExpressions(ast.left, $filter, astIsPure);
findConstantAndWatchExpressions(ast.right, $filter, astIsPure);
ast.constant = ast.left.constant && ast.right.constant;
ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch);
break;
case AST.LogicalExpression:
findConstantAndWatchExpressions(ast.left, $filter, astIsPure);
findConstantAndWatchExpressions(ast.right, $filter, astIsPure);
ast.constant = ast.left.constant && ast.right.constant;
ast.toWatch = ast.constant ? [] : [ast];
break;
case AST.ConditionalExpression:
findConstantAndWatchExpressions(ast.test, $filter, astIsPure);
findConstantAndWatchExpressions(ast.alternate, $filter, astIsPure);
findConstantAndWatchExpressions(ast.consequent, $filter, astIsPure);
ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant;
ast.toWatch = ast.constant ? [] : [ast];
break;
case AST.Identifier:
ast.constant = false;
ast.toWatch = [ast];
break;
case AST.MemberExpression:
findConstantAndWatchExpressions(ast.object, $filter, astIsPure);
if (ast.computed) {
findConstantAndWatchExpressions(ast.property, $filter, astIsPure);
}
ast.constant = ast.object.constant && (!ast.computed || ast.property.constant);
ast.toWatch = ast.constant ? [] : [ast];
break;
case AST.CallExpression:
isStatelessFilter = ast.filter ? isStateless($filter, ast.callee.name) : false;
allConstants = isStatelessFilter;
argsToWatch = [];
forEach(ast.arguments, function(expr) {
findConstantAndWatchExpressions(expr, $filter, astIsPure);
allConstants = allConstants && expr.constant;
argsToWatch.push.apply(argsToWatch, expr.toWatch);
});
ast.constant = allConstants;
ast.toWatch = isStatelessFilter ? argsToWatch : [ast];
break;
case AST.AssignmentExpression:
findConstantAndWatchExpressions(ast.left, $filter, astIsPure);
findConstantAndWatchExpressions(ast.right, $filter, astIsPure);
ast.constant = ast.left.constant && ast.right.constant;
ast.toWatch = [ast];
break;
case AST.ArrayExpression:
allConstants = true;
argsToWatch = [];
forEach(ast.elements, function(expr) {
findConstantAndWatchExpressions(expr, $filter, astIsPure);
allConstants = allConstants && expr.constant;
argsToWatch.push.apply(argsToWatch, expr.toWatch);
});
ast.constant = allConstants;
ast.toWatch = argsToWatch;
break;
case AST.ObjectExpression:
allConstants = true;
argsToWatch = [];
forEach(ast.properties, function(property) {
findConstantAndWatchExpressions(property.value, $filter, astIsPure);
allConstants = allConstants && property.value.constant;
argsToWatch.push.apply(argsToWatch, property.value.toWatch);
if (property.computed) {
//`{[key]: value}` implicitly does `key.toString()` which may be non-pure
findConstantAndWatchExpressions(property.key, $filter, /*parentIsPure=*/false);
allConstants = allConstants && property.key.constant;
argsToWatch.push.apply(argsToWatch, property.key.toWatch);
}
});
ast.constant = allConstants;
ast.toWatch = argsToWatch;
break;
case AST.ThisExpression:
ast.constant = false;
ast.toWatch = [];
break;
case AST.LocalsExpression:
ast.constant = false;
ast.toWatch = [];
break;
}
}
function getInputs(body) {
if (body.length !== 1) return;
var lastExpression = body[0].expression;
var candidate = lastExpression.toWatch;
if (candidate.length !== 1) return candidate;
return candidate[0] !== lastExpression ? candidate : undefined;
}
function isAssignable(ast) {
return ast.type === AST.Identifier || ast.type === AST.MemberExpression;
}
function assignableAST(ast) {
if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) {
return {type: AST.AssignmentExpression, left: ast.body[0].expression, right: {type: AST.NGValueParameter}, operator: '='};
}
}
function isLiteral(ast) {
return ast.body.length === 0 ||
ast.body.length === 1 && (
ast.body[0].expression.type === AST.Literal ||
ast.body[0].expression.type === AST.ArrayExpression ||
ast.body[0].expression.type === AST.ObjectExpression);
}
function isConstant(ast) {
return ast.constant;
}
function ASTCompiler($filter) {
this.$filter = $filter;
}
ASTCompiler.prototype = {
compile: function(ast) {
var self = this;
this.state = {
nextId: 0,
filters: {},
fn: {vars: [], body: [], own: {}},
assign: {vars: [], body: [], own: {}},
inputs: []
};
findConstantAndWatchExpressions(ast, self.$filter);
var extra = '';
var assignable;
this.stage = 'assign';
if ((assignable = assignableAST(ast))) {
this.state.computing = 'assign';
var result = this.nextId();
this.recurse(assignable, result);
this.return_(result);
extra = 'fn.assign=' + this.generateFunction('assign', 's,v,l');
}
var toWatch = getInputs(ast.body);
self.stage = 'inputs';
forEach(toWatch, function(watch, key) {
var fnKey = 'fn' + key;
self.state[fnKey] = {vars: [], body: [], own: {}};
self.state.computing = fnKey;
var intoId = self.nextId();
self.recurse(watch, intoId);
self.return_(intoId);
self.state.inputs.push({name: fnKey, isPure: watch.isPure});
watch.watchId = key;
});
this.state.computing = 'fn';
this.stage = 'main';
this.recurse(ast);
var fnString =
// The build and minification steps remove the string "use strict" from the code, but this is done using a regex.
// This is a workaround for this until we do a better job at only removing the prefix only when we should.
'"' + this.USE + ' ' + this.STRICT + '";\n' +
this.filterPrefix() +
'var fn=' + this.generateFunction('fn', 's,l,a,i') +
extra +
this.watchFns() +
'return fn;';
// eslint-disable-next-line no-new-func
var fn = (new Function('$filter',
'getStringValue',
'ifDefined',
'plus',
fnString))(
this.$filter,
getStringValue,
ifDefined,
plusFn);
this.state = this.stage = undefined;
return fn;
},
USE: 'use',
STRICT: 'strict',
watchFns: function() {
var result = [];
var inputs = this.state.inputs;
var self = this;
forEach(inputs, function(input) {
result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's'));
if (input.isPure) {
result.push(input.name, '.isPure=' + JSON.stringify(input.isPure) + ';');
}
});
if (inputs.length) {
result.push('fn.inputs=[' + inputs.map(function(i) { return i.name; }).join(',') + '];');
}
return result.join('');
},
generateFunction: function(name, params) {
return 'function(' + params + '){' +
this.varsPrefix(name) +
this.body(name) +
'};';
},
filterPrefix: function() {
var parts = [];
var self = this;
forEach(this.state.filters, function(id, filter) {
parts.push(id + '=$filter(' + self.escape(filter) + ')');
});
if (parts.length) return 'var ' + parts.join(',') + ';';
return '';
},
varsPrefix: function(section) {
return this.state[section].vars.length ? 'var ' + this.state[section].vars.join(',') + ';' : '';
},
body: function(section) {
return this.state[section].body.join('');
},
recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
var left, right, self = this, args, expression, computed;
recursionFn = recursionFn || noop;
if (!skipWatchIdCheck && isDefined(ast.watchId)) {
intoId = intoId || this.nextId();
this.if_('i',
this.lazyAssign(intoId, this.computedMember('i', ast.watchId)),
this.lazyRecurse(ast, intoId, nameId, recursionFn, create, true)
);
return;
}
switch (ast.type) {
case AST.Program:
forEach(ast.body, function(expression, pos) {
self.recurse(expression.expression, undefined, undefined, function(expr) { right = expr; });
if (pos !== ast.body.length - 1) {
self.current().body.push(right, ';');
} else {
self.return_(right);
}
});
break;
case AST.Literal:
expression = this.escape(ast.value);
this.assign(intoId, expression);
recursionFn(intoId || expression);
break;
case AST.UnaryExpression:
this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; });
expression = ast.operator + '(' + this.ifDefined(right, 0) + ')';
this.assign(intoId, expression);
recursionFn(expression);
break;
case AST.BinaryExpression:
this.recurse(ast.left, undefined, undefined, function(expr) { left = expr; });
this.recurse(ast.right, undefined, undefined, function(expr) { right = expr; });
if (ast.operator === '+') {
expression = this.plus(left, right);
} else if (ast.operator === '-') {
expression = this.ifDefined(left, 0) + ast.operator + this.ifDefined(right, 0);
} else {
expression = '(' + left + ')' + ast.operator + '(' + right + ')';
}
this.assign(intoId, expression);
recursionFn(expression);
break;
case AST.LogicalExpression:
intoId = intoId || this.nextId();
self.recurse(ast.left, intoId);
self.if_(ast.operator === '&&' ? intoId : self.not(intoId), self.lazyRecurse(ast.right, intoId));
recursionFn(intoId);
break;
case AST.ConditionalExpression:
intoId = intoId || this.nextId();
self.recurse(ast.test, intoId);
self.if_(intoId, self.lazyRecurse(ast.alternate, intoId), self.lazyRecurse(ast.consequent, intoId));
recursionFn(intoId);
break;
case AST.Identifier:
intoId = intoId || this.nextId();
if (nameId) {
nameId.context = self.stage === 'inputs' ? 's' : this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name) + '?l:s');
nameId.computed = false;
nameId.name = ast.name;
}
self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)),
function() {
self.if_(self.stage === 'inputs' || 's', function() {
if (create && create !== 1) {
self.if_(
self.isNull(self.nonComputedMember('s', ast.name)),
self.lazyAssign(self.nonComputedMember('s', ast.name), '{}'));
}
self.assign(intoId, self.nonComputedMember('s', ast.name));
});
}, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name))
);
recursionFn(intoId);
break;
case AST.MemberExpression:
left = nameId && (nameId.context = this.nextId()) || this.nextId();
intoId = intoId || this.nextId();
self.recurse(ast.object, left, undefined, function() {
self.if_(self.notNull(left), function() {
if (ast.computed) {
right = self.nextId();
self.recurse(ast.property, right);
self.getStringValue(right);
if (create && create !== 1) {
self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}'));
}
expression = self.computedMember(left, right);
self.assign(intoId, expression);
if (nameId) {
nameId.computed = true;
nameId.name = right;
}
} else {
if (create && create !== 1) {
self.if_(self.isNull(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}'));
}
expression = self.nonComputedMember(left, ast.property.name);
self.assign(intoId, expression);
if (nameId) {
nameId.computed = false;
nameId.name = ast.property.name;
}
}
}, function() {
self.assign(intoId, 'undefined');
});
recursionFn(intoId);
}, !!create);
break;
case AST.CallExpression:
intoId = intoId || this.nextId();
if (ast.filter) {
right = self.filter(ast.callee.name);
args = [];
forEach(ast.arguments, function(expr) {
var argument = self.nextId();
self.recurse(expr, argument);
args.push(argument);
});
expression = right + '(' + args.join(',') + ')';
self.assign(intoId, expression);
recursionFn(intoId);
} else {
right = self.nextId();
left = {};
args = [];
self.recurse(ast.callee, right, left, function() {
self.if_(self.notNull(right), function() {
forEach(ast.arguments, function(expr) {
self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) {
args.push(argument);
});
});
if (left.name) {
expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')';
} else {
expression = right + '(' + args.join(',') + ')';
}
self.assign(intoId, expression);
}, function() {
self.assign(intoId, 'undefined');
});
recursionFn(intoId);
});
}
break;
case AST.AssignmentExpression:
right = this.nextId();
left = {};
this.recurse(ast.left, undefined, left, function() {
self.if_(self.notNull(left.context), function() {
self.recurse(ast.right, right);
expression = self.member(left.context, left.name, left.computed) + ast.operator + right;
self.assign(intoId, expression);
recursionFn(intoId || expression);
});
}, 1);
break;
case AST.ArrayExpression:
args = [];
forEach(ast.elements, function(expr) {
self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) {
args.push(argument);
});
});
expression = '[' + args.join(',') + ']';
this.assign(intoId, expression);
recursionFn(intoId || expression);
break;
case AST.ObjectExpression:
args = [];
computed = false;
forEach(ast.properties, function(property) {
if (property.computed) {
computed = true;
}
});
if (computed) {
intoId = intoId || this.nextId();
this.assign(intoId, '{}');
forEach(ast.properties, function(property) {
if (property.computed) {
left = self.nextId();
self.recurse(property.key, left);
} else {
left = property.key.type === AST.Identifier ?
property.key.name :
('' + property.key.value);
}
right = self.nextId();
self.recurse(property.value, right);
self.assign(self.member(intoId, left, property.computed), right);
});
} else {
forEach(ast.properties, function(property) {
self.recurse(property.value, ast.constant ? undefined : self.nextId(), undefined, function(expr) {
args.push(self.escape(
property.key.type === AST.Identifier ? property.key.name :
('' + property.key.value)) +
':' + expr);
});
});
expression = '{' + args.join(',') + '}';
this.assign(intoId, expression);
}
recursionFn(intoId || expression);
break;
case AST.ThisExpression:
this.assign(intoId, 's');
recursionFn(intoId || 's');
break;
case AST.LocalsExpression:
this.assign(intoId, 'l');
recursionFn(intoId || 'l');
break;
case AST.NGValueParameter:
this.assign(intoId, 'v');
recursionFn(intoId || 'v');
break;
}
},
getHasOwnProperty: function(element, property) {
var key = element + '.' + property;
var own = this.current().own;
if (!own.hasOwnProperty(key)) {
own[key] = this.nextId(false, element + '&&(' + this.escape(property) + ' in ' + element + ')');
}
return own[key];
},
assign: function(id, value) {
if (!id) return;
this.current().body.push(id, '=', value, ';');
return id;
},
filter: function(filterName) {
if (!this.state.filters.hasOwnProperty(filterName)) {
this.state.filters[filterName] = this.nextId(true);
}
return this.state.filters[filterName];
},
ifDefined: function(id, defaultValue) {
return 'ifDefined(' + id + ',' + this.escape(defaultValue) + ')';
},
plus: function(left, right) {
return 'plus(' + left + ',' + right + ')';
},
return_: function(id) {
this.current().body.push('return ', id, ';');
},
if_: function(test, alternate, consequent) {
if (test === true) {
alternate();
} else {
var body = this.current().body;
body.push('if(', test, '){');
alternate();
body.push('}');
if (consequent) {
body.push('else{');
consequent();
body.push('}');
}
}
},
not: function(expression) {
return '!(' + expression + ')';
},
isNull: function(expression) {
return expression + '==null';
},
notNull: function(expression) {
return expression + '!=null';
},
nonComputedMember: function(left, right) {
var SAFE_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/;
var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g;
if (SAFE_IDENTIFIER.test(right)) {
return left + '.' + right;
} else {
return left + '["' + right.replace(UNSAFE_CHARACTERS, this.stringEscapeFn) + '"]';
}
},
computedMember: function(left, right) {
return left + '[' + right + ']';
},
member: function(left, right, computed) {
if (computed) return this.computedMember(left, right);
return this.nonComputedMember(left, right);
},
getStringValue: function(item) {
this.assign(item, 'getStringValue(' + item + ')');
},
lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
var self = this;
return function() {
self.recurse(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck);
};
},
lazyAssign: function(id, value) {
var self = this;
return function() {
self.assign(id, value);
};
},
stringEscapeRegex: /[^ a-zA-Z0-9]/g,
stringEscapeFn: function(c) {
return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
},
escape: function(value) {
if (isString(value)) return '\'' + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + '\'';
if (isNumber(value)) return value.toString();
if (value === true) return 'true';
if (value === false) return 'false';
if (value === null) return 'null';
if (typeof value === 'undefined') return 'undefined';
throw $parseMinErr('esc', 'IMPOSSIBLE');
},
nextId: function(skip, init) {
var id = 'v' + (this.state.nextId++);
if (!skip) {
this.current().vars.push(id + (init ? '=' + init : ''));
}
return id;
},
current: function() {
return this.state[this.state.computing];
}
};
function ASTInterpreter($filter) {
this.$filter = $filter;
}
ASTInterpreter.prototype = {
compile: function(ast) {
var self = this;
findConstantAndWatchExpressions(ast, self.$filter);
var assignable;
var assign;
if ((assignable = assignableAST(ast))) {
assign = this.recurse(assignable);
}
var toWatch = getInputs(ast.body);
var inputs;
if (toWatch) {
inputs = [];
forEach(toWatch, function(watch, key) {
var input = self.recurse(watch);
input.isPure = watch.isPure;
watch.input = input;
inputs.push(input);
watch.watchId = key;
});
}
var expressions = [];
forEach(ast.body, function(expression) {
expressions.push(self.recurse(expression.expression));
});
var fn = ast.body.length === 0 ? noop :
ast.body.length === 1 ? expressions[0] :
function(scope, locals) {
var lastValue;
forEach(expressions, function(exp) {
lastValue = exp(scope, locals);
});
return lastValue;
};
if (assign) {
fn.assign = function(scope, value, locals) {
return assign(scope, locals, value);
};
}
if (inputs) {
fn.inputs = inputs;
}
return fn;
},
recurse: function(ast, context, create) {
var left, right, self = this, args;
if (ast.input) {
return this.inputs(ast.input, ast.watchId);
}
switch (ast.type) {
case AST.Literal:
return this.value(ast.value, context);
case AST.UnaryExpression:
right = this.recurse(ast.argument);
return this['unary' + ast.operator](right, context);
case AST.BinaryExpression:
left = this.recurse(ast.left);
right = this.recurse(ast.right);
return this['binary' + ast.operator](left, right, context);
case AST.LogicalExpression:
left = this.recurse(ast.left);
right = this.recurse(ast.right);
return this['binary' + ast.operator](left, right, context);
case AST.ConditionalExpression:
return this['ternary?:'](
this.recurse(ast.test),
this.recurse(ast.alternate),
this.recurse(ast.consequent),
context
);
case AST.Identifier:
return self.identifier(ast.name, context, create);
case AST.MemberExpression:
left = this.recurse(ast.object, false, !!create);
if (!ast.computed) {
right = ast.property.name;
}
if (ast.computed) right = this.recurse(ast.property);
return ast.computed ?
this.computedMember(left, right, context, create) :
this.nonComputedMember(left, right, context, create);
case AST.CallExpression:
args = [];
forEach(ast.arguments, function(expr) {
args.push(self.recurse(expr));
});
if (ast.filter) right = this.$filter(ast.callee.name);
if (!ast.filter) right = this.recurse(ast.callee, true);
return ast.filter ?
function(scope, locals, assign, inputs) {
var values = [];
for (var i = 0; i < args.length; ++i) {
values.push(args[i](scope, locals, assign, inputs));
}
var value = right.apply(undefined, values, inputs);
return context ? {context: undefined, name: undefined, value: value} : value;
} :
function(scope, locals, assign, inputs) {
var rhs = right(scope, locals, assign, inputs);
var value;
if (rhs.value != null) {
var values = [];
for (var i = 0; i < args.length; ++i) {
values.push(args[i](scope, locals, assign, inputs));
}
value = rhs.value.apply(rhs.context, values);
}
return context ? {value: value} : value;
};
case AST.AssignmentExpression:
left = this.recurse(ast.left, true, 1);
right = this.recurse(ast.right);
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
var rhs = right(scope, locals, assign, inputs);
lhs.context[lhs.name] = rhs;
return context ? {value: rhs} : rhs;
};
case AST.ArrayExpression:
args = [];
forEach(ast.elements, function(expr) {
args.push(self.recurse(expr));
});
return function(scope, locals, assign, inputs) {
var value = [];
for (var i = 0; i < args.length; ++i) {
value.push(args[i](scope, locals, assign, inputs));
}
return context ? {value: value} : value;
};
case AST.ObjectExpression:
args = [];
forEach(ast.properties, function(property) {
if (property.computed) {
args.push({key: self.recurse(property.key),
computed: true,
value: self.recurse(property.value)
});
} else {
args.push({key: property.key.type === AST.Identifier ?
property.key.name :
('' + property.key.value),
computed: false,
value: self.recurse(property.value)
});
}
});
return function(scope, locals, assign, inputs) {
var value = {};
for (var i = 0; i < args.length; ++i) {
if (args[i].computed) {
value[args[i].key(scope, locals, assign, inputs)] = args[i].value(scope, locals, assign, inputs);
} else {
value[args[i].key] = args[i].value(scope, locals, assign, inputs);
}
}
return context ? {value: value} : value;
};
case AST.ThisExpression:
return function(scope) {
return context ? {value: scope} : scope;
};
case AST.LocalsExpression:
return function(scope, locals) {
return context ? {value: locals} : locals;
};
case AST.NGValueParameter:
return function(scope, locals, assign) {
return context ? {value: assign} : assign;
};
}
},
'unary+': function(argument, context) {
return function(scope, locals, assign, inputs) {
var arg = argument(scope, locals, assign, inputs);
if (isDefined(arg)) {
arg = +arg;
} else {
arg = 0;
}
return context ? {value: arg} : arg;
};
},
'unary-': function(argument, context) {
return function(scope, locals, assign, inputs) {
var arg = argument(scope, locals, assign, inputs);
if (isDefined(arg)) {
arg = -arg;
} else {
arg = -0;
}
return context ? {value: arg} : arg;
};
},
'unary!': function(argument, context) {
return function(scope, locals, assign, inputs) {
var arg = !argument(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary+': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
var rhs = right(scope, locals, assign, inputs);
var arg = plusFn(lhs, rhs);
return context ? {value: arg} : arg;
};
},
'binary-': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
var rhs = right(scope, locals, assign, inputs);
var arg = (isDefined(lhs) ? lhs : 0) - (isDefined(rhs) ? rhs : 0);
return context ? {value: arg} : arg;
};
},
'binary*': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var arg = left(scope, locals, assign, inputs) * right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary/': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var arg = left(scope, locals, assign, inputs) / right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary%': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var arg = left(scope, locals, assign, inputs) % right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary===': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var arg = left(scope, locals, assign, inputs) === right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary!==': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var arg = left(scope, locals, assign, inputs) !== right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary==': function(left, right, context) {
return function(scope, locals, assign, inputs) {
// eslint-disable-next-line eqeqeq
var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary!=': function(left, right, context) {
return function(scope, locals, assign, inputs) {
// eslint-disable-next-line eqeqeq
var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary<': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var arg = left(scope, locals, assign, inputs) < right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary>': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var arg = left(scope, locals, assign, inputs) > right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary<=': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var arg = left(scope, locals, assign, inputs) <= right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary>=': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var arg = left(scope, locals, assign, inputs) >= right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary&&': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var arg = left(scope, locals, assign, inputs) && right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary||': function(left, right, context) {
return function(scope, locals, assign, inputs) {
var arg = left(scope, locals, assign, inputs) || right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'ternary?:': function(test, alternate, consequent, context) {
return function(scope, locals, assign, inputs) {
var arg = test(scope, locals, assign, inputs) ? alternate(scope, locals, assign, inputs) : consequent(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
value: function(value, context) {
return function() { return context ? {context: undefined, name: undefined, value: value} : value; };
},
identifier: function(name, context, create) {
return function(scope, locals, assign, inputs) {
var base = locals && (name in locals) ? locals : scope;
if (create && create !== 1 && base && base[name] == null) {
base[name] = {};
}
var value = base ? base[name] : undefined;
if (context) {
return {context: base, name: name, value: value};
} else {
return value;
}
};
},
computedMember: function(left, right, context, create) {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
var rhs;
var value;
if (lhs != null) {
rhs = right(scope, locals, assign, inputs);
rhs = getStringValue(rhs);
if (create && create !== 1) {
if (lhs && !(lhs[rhs])) {
lhs[rhs] = {};
}
}
value = lhs[rhs];
}
if (context) {
return {context: lhs, name: rhs, value: value};
} else {
return value;
}
};
},
nonComputedMember: function(left, right, context, create) {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
if (create && create !== 1) {
if (lhs && lhs[right] == null) {
lhs[right] = {};
}
}
var value = lhs != null ? lhs[right] : undefined;
if (context) {
return {context: lhs, name: right, value: value};
} else {
return value;
}
};
},
inputs: function(input, watchId) {
return function(scope, value, locals, inputs) {
if (inputs) return inputs[watchId];
return input(scope, value, locals);
};
}
};
/**
* @constructor
*/
function Parser(lexer, $filter, options) {
this.ast = new AST(lexer, options);
this.astCompiler = options.csp ? new ASTInterpreter($filter) :
new ASTCompiler($filter);
}
Parser.prototype = {
constructor: Parser,
parse: function(text) {
var ast = this.getAst(text);
var fn = this.astCompiler.compile(ast.ast);
fn.literal = isLiteral(ast.ast);
fn.constant = isConstant(ast.ast);
fn.oneTime = ast.oneTime;
return fn;
},
getAst: function(exp) {
var oneTime = false;
exp = exp.trim();
if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
oneTime = true;
exp = exp.substring(2);
}
return {
ast: this.ast.ast(exp),
oneTime: oneTime
};
}
};
function getValueOf(value) {
return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value);
}
///////////////////////////////////
/**
* @ngdoc service
* @name $parse
* @kind function
*
* @description
*
* Converts AngularJS {@link guide/expression expression} into a function.
*
* ```js
* var getter = $parse('user.name');
* var setter = getter.assign;
* var context = {user:{name:'AngularJS'}};
* var locals = {user:{name:'local'}};
*
* expect(getter(context)).toEqual('AngularJS');
* 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
* @this
*
* @description
* `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse}
* service.
*/
function $ParseProvider() {
var cache = createMap();
var literals = {
'true': true,
'false': false,
'null': null,
'undefined': undefined
};
var identStart, identContinue;
/**
* @ngdoc method
* @name $parseProvider#addLiteral
* @description
*
* Configure $parse service to add literal values that will be present as literal at expressions.
*
* @param {string} literalName Token for the literal value. The literal name value must be a valid literal name.
* @param {*} literalValue Value for this literal. All literal values must be primitives or `undefined`.
*
**/
this.addLiteral = function(literalName, literalValue) {
literals[literalName] = literalValue;
};
/**
* @ngdoc method
* @name $parseProvider#setIdentifierFns
*
* @description
*
* Allows defining the set of characters that are allowed in AngularJS expressions. The function
* `identifierStart` will get called to know if a given character is a valid character to be the
* first character for an identifier. The function `identifierContinue` will get called to know if
* a given character is a valid character to be a follow-up identifier character. The functions
* `identifierStart` and `identifierContinue` will receive as arguments the single character to be
* identifier and the character code point. These arguments will be `string` and `numeric`. Keep in
* mind that the `string` parameter can be two characters long depending on the character
* representation. It is expected for the function to return `true` or `false`, whether that
* character is allowed or not.
*
* Since this function will be called extensively, keep the implementation of these functions fast,
* as the performance of these functions have a direct impact on the expressions parsing speed.
*
* @param {function=} identifierStart The function that will decide whether the given character is
* a valid identifier start character.
* @param {function=} identifierContinue The function that will decide whether the given character is
* a valid identifier continue character.
*/
this.setIdentifierFns = function(identifierStart, identifierContinue) {
identStart = identifierStart;
identContinue = identifierContinue;
return this;
};
this.$get = ['$filter', function($filter) {
var noUnsafeEval = csp().noUnsafeEval;
var $parseOptions = {
csp: noUnsafeEval,
literals: copy(literals),
isIdentifierStart: isFunction(identStart) && identStart,
isIdentifierContinue: isFunction(identContinue) && identContinue
};
$parse.$$getAst = $$getAst;
return $parse;
function $parse(exp, interceptorFn) {
var parsedExpression, cacheKey;
switch (typeof exp) {
case 'string':
exp = exp.trim();
cacheKey = exp;
parsedExpression = cache[cacheKey];
if (!parsedExpression) {
var lexer = new Lexer($parseOptions);
var parser = new Parser(lexer, $filter, $parseOptions);
parsedExpression = parser.parse(exp);
cache[cacheKey] = addWatchDelegate(parsedExpression);
}
return addInterceptor(parsedExpression, interceptorFn);
case 'function':
return addInterceptor(exp, interceptorFn);
default:
return addInterceptor(noop, interceptorFn);
}
}
function $$getAst(exp) {
var lexer = new Lexer($parseOptions);
var parser = new Parser(lexer, $filter, $parseOptions);
return parser.getAst(exp).ast;
}
function expressionInputDirtyCheck(newValue, oldValueOfValue, compareObjectIdentity) {
if (newValue == null || oldValueOfValue == null) { // null/undefined
return newValue === oldValueOfValue;
}
if (typeof newValue === 'object') {
// attempt to convert the value to a primitive type
// TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
// be cheaply dirty-checked
newValue = getValueOf(newValue);
if (typeof newValue === 'object' && !compareObjectIdentity) {
// objects/arrays are not supported - deep-watching them would be too expensive
return false;
}
// fall-through to the primitive equality check
}
//Primitive or NaN
// eslint-disable-next-line no-self-compare
return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue);
}
function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) {
var inputExpressions = parsedExpression.inputs;
var lastResult;
if (inputExpressions.length === 1) {
var oldInputValueOf = expressionInputDirtyCheck; // init to something unique so that equals check fails
inputExpressions = inputExpressions[0];
return scope.$watch(function expressionInputWatch(scope) {
var newInputValue = inputExpressions(scope);
if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, inputExpressions.isPure)) {
lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]);
oldInputValueOf = newInputValue && getValueOf(newInputValue);
}
return lastResult;
}, listener, objectEquality, prettyPrintExpression);
}
var oldInputValueOfValues = [];
var oldInputValues = [];
for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
oldInputValueOfValues[i] = expressionInputDirtyCheck; // init to something unique so that equals check fails
oldInputValues[i] = null;
}
return scope.$watch(function expressionInputsWatch(scope) {
var changed = false;
for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
var newInputValue = inputExpressions[i](scope);
if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], inputExpressions[i].isPure))) {
oldInputValues[i] = newInputValue;
oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue);
}
}
if (changed) {
lastResult = parsedExpression(scope, undefined, undefined, oldInputValues);
}
return lastResult;
}, listener, objectEquality, prettyPrintExpression);
}
function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) {
var isDone = parsedExpression.literal ? isAllDefined : isDefined;
var unwatch, lastValue;
var exp = parsedExpression.$$intercepted || parsedExpression;
var post = parsedExpression.$$interceptor || identity;
var useInputs = parsedExpression.inputs && !exp.inputs;
// Propagate the literal/inputs/constant attributes
// ... but not oneTime since we are handling it
oneTimeWatch.literal = parsedExpression.literal;
oneTimeWatch.constant = parsedExpression.constant;
oneTimeWatch.inputs = parsedExpression.inputs;
// Allow other delegates to run on this wrapped expression
addWatchDelegate(oneTimeWatch);
unwatch = scope.$watch(oneTimeWatch, listener, objectEquality, prettyPrintExpression);
return unwatch;
function unwatchIfDone() {
if (isDone(lastValue)) {
unwatch();
}
}
function oneTimeWatch(scope, locals, assign, inputs) {
lastValue = useInputs && inputs ? inputs[0] : exp(scope, locals, assign, inputs);
if (isDone(lastValue)) {
scope.$$postDigest(unwatchIfDone);
}
return post(lastValue);
}
}
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 = scope.$watch(function constantWatch(scope) {
unwatch();
return parsedExpression(scope);
}, listener, objectEquality);
return unwatch;
}
function addWatchDelegate(parsedExpression) {
if (parsedExpression.constant) {
parsedExpression.$$watchDelegate = constantWatchDelegate;
} else if (parsedExpression.oneTime) {
parsedExpression.$$watchDelegate = oneTimeWatchDelegate;
} else if (parsedExpression.inputs) {
parsedExpression.$$watchDelegate = inputsWatchDelegate;
}
return parsedExpression;
}
function chainInterceptors(first, second) {
function chainedInterceptor(value) {
return second(first(value));
}
chainedInterceptor.$stateful = first.$stateful || second.$stateful;
chainedInterceptor.$$pure = first.$$pure && second.$$pure;
return chainedInterceptor;
}
function addInterceptor(parsedExpression, interceptorFn) {
if (!interceptorFn) return parsedExpression;
// Extract any existing interceptors out of the parsedExpression
// to ensure the original parsedExpression is always the $$intercepted
if (parsedExpression.$$interceptor) {
interceptorFn = chainInterceptors(parsedExpression.$$interceptor, interceptorFn);
parsedExpression = parsedExpression.$$intercepted;
}
var useInputs = false;
var fn = function interceptedExpression(scope, locals, assign, inputs) {
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
return interceptorFn(value);
};
// Maintain references to the interceptor/intercepted
fn.$$intercepted = parsedExpression;
fn.$$interceptor = interceptorFn;
// Propagate the literal/oneTime/constant attributes
fn.literal = parsedExpression.literal;
fn.oneTime = parsedExpression.oneTime;
fn.constant = parsedExpression.constant;
// Treat the interceptor like filters.
// If it is not $stateful then only watch its inputs.
// If the expression itself has no inputs then use the full expression as an input.
if (!interceptorFn.$stateful) {
useInputs = !parsedExpression.inputs;
fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression];
if (!interceptorFn.$$pure) {
fn.inputs = fn.inputs.map(function(e) {
// Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a
// non-pure interceptor function.
if (e.isPure === PURITY_RELATIVE) {
return function depurifier(s) { return e(s); };
}
return e;
});
}
}
return addWatchDelegate(fn);
}
}];
}