diff --git a/src/builtin/filters/array.ts b/src/builtin/filters/array.ts index 885e2345a..2e760948e 100644 --- a/src/builtin/filters/array.ts +++ b/src/builtin/filters/array.ts @@ -1,5 +1,6 @@ import { isArray, last } from '../../util/underscore' import { isTruthy } from '../../render/boolean' +import { FilterImpl } from '../../template/filter/filter-impl' export default { 'join': (v: any[], arg: string) => v.join(arg === undefined ? ' ' : arg), @@ -28,8 +29,11 @@ function slice (v: T[], begin: number, length = 1): T[] { return v.slice(begin, begin + length) } -function where (arr: T[], property: string, value?: any): T[] { - return arr.filter(obj => value === undefined ? isTruthy(obj[property]) : obj[property] === value) +function where (this: FilterImpl, arr: T[], property: string, expected?: any): T[] { + return arr.filter(obj => { + const value = this.context.getFromScope(obj, property) + return expected === undefined ? isTruthy(value) : value === expected + }) } function uniq (arr: T[]): T[] { diff --git a/src/context/context.ts b/src/context/context.ts index d5b8a0de9..3d211c8f7 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -1,9 +1,9 @@ -import * as _ from '../util/underscore' import { Drop } from '../drop/drop' import { __assign } from 'tslib' import { assert } from '../util/assert' import { NormalizedFullOptions, applyDefault } from '../liquid-options' import { Scope } from './scope' +import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore' export class Context { private scopes: Scope[] = [{}] @@ -28,14 +28,18 @@ export class Context { } public get (path: string) { const paths = this.parseProp(path) - let ctx = this.findScope(paths[0]) || this.environments - for (const path of paths) { - ctx = readProperty(ctx, path) - if (_.isNil(ctx) && this.opts.strictVariables) { + const scope = this.findScope(paths[0]) || this.environments + return this.getFromScope(scope, paths) + } + public getFromScope (scope: object, paths: string[] | string) { + if (!isArray(paths)) paths = this.parseProp(paths) + return paths.reduce((scope, path) => { + scope = readProperty(scope, path) + if (isNil(scope) && this.opts.strictVariables) { throw new TypeError(`undefined variable: ${path}`) } - } - return ctx + return scope + }, scope) } public push (ctx: object) { return this.scopes.push(ctx) @@ -115,11 +119,11 @@ export class Context { } } -function readProperty (obj: Scope, key: string) { - if (_.isNil(obj)) return obj - obj = _.toLiquid(obj) +export function readProperty (obj: Scope, key: string) { + if (isNil(obj)) return obj + obj = toLiquid(obj) if (obj instanceof Drop) { - if (_.isFunction(obj[key])) return obj[key]() + if (isFunction(obj[key])) return obj[key]() if (obj.hasOwnProperty(key)) return obj[key] return obj.liquidMethodMissing(key) } @@ -130,17 +134,17 @@ function readProperty (obj: Scope, key: string) { } function readFirst (obj: Scope) { - if (_.isArray(obj)) return obj[0] + if (isArray(obj)) return obj[0] return obj['first'] } function readLast (obj: Scope) { - if (_.isArray(obj)) return obj[obj.length - 1] + if (isArray(obj)) return obj[obj.length - 1] return obj['last'] } function readSize (obj: Scope) { - if (_.isArray(obj) || _.isString(obj)) return obj.length + if (isArray(obj) || isString(obj)) return obj.length return obj['size'] } diff --git a/src/template/filter/filter.ts b/src/template/filter/filter.ts index 5f0008045..c574de374 100644 --- a/src/template/filter/filter.ts +++ b/src/template/filter/filter.ts @@ -8,9 +8,9 @@ type FilterArg = string|KeyValuePair export type FilterArgs = FilterArg[] export class Filter { - private name: string + public name: string + public args: FilterArgs private impl: FilterImplOptions - private args: FilterArgs private static impls: {[key: string]: FilterImplOptions} = {} public constructor (name: string, args: FilterArgs, strictFilters: boolean) { diff --git a/src/template/value.ts b/src/template/value.ts index eaf9e4053..5bf2d4bb0 100644 --- a/src/template/value.ts +++ b/src/template/value.ts @@ -3,9 +3,9 @@ import { FilterArgs, Filter } from './filter/filter' import { Context } from '../context/context' export class Value { + public readonly filters: Filter[] = [] + public readonly initial: string private strictFilters: boolean - private initial: string - private filters: Filter[] = [] /** * @param str value string, like: "i have a dream | truncate: 3 diff --git a/test/integration/builtin/filters/array.ts b/test/integration/builtin/filters/array.ts index ff3646797..344ac335a 100644 --- a/test/integration/builtin/filters/array.ts +++ b/test/integration/builtin/filters/array.ts @@ -169,5 +169,22 @@ Available products: - Boring sneakers `) }) + it('should support nested property', async function () { + const authors = [ + { name: 'Alice', books: { year: 2019 } }, + { name: 'Bob', books: { year: 2018 } } + ] + const html = await liquid.parseAndRender( + `{% assign recentAuthors = authors | where: 'books.year', 2019 %} +Recent Authors: +{%- for author in recentAuthors %} + - {{author.name}} +{%- endfor %}`, + { authors } + ) + expect(html).to.equal(` +Recent Authors: + - Alice`) + }) }) }) diff --git a/test/unit/template/value.ts b/test/unit/template/value.ts index fe3ea1206..c03ccf42c 100644 --- a/test/unit/template/value.ts +++ b/test/unit/template/value.ts @@ -15,70 +15,70 @@ describe('Value', function () { describe('#constructor()', function () { it('should parse "foo', function () { - const tpl = new Value('foo', false) as any + const tpl = new Value('foo', false) expect(tpl.initial).to.equal('foo') expect(tpl.filters).to.deep.equal([]) }) it('should parse "foo | add"', function () { - const tpl = new Value('foo | add', false) as any + const tpl = new Value('foo | add', false) expect(tpl.initial).to.equal('foo') expect(tpl.filters.length).to.equal(1) expect(tpl.filters[0].args).to.eql([]) }) it('should parse "foo,foo | add"', function () { - const tpl = new Value('foo,foo | add', false) as any - expect(tpl.initial).to.equal('foo') as any + const tpl = new Value('foo,foo | add', false) + expect(tpl.initial).to.equal('foo') expect(tpl.filters.length).to.equal(1) expect(tpl.filters[0].args).to.eql([]) }) it('should parse "foo | add: 3, false"', function () { - const tpl = new Value('foo | add: 3, "foo"', false) as any + const tpl = new Value('foo | add: 3, "foo"', false) expect(tpl.initial).to.equal('foo') expect(tpl.filters.length).to.equal(1) expect(tpl.filters[0].args).to.eql(['3', '"foo"']) }) it('should parse "foo | add: "foo" bar, 3"', function () { - const tpl = new Value('foo | add: "foo" bar, 3', false) as any + const tpl = new Value('foo | add: "foo" bar, 3', false) expect(tpl.initial).to.equal('foo') expect(tpl.filters.length).to.equal(1) expect(tpl.filters[0].name).to.eql('add') expect(tpl.filters[0].args).to.eql(['"foo"', '3']) }) it('should parse "foo | add: "|", 3', function () { - const tpl = new Value('foo | add: "|", 3', false) as any + const tpl = new Value('foo | add: "|", 3', false) expect(tpl.initial).to.equal('foo') expect(tpl.filters.length).to.equal(1) expect(tpl.filters[0].args).to.eql(['"|"', '3']) }) it('should parse "foo | add: "|", 3', function () { - const tpl = new Value('foo | add: "|", 3', false) as any + const tpl = new Value('foo | add: "|", 3', false) expect(tpl.initial).to.equal('foo') expect(tpl.filters.length).to.equal(1) expect(tpl.filters[0].args).to.eql(['"|"', '3']) }) it('should support arguments as named key/values', function () { - const f = new Value('o | foo: key1: "literal1", key2: value2', false) as any + const f = new Value('o | foo: key1: "literal1", key2: value2', false) expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].args).to.eql([['key1', '"literal1"'], ['key2', 'value2']]) }) it('should support arguments as named key/values with inline literals', function () { - const f = new Value('o | foo: "test0", key1: "literal1", key2: value2', false) as any + const f = new Value('o | foo: "test0", key1: "literal1", key2: value2', false) expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].args).to.deep.equal(['"test0"', ['key1', '"literal1"'], ['key2', 'value2']]) }) it('should support arguments as named key/values with inline values', function () { - const f = new Value('o | foo: test0, key1: "literal1", key2: value2', false) as any + const f = new Value('o | foo: test0, key1: "literal1", key2: value2', false) expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].args).to.deep.equal(['test0', ['key1', '"literal1"'], ['key2', 'value2']]) }) it('should support argument values named same as keys', function () { - const f = new Value('o | foo: a: a', false) as any + const f = new Value('o | foo: a: a', false) expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].args).to.deep.equal([['a', 'a']]) }) it('should support argument literals named same as keys', function () { - const f = new Value('o | foo: a: "a"', false) as any + const f = new Value('o | foo: a: "a"', false) expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].args).to.deep.equal([['a', '"a"']]) })