diff --git a/src/builtin/tags/for.ts b/src/builtin/tags/for.ts index b8f43d667b..5b217c6947 100644 --- a/src/builtin/tags/for.ts +++ b/src/builtin/tags/for.ts @@ -3,14 +3,18 @@ import { toEnumerable } from '../../util/collection' import { ForloopDrop } from '../../drop/forloop-drop' import { Hash } from '../../template/tag/hash' +const MODIFIERS = ['offset', 'limit', 'reversed'] + +type valueof = T[keyof T] + export default { type: 'block', parse: function (token: TagToken, remainTokens: TopLevelToken[]) { - const toknenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie) + const tokenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie) - const variable = toknenizer.readIdentifier() - const inStr = toknenizer.readIdentifier() - const collection = toknenizer.readValue() + const variable = tokenizer.readIdentifier() + const inStr = tokenizer.readIdentifier() + const collection = tokenizer.readValue() assert( variable.size() && inStr.content === 'in' && collection, () => `illegal tag: ${token.getText()}` @@ -44,14 +48,15 @@ export default { } const hash = yield this.hash.render(ctx) - const offset = hash.offset || 0 - const limit = (hash.limit === undefined) ? collection.length : hash.limit - const reversedIndex = Reflect.ownKeys(hash).indexOf('reversed') + const modifiers = this.liquid.options.orderedFilterParameters + ? Object.keys(hash).filter(x => MODIFIERS.includes(x)) + : MODIFIERS.filter(x => hash[x] !== undefined) - // reverse collection before slicing if 'reversed' is 1st parameter - if (reversedIndex === 0) collection.reverse() - collection = collection.slice(offset, offset + limit) - if (reversedIndex > 0) collection.reverse() + collection = modifiers.reduce((collection, modifier: valueof) => { + if (modifier === 'offset') return offset(collection, hash['offset']) + if (modifier === 'limit') return limit(collection, hash['limit']) + return reversed(collection) + }, collection) const scope = { forloop: new ForloopDrop(collection.length) } ctx.push(scope) @@ -68,3 +73,15 @@ export default { ctx.pop() } } as TagImplOptions + +function reversed (arr: Array) { + return [...arr].reverse() +} + +function offset (arr: Array, count: number) { + return arr.slice(count) +} + +function limit (arr: Array, count: number) { + return arr.slice(0, count) +} diff --git a/src/liquid-options.ts b/src/liquid-options.ts index f65905065a..ba3978c297 100644 --- a/src/liquid-options.ts +++ b/src/liquid-options.ts @@ -55,6 +55,8 @@ export interface LiquidOptions { keepOutputType?: boolean; /** An object of operators for conditional statements. Defaults to the regular Liquid operators. */ operators?: Operators; + /** Respect parameter order when using filters like "for ... reversed limit", Defaults to `false`. */ + orderedFilterParameters?: boolean; } interface NormalizedOptions extends LiquidOptions { diff --git a/src/template/tag/hash.ts b/src/template/tag/hash.ts index 37a1003e2d..427f7951e4 100644 --- a/src/template/tag/hash.ts +++ b/src/template/tag/hash.ts @@ -21,7 +21,7 @@ export class Hash { * render (ctx: Context) { const hash = {} for (const key of Object.keys(this.hash)) { - hash[key] = yield evalToken(this.hash[key], ctx) + hash[key] = this.hash[key] === undefined ? true : yield evalToken(this.hash[key], ctx) } return hash } diff --git a/test/integration/builtin/tags/for.ts b/test/integration/builtin/tags/for.ts index 81ccb97e0a..fdc1d5ebc8 100644 --- a/test/integration/builtin/tags/for.ts +++ b/test/integration/builtin/tags/for.ts @@ -215,6 +215,13 @@ describe('tags/for', function () { }) it('should support for reversed in the first position', async function () { + const src = '{% for i in (1..8) reversed limit:2 %}{{ i }}{% endfor %}' + const html = await liquid.parseAndRender(src, scope) + return expect(html).to.equal('21') + }) + + it('should support for reversed in the first position with orderedFilterParameters=true', async function () { + const liquid = new Liquid({ orderedFilterParameters: true }) const src = '{% for i in (1..8) reversed limit:2 %}{{ i }}{% endfor %}' const html = await liquid.parseAndRender(src, scope) return expect(html).to.equal('87') @@ -225,6 +232,13 @@ describe('tags/for', function () { const html = await liquid.parseAndRender(src) return expect(html).to.equal('543') }) + + it('should support for reversed in the middle position with orderedFilterParameters=true', async function () { + const liquid = new Liquid({ orderedFilterParameters: true }) + const src = '{% for i in (1..8) offset:2 reversed limit:3 %}{{ i }}{% endfor %}' + const html = await liquid.parseAndRender(src) + return expect(html).to.equal('876') + }) }) describe('sync', function () { diff --git a/test/unit/template/hash.ts b/test/unit/template/hash.ts index 1966943775..5e12ac8ea4 100644 --- a/test/unit/template/hash.ts +++ b/test/unit/template/hash.ts @@ -9,7 +9,7 @@ describe('Hash', function () { it('should parse "reverse"', async function () { const hash = await toThenable(new Hash('reverse').render(new Context({ foo: 3 }))) expect(hash).to.haveOwnProperty('reverse') - expect(hash.reverse).to.be.undefined + expect(hash.reverse).to.be.true }) it('should parse "num:foo"', async function () { const hash = await toThenable(new Hash('num:foo').render(new Context({ foo: 3 }))) @@ -37,7 +37,7 @@ describe('Hash', function () { const hash = await toThenable(new Hash('num1:2.3 reverse,num2:bar.coo\n num3: arr[0]').render(ctx)) expect(hash).to.deep.equal({ num1: 2.3, - reverse: undefined, + reverse: true, num2: 3, num3: 4 })