diff --git a/docs/wiki b/docs/wiki index 169dc5a2..cd1010ec 160000 --- a/docs/wiki +++ b/docs/wiki @@ -1 +1 @@ -Subproject commit 169dc5a2a542849afd8c057405a13bfdd4b191c3 +Subproject commit cd1010ec51d0f8dd3ad5a473a168e94852bfae96 diff --git a/src/twig.expression.js b/src/twig.expression.js index 80a8b4dc..9dee5784 100644 --- a/src/twig.expression.js +++ b/src/twig.expression.js @@ -35,6 +35,7 @@ module.exports = function (Twig) { unary: 'Twig.expression.type.operator.unary', binary: 'Twig.expression.type.operator.binary' }, + arrowFunction: 'Twig.expression.type.arrowFunction', string: 'Twig.expression.type.string', bool: 'Twig.expression.type.bool', slice: 'Twig.expression.type.slice', @@ -71,6 +72,7 @@ module.exports = function (Twig) { // What can follow an expression (in general) operations: [ Twig.expression.type.filter, + Twig.expression.type.arrowFunction, Twig.expression.type.operator.unary, Twig.expression.type.operator.binary, Twig.expression.type.array.end, @@ -212,9 +214,9 @@ module.exports = function (Twig) { }, { type: Twig.expression.type.operator.binary, - // Match any of ??, ?:, +, *, /, -, %, ~, <, <=, >, >=, !=, ==, **, ?, :, and, b-and, or, b-or, b-xor, in, not in + // Match any of ??, ?:, +, *, /, -, %, ~, <=>, <, <=, >, >=, !=, ==, **, ?, :, and, b-and, or, b-or, b-xor, in, not in // and, or, in, not in, matches, starts with, ends with can be followed by a space or parenthesis - regex: /(^\?\?|^\?:|^(b-and)|^(b-or)|^(b-xor)|^[+\-~%?]|^[:](?!\d\])|^[!=]==?|^[!<>]=?|^\*\*?|^\/\/?|^(and)[(|\s+]|^(or)[(|\s+]|^(in)[(|\s+]|^(not in)[(|\s+]|^(matches)|^(starts with)|^(ends with)|^\.\.)/, + regex: /(^\?\?|^\?:|^(b-and)|^(b-or)|^(b-xor)|^[+\-~%?]|^(<=>)|^[:](?!\d\])|^[!=]==?|^[!<>]=?|^\*\*?|^\/\/?|^(and)[(|\s+]|^(or)[(|\s+]|^(in)[(|\s+]|^(not in)[(|\s+]|^(matches)|^(starts with)|^(ends with)|^\.\.)/, next: Twig.expression.set.expressions, transform(match, tokens) { switch (match[0]) { @@ -479,6 +481,31 @@ module.exports = function (Twig) { throw new Twig.Error('Unexpected subexpression end when token is not marked as an expression'); } }, + { + /** + * Match an arrow function after a filter. + * ((params) => body) + */ + type: Twig.expression.type.arrowFunction, + regex: /^\(\(*\s*([a-zA-Z_]\w*\s*(?:\s?,\s?[a-zA-Z_]\w*\s*)*)\)*\s*=>\s*([^,]*),*\s*(\w*(?:,\s?\w*)*)\s*\)/, + next: Twig.expression.set.expressions.concat([Twig.expression.type.subexpression.end]), + compile(token, stack, output) { + const filter = output.pop(); + if (filter.type !== Twig.expression.type.filter) { + throw new Twig.Error('Expected filter before arrow function.'); + } + + token.params = token.match[1].trim(); + token.body = '{{ ' + token.match[2] + ' }}'; + token.args = token.match[3].trim(); + filter.params = token; + + output.push(filter); + }, + parse(token, stack) { + stack.push(token); + } + }, { /** * Match a parameter set start. @@ -756,6 +783,7 @@ module.exports = function (Twig) { // Match a | then a letter or _, then any number of letters, numbers, _ or - regex: /^\|\s?([a-zA-Z_][a-zA-Z0-9_-]*)/, next: Twig.expression.set.operationsExtended.concat([ + Twig.expression.type.arrowFunction, Twig.expression.type.parameter.start ]), compile(token, stack, output) { diff --git a/src/twig.expression.operator.js b/src/twig.expression.operator.js index 452add4e..027382e1 100644 --- a/src/twig.expression.operator.js +++ b/src/twig.expression.operator.js @@ -93,6 +93,11 @@ module.exports = function (Twig) { token.associativity = Twig.expression.operator.leftToRight; break; + case '<=>': + token.precidence = 9; + token.associativity = Twig.expression.operator.leftToRight; + break; + case '<': case '<=': case '>': @@ -275,6 +280,10 @@ module.exports = function (Twig) { stack.push(!Twig.lib.boolval(b)); break; + case '<=>': + stack.push(a === b ? 0 : (a < b ? -1 : 1)); + break; + case '<': stack.push(a < b); break; diff --git a/src/twig.filters.js b/src/twig.filters.js index 18b3fd89..f1b744ee 100644 --- a/src/twig.filters.js +++ b/src/twig.filters.js @@ -72,9 +72,24 @@ module.exports = function (Twig) { return value; } }, - sort(value) { + sort(value, params) { if (is('Array', value)) { - return value.sort(); + let ret; + if (params) { + const callBackParams = params.params.split(','); + ret = value.sort((_a, _b) => { + const data = {}; + data[callBackParams[0]] = _a; + data[callBackParams[1]] = _b; + + const template = Twig.exports.twig({data: params.body}); + return template.render(data); + }); + } else { + ret = value.sort(); + } + + return ret; } if (is('Object', value)) { @@ -820,6 +835,50 @@ module.exports = function (Twig) { }, spaceless(value) { return value.replace(/>\s+<').trim(); + }, + filter(value, params) { + if (is('Array', value)) { + return value.filter(_a => { + const data = {}; + data[params.params] = _a; + + const template = Twig.exports.twig({data: params.body}); + return template.render(data) === 'true'; + }); + } + }, + map(value, params) { + if (is('Array', value)) { + const callBackParams = params.params.split(','); + // Since Javascript does not support a callBack function to map() with both keys and values; we use forEach here + // Note: Twig and PHP use ((value[, key])) for map(); whereas Javascript uses (([key, ]value)) for forEach() + const newValue = []; + value.forEach((_b, _a) => { + const data = {}; + data[callBackParams[0].trim()] = _b; + if (callBackParams[1]) { + data[callBackParams[1].trim()] = _a; + } + + const template = Twig.exports.twig({data: params.body}); + newValue[_a] = template.render(data); + }); + return newValue; + } + }, + reduce(value, params) { + if (is('Array', value)) { + const callBackParams = params.params.split(','); + return value.reduce((_carry, _v, _k) => { + const data = {}; + data[callBackParams[0]] = _carry; + data[callBackParams[1].trim()] = _v; + data[callBackParams[2].trim()] = _k; + + const template = Twig.exports.twig({data: params.body}); + return template.render(data); + }, params.args || 0); + } } }; diff --git a/test/test.expressions.js b/test/test.expressions.js index fb414283..6620479a 100644 --- a/test/test.expressions.js +++ b/test/test.expressions.js @@ -173,6 +173,13 @@ describe('Twig.js Expressions ->', function () { {a: false, b: true}, {a: false, b: false} ]; + it('should support spaceship operator', function () { + const testTemplate = twig({data: '{{ a <=> b }}'}); + numericTestData.forEach(pair => { + const output = testTemplate.render(pair); + output.should.equal((pair.a === pair.b ? 0 : (pair.a < pair.b ? -1 : 1)).toString()); + }); + }); it('should support less then', function () { const testTemplate = twig({data: '{{ a < b }}'}); numericTestData.forEach(pair => { diff --git a/test/test.filters.js b/test/test.filters.js index a0323ab0..35960215 100644 --- a/test/test.filters.js +++ b/test/test.filters.js @@ -151,6 +151,10 @@ describe('Twig.js Filters ->', function () { testTemplate = twig({data: '{% set obj = {\'z\':\'abc\',\'a\':2,\'y\':7,\'m\':\'test\'} %}{% for key,value in obj|sort %}{{key}}:{{value}} {%endfor %}'}); testTemplate.render().should.equal('a:2 y:7 z:abc m:test '); }); + it('should sort an array of objects with a callback function', function () { + let testTemplate = twig({data: '{% for item in items|sort((left,right) => left.num - right.num) %}{{ item|json_encode }}{% endfor %}'}); + testTemplate.render({items:[{id: 1,num: 6},{id: 2, num: 3},{id: 3, num: 4}]}).should.equal('{"id":2,"num":3}{"id":3,"num":4}{"id":1,"num":6}'); + }); it('should handle undefined', function () { const testTemplate = twig({data: '{% set obj = undef|sort %}{% for key, value in obj|sort %}{{key}}:{{value}}{%endfor%}'}); @@ -855,6 +859,42 @@ describe('Twig.js Filters ->', function () { }); }); + describe('filter ->', function () { + it('should filter an array (with perenthesis)', function () { + let testTemplate = twig({data: '{{ [1,5,2,7,8]|filter((f) => f % 2 == 0) }}'}); + testTemplate.render().should.equal('2,8'); + }); + + it('should filter an array (without perenthesis)', function () { + let testTemplate = twig({data: '{{ [1,5,2,7,8]|filter(f => f % 2 == 0) }}'}); + testTemplate.render().should.equal('2,8'); + }); + }); + + describe('map ->', function () { + it('should map an array (with keys)', function () { + let testTemplate = twig({data: '{{ [1,5,2,7,8]|map((v, k) => v*v+k) }}'}); + testTemplate.render().should.equal('1,26,6,52,68'); + }); + + it('should map an array', function () { + let testTemplate = twig({data: '{{ [1,5,2,7,8]|map((v) => v*v) }}'}); + testTemplate.render().should.equal('1,25,4,49,64'); + }); + }); + + describe('reduce ->', function () { + it('should reduce an array (with inital value)', function () { + let testTemplate = twig({data: '{{ [1,2,3]|reduce((carry, v, k) => carry + v * k) }}'}); + testTemplate.render().should.equal('8'); + }); + + it('should reduce an array)', function () { + let testTemplate = twig({data: '{{ [1,2,3]|reduce((carry, v, k) => carry + v * k, 10) }}'}); + testTemplate.render().should.equal('18'); + }); + }); + it('should chain', function () { const testTemplate = twig({data: '{{ ["a", "b", "c"]|keys|reverse }}'}); testTemplate.render().should.equal('2,1,0');