mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-05-03 12:44:43 +00:00
JSON parser is now strict (ie, expressions are not allowed for security)
Close #57
This commit is contained in:
parent
352dbfa38f
commit
9e9bdbdc40
11 changed files with 381 additions and 342 deletions
|
|
@ -99,6 +99,7 @@ function inherit(parent, extra) {
|
||||||
|
|
||||||
function noop() {}
|
function noop() {}
|
||||||
function identity($) {return $;}
|
function identity($) {return $;}
|
||||||
|
function valueFn(value) {return function(){ return value; };}
|
||||||
function extensionMap(angular, name, transform) {
|
function extensionMap(angular, name, transform) {
|
||||||
var extPoint;
|
var extPoint;
|
||||||
return angular[name] || (extPoint = angular[name] = function (name, fn, prop){
|
return angular[name] || (extPoint = angular[name] = function (name, fn, prop){
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ function toJson(obj, pretty){
|
||||||
function fromJson(json) {
|
function fromJson(json) {
|
||||||
if (!json) return json;
|
if (!json) return json;
|
||||||
try {
|
try {
|
||||||
var parser = new Parser(json, true);
|
var p = parser(json, true);
|
||||||
var expression = parser.primary();
|
var expression = p.primary();
|
||||||
parser.assertAllConsumed();
|
p.assertAllConsumed();
|
||||||
return expression();
|
return expression();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error("fromJson error: ", json, e);
|
error("fromJson error: ", json, e);
|
||||||
|
|
|
||||||
464
src/Parser.js
464
src/Parser.js
|
|
@ -25,52 +25,37 @@ var OPERATORS = {
|
||||||
};
|
};
|
||||||
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
|
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
|
||||||
|
|
||||||
function lex(text, parseStrings){
|
function lex(text, parseStringsForObjects){
|
||||||
var dateParseLength = parseStrings ? 20 : -1,
|
var dateParseLength = parseStringsForObjects ? 20 : -1,
|
||||||
tokens = [],
|
tokens = [],
|
||||||
|
token,
|
||||||
index = 0,
|
index = 0,
|
||||||
canStartRegExp = true;
|
json = [],
|
||||||
|
ch,
|
||||||
|
lastCh = ','; // can start regexp
|
||||||
|
|
||||||
while (index < text.length) {
|
while (index < text.length) {
|
||||||
var ch = text.charAt(index);
|
ch = text.charAt(index);
|
||||||
if (ch == '"' || ch == "'") {
|
if (is('"\'')) {
|
||||||
readString(ch);
|
readString(ch);
|
||||||
canStartRegExp = true;
|
} else if (isNumber(ch) || is('.') && isNumber(peek())) {
|
||||||
} else if (ch == '(' || ch == '[') {
|
|
||||||
tokens.push({index:index, text:ch});
|
|
||||||
index++;
|
|
||||||
} else if (ch == '{' ) {
|
|
||||||
var peekCh = peek();
|
|
||||||
if (peekCh == ':' || peekCh == '(') {
|
|
||||||
tokens.push({index:index, text:ch + peekCh});
|
|
||||||
index++;
|
|
||||||
} else {
|
|
||||||
tokens.push({index:index, text:ch});
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
canStartRegExp = true;
|
|
||||||
} else if (ch == ')' || ch == ']' || ch == '}' ) {
|
|
||||||
tokens.push({index:index, text:ch});
|
|
||||||
index++;
|
|
||||||
canStartRegExp = false;
|
|
||||||
} else if (ch == '.' && isNumber(peek())) {
|
|
||||||
readNumber();
|
readNumber();
|
||||||
canStartRegExp = false;
|
} else if ( was('({[:,;') && is('/') ) {
|
||||||
} else if ( ch == ':' || ch == '.' || ch == ',' || ch == ';') {
|
|
||||||
tokens.push({index:index, text:ch});
|
|
||||||
index++;
|
|
||||||
canStartRegExp = true;
|
|
||||||
} else if ( canStartRegExp && ch == '/' ) {
|
|
||||||
readRegexp();
|
readRegexp();
|
||||||
canStartRegExp = false;
|
|
||||||
} else if ( isNumber(ch) ) {
|
|
||||||
readNumber();
|
|
||||||
canStartRegExp = false;
|
|
||||||
} else if (isIdent(ch)) {
|
} else if (isIdent(ch)) {
|
||||||
readIdent();
|
readIdent();
|
||||||
canStartRegExp = false;
|
if (was('{,') && json[0]=='{' &&
|
||||||
|
(token=tokens[tokens.length-1])) {
|
||||||
|
token.json = token.text.indexOf('.') == -1;
|
||||||
|
}
|
||||||
|
} else if (is('(){}[].,;:')) {
|
||||||
|
tokens.push({index:index, text:ch, json:is('{}[]:,')});
|
||||||
|
if (is('{[')) json.unshift(ch);
|
||||||
|
if (is('}]')) json.shift();
|
||||||
|
index++;
|
||||||
} else if (isWhitespace(ch)) {
|
} else if (isWhitespace(ch)) {
|
||||||
index++;
|
index++;
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
var ch2 = ch + peek(),
|
var ch2 = ch + peek(),
|
||||||
fn = OPERATORS[ch],
|
fn = OPERATORS[ch],
|
||||||
|
|
@ -87,11 +72,19 @@ function lex(text, parseStrings){
|
||||||
"] in expression '" + text +
|
"] in expression '" + text +
|
||||||
"' at column '" + (index+1) + "'.";
|
"' at column '" + (index+1) + "'.";
|
||||||
}
|
}
|
||||||
canStartRegExp = true;
|
|
||||||
}
|
}
|
||||||
|
lastCh = ch;
|
||||||
}
|
}
|
||||||
return tokens;
|
return tokens;
|
||||||
|
|
||||||
|
function is(chars) {
|
||||||
|
return chars.indexOf(ch) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function was(chars) {
|
||||||
|
return chars.indexOf(lastCh) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
function peek() {
|
function peek() {
|
||||||
return index + 1 < text.length ? text.charAt(index + 1) : false;
|
return index + 1 < text.length ? text.charAt(index + 1) : false;
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +129,7 @@ function lex(text, parseStrings){
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
number = 1 * number;
|
number = 1 * number;
|
||||||
tokens.push({index:start, text:number,
|
tokens.push({index:start, text:number, json:true,
|
||||||
fn:function(){return number;}});
|
fn:function(){return number;}});
|
||||||
}
|
}
|
||||||
function readIdent() {
|
function readIdent() {
|
||||||
|
|
@ -156,8 +149,9 @@ function lex(text, parseStrings){
|
||||||
fn = getterFn(ident);
|
fn = getterFn(ident);
|
||||||
fn.isAssignable = ident;
|
fn.isAssignable = ident;
|
||||||
}
|
}
|
||||||
tokens.push({index:start, text:ident, fn:fn});
|
tokens.push({index:start, text:ident, fn:fn, json: OPERATORS[ident]});
|
||||||
}
|
}
|
||||||
|
|
||||||
function readString(quote) {
|
function readString(quote) {
|
||||||
var start = index;
|
var start = index;
|
||||||
index++;
|
index++;
|
||||||
|
|
@ -189,7 +183,7 @@ function lex(text, parseStrings){
|
||||||
escape = true;
|
escape = true;
|
||||||
} else if (ch == quote) {
|
} else if (ch == quote) {
|
||||||
index++;
|
index++;
|
||||||
tokens.push({index:start, text:rawString, string:string,
|
tokens.push({index:start, text:rawString, string:string, json:true,
|
||||||
fn:function(){
|
fn:function(){
|
||||||
return (string.length == dateParseLength) ?
|
return (string.length == dateParseLength) ?
|
||||||
angular['String']['toDate'](string) : string;
|
angular['String']['toDate'](string) : string;
|
||||||
|
|
@ -241,32 +235,34 @@ function lex(text, parseStrings){
|
||||||
|
|
||||||
/////////////////////////////////////////
|
/////////////////////////////////////////
|
||||||
|
|
||||||
function Parser(text, parseStrings){
|
function parser(text, json){
|
||||||
this.text = text;
|
var ZERO = valueFn(0),
|
||||||
this.tokens = lex(text, parseStrings);
|
tokens = lex(text, json);
|
||||||
this.index = 0;
|
return {
|
||||||
}
|
assertAllConsumed: assertAllConsumed,
|
||||||
|
primary: primary,
|
||||||
|
statements: statements,
|
||||||
|
validator: validator,
|
||||||
|
filter: filter,
|
||||||
|
watch: watch
|
||||||
|
};
|
||||||
|
|
||||||
var ZERO = function(){
|
///////////////////////////////////
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
Parser.prototype = {
|
function error(msg, token) {
|
||||||
error: function(msg, token) {
|
|
||||||
throw "Token '" + token.text +
|
throw "Token '" + token.text +
|
||||||
"' is " + msg + " at column='" +
|
"' is " + msg + " at column='" +
|
||||||
(token.index + 1) + "' of expression '" +
|
(token.index + 1) + "' of expression '" +
|
||||||
this.text + "' starting at '" + this.text.substring(token.index) + "'.";
|
text + "' starting at '" + text.substring(token.index) + "'.";
|
||||||
},
|
}
|
||||||
|
|
||||||
peekToken: function() {
|
function peekToken() {
|
||||||
if (this.tokens.length === 0)
|
if (tokens.length === 0)
|
||||||
throw "Unexpected end of expression: " + this.text;
|
throw "Unexpected end of expression: " + text;
|
||||||
return this.tokens[0];
|
return tokens[0];
|
||||||
},
|
}
|
||||||
|
|
||||||
peek: function(e1, e2, e3, e4) {
|
function peek(e1, e2, e3, e4) {
|
||||||
var tokens = this.tokens;
|
|
||||||
if (tokens.length > 0) {
|
if (tokens.length > 0) {
|
||||||
var token = tokens[0];
|
var token = tokens[0];
|
||||||
var t = token.text;
|
var t = token.text;
|
||||||
|
|
@ -276,57 +272,64 @@ Parser.prototype = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
expect: function(e1, e2, e3, e4){
|
function expect(e1, e2, e3, e4){
|
||||||
var token = this.peek(e1, e2, e3, e4);
|
var token = peek(e1, e2, e3, e4);
|
||||||
if (token) {
|
if (token) {
|
||||||
this.tokens.shift();
|
if (json && !token.json) {
|
||||||
|
index = token.index;
|
||||||
|
throw "Expression at column='" +
|
||||||
|
token.index + "' of expression '" +
|
||||||
|
text + "' starting at '" + text.substring(token.index) +
|
||||||
|
"' is not valid json.";
|
||||||
|
}
|
||||||
|
tokens.shift();
|
||||||
this.currentToken = token;
|
this.currentToken = token;
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
consume: function(e1){
|
function consume(e1){
|
||||||
if (!this.expect(e1)) {
|
if (!expect(e1)) {
|
||||||
var token = this.peek();
|
var token = peek();
|
||||||
throw "Expecting '" + e1 + "' at column '" +
|
throw "Expecting '" + e1 + "' at column '" +
|
||||||
(token.index+1) + "' in '" +
|
(token.index+1) + "' in '" +
|
||||||
this.text + "' got '" +
|
text + "' got '" +
|
||||||
this.text.substring(token.index) + "'.";
|
text.substring(token.index) + "'.";
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_unary: function(fn, right) {
|
function unaryFn(fn, right) {
|
||||||
return function(self) {
|
return function(self) {
|
||||||
return fn(self, right(self));
|
return fn(self, right(self));
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
_binary: function(left, fn, right) {
|
function binaryFn(left, fn, right) {
|
||||||
return function(self) {
|
return function(self) {
|
||||||
return fn(self, left(self), right(self));
|
return fn(self, left(self), right(self));
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
hasTokens: function () {
|
function hasTokens () {
|
||||||
return this.tokens.length > 0;
|
return tokens.length > 0;
|
||||||
},
|
}
|
||||||
|
|
||||||
assertAllConsumed: function(){
|
function assertAllConsumed(){
|
||||||
if (this.tokens.length !== 0) {
|
if (tokens.length !== 0) {
|
||||||
throw "Did not understand '" + this.text.substring(this.tokens[0].index) +
|
throw "Did not understand '" + text.substring(tokens[0].index) +
|
||||||
"' while evaluating '" + this.text + "'.";
|
"' while evaluating '" + text + "'.";
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
statements: function(){
|
function statements(){
|
||||||
var statements = [];
|
var statements = [];
|
||||||
while(true) {
|
while(true) {
|
||||||
if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']'))
|
if (tokens.length > 0 && !peek('}', ')', ';', ']'))
|
||||||
statements.push(this.filterChain());
|
statements.push(filterChain());
|
||||||
if (!this.expect(';')) {
|
if (!expect(';')) {
|
||||||
return function (self){
|
return function (self){
|
||||||
var value;
|
var value;
|
||||||
for ( var i = 0; i < statements.length; i++) {
|
for ( var i = 0; i < statements.length; i++) {
|
||||||
|
|
@ -338,35 +341,35 @@ Parser.prototype = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
filterChain: function(){
|
function filterChain(){
|
||||||
var left = this.expression();
|
var left = expression();
|
||||||
var token;
|
var token;
|
||||||
while(true) {
|
while(true) {
|
||||||
if ((token = this.expect('|'))) {
|
if ((token = expect('|'))) {
|
||||||
left = this._binary(left, token.fn, this.filter());
|
left = binaryFn(left, token.fn, filter());
|
||||||
} else {
|
} else {
|
||||||
return left;
|
return left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
filter: function(){
|
function filter(){
|
||||||
return this._pipeFunction(angularFilter);
|
return pipeFunction(angularFilter);
|
||||||
},
|
}
|
||||||
|
|
||||||
validator: function(){
|
function validator(){
|
||||||
return this._pipeFunction(angularValidator);
|
return pipeFunction(angularValidator);
|
||||||
},
|
}
|
||||||
|
|
||||||
_pipeFunction: function(fnScope){
|
function pipeFunction(fnScope){
|
||||||
var fn = this.functionIdent(fnScope);
|
var fn = functionIdent(fnScope);
|
||||||
var argsFn = [];
|
var argsFn = [];
|
||||||
var token;
|
var token;
|
||||||
while(true) {
|
while(true) {
|
||||||
if ((token = this.expect(':'))) {
|
if ((token = expect(':'))) {
|
||||||
argsFn.push(this.expression());
|
argsFn.push(expression());
|
||||||
} else {
|
} else {
|
||||||
var fnInvoke = function(self, input){
|
var fnInvoke = function(self, input){
|
||||||
var args = [input];
|
var args = [input];
|
||||||
|
|
@ -380,111 +383,111 @@ Parser.prototype = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
expression: function(){
|
function expression(){
|
||||||
return this.throwStmt();
|
return throwStmt();
|
||||||
},
|
}
|
||||||
|
|
||||||
throwStmt: function(){
|
function throwStmt(){
|
||||||
if (this.expect('throw')) {
|
if (expect('throw')) {
|
||||||
var throwExp = this.assignment();
|
var throwExp = assignment();
|
||||||
return function (self) {
|
return function (self) {
|
||||||
throw throwExp(self);
|
throw throwExp(self);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return this.assignment();
|
return assignment();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
assignment: function(){
|
function assignment(){
|
||||||
var left = this.logicalOR();
|
var left = logicalOR();
|
||||||
var token;
|
var token;
|
||||||
if (token = this.expect('=')) {
|
if (token = expect('=')) {
|
||||||
if (!left.isAssignable) {
|
if (!left.isAssignable) {
|
||||||
throw "Left hand side '" +
|
throw "Left hand side '" +
|
||||||
this.text.substring(0, token.index) + "' of assignment '" +
|
text.substring(0, token.index) + "' of assignment '" +
|
||||||
this.text.substring(token.index) + "' is not assignable.";
|
text.substring(token.index) + "' is not assignable.";
|
||||||
}
|
}
|
||||||
var ident = function(){return left.isAssignable;};
|
var ident = function(){return left.isAssignable;};
|
||||||
return this._binary(ident, token.fn, this.logicalOR());
|
return binaryFn(ident, token.fn, logicalOR());
|
||||||
} else {
|
} else {
|
||||||
return left;
|
return left;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
logicalOR: function(){
|
function logicalOR(){
|
||||||
var left = this.logicalAND();
|
var left = logicalAND();
|
||||||
var token;
|
var token;
|
||||||
while(true) {
|
while(true) {
|
||||||
if ((token = this.expect('||'))) {
|
if ((token = expect('||'))) {
|
||||||
left = this._binary(left, token.fn, this.logicalAND());
|
left = binaryFn(left, token.fn, logicalAND());
|
||||||
} else {
|
} else {
|
||||||
return left;
|
return left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
logicalAND: function(){
|
function logicalAND(){
|
||||||
var left = this.equality();
|
var left = equality();
|
||||||
var token;
|
var token;
|
||||||
if ((token = this.expect('&&'))) {
|
if ((token = expect('&&'))) {
|
||||||
left = this._binary(left, token.fn, this.logicalAND());
|
left = binaryFn(left, token.fn, logicalAND());
|
||||||
}
|
}
|
||||||
return left;
|
return left;
|
||||||
},
|
}
|
||||||
|
|
||||||
equality: function(){
|
function equality(){
|
||||||
var left = this.relational();
|
var left = relational();
|
||||||
var token;
|
var token;
|
||||||
if ((token = this.expect('==','!='))) {
|
if ((token = expect('==','!='))) {
|
||||||
left = this._binary(left, token.fn, this.equality());
|
left = binaryFn(left, token.fn, equality());
|
||||||
}
|
}
|
||||||
return left;
|
return left;
|
||||||
},
|
}
|
||||||
|
|
||||||
relational: function(){
|
function relational(){
|
||||||
var left = this.additive();
|
var left = additive();
|
||||||
var token;
|
var token;
|
||||||
if (token = this.expect('<', '>', '<=', '>=')) {
|
if (token = expect('<', '>', '<=', '>=')) {
|
||||||
left = this._binary(left, token.fn, this.relational());
|
left = binaryFn(left, token.fn, relational());
|
||||||
}
|
}
|
||||||
return left;
|
return left;
|
||||||
},
|
}
|
||||||
|
|
||||||
additive: function(){
|
function additive(){
|
||||||
var left = this.multiplicative();
|
var left = multiplicative();
|
||||||
var token;
|
var token;
|
||||||
while(token = this.expect('+','-')) {
|
while(token = expect('+','-')) {
|
||||||
left = this._binary(left, token.fn, this.multiplicative());
|
left = binaryFn(left, token.fn, multiplicative());
|
||||||
}
|
}
|
||||||
return left;
|
return left;
|
||||||
},
|
}
|
||||||
|
|
||||||
multiplicative: function(){
|
function multiplicative(){
|
||||||
var left = this.unary();
|
var left = unary();
|
||||||
var token;
|
var token;
|
||||||
while(token = this.expect('*','/','%')) {
|
while(token = expect('*','/','%')) {
|
||||||
left = this._binary(left, token.fn, this.unary());
|
left = binaryFn(left, token.fn, unary());
|
||||||
}
|
}
|
||||||
return left;
|
return left;
|
||||||
},
|
}
|
||||||
|
|
||||||
unary: function(){
|
function unary(){
|
||||||
var token;
|
var token;
|
||||||
if (this.expect('+')) {
|
if (expect('+')) {
|
||||||
return this.primary();
|
return primary();
|
||||||
} else if (token = this.expect('-')) {
|
} else if (token = expect('-')) {
|
||||||
return this._binary(ZERO, token.fn, this.unary());
|
return binaryFn(ZERO, token.fn, unary());
|
||||||
} else if (token = this.expect('!')) {
|
} else if (token = expect('!')) {
|
||||||
return this._unary(token.fn, this.unary());
|
return unaryFn(token.fn, unary());
|
||||||
} else {
|
} else {
|
||||||
return this.primary();
|
return primary();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
functionIdent: function(fnScope) {
|
function functionIdent(fnScope) {
|
||||||
var token = this.expect();
|
var token = expect();
|
||||||
var element = token.text.split('.');
|
var element = token.text.split('.');
|
||||||
var instance = fnScope;
|
var instance = fnScope;
|
||||||
var key;
|
var key;
|
||||||
|
|
@ -495,58 +498,58 @@ Parser.prototype = {
|
||||||
}
|
}
|
||||||
if (typeof instance != $function) {
|
if (typeof instance != $function) {
|
||||||
throw "Function '" + token.text + "' at column '" +
|
throw "Function '" + token.text + "' at column '" +
|
||||||
(token.index+1) + "' in '" + this.text + "' is not defined.";
|
(token.index+1) + "' in '" + text + "' is not defined.";
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
},
|
}
|
||||||
|
|
||||||
primary: function() {
|
function primary() {
|
||||||
var primary;
|
var primary;
|
||||||
if (this.expect('(')) {
|
if (expect('(')) {
|
||||||
var expression = this.filterChain();
|
var expression = filterChain();
|
||||||
this.consume(')');
|
consume(')');
|
||||||
primary = expression;
|
primary = expression;
|
||||||
} else if (this.expect('[')) {
|
} else if (expect('[')) {
|
||||||
primary = this.arrayDeclaration();
|
primary = arrayDeclaration();
|
||||||
} else if (this.expect('{')) {
|
} else if (expect('{')) {
|
||||||
primary = this.object();
|
primary = object();
|
||||||
} else {
|
} else {
|
||||||
var token = this.expect();
|
var token = expect();
|
||||||
primary = token.fn;
|
primary = token.fn;
|
||||||
if (!primary) {
|
if (!primary) {
|
||||||
this.error("not a primary expression", token);
|
error("not a primary expression", token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var next;
|
var next;
|
||||||
while (next = this.expect('(', '[', '.')) {
|
while (next = expect('(', '[', '.')) {
|
||||||
if (next.text === '(') {
|
if (next.text === '(') {
|
||||||
primary = this.functionCall(primary);
|
primary = functionCall(primary);
|
||||||
} else if (next.text === '[') {
|
} else if (next.text === '[') {
|
||||||
primary = this.objectIndex(primary);
|
primary = objectIndex(primary);
|
||||||
} else if (next.text === '.') {
|
} else if (next.text === '.') {
|
||||||
primary = this.fieldAccess(primary);
|
primary = fieldAccess(primary);
|
||||||
} else {
|
} else {
|
||||||
throw "IMPOSSIBLE";
|
throw "IMPOSSIBLE";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return primary;
|
return primary;
|
||||||
},
|
}
|
||||||
|
|
||||||
fieldAccess: function(object) {
|
function fieldAccess(object) {
|
||||||
var field = this.expect().text;
|
var field = expect().text;
|
||||||
var getter = getterFn(field);
|
var getter = getterFn(field);
|
||||||
var fn = function (self){
|
var fn = function (self){
|
||||||
return getter(object(self));
|
return getter(object(self));
|
||||||
};
|
};
|
||||||
fn.isAssignable = field;
|
fn.isAssignable = field;
|
||||||
return fn;
|
return fn;
|
||||||
},
|
}
|
||||||
|
|
||||||
objectIndex: function(obj) {
|
function objectIndex(obj) {
|
||||||
var indexFn = this.expression();
|
var indexFn = expression();
|
||||||
this.consume(']');
|
consume(']');
|
||||||
if (this.expect('=')) {
|
if (expect('=')) {
|
||||||
var rhs = this.expression();
|
var rhs = expression();
|
||||||
return function (self){
|
return function (self){
|
||||||
return obj(self)[indexFn(self)] = rhs(self);
|
return obj(self)[indexFn(self)] = rhs(self);
|
||||||
};
|
};
|
||||||
|
|
@ -557,38 +560,38 @@ Parser.prototype = {
|
||||||
return (o) ? o[i] : _undefined;
|
return (o) ? o[i] : _undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
functionCall: function(fn) {
|
function functionCall(fn) {
|
||||||
var argsFn = [];
|
var argsFn = [];
|
||||||
if (this.peekToken().text != ')') {
|
if (peekToken().text != ')') {
|
||||||
do {
|
do {
|
||||||
argsFn.push(this.expression());
|
argsFn.push(expression());
|
||||||
} while (this.expect(','));
|
} while (expect(','));
|
||||||
}
|
}
|
||||||
this.consume(')');
|
consume(')');
|
||||||
return function (self){
|
return function (self){
|
||||||
var args = [];
|
var args = [];
|
||||||
for ( var i = 0; i < argsFn.length; i++) {
|
for ( var i = 0; i < argsFn.length; i++) {
|
||||||
args.push(argsFn[i](self));
|
args.push(argsFn[i](self));
|
||||||
}
|
}
|
||||||
var fnPtr = fn(self) || noop;
|
var fnPtr = fn(self) || noop;
|
||||||
// IE stupidity!
|
// IE stupidity!
|
||||||
return fnPtr.apply ?
|
return fnPtr.apply ?
|
||||||
fnPtr.apply(self, args) :
|
fnPtr.apply(self, args) :
|
||||||
fnPtr(args[0], args[1], args[2], args[3], args[4]);
|
fnPtr(args[0], args[1], args[2], args[3], args[4]);
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
// This is used with json array declaration
|
// This is used with json array declaration
|
||||||
arrayDeclaration: function () {
|
function arrayDeclaration () {
|
||||||
var elementFns = [];
|
var elementFns = [];
|
||||||
if (this.peekToken().text != ']') {
|
if (peekToken().text != ']') {
|
||||||
do {
|
do {
|
||||||
elementFns.push(this.expression());
|
elementFns.push(expression());
|
||||||
} while (this.expect(','));
|
} while (expect(','));
|
||||||
}
|
}
|
||||||
this.consume(']');
|
consume(']');
|
||||||
return function (self){
|
return function (self){
|
||||||
var array = [];
|
var array = [];
|
||||||
for ( var i = 0; i < elementFns.length; i++) {
|
for ( var i = 0; i < elementFns.length; i++) {
|
||||||
|
|
@ -596,20 +599,20 @@ Parser.prototype = {
|
||||||
}
|
}
|
||||||
return array;
|
return array;
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
object: function () {
|
function object () {
|
||||||
var keyValues = [];
|
var keyValues = [];
|
||||||
if (this.peekToken().text != '}') {
|
if (peekToken().text != '}') {
|
||||||
do {
|
do {
|
||||||
var token = this.expect(),
|
var token = expect(),
|
||||||
key = token.string || token.text;
|
key = token.string || token.text;
|
||||||
this.consume(":");
|
consume(":");
|
||||||
var value = this.expression();
|
var value = expression();
|
||||||
keyValues.push({key:key, value:value});
|
keyValues.push({key:key, value:value});
|
||||||
} while (this.expect(','));
|
} while (expect(','));
|
||||||
}
|
}
|
||||||
this.consume('}');
|
consume('}');
|
||||||
return function (self){
|
return function (self){
|
||||||
var object = {};
|
var object = {};
|
||||||
for ( var i = 0; i < keyValues.length; i++) {
|
for ( var i = 0; i < keyValues.length; i++) {
|
||||||
|
|
@ -619,39 +622,42 @@ Parser.prototype = {
|
||||||
}
|
}
|
||||||
return object;
|
return object;
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
watch: function () {
|
function watch () {
|
||||||
var decl = [];
|
var decl = [];
|
||||||
while(this.hasTokens()) {
|
while(hasTokens()) {
|
||||||
decl.push(this.watchDecl());
|
decl.push(watchDecl());
|
||||||
if (!this.expect(';')) {
|
if (!expect(';')) {
|
||||||
this.assertAllConsumed();
|
assertAllConsumed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.assertAllConsumed();
|
assertAllConsumed();
|
||||||
return function (self){
|
return function (self){
|
||||||
for ( var i = 0; i < decl.length; i++) {
|
for ( var i = 0; i < decl.length; i++) {
|
||||||
var d = decl[i](self);
|
var d = decl[i](self);
|
||||||
self.addListener(d.name, d.fn);
|
self.addListener(d.name, d.fn);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
watchDecl: function () {
|
function watchDecl () {
|
||||||
var anchorName = this.expect().text;
|
var anchorName = expect().text;
|
||||||
this.consume(":");
|
consume(":");
|
||||||
var expression;
|
var expressionFn;
|
||||||
if (this.peekToken().text == '{') {
|
if (peekToken().text == '{') {
|
||||||
this.consume("{");
|
consume("{");
|
||||||
expression = this.statements();
|
expressionFn = statements();
|
||||||
this.consume("}");
|
consume("}");
|
||||||
} else {
|
} else {
|
||||||
expression = this.expression();
|
expressionFn = expression();
|
||||||
}
|
}
|
||||||
return function(self) {
|
return function(self) {
|
||||||
return {name:anchorName, fn:expression};
|
return {name:anchorName, fn:expressionFn};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,9 +90,9 @@ function expressionCompile(exp){
|
||||||
if (typeof exp === $function) return exp;
|
if (typeof exp === $function) return exp;
|
||||||
var fn = compileCache[exp];
|
var fn = compileCache[exp];
|
||||||
if (!fn) {
|
if (!fn) {
|
||||||
var parser = new Parser(exp);
|
var p = parser(exp);
|
||||||
var fnSelf = parser.statements();
|
var fnSelf = p.statements();
|
||||||
parser.assertAllConsumed();
|
p.assertAllConsumed();
|
||||||
fn = compileCache[exp] = extend(
|
fn = compileCache[exp] = extend(
|
||||||
function(){ return fnSelf(this);},
|
function(){ return fnSelf(this);},
|
||||||
{fnSelf: fnSelf});
|
{fnSelf: fnSelf});
|
||||||
|
|
|
||||||
2
src/angular-bootstrap.js
vendored
2
src/angular-bootstrap.js
vendored
|
|
@ -43,7 +43,7 @@
|
||||||
addScript("/Scope.js");
|
addScript("/Scope.js");
|
||||||
addScript("/Injector.js");
|
addScript("/Injector.js");
|
||||||
addScript("/jqLite.js");
|
addScript("/jqLite.js");
|
||||||
addScript("/Parser.js");
|
addScript("/parser.js");
|
||||||
addScript("/Resource.js");
|
addScript("/Resource.js");
|
||||||
addScript("/Browser.js");
|
addScript("/Browser.js");
|
||||||
addScript("/AngularPublic.js");
|
addScript("/AngularPublic.js");
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ angularDirective("ng:click", function(expression, element){
|
||||||
angularDirective("ng:watch", function(expression, element){
|
angularDirective("ng:watch", function(expression, element){
|
||||||
return function(element){
|
return function(element){
|
||||||
var self = this;
|
var self = this;
|
||||||
new Parser(expression).watch()({
|
parser(expression).watch()({
|
||||||
addListener:function(watch, exp){
|
addListener:function(watch, exp){
|
||||||
self.$watch(watch, function(){
|
self.$watch(watch, function(){
|
||||||
return exp(self);
|
return exp(self);
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ function dateGetter(name, size, offset, trim) {
|
||||||
var value = date['get' + name].call(date);
|
var value = date['get' + name].call(date);
|
||||||
if (offset > 0 || value > -offset)
|
if (offset > 0 || value > -offset)
|
||||||
value += offset;
|
value += offset;
|
||||||
if (value == 0 && offset == -12 ) value = 12;
|
if (value === 0 && offset == -12 ) value = 12;
|
||||||
return padNumber(value, size, trim);
|
return padNumber(value, size, trim);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ function modelFormattedAccessor(scope, element) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileValidator(expr) {
|
function compileValidator(expr) {
|
||||||
return new Parser(expr).validator()();
|
return parser(expr).validator()();
|
||||||
}
|
}
|
||||||
|
|
||||||
function valueAccessor(scope, element) {
|
function valueAccessor(scope, element) {
|
||||||
|
|
|
||||||
134
test/JsonSpec.js
Normal file
134
test/JsonSpec.js
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
describe('json', function(){
|
||||||
|
it('should parse Primitives', function() {
|
||||||
|
assertEquals("null", toJson(0/0));
|
||||||
|
assertEquals("null", toJson(null));
|
||||||
|
assertEquals("true", toJson(true));
|
||||||
|
assertEquals("false", toJson(false));
|
||||||
|
assertEquals("123.45", toJson(123.45));
|
||||||
|
assertEquals('"abc"', toJson("abc"));
|
||||||
|
assertEquals('"a \\t \\n \\r b \\\\"', toJson("a \t \n \r b \\"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse Escaping', function() {
|
||||||
|
assertEquals("\"7\\\\\\\"7\"", toJson("7\\\"7"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse Objects', function() {
|
||||||
|
assertEquals('{"a":1,"b":2}', toJson({a:1,b:2}));
|
||||||
|
assertEquals('{"a":{"b":2}}', toJson({a:{b:2}}));
|
||||||
|
assertEquals('{"a":{"b":{"c":0}}}', toJson({a:{b:{c:0}}}));
|
||||||
|
assertEquals('{"a":{"b":null}}', toJson({a:{b:0/0}}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse ObjectPretty', function() {
|
||||||
|
assertEquals('{\n "a":1,\n "b":2}', toJson({a:1,b:2}, true));
|
||||||
|
assertEquals('{\n "a":{\n "b":2}}', toJson({a:{b:2}}, true));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse Array', function() {
|
||||||
|
assertEquals('[]', toJson([]));
|
||||||
|
assertEquals('[1,"b"]', toJson([1,"b"]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse IgnoreFunctions', function() {
|
||||||
|
assertEquals('[null,1]', toJson([function(){},1]));
|
||||||
|
assertEquals('{}', toJson({a:function(){}}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse ParseNull', function() {
|
||||||
|
assertNull(fromJson("null"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse ParseBoolean', function() {
|
||||||
|
assertTrue(fromJson("true"));
|
||||||
|
assertFalse(fromJson("false"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse $$isIgnored', function() {
|
||||||
|
assertEquals("{}", toJson({$$:0}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse ArrayWithEmptyItems', function() {
|
||||||
|
var a = [];
|
||||||
|
a[1] = "X";
|
||||||
|
assertEquals('[null,"X"]', toJson(a));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse ItShouldEscapeUnicode', function() {
|
||||||
|
assertEquals(1, "\u00a0".length);
|
||||||
|
assertEquals(8, toJson("\u00a0").length);
|
||||||
|
assertEquals(1, fromJson(toJson("\u00a0")).length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse ItShouldUTCDates', function() {
|
||||||
|
var date = angular.String.toDate("2009-10-09T01:02:03Z");
|
||||||
|
assertEquals('"2009-10-09T01:02:03Z"', toJson(date));
|
||||||
|
assertEquals(date.getTime(),
|
||||||
|
fromJson('"2009-10-09T01:02:03Z"').getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse ItShouldPreventRecursion', function() {
|
||||||
|
var obj = {a:'b'};
|
||||||
|
obj.recursion = obj;
|
||||||
|
assertEquals('{"a":"b","recursion":RECURSION}', angular.toJson(obj));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse ItShouldIgnore$Properties', function() {
|
||||||
|
var scope = createScope();
|
||||||
|
scope.a = 'a';
|
||||||
|
scope['$b'] = '$b';
|
||||||
|
scope.c = 'c';
|
||||||
|
expect(angular.toJson(scope)).toEqual('{"a":"a","c":"c","this":RECURSION}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse ItShouldSerializeInheritedProperties', function() {
|
||||||
|
var scope = createScope({p:'p'});
|
||||||
|
scope.a = 'a';
|
||||||
|
expect(angular.toJson(scope)).toEqual('{"a":"a","p":"p","this":RECURSION}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse ItShouldSerializeSameObjectsMultipleTimes', function() {
|
||||||
|
var obj = {a:'b'};
|
||||||
|
assertEquals('{"A":{"a":"b"},"B":{"a":"b"}}', angular.toJson({A:obj, B:obj}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse ItShouldNotSerializeUndefinedValues', function() {
|
||||||
|
assertEquals('{}', angular.toJson({A:undefined}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse ItShouldParseFloats', function() {
|
||||||
|
expect(fromJson("{value:2.55, name:'misko'}")).toEqual({value:2.55, name:'misko'});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('security', function(){
|
||||||
|
it('should not allow naked expressions', function(){
|
||||||
|
expect(function(){fromJson('1+2');}).toThrow("Did not understand '+2' while evaluating '1+2'.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow naked expressions group', function(){
|
||||||
|
expect(function(){fromJson('(1+2)');}).toThrow("Expression at column='0' of expression '(1+2)' starting at '(1+2)' is not valid json.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow expressions in objects', function(){
|
||||||
|
expect(function(){fromJson('{a:abc()}');}).toThrow("Expression at column='3' of expression '{a:abc()}' starting at 'abc()}' is not valid json.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow expressions in arrays', function(){
|
||||||
|
expect(function(){fromJson('[1+2]');}).toThrow("Expression at column='2' of expression '[1+2]' starting at '+2]' is not valid json.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow vars', function(){
|
||||||
|
expect(function(){fromJson('[1, x]');}).toThrow("Expression at column='4' of expression '[1, x]' starting at 'x]' is not valid json.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow dereference', function(){
|
||||||
|
expect(function(){fromJson('["".constructor]');}).toThrow("Expression at column='3' of expression '[\"\".constructor]' starting at '.constructor]' is not valid json.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow expressions ofter valid json', function(){
|
||||||
|
expect(function(){fromJson('[].constructor');}).toThrow("Expression at column='2' of expression '[].constructor' starting at '.constructor' is not valid json.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
102
test/JsonTest.js
102
test/JsonTest.js
|
|
@ -1,102 +0,0 @@
|
||||||
JsonTest = TestCase("JsonTest");
|
|
||||||
|
|
||||||
JsonTest.prototype.testPrimitives = function () {
|
|
||||||
assertEquals("null", toJson(0/0));
|
|
||||||
assertEquals("null", toJson(null));
|
|
||||||
assertEquals("true", toJson(true));
|
|
||||||
assertEquals("false", toJson(false));
|
|
||||||
assertEquals("123.45", toJson(123.45));
|
|
||||||
assertEquals('"abc"', toJson("abc"));
|
|
||||||
assertEquals('"a \\t \\n \\r b \\\\"', toJson("a \t \n \r b \\"));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testEscaping = function () {
|
|
||||||
assertEquals("\"7\\\\\\\"7\"", toJson("7\\\"7"));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testObjects = function () {
|
|
||||||
assertEquals('{"a":1,"b":2}', toJson({a:1,b:2}));
|
|
||||||
assertEquals('{"a":{"b":2}}', toJson({a:{b:2}}));
|
|
||||||
assertEquals('{"a":{"b":{"c":0}}}', toJson({a:{b:{c:0}}}));
|
|
||||||
assertEquals('{"a":{"b":null}}', toJson({a:{b:0/0}}));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testObjectPretty = function () {
|
|
||||||
assertEquals('{\n "a":1,\n "b":2}', toJson({a:1,b:2}, true));
|
|
||||||
assertEquals('{\n "a":{\n "b":2}}', toJson({a:{b:2}}, true));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testArray = function () {
|
|
||||||
assertEquals('[]', toJson([]));
|
|
||||||
assertEquals('[1,"b"]', toJson([1,"b"]));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testIgnoreFunctions = function () {
|
|
||||||
assertEquals('[null,1]', toJson([function(){},1]));
|
|
||||||
assertEquals('{}', toJson({a:function(){}}));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testParseNull = function () {
|
|
||||||
assertNull(fromJson("null"));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testParseBoolean = function () {
|
|
||||||
assertTrue(fromJson("true"));
|
|
||||||
assertFalse(fromJson("false"));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.test$$isIgnored = function () {
|
|
||||||
assertEquals("{}", toJson({$$:0}));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testArrayWithEmptyItems = function () {
|
|
||||||
var a = [];
|
|
||||||
a[1] = "X";
|
|
||||||
assertEquals('[null,"X"]', toJson(a));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testItShouldEscapeUnicode = function () {
|
|
||||||
assertEquals(1, "\u00a0".length);
|
|
||||||
assertEquals(8, toJson("\u00a0").length);
|
|
||||||
assertEquals(1, fromJson(toJson("\u00a0")).length);
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testItShouldUTCDates = function() {
|
|
||||||
var date = angular.String.toDate("2009-10-09T01:02:03Z");
|
|
||||||
assertEquals('"2009-10-09T01:02:03Z"', toJson(date));
|
|
||||||
assertEquals(date.getTime(),
|
|
||||||
fromJson('"2009-10-09T01:02:03Z"').getTime());
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testItShouldPreventRecursion = function () {
|
|
||||||
var obj = {a:'b'};
|
|
||||||
obj.recursion = obj;
|
|
||||||
assertEquals('{"a":"b","recursion":RECURSION}', angular.toJson(obj));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testItShouldIgnore$Properties = function() {
|
|
||||||
var scope = createScope();
|
|
||||||
scope.a = 'a';
|
|
||||||
scope['$b'] = '$b';
|
|
||||||
scope.c = 'c';
|
|
||||||
expect(angular.toJson(scope)).toEqual('{"a":"a","c":"c","this":RECURSION}');
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testItShouldSerializeInheritedProperties = function() {
|
|
||||||
var scope = createScope({p:'p'});
|
|
||||||
scope.a = 'a';
|
|
||||||
expect(angular.toJson(scope)).toEqual('{"a":"a","p":"p","this":RECURSION}');
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testItShouldSerializeSameObjectsMultipleTimes = function () {
|
|
||||||
var obj = {a:'b'};
|
|
||||||
assertEquals('{"A":{"a":"b"},"B":{"a":"b"}}', angular.toJson({A:obj, B:obj}));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testItShouldNotSerializeUndefinedValues = function () {
|
|
||||||
assertEquals('{}', angular.toJson({A:undefined}));
|
|
||||||
};
|
|
||||||
|
|
||||||
JsonTest.prototype.testItShouldParseFloats = function () {
|
|
||||||
expect(fromJson("{value:2.55, name:'misko'}")).toEqual({value:2.55, name:'misko'});
|
|
||||||
};
|
|
||||||
|
|
@ -443,7 +443,7 @@ describe('parser', function(){
|
||||||
assertEquals(12/6/2, scope.$eval("12/6/2"));
|
assertEquals(12/6/2, scope.$eval("12/6/2"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse BugStringConfusesParser', function(){
|
it('should parse BugStringConfusesparser', function(){
|
||||||
var scope = createScope();
|
var scope = createScope();
|
||||||
assertEquals('!', scope.$eval('suffix = "!"'));
|
assertEquals('!', scope.$eval('suffix = "!"'));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue