From 16af3b855198dc377083251cad5c7bead3c0d875 Mon Sep 17 00:00:00 2001 From: Zach Snow Date: Mon, 22 Apr 2013 17:42:34 -0700 Subject: [PATCH 1/2] feat($parse): add support for ternary operators to parser Add '?' token to lexer, add ternary rule to parser at (hopefully) proper precedence and associativity (based on https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Operator_Precedence). Since (exp1 && exp2 || exp3) is supported by the parser, and (exp1 ? exp2 : exp3) works the same way, it seems reasonable to add this minor form of control to templates (see #719). --- src/ng/parse.js | 34 +++++++++++++++++++++++++--- test/ng/parseSpec.js | 54 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index f931f61373f3..6d72e2ae23d4 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -58,7 +58,7 @@ function lex(text, csp){ (token=tokens[tokens.length-1])) { token.json = token.text.indexOf('.') == -1; } - } else if (is('(){}[].,;:')) { + } else if (is('(){}[].,;:?')) { tokens.push({ index:index, text:ch, @@ -359,6 +359,14 @@ function parser(text, json, $filter, csp){ }); } + function ternaryFn(left, middle, right){ + return extend(function(self, locals){ + return left(self, locals) ? middle(self, locals) : right(self, locals); + }, { + constant: left.constant && middle.constant && right.constant + }); + } + function binaryFn(left, fn, right) { return extend(function(self, locals) { return fn(self, locals, left, right); @@ -427,9 +435,10 @@ function parser(text, json, $filter, csp){ function expression() { return assignment(); } + function _assignment() { - var left = logicalOR(); + var left = ternary(); var right; var token; if ((token = expect('='))) { @@ -437,7 +446,7 @@ function parser(text, json, $filter, csp){ throwError("implies assignment but [" + text.substring(0, token.index) + "] can not be assigned to", token); } - right = logicalOR(); + right = ternary(); return function(self, locals){ return left.assign(self, right(self, locals), locals); }; @@ -446,6 +455,25 @@ function parser(text, json, $filter, csp){ } } + function ternary() { + var left = logicalOR(); + var middle; + var right; + var token; + if((token = expect('?'))){ + middle = ternary(); + if((token = expect(':'))){ + return ternaryFn(left, middle, ternary()); + } + else { + throwError('expected :', token); + } + } + else { + return left; + } + } + function logicalOR() { var left = logicalAND(); var token; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 020a1d6fb7ea..d47959fcb17b 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -103,6 +103,14 @@ describe('parser', function() { expect(tokens[7].text).toEqual('==='); expect(tokens[8].text).toEqual('!=='); }); + + it('should tokenize logical and ternary', function() { + var tokens = lex("&& || ? :"); + expect(tokens[0].text).toEqual('&&'); + expect(tokens[1].text).toEqual('||'); + expect(tokens[2].text).toEqual('?'); + expect(tokens[3].text).toEqual(':'); + }); it('should tokenize statements', function() { var tokens = lex("a;b;"); @@ -220,6 +228,52 @@ describe('parser', function() { expect(scope.$eval("0||2")).toEqual(0||2); expect(scope.$eval("0||1&&2")).toEqual(0||1&&2); }); + + it('should parse ternary', function(){ + var f = scope.f = function(){ return true; }; + var g = scope.g = function(){ return false; }; + var h = scope.h = function(){ return 'asd'; }; + var i = scope.i = function(){ return 123; }; + var id = scope.id = function(x){ return x; }; + + // Simple. + expect(scope.$eval('0?0:2')).toEqual(0?0:2); + expect(scope.$eval('1?0:2')).toEqual(1?0:2); + + // Nested on the left. + expect(scope.$eval('0?0?0:0:2')).toEqual(0?0?0:0:2); + expect(scope.$eval('1?0?0:0:2')).toEqual(1?0?0:0:2); + expect(scope.$eval('0?1?0:0:2')).toEqual(0?1?0:0:2); + expect(scope.$eval('0?0?1:0:2')).toEqual(0?0?1:0:2); + expect(scope.$eval('0?0?0:2:3')).toEqual(0?0?0:2:3); + expect(scope.$eval('1?1?0:0:2')).toEqual(1?1?0:0:2); + expect(scope.$eval('1?1?1:0:2')).toEqual(1?1?1:0:2); + expect(scope.$eval('1?1?1:2:3')).toEqual(1?1?1:2:3); + expect(scope.$eval('1?1?1:2:3')).toEqual(1?1?1:2:3); + + // Nested on the right. + expect(scope.$eval('0?0:0?0:2')).toEqual(0?0:0?0:2); + expect(scope.$eval('1?0:0?0:2')).toEqual(1?0:0?0:2); + expect(scope.$eval('0?1:0?0:2')).toEqual(0?1:0?0:2); + expect(scope.$eval('0?0:1?0:2')).toEqual(0?0:1?0:2); + expect(scope.$eval('0?0:0?2:3')).toEqual(0?0:0?2:3); + expect(scope.$eval('1?1:0?0:2')).toEqual(1?1:0?0:2); + expect(scope.$eval('1?1:1?0:2')).toEqual(1?1:1?0:2); + expect(scope.$eval('1?1:1?2:3')).toEqual(1?1:1?2:3); + expect(scope.$eval('1?1:1?2:3')).toEqual(1?1:1?2:3); + + // Precedence with respect to logical operators. + expect(scope.$eval('0&&1?0:1')).toEqual(0&&1?0:1); + expect(scope.$eval('0&&1?0:1')).toEqual((0&&1)?0:1); + expect(scope.$eval('1||0?0:0')).toEqual(1||0?0:0); + expect(scope.$eval('1||0?0:0')).toEqual((1||0)?0:0); + + // Function calls. + expect(scope.$eval('f() ? h() : i()')).toEqual(f() ? h() : i()); + expect(scope.$eval('g() ? h() : i()')).toEqual(g() ? h() : i()); + expect(scope.$eval('f() ? h() : i()')).toEqual(f() ? h() : i()); + expect(scope.$eval('id(g() ? h() : i())')).toEqual(id(g() ? h() : i())); + }); it('should parse string', function() { expect(scope.$eval("'a' + 'b c'")).toEqual("ab c"); From f7da5380007319ba226cb5256c7489143660e419 Mon Sep 17 00:00:00 2001 From: Zach Snow Date: Sun, 12 May 2013 13:12:37 -0700 Subject: [PATCH 2/2] Clarify parsing of ternaries using more descriptive variable names; improve clarity of tests; remove redundant tests. --- src/ng/parse.js | 3 ++- test/ng/parseSpec.js | 40 +++++++++++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 6d72e2ae23d4..32fdf1d793ed 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -463,7 +463,8 @@ function parser(text, json, $filter, csp){ if((token = expect('?'))){ middle = ternary(); if((token = expect(':'))){ - return ternaryFn(left, middle, ternary()); + right = ternary(); + return ternaryFn(left, middle, right); } else { throwError('expected :', token); diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index d47959fcb17b..2588a22051d0 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -230,11 +230,11 @@ describe('parser', function() { }); it('should parse ternary', function(){ - var f = scope.f = function(){ return true; }; - var g = scope.g = function(){ return false; }; - var h = scope.h = function(){ return 'asd'; }; - var i = scope.i = function(){ return 123; }; - var id = scope.id = function(x){ return x; }; + var returnTrue = scope.returnTrue = function(){ return true; }; + var returnFalse = scope.returnFalse = function(){ return false; }; + var returnString = scope.returnString = function(){ return 'asd'; }; + var returnInt = scope.returnInt = function(){ return 123; }; + var identity = scope.identity = function(x){ return x; }; // Simple. expect(scope.$eval('0?0:2')).toEqual(0?0:2); @@ -264,15 +264,33 @@ describe('parser', function() { // Precedence with respect to logical operators. expect(scope.$eval('0&&1?0:1')).toEqual(0&&1?0:1); - expect(scope.$eval('0&&1?0:1')).toEqual((0&&1)?0:1); expect(scope.$eval('1||0?0:0')).toEqual(1||0?0:0); - expect(scope.$eval('1||0?0:0')).toEqual((1||0)?0:0); + + expect(scope.$eval('0?0&&1:2')).toEqual(0?0&&1:2); + expect(scope.$eval('0?1&&1:2')).toEqual(0?1&&1:2); + expect(scope.$eval('0?0||0:1')).toEqual(0?0||0:1); + expect(scope.$eval('0?0||1:2')).toEqual(0?0||1:2); + + expect(scope.$eval('1?0&&1:2')).toEqual(1?0&&1:2); + expect(scope.$eval('1?1&&1:2')).toEqual(1?1&&1:2); + expect(scope.$eval('1?0||0:1')).toEqual(1?0||0:1); + expect(scope.$eval('1?0||1:2')).toEqual(1?0||1:2); + + expect(scope.$eval('0?1:0&&1')).toEqual(0?1:0&&1); + expect(scope.$eval('0?2:1&&1')).toEqual(0?2:1&&1); + expect(scope.$eval('0?1:0||0')).toEqual(0?1:0||0); + expect(scope.$eval('0?2:0||1')).toEqual(0?2:0||1); + + expect(scope.$eval('1?1:0&&1')).toEqual(1?1:0&&1); + expect(scope.$eval('1?2:1&&1')).toEqual(1?2:1&&1); + expect(scope.$eval('1?1:0||0')).toEqual(1?1:0||0); + expect(scope.$eval('1?2:0||1')).toEqual(1?2:0||1); // Function calls. - expect(scope.$eval('f() ? h() : i()')).toEqual(f() ? h() : i()); - expect(scope.$eval('g() ? h() : i()')).toEqual(g() ? h() : i()); - expect(scope.$eval('f() ? h() : i()')).toEqual(f() ? h() : i()); - expect(scope.$eval('id(g() ? h() : i())')).toEqual(id(g() ? h() : i())); + expect(scope.$eval('returnTrue() ? returnString() : returnInt()')).toEqual(returnTrue() ? returnString() : returnInt()); + expect(scope.$eval('returnFalse() ? returnString() : returnInt()')).toEqual(returnFalse() ? returnString() : returnInt()); + expect(scope.$eval('returnTrue() ? returnString() : returnInt()')).toEqual(returnTrue() ? returnString() : returnInt()); + expect(scope.$eval('identity(returnFalse() ? returnString() : returnInt())')).toEqual(identity(returnFalse() ? returnString() : returnInt())); }); it('should parse string', function() {