diff --git a/src/twig.expression.js b/src/twig.expression.js index 60040bdf..b9175b68 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, @@ -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. @@ -784,6 +811,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.filters.js b/src/twig.filters.js index 30ad66a8..17cf2166 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)) { @@ -824,6 +839,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.filters.js b/test/test.filters.js index c832d2c8..834c2bce 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 an arrow 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%}'}); @@ -859,6 +863,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');