Skip to content

Commit 60ec74f

Browse files
committed
feat: nested property for the where filter, #178
1 parent 6502984 commit 60ec74f

File tree

6 files changed

+58
-33
lines changed

6 files changed

+58
-33
lines changed

src/builtin/filters/array.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isArray, last } from '../../util/underscore'
22
import { isTruthy } from '../../render/boolean'
3+
import { FilterImpl } from '../../template/filter/filter-impl'
34

45
export default {
56
'join': (v: any[], arg: string) => v.join(arg === undefined ? ' ' : arg),
@@ -28,8 +29,11 @@ function slice<T> (v: T[], begin: number, length = 1): T[] {
2829
return v.slice(begin, begin + length)
2930
}
3031

31-
function where<T> (arr: T[], property: string, value?: any): T[] {
32-
return arr.filter(obj => value === undefined ? isTruthy(obj[property]) : obj[property] === value)
32+
function where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): T[] {
33+
return arr.filter(obj => {
34+
const value = this.context.getFromScope(obj, property)
35+
return expected === undefined ? isTruthy(value) : value === expected
36+
})
3337
}
3438

3539
function uniq<T> (arr: T[]): T[] {

src/context/context.ts

+18-14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import * as _ from '../util/underscore'
21
import { Drop } from '../drop/drop'
32
import { __assign } from 'tslib'
43
import { assert } from '../util/assert'
54
import { NormalizedFullOptions, applyDefault } from '../liquid-options'
65
import { Scope } from './scope'
6+
import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore'
77

88
export class Context {
99
private scopes: Scope[] = [{}]
@@ -28,14 +28,18 @@ export class Context {
2828
}
2929
public get (path: string) {
3030
const paths = this.parseProp(path)
31-
let ctx = this.findScope(paths[0]) || this.environments
32-
for (const path of paths) {
33-
ctx = readProperty(ctx, path)
34-
if (_.isNil(ctx) && this.opts.strictVariables) {
31+
const scope = this.findScope(paths[0]) || this.environments
32+
return this.getFromScope(scope, paths)
33+
}
34+
public getFromScope (scope: object, paths: string[] | string) {
35+
if (!isArray(paths)) paths = this.parseProp(paths)
36+
return paths.reduce((scope, path) => {
37+
scope = readProperty(scope, path)
38+
if (isNil(scope) && this.opts.strictVariables) {
3539
throw new TypeError(`undefined variable: ${path}`)
3640
}
37-
}
38-
return ctx
41+
return scope
42+
}, scope)
3943
}
4044
public push (ctx: object) {
4145
return this.scopes.push(ctx)
@@ -115,11 +119,11 @@ export class Context {
115119
}
116120
}
117121

118-
function readProperty (obj: Scope, key: string) {
119-
if (_.isNil(obj)) return obj
120-
obj = _.toLiquid(obj)
122+
export function readProperty (obj: Scope, key: string) {
123+
if (isNil(obj)) return obj
124+
obj = toLiquid(obj)
121125
if (obj instanceof Drop) {
122-
if (_.isFunction(obj[key])) return obj[key]()
126+
if (isFunction(obj[key])) return obj[key]()
123127
if (obj.hasOwnProperty(key)) return obj[key]
124128
return obj.liquidMethodMissing(key)
125129
}
@@ -130,17 +134,17 @@ function readProperty (obj: Scope, key: string) {
130134
}
131135

132136
function readFirst (obj: Scope) {
133-
if (_.isArray(obj)) return obj[0]
137+
if (isArray(obj)) return obj[0]
134138
return obj['first']
135139
}
136140

137141
function readLast (obj: Scope) {
138-
if (_.isArray(obj)) return obj[obj.length - 1]
142+
if (isArray(obj)) return obj[obj.length - 1]
139143
return obj['last']
140144
}
141145

142146
function readSize (obj: Scope) {
143-
if (_.isArray(obj) || _.isString(obj)) return obj.length
147+
if (isArray(obj) || isString(obj)) return obj.length
144148
return obj['size']
145149
}
146150

src/template/filter/filter.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ type FilterArg = string|KeyValuePair
88
export type FilterArgs = FilterArg[]
99

1010
export class Filter {
11-
private name: string
11+
public name: string
12+
public args: FilterArgs
1213
private impl: FilterImplOptions
13-
private args: FilterArgs
1414
private static impls: {[key: string]: FilterImplOptions} = {}
1515

1616
public constructor (name: string, args: FilterArgs, strictFilters: boolean) {

src/template/value.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { FilterArgs, Filter } from './filter/filter'
33
import { Context } from '../context/context'
44

55
export class Value {
6+
public readonly filters: Filter[] = []
7+
public readonly initial: string
68
private strictFilters: boolean
7-
private initial: string
8-
private filters: Filter[] = []
99

1010
/**
1111
* @param str value string, like: "i have a dream | truncate: 3

test/integration/builtin/filters/array.ts

+17
Original file line numberDiff line numberDiff line change
@@ -169,5 +169,22 @@ Available products:
169169
- Boring sneakers
170170
`)
171171
})
172+
it('should support nested property', async function () {
173+
const authors = [
174+
{ name: 'Alice', books: { year: 2019 } },
175+
{ name: 'Bob', books: { year: 2018 } }
176+
]
177+
const html = await liquid.parseAndRender(
178+
`{% assign recentAuthors = authors | where: 'books.year', 2019 %}
179+
Recent Authors:
180+
{%- for author in recentAuthors %}
181+
- {{author.name}}
182+
{%- endfor %}`,
183+
{ authors }
184+
)
185+
expect(html).to.equal(`
186+
Recent Authors:
187+
- Alice`)
188+
})
172189
})
173190
})

test/unit/template/value.ts

+13-13
Original file line numberDiff line numberDiff line change
@@ -15,70 +15,70 @@ describe('Value', function () {
1515

1616
describe('#constructor()', function () {
1717
it('should parse "foo', function () {
18-
const tpl = new Value('foo', false) as any
18+
const tpl = new Value('foo', false)
1919
expect(tpl.initial).to.equal('foo')
2020
expect(tpl.filters).to.deep.equal([])
2121
})
2222

2323
it('should parse "foo | add"', function () {
24-
const tpl = new Value('foo | add', false) as any
24+
const tpl = new Value('foo | add', false)
2525
expect(tpl.initial).to.equal('foo')
2626
expect(tpl.filters.length).to.equal(1)
2727
expect(tpl.filters[0].args).to.eql([])
2828
})
2929
it('should parse "foo,foo | add"', function () {
30-
const tpl = new Value('foo,foo | add', false) as any
31-
expect(tpl.initial).to.equal('foo') as any
30+
const tpl = new Value('foo,foo | add', false)
31+
expect(tpl.initial).to.equal('foo')
3232
expect(tpl.filters.length).to.equal(1)
3333
expect(tpl.filters[0].args).to.eql([])
3434
})
3535
it('should parse "foo | add: 3, false"', function () {
36-
const tpl = new Value('foo | add: 3, "foo"', false) as any
36+
const tpl = new Value('foo | add: 3, "foo"', false)
3737
expect(tpl.initial).to.equal('foo')
3838
expect(tpl.filters.length).to.equal(1)
3939
expect(tpl.filters[0].args).to.eql(['3', '"foo"'])
4040
})
4141
it('should parse "foo | add: "foo" bar, 3"', function () {
42-
const tpl = new Value('foo | add: "foo" bar, 3', false) as any
42+
const tpl = new Value('foo | add: "foo" bar, 3', false)
4343
expect(tpl.initial).to.equal('foo')
4444
expect(tpl.filters.length).to.equal(1)
4545
expect(tpl.filters[0].name).to.eql('add')
4646
expect(tpl.filters[0].args).to.eql(['"foo"', '3'])
4747
})
4848
it('should parse "foo | add: "|", 3', function () {
49-
const tpl = new Value('foo | add: "|", 3', false) as any
49+
const tpl = new Value('foo | add: "|", 3', false)
5050
expect(tpl.initial).to.equal('foo')
5151
expect(tpl.filters.length).to.equal(1)
5252
expect(tpl.filters[0].args).to.eql(['"|"', '3'])
5353
})
5454
it('should parse "foo | add: "|", 3', function () {
55-
const tpl = new Value('foo | add: "|", 3', false) as any
55+
const tpl = new Value('foo | add: "|", 3', false)
5656
expect(tpl.initial).to.equal('foo')
5757
expect(tpl.filters.length).to.equal(1)
5858
expect(tpl.filters[0].args).to.eql(['"|"', '3'])
5959
})
6060
it('should support arguments as named key/values', function () {
61-
const f = new Value('o | foo: key1: "literal1", key2: value2', false) as any
61+
const f = new Value('o | foo: key1: "literal1", key2: value2', false)
6262
expect(f.filters[0].name).to.equal('foo')
6363
expect(f.filters[0].args).to.eql([['key1', '"literal1"'], ['key2', 'value2']])
6464
})
6565
it('should support arguments as named key/values with inline literals', function () {
66-
const f = new Value('o | foo: "test0", key1: "literal1", key2: value2', false) as any
66+
const f = new Value('o | foo: "test0", key1: "literal1", key2: value2', false)
6767
expect(f.filters[0].name).to.equal('foo')
6868
expect(f.filters[0].args).to.deep.equal(['"test0"', ['key1', '"literal1"'], ['key2', 'value2']])
6969
})
7070
it('should support arguments as named key/values with inline values', function () {
71-
const f = new Value('o | foo: test0, key1: "literal1", key2: value2', false) as any
71+
const f = new Value('o | foo: test0, key1: "literal1", key2: value2', false)
7272
expect(f.filters[0].name).to.equal('foo')
7373
expect(f.filters[0].args).to.deep.equal(['test0', ['key1', '"literal1"'], ['key2', 'value2']])
7474
})
7575
it('should support argument values named same as keys', function () {
76-
const f = new Value('o | foo: a: a', false) as any
76+
const f = new Value('o | foo: a: a', false)
7777
expect(f.filters[0].name).to.equal('foo')
7878
expect(f.filters[0].args).to.deep.equal([['a', 'a']])
7979
})
8080
it('should support argument literals named same as keys', function () {
81-
const f = new Value('o | foo: a: "a"', false) as any
81+
const f = new Value('o | foo: a: "a"', false)
8282
expect(f.filters[0].name).to.equal('foo')
8383
expect(f.filters[0].args).to.deep.equal([['a', '"a"']])
8484
})

0 commit comments

Comments
 (0)