Repository: twigjs/twig.js (js)

Issue:
The slice filter shorthand doesn't support variables
The slice filter type regex is `regex: /^\[(\d*:\d*)\]/,` which only matches something like `[10:]` but not `[length:]`. I can just use `|slice(length)` and `|slice(0, length)` but it's a little unexpected coming from php twig.

Pull request description:
Support variables in slice filter shorthand
Resolves #769 

Patch:
diff --git a/src/twig.expression.js b/src/twig.expression.js
index e58322c..60040bd 100644
--- a/src/twig.expression.js
+++ b/src/twig.expression.js
@@ -576,14 +576,14 @@ module.exports = function (Twig) {
         },
         {
             type: Twig.expression.type.slice,
-            regex: /^\[(\d*:\d*)\]/,
+            regex: /^\[(-?\w*:-?\w*)\]/,
             next: Twig.expression.set.operationsExtended,
             compile(token, stack, output) {
                 const sliceRange = token.match[1].split(':');
 
                 // SliceStart can be undefined when we pass parameters to the slice filter later
-                const sliceStart = (sliceRange[0]) ? parseInt(sliceRange[0], 10) : undefined;
-                const sliceEnd = (sliceRange[1]) ? parseInt(sliceRange[1], 10) : undefined;
+                const sliceStart = sliceRange[0];
+                const sliceEnd = sliceRange[1];
 
                 token.value = 'slice';
                 token.params = [sliceStart, sliceEnd];
@@ -596,11 +596,39 @@ module.exports = function (Twig) {
 
                 output.push(token);
             },
-            parse(token, stack) {
+            parse(token, stack, context) {
                 const input = stack.pop();
-                const {params} = token;
+                let {params} = token;
                 const state = this;
 
+                if (parseInt(params[0], 10).toString() === params[0]) {
+                    params[0] = parseInt(params[0], 10);
+                } else {
+                    const value = context[params[0]];
+                    if (state.template.options.strictVariables && value === undefined) {
+                        throw new Twig.Error('Variable "' + params[0] + '" does not exist.');
+                    }
+
+                    params[0] = value;
+                }
+
+                if (params[1]) {
+                    if (parseInt(params[1], 10).toString() === params[1]) {
+                        params[1] = parseInt(params[1], 10);
+                    } else {
+                        const value = context[params[1]];
+                        if (state.template.options.strictVariables && value === undefined) {
+                            throw new Twig.Error('Variable "' + params[1] + '" does not exist.');
+                        }
+
+                        if (value === undefined) {
+                            params = [params[0]];
+                        } else {
+                            params[1] = value;
+                        }
+                    }
+                }
+
                 stack.push(Twig.filter.call(state, token.value, input, params));
             }
         },
diff --git a/src/twig.filters.js b/src/twig.filters.js
index 18b3fd8..30ad66a 100644
--- a/src/twig.filters.js
+++ b/src/twig.filters.js
@@ -644,9 +644,13 @@ module.exports = function (Twig) {
             // Default to start of string
             const start = params[0] || 0;
             // Default to length of string
-            const length = params.length > 1 ? params[1] : value.length;
+            let length = params.length > 1 ? params[1] : value.length;
             // Handle negative start values
             const startIndex = start >= 0 ? start : Math.max(value.length + start, 0);
+            // Handle negative length values
+            if (length < 0) {
+                length = value.length - startIndex + length;
+            }
 
             if (Twig.lib.is('Array', value)) {
                 const output = [];


Test patch:
diff --git a/test/test.expressions.js b/test/test.expressions.js
index 3563bd8..1552ab5 100644
--- a/test/test.expressions.js
+++ b/test/test.expressions.js
@@ -467,6 +467,36 @@ describe('Twig.js Expressions ->', function () {
             output.should.equal('23');
         });
 
+        it('should support slice shorthand (full form) with negative start', function () {
+            const testTemplate = twig({data: '{{ "12345"[-2:1] }}'});
+            const output = testTemplate.render();
+            output.should.equal('4');
+        });
+
+        it('should support slice shorthand (full form) with negative lenght', function () {
+            const testTemplate = twig({data: '{{ "12345"[2:-1] }}'});
+            const output = testTemplate.render();
+            output.should.equal('34');
+        });
+
+        it('should support slice shorthand (full form) with variables as arguments', function () {
+            const testTemplate = twig({data: '{{ "12345"[start:length] }}'});
+            const output = testTemplate.render({start: 2, length: 3});
+            output.should.equal('345');
+        });
+
+        it('should support slice shorthand (full form) with variable as argument (omit first)', function () {
+            const testTemplate = twig({data: '{{ "12345"[:length] }}'});
+            const output = testTemplate.render({length: 3});
+            output.should.equal('123');
+        });
+
+        it('should support slice shorthand (full form) variable as argument (omit last)', function () {
+            const testTemplate = twig({data: '{{ "12345"[start:] }}'});
+            const output = testTemplate.render({start: 2});
+            output.should.equal('345');
+        });
+
         it('should support slice shorthand (omit first)', function () {
             const testTemplate = twig({data: '{{ "12345"[:2] }}'});
             const output = testTemplate.render();
diff --git a/test/test.filters.js b/test/test.filters.js
index a0323ab..c832d2c 100644
--- a/test/test.filters.js
+++ b/test/test.filters.js
@@ -640,6 +640,10 @@ describe('Twig.js Filters ->', function () {
             const testTemplate = twig({data: '{{ \'12345\'|slice(1, 2) }}'});
             testTemplate.render().should.equal('23');
         });
+        it('should slice a string with variables as arguments', function () {
+            const testTemplate = twig({data: '{{ \'12345\'|slice(start, length) }}'});
+            testTemplate.render({start: 2, length: 3}).should.equal('345');
+        });
         it('should slice a string to the end', function () {
             const testTemplate = twig({data: '{{ \'12345\'|slice(2) }}'});
             testTemplate.render().should.equal('345');