From 95c0f174e92dfeb4a0f5310f2d6b9c5163de02c0 Mon Sep 17 00:00:00 2001 From: Evgeniy Abduzhapparov Date: Fri, 14 Aug 2015 21:21:10 +0300 Subject: [PATCH 1/4] Rename "precidence" to "precedence" Fixes #254 --- src/twig.expression.js | 8 ++++---- src/twig.expression.operator.js | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/twig.expression.js b/src/twig.expression.js index c898d97b..cb11dca7 100644 --- a/src/twig.expression.js +++ b/src/twig.expression.js @@ -192,10 +192,10 @@ var Twig = (function (Twig) { (stack[stack.length-1].type == Twig.expression.type.operator.unary || stack[stack.length-1].type == Twig.expression.type.operator.binary) && ( (operator.associativity === Twig.expression.operator.leftToRight && - operator.precidence >= stack[stack.length-1].precidence) || + operator.precedence >= stack[stack.length-1].precedence) || (operator.associativity === Twig.expression.operator.rightToLeft && - operator.precidence > stack[stack.length-1].precidence) + operator.precedence > stack[stack.length-1].precedence) ) ) { var temp = stack.pop(); @@ -254,10 +254,10 @@ var Twig = (function (Twig) { (stack[stack.length-1].type == Twig.expression.type.operator.unary || stack[stack.length-1].type == Twig.expression.type.operator.binary) && ( (operator.associativity === Twig.expression.operator.leftToRight && - operator.precidence >= stack[stack.length-1].precidence) || + operator.precedence >= stack[stack.length-1].precedence) || (operator.associativity === Twig.expression.operator.rightToLeft && - operator.precidence > stack[stack.length-1].precidence) + operator.precedence > stack[stack.length-1].precedence) ) ) { var temp = stack.pop(); diff --git a/src/twig.expression.operator.js b/src/twig.expression.operator.js index 3aa72faa..0826b275 100644 --- a/src/twig.expression.operator.js +++ b/src/twig.expression.operator.js @@ -33,7 +33,7 @@ var Twig = (function (Twig) { }; /** - * Get the precidence and associativity of an operator. These follow the order that C/C++ use. + * Get the precedence and associativity of an operator. These follow the order that C/C++ use. * See http://en.wikipedia.org/wiki/Operators_in_C_and_C++ for the table of values. */ Twig.expression.operator.lookup = function (operator, token) { @@ -41,35 +41,35 @@ var Twig = (function (Twig) { case "..": case 'not in': case 'in': - token.precidence = 20; + token.precedence = 20; token.associativity = Twig.expression.operator.leftToRight; break; case ',': - token.precidence = 18; + token.precedence = 18; token.associativity = Twig.expression.operator.leftToRight; break; // Ternary case '?': case ':': - token.precidence = 16; + token.precedence = 16; token.associativity = Twig.expression.operator.rightToLeft; break; case 'or': - token.precidence = 14; + token.precedence = 14; token.associativity = Twig.expression.operator.leftToRight; break; case 'and': - token.precidence = 13; + token.precedence = 13; token.associativity = Twig.expression.operator.leftToRight; break; case '==': case '!=': - token.precidence = 9; + token.precedence = 9; token.associativity = Twig.expression.operator.leftToRight; break; @@ -77,7 +77,7 @@ var Twig = (function (Twig) { case '<=': case '>': case '>=': - token.precidence = 8; + token.precedence = 8; token.associativity = Twig.expression.operator.leftToRight; break; @@ -85,7 +85,7 @@ var Twig = (function (Twig) { case '~': // String concatination case '+': case '-': - token.precidence = 6; + token.precedence = 6; token.associativity = Twig.expression.operator.leftToRight; break; @@ -94,12 +94,12 @@ var Twig = (function (Twig) { case '*': case '/': case '%': - token.precidence = 5; + token.precedence = 5; token.associativity = Twig.expression.operator.leftToRight; break; case 'not': - token.precidence = 3; + token.precedence = 3; token.associativity = Twig.expression.operator.rightToLeft; break; From dd98796305c752abc4fcd8c48efb701a8a47beca Mon Sep 17 00:00:00 2001 From: Evgeniy Abduzhapparov Date: Fri, 14 Aug 2015 21:43:51 +0300 Subject: [PATCH 2/4] Use the operator precedence and associativity from Twig.php Reverse the precedence of operators. Change the associativity of the ** operator. Fixes #255 --- src/twig.expression.js | 8 ++++---- src/twig.expression.operator.js | 35 ++++++++++++++++++++------------- test/test.expressions.js | 4 ++++ 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/twig.expression.js b/src/twig.expression.js index cb11dca7..dbecf3e2 100644 --- a/src/twig.expression.js +++ b/src/twig.expression.js @@ -192,10 +192,10 @@ var Twig = (function (Twig) { (stack[stack.length-1].type == Twig.expression.type.operator.unary || stack[stack.length-1].type == Twig.expression.type.operator.binary) && ( (operator.associativity === Twig.expression.operator.leftToRight && - operator.precedence >= stack[stack.length-1].precedence) || + operator.precedence <= stack[stack.length-1].precedence) || (operator.associativity === Twig.expression.operator.rightToLeft && - operator.precedence > stack[stack.length-1].precedence) + operator.precedence < stack[stack.length-1].precedence) ) ) { var temp = stack.pop(); @@ -254,10 +254,10 @@ var Twig = (function (Twig) { (stack[stack.length-1].type == Twig.expression.type.operator.unary || stack[stack.length-1].type == Twig.expression.type.operator.binary) && ( (operator.associativity === Twig.expression.operator.leftToRight && - operator.precedence >= stack[stack.length-1].precedence) || + operator.precedence <= stack[stack.length-1].precedence) || (operator.associativity === Twig.expression.operator.rightToLeft && - operator.precedence > stack[stack.length-1].precedence) + operator.precedence < stack[stack.length-1].precedence) ) ) { var temp = stack.pop(); diff --git a/src/twig.expression.operator.js b/src/twig.expression.operator.js index 0826b275..4820e827 100644 --- a/src/twig.expression.operator.js +++ b/src/twig.expression.operator.js @@ -33,12 +33,16 @@ var Twig = (function (Twig) { }; /** - * Get the precedence and associativity of an operator. These follow the order that C/C++ use. - * See http://en.wikipedia.org/wiki/Operators_in_C_and_C++ for the table of values. + * Get the precedence and associativity of an operator. These inherit the order from Twig.php. + * See https://github.com/twigphp/Twig/blob/1.x/lib/Twig/Extension/Core.php for the table of precedence. */ Twig.expression.operator.lookup = function (operator, token) { switch (operator) { case "..": + token.precedence = 25; + token.associativity = Twig.expression.operator.leftToRight; + break; + case 'not in': case 'in': token.precedence = 20; @@ -58,48 +62,51 @@ var Twig = (function (Twig) { break; case 'or': - token.precedence = 14; + token.precedence = 10; token.associativity = Twig.expression.operator.leftToRight; break; case 'and': - token.precedence = 13; + token.precedence = 15; token.associativity = Twig.expression.operator.leftToRight; break; case '==': case '!=': - token.precedence = 9; - token.associativity = Twig.expression.operator.leftToRight; - break; - case '<': case '<=': case '>': case '>=': - token.precedence = 8; + token.precedence = 20; token.associativity = Twig.expression.operator.leftToRight; break; + case '~': // String concatenation + token.precedence = 40; + token.associativity = Twig.expression.operator.leftToRight; + break; - case '~': // String concatination case '+': case '-': - token.precedence = 6; + token.precedence = 30; token.associativity = Twig.expression.operator.leftToRight; break; - case '//': case '**': + token.precedence = 200; + token.associativity = Twig.expression.operator.rightToLeft; + break; + + case '//': case '*': case '/': case '%': - token.precedence = 5; + token.precedence = 60; token.associativity = Twig.expression.operator.leftToRight; break; case 'not': - token.precedence = 3; + token.precedence = 50; token.associativity = Twig.expression.operator.rightToLeft; break; diff --git a/test/test.expressions.js b/test/test.expressions.js index cfa74341..a0a95147 100644 --- a/test/test.expressions.js +++ b/test/test.expressions.js @@ -91,6 +91,10 @@ describe("Twig.js Expressions ->", function() { }); }); + it("should use right associativity of the power operator", function() { + twig({data: '{{ a ** b ** c }}'}).render({a:2, b:1, c:2}).should.equal('2'); + }); + it("should concatanate values", function() { twig({data: '{{ "test" ~ a }}'}).render({a:1234}).should.equal("test1234"); twig({data: '{{ a ~ "test" ~ a }}'}).render({a:1234}).should.equal("1234test1234"); From 474d73639f790dead4f6010583a6dcdfc3566fd6 Mon Sep 17 00:00:00 2001 From: Evgeniy Abduzhapparov Date: Fri, 14 Aug 2015 22:10:42 +0300 Subject: [PATCH 3/4] Operators "starts with", "ends with", "matches" Fixes #256 --- src/twig.expression.js | 3 +- src/twig.expression.operator.js | 50 +++++++++++++++++++++++++++++++++ test/test.expressions.js | 35 +++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/twig.expression.js b/src/twig.expression.js index dbecf3e2..5f634b72 100644 --- a/src/twig.expression.js +++ b/src/twig.expression.js @@ -177,12 +177,13 @@ var Twig = (function (Twig) { { type: Twig.expression.type.operator.binary, // Match any of +, *, /, -, %, ~, <, <=, >, >=, !=, ==, **, ?, :, and, or, not - regex: /(^[\+\-~%\?\:]|^[!=]==?|^[!<>]=?|^\*\*?|^\/\/?|^and\s+|^or\s+|^in\s+|^not in\s+|^\.\.)/, + regex: /(^[\+\-~%\?\:]|^[!=]==?|^[!<>]=?|^\*\*?|^\/\/?|^and\s+|^or\s+|^in\s+|^not in\s+|^\.\.|^starts\s+with\s+|^ends\s+with\s+|^matches\s+)/, next: Twig.expression.set.expressions.concat([Twig.expression.type.operator.unary]), compile: function(token, stack, output) { delete token.match; token.value = token.value.trim(); + token.value = token.value.replace(/\s+/, ' '); var value = token.value, operator = Twig.expression.operator.lookup(value, token); diff --git a/src/twig.expression.operator.js b/src/twig.expression.operator.js index 4820e827..69a120a3 100644 --- a/src/twig.expression.operator.js +++ b/src/twig.expression.operator.js @@ -61,6 +61,13 @@ var Twig = (function (Twig) { token.associativity = Twig.expression.operator.rightToLeft; break; + case 'starts with': + case 'ends with': + case 'matches': + token.precedence = 20; + token.associativity = Twig.expression.operator.leftToRight; + break; + case 'or': token.precedence = 10; token.associativity = Twig.expression.operator.leftToRight; @@ -274,6 +281,49 @@ var Twig = (function (Twig) { stack.push( Twig.functions.range(a, b) ); break; + case 'starts with': + b = stack.pop(); + a = stack.pop(); + stack.push(Twig.lib.is('String', a) + && Twig.lib.is('String', b) + && a.indexOf(b) === 0); + break; + + case 'ends with': + b = stack.pop(); + a = stack.pop(); + stack.push(Twig.lib.is('String', a) + && Twig.lib.is('String', b) + && (b === '' || a.substr(-b.length) === b)); + break; + + case 'matches': + b = stack.pop(); + a = stack.pop(); + + // TODO: remove this block when + // we start casting to string the PHP way: + // true => '1' + // null, undefined, false => '' + if (a == null || a === 0 || a === false) { + a = ''; + } else if (a === true) { + a = '1'; + } + + // PHP supports different delimiters + // We need to care of quoted chars + var delimiter = b[0]; + var parts = b.split(delimiter); + var flags = parts.pop(); + parts.shift(); + var pattern = parts.join(delimiter); + pattern = pattern.replace('\\'+delimiter, delimiter); + var regexp = new RegExp(pattern, flags); + + stack.push(regexp.exec(a)); + break; + default: throw new Twig.Error(operator + " is an unknown operator."); } diff --git a/test/test.expressions.js b/test/test.expressions.js index a0a95147..841769c6 100644 --- a/test/test.expressions.js +++ b/test/test.expressions.js @@ -292,5 +292,40 @@ describe("Twig.js Expressions ->", function() { var test_template = twig({data: '{{ "d" not in {"key_a" : "no"} }}'}); test_template.render().should.equal(true.toString()); }); + + it("should support the 'starts with' operator", function() { + twig({data: "{{ 'foo' starts with 'f' ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ not ('foo' starts with 'oo') ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ not ('foo' starts with 'foowaytoolong') ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ 'foo' starts with 'f' ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ 'foo' starts\nwith 'f' ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ 'foo' starts with '' ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ '1' starts with true ? 'OK' : 'KO' }}"}).render().should.equal('KO'); + twig({data: "{{ '' starts with false ? 'OK' : 'KO' }}"}).render().should.equal('KO'); + twig({data: "{{ 'a' starts with false ? 'OK' : 'KO' }}"}).render().should.equal('KO'); + twig({data: "{{ false starts with '' ? 'OK' : 'KO' }}"}).render().should.equal('KO'); + }); + + it("should support the 'ends with' operator", function() { + twig({data: "{{ 'foo' ends with 'o' ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ not ('foo' ends with 'f') ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ not ('foo' ends with 'foowaytoolong') ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ 'foo' ends with '' ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ '1' ends with true ? 'OK' : 'KO' }}"}).render().should.equal('KO'); + twig({data: "{{ 1 ends with true ? 'OK' : 'KO' }}"}).render().should.equal('KO'); + twig({data: "{{ 0 ends with false ? 'OK' : 'KO' }}"}).render().should.equal('KO'); + twig({data: "{{ '' ends with false ? 'OK' : 'KO' }}"}).render().should.equal('KO'); + twig({data: "{{ false ends with false ? 'OK' : 'KO' }}"}).render().should.equal('KO'); + twig({data: "{{ false ends with '' ? 'OK' : 'KO' }}"}).render().should.equal('KO'); + }); + + it("should support the 'matches' operator", function() { + twig({data: "{{ 'foo' matches '/o/' ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ 'foo' matches '/^fo/' ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ 'foo' matches '/O/i' ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ 'foo' matches '#O#i' ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ 'fo#o' matches '#O\##i' ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + twig({data: "{{ 'http://example.com' matches '/^http:\/\//' ? 'OK' : 'KO' }}"}).render().should.equal('OK'); + }); }); }); From a9c924c8374416e8a1231a60abe71097bed5d81a Mon Sep 17 00:00:00 2001 From: Evgeniy Abduzhapparov Date: Sun, 23 Aug 2015 22:08:41 +0300 Subject: [PATCH 4/4] Change the precedence of the ternary operator to the lowest value --- src/twig.expression.operator.js | 2 +- test/test.expressions.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/twig.expression.operator.js b/src/twig.expression.operator.js index 69a120a3..bc819859 100644 --- a/src/twig.expression.operator.js +++ b/src/twig.expression.operator.js @@ -57,7 +57,7 @@ var Twig = (function (Twig) { // Ternary case '?': case ':': - token.precedence = 16; + token.precedence = 0; token.associativity = Twig.expression.operator.rightToLeft; break; diff --git a/test/test.expressions.js b/test/test.expressions.js index 841769c6..0d6f28e3 100644 --- a/test/test.expressions.js +++ b/test/test.expressions.js @@ -244,6 +244,11 @@ describe("Twig.js Expressions ->", function() { output2.should.equal( "1" ); }); + it("should have the lowest precedence for the ternary operator", function() { + twig({data: '{{ 1 in [1] ? 2 : 3 }}'}).render().should.equal('2'); + twig({data: '{{ 1 or 2 ? 3 : 4 }}'}).render().should.equal('3'); + }); + it("should support in/containment functionality for arrays", function() { var test_template = twig({data: '{{ "a" in ["a", "b", "c"] }}'}); test_template.render().should.equal(true.toString());