diff --git a/src/context/context.ts b/src/context/context.ts index d5a1b2f86..ab09843fc 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -5,6 +5,8 @@ import { Scope } from './scope' import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore' import { InternalUndefinedVariableError } from '../util/error' +type PropertyKey = string | number; + export class Context { /** * insert a Context-level empty scope, @@ -54,11 +56,11 @@ export class Context { return [this.globals, this.environments, ...this.scopes] .reduce((ctx, val) => __assign(ctx, val), {}) } - public get (paths: string[]) { + public get (paths: PropertyKey[]) { const scope = this.findScope(paths[0]) return this.getFromScope(scope, paths) } - public getFromScope (scope: object, paths: string[] | string) { + public getFromScope (scope: object, paths: PropertyKey[] | string) { if (isString(paths)) paths = paths.split('.') return paths.reduce((scope, path, i) => { scope = readProperty(scope, path, this.opts.ownPropertyOnly) @@ -77,7 +79,7 @@ export class Context { public bottom () { return this.scopes[0] } - private findScope (key: string) { + private findScope (key: string | number) { for (let i = this.scopes.length - 1; i >= 0; i--) { const candidate = this.scopes[i] if (key in candidate) return candidate @@ -87,21 +89,19 @@ export class Context { } } -export function readProperty (obj: Scope, key: string, ownPropertyOnly: boolean) { +export function readProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) { if (isNil(obj)) return obj obj = toLiquid(obj) + if (isArray(obj) && key < 0) return obj[obj.length + +key] const jsProperty = readJSProperty(obj, key, ownPropertyOnly) + if (jsProperty === undefined && obj instanceof Drop) return obj.liquidMethodMissing(key) if (isFunction(jsProperty)) return jsProperty.call(obj) - if (obj instanceof Drop) { - if (obj.hasOwnProperty(key)) return obj[key] - return obj.liquidMethodMissing(key) - } if (key === 'size') return readSize(obj) - if (key === 'first') return readFirst(obj) - if (key === 'last') return readLast(obj) + else if (key === 'first') return readFirst(obj) + else if (key === 'last') return readLast(obj) return jsProperty } -export function readJSProperty (obj: Scope, key: string, ownPropertyOnly: boolean) { +export function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) { if (ownPropertyOnly && !Object.hasOwnProperty.call(obj, key)) return undefined return obj[key] } diff --git a/src/drop/drop.ts b/src/drop/drop.ts index 8ea839c75..b4a2c98ad 100644 --- a/src/drop/drop.ts +++ b/src/drop/drop.ts @@ -3,7 +3,7 @@ export abstract class Drop { return undefined } - public liquidMethodMissing (key: string): Promise | string | undefined { + public liquidMethodMissing (key: string | number): Promise | string | undefined { return undefined } } diff --git a/test/e2e/issues.ts b/test/e2e/issues.ts index 882e45e25..1734400d4 100644 --- a/test/e2e/issues.ts +++ b/test/e2e/issues.ts @@ -227,4 +227,9 @@ describe('Issues', function () { const html = await engine.parseAndRender(`{{ foo | concat | json }}`) expect(html).to.equal('[]') }) + it('#486 Access array items from the right with negative indexes', async () => { + const engine = new Liquid() + const html = await engine.parseAndRender(`{% assign a = "x,y,z" | split: ',' -%}{{ a[-1] }} {{ a[-3] }} {{ a[-8] }}`) + expect(html).to.equal('z x ') + }) }) diff --git a/test/unit/context/context.ts b/test/unit/context/context.ts index 3b277686d..3436334fa 100644 --- a/test/unit/context/context.ts +++ b/test/unit/context/context.ts @@ -22,7 +22,8 @@ describe('Context', function () { zoo: 'coo', 'Mr.Smith': 'John', arr: ['a', 'b'] - } + }, + arr: ['a', 'b', 'c', 'd'] } ctx = new Context(scope) }) @@ -69,6 +70,12 @@ describe('Context', function () { it('should read .last of array', async function () { expect(ctx.get(['bar', 'arr', 'last'])).to.equal('b') }) + it('should read element of array', async function () { + expect(ctx.get(['arr', 1])).to.equal('b') + }) + it('should read element of array from end', async function () { + expect(ctx.get(['arr', -2])).to.equal('c') + }) it('should call function', async function () { expect(ctx.get(['func'])).to.equal('FUNC') })