diff --git a/src/twig.expression.js b/src/twig.expression.js index c898d97b..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); @@ -192,10 +193,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 +255,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..bc819859 100644 --- a/src/twig.expression.operator.js +++ b/src/twig.expression.operator.js @@ -33,73 +33,87 @@ var Twig = (function (Twig) { }; /** - * Get the precidence 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.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 = 0; 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.precidence = 14; + token.precedence = 10; token.associativity = Twig.expression.operator.leftToRight; break; case 'and': - token.precidence = 13; + token.precedence = 15; token.associativity = Twig.expression.operator.leftToRight; break; case '==': case '!=': - token.precidence = 9; - token.associativity = Twig.expression.operator.leftToRight; - break; - case '<': case '<=': case '>': case '>=': - token.precidence = 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.precidence = 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.precidence = 5; + token.precedence = 60; token.associativity = Twig.expression.operator.leftToRight; break; case 'not': - token.precidence = 3; + token.precedence = 50; token.associativity = Twig.expression.operator.rightToLeft; break; @@ -267,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 cfa74341..0d6f28e3 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"); @@ -240,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()); @@ -288,5 +297,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'); + }); }); });