feat: Access array item by negative index, closes #486

This commit is contained in:
Harttle
2022-03-06 00:52:22 +08:00
parent c503cb23df
commit 049685b9a0
4 changed files with 25 additions and 13 deletions
+11 -11
View File
@@ -5,6 +5,8 @@ import { Scope } from './scope'
import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore' import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore'
import { InternalUndefinedVariableError } from '../util/error' import { InternalUndefinedVariableError } from '../util/error'
type PropertyKey = string | number;
export class Context { export class Context {
/** /**
* insert a Context-level empty scope, * insert a Context-level empty scope,
@@ -54,11 +56,11 @@ export class Context {
return [this.globals, this.environments, ...this.scopes] return [this.globals, this.environments, ...this.scopes]
.reduce((ctx, val) => __assign(ctx, val), {}) .reduce((ctx, val) => __assign(ctx, val), {})
} }
public get (paths: string[]) { public get (paths: PropertyKey[]) {
const scope = this.findScope(paths[0]) const scope = this.findScope(paths[0])
return this.getFromScope(scope, paths) 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('.') if (isString(paths)) paths = paths.split('.')
return paths.reduce((scope, path, i) => { return paths.reduce((scope, path, i) => {
scope = readProperty(scope, path, this.opts.ownPropertyOnly) scope = readProperty(scope, path, this.opts.ownPropertyOnly)
@@ -77,7 +79,7 @@ export class Context {
public bottom () { public bottom () {
return this.scopes[0] return this.scopes[0]
} }
private findScope (key: string) { private findScope (key: string | number) {
for (let i = this.scopes.length - 1; i >= 0; i--) { for (let i = this.scopes.length - 1; i >= 0; i--) {
const candidate = this.scopes[i] const candidate = this.scopes[i]
if (key in candidate) return candidate 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 if (isNil(obj)) return obj
obj = toLiquid(obj) obj = toLiquid(obj)
if (isArray(obj) && key < 0) return obj[obj.length + +key]
const jsProperty = readJSProperty(obj, key, ownPropertyOnly) const jsProperty = readJSProperty(obj, key, ownPropertyOnly)
if (jsProperty === undefined && obj instanceof Drop) return obj.liquidMethodMissing(key)
if (isFunction(jsProperty)) return jsProperty.call(obj) 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 === 'size') return readSize(obj)
if (key === 'first') return readFirst(obj) else if (key === 'first') return readFirst(obj)
if (key === 'last') return readLast(obj) else if (key === 'last') return readLast(obj)
return jsProperty 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 if (ownPropertyOnly && !Object.hasOwnProperty.call(obj, key)) return undefined
return obj[key] return obj[key]
} }
+1 -1
View File
@@ -3,7 +3,7 @@ export abstract class Drop {
return undefined return undefined
} }
public liquidMethodMissing (key: string): Promise<string | undefined> | string | undefined { public liquidMethodMissing (key: string | number): Promise<string | undefined> | string | undefined {
return undefined return undefined
} }
} }
+5
View File
@@ -227,4 +227,9 @@ describe('Issues', function () {
const html = await engine.parseAndRender(`{{ foo | concat | json }}`) const html = await engine.parseAndRender(`{{ foo | concat | json }}`)
expect(html).to.equal('[]') 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 ')
})
}) })
+8 -1
View File
@@ -22,7 +22,8 @@ describe('Context', function () {
zoo: 'coo', zoo: 'coo',
'Mr.Smith': 'John', 'Mr.Smith': 'John',
arr: ['a', 'b'] arr: ['a', 'b']
} },
arr: ['a', 'b', 'c', 'd']
} }
ctx = new Context(scope) ctx = new Context(scope)
}) })
@@ -69,6 +70,12 @@ describe('Context', function () {
it('should read .last of array', async function () { it('should read .last of array', async function () {
expect(ctx.get(['bar', 'arr', 'last'])).to.equal('b') 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 () { it('should call function', async function () {
expect(ctx.get(['func'])).to.equal('FUNC') expect(ctx.get(['func'])).to.equal('FUNC')
}) })