Skip to content

Commit c923598

Browse files
committed
fix: coerce to Array in map and where filter
1 parent d65ed40 commit c923598

File tree

12 files changed

+187
-284
lines changed

12 files changed

+187
-284
lines changed

src/builtin/filters/array.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isArray, last as arrayLast } from '../../util/underscore'
2+
import { toArray } from '../../util/collection'
23
import { isTruthy } from '../../render/boolean'
34
import { FilterImpl } from '../../template/filter/filter-impl'
45

@@ -10,11 +11,11 @@ export const sort = <T>(v: T[], arg: (lhs: T, rhs: T) => number) => v.sort(arg)
1011
export const size = (v: string | any[]) => (v && v.length) || 0
1112

1213
export function map<T1, T2> (arr: {[key: string]: T1}[], arg: string): T1[] {
13-
return arr.map(v => v[arg])
14+
return toArray(arr).map(v => v[arg])
1415
}
1516

1617
export function concat<T1, T2> (v: T1[], arg: T2[] | T2): (T1 | T2)[] {
17-
return Array.prototype.concat.call(v, arg)
18+
return toArray(v).concat(arg)
1819
}
1920

2021
export function slice<T> (v: T[], begin: number, length = 1): T[] {
@@ -23,8 +24,8 @@ export function slice<T> (v: T[], begin: number, length = 1): T[] {
2324
}
2425

2526
export function where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): T[] {
26-
return arr.filter(obj => {
27-
const value = this.context.getFromScope(obj, property.split('.'))
27+
return toArray(arr).filter(obj => {
28+
const value = this.context.getFromScope(obj, String(property).split('.'))
2829
return expected === undefined ? isTruthy(value) : value === expected
2930
})
3031
}

src/builtin/tags/for.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { assert, Tokenizer, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types'
2-
import { toCollection } from '../../util/collection'
2+
import { toEnumerable } from '../../util/collection'
33
import { ForloopDrop } from '../../drop/forloop-drop'
44
import { Hash } from '../../template/tag/hash'
55

@@ -36,7 +36,7 @@ export default {
3636
},
3737
render: function * (ctx: Context, emitter: Emitter) {
3838
const r = this.liquid.renderer
39-
let collection = toCollection(evalToken(this.collection, ctx))
39+
let collection = toEnumerable(evalToken(this.collection, ctx))
4040

4141
if (!collection.length) {
4242
yield r.renderTemplates(this.elseTemplates, ctx, emitter)

src/builtin/tags/render.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { assert } from '../../util/assert'
22
import { ForloopDrop } from '../../drop/forloop-drop'
3-
import { toCollection } from '../../util/collection'
3+
import { toEnumerable } from '../../util/collection'
44
import { evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
55

66
export default {
@@ -60,7 +60,7 @@ export default {
6060
if (this['for']) {
6161
const { value, alias } = this['for']
6262
let collection = evalToken(value, ctx)
63-
collection = toCollection(collection)
63+
collection = toEnumerable(collection)
6464
scope['forloop'] = new ForloopDrop(collection.length)
6565
for (const item of collection) {
6666
scope[alias] = item

src/builtin/tags/tablerow.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { toCollection } from '../../util/collection'
1+
import { toEnumerable } from '../../util/collection'
22
import { assert, evalToken, Emitter, Hash, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types'
33
import { TablerowloopDrop } from '../../drop/tablerowloop-drop'
44
import { Tokenizer } from '../../parser/tokenizer'
@@ -30,7 +30,7 @@ export default {
3030
},
3131

3232
render: function * (ctx: Context, emitter: Emitter) {
33-
let collection = toCollection(evalToken(this.collection, ctx))
33+
let collection = toEnumerable(evalToken(this.collection, ctx))
3434
const hash = yield this.hash.render(ctx)
3535
const offset = hash.offset || 0
3636
const limit = (hash.limit === undefined) ? collection.length : hash.limit

src/template/value.ts

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export class Value {
1616
const tokenizer = new Tokenizer(str)
1717
this.initial = tokenizer.readValue()
1818
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.filterMap.get(name), args))
19-
tokenizer.skipBlank()
2019
}
2120
public * value (ctx: Context) {
2221
let val = yield evalToken(this.initial, ctx)

src/util/collection.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { isString, isObject, isArray } from './underscore'
22

3-
export function toCollection (val: any) {
3+
export function toEnumerable (val: any) {
44
if (isArray(val)) return val
55
if (isString(val) && val.length > 0) return [val]
66
if (isObject(val)) return Object.keys(val).map((key) => [key, val[key]])
77
return []
88
}
9+
10+
export function toArray (val: any) {
11+
if (isArray(val)) return val
12+
return [ val ]
13+
}
+97-117
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import { test } from '../../../stub/render'
2-
import { Liquid } from '../../../../src/liquid'
1+
import { test, render } from '../../../stub/render'
32
import { expect, use } from 'chai'
43
import * as chaiAsPromised from 'chai-as-promised'
54
use(chaiAsPromised)
65

76
describe('filters/array', function () {
8-
let liquid: Liquid
9-
beforeEach(function () {
10-
liquid = new Liquid()
11-
})
127
describe('index', function () {
138
it('should support index', function () {
149
const src = '{% assign beatles = "John, Paul, George, Ringo" | split: ", " %}' +
@@ -30,109 +25,78 @@ describe('filters/array', function () {
3025
it('should throw when comma missing', async () => {
3126
const src = '{% assign beatles = "John, Paul, George, Ringo" | split: ", " %}' +
3227
'{{ beatles | join " and " }}'
33-
return expect(liquid.parseAndRender(src)).to.be.rejectedWith('unexpected token at "\\" and \\"", line:1, col:65')
28+
return expect(render(src)).to.be.rejectedWith('unexpected token at "\\" and \\"", line:1, col:65')
3429
})
3530
})
3631
it('should support split/last', function () {
3732
const src = '{% assign my_array = "zebra, octopus, giraffe, tiger" | split: ", " %}' +
3833
'{{ my_array|last }}'
3934
return test(src, 'tiger')
4035
})
41-
it('should support map', function () {
42-
return test('{{posts | map: "category"}}', 'foo,bar')
36+
describe('map', () => {
37+
it('should support map', function () {
38+
const posts = [{ category: 'foo' }, { category: 'bar' }]
39+
return test('{{posts | map: "category"}}', { posts }, 'foo,bar')
40+
})
41+
it('should normalize non-array input', function () {
42+
const post = { category: 'foo' }
43+
return test('{{post | map: "category"}}', { post }, 'foo')
44+
})
4345
})
4446
describe('reverse', function () {
45-
it('should support reverse', async function () {
46-
const html = await liquid.parseAndRender('{{ "Ground control to Major Tom." | split: "" | reverse | join: "" }}')
47-
expect(html).to.equal('.moT rojaM ot lortnoc dnuorG')
48-
})
49-
it('should be pure', async function () {
47+
it('should support reverse', () => test(
48+
'{{ "Ground control to Major Tom." | split: "" | reverse | join: "" }}',
49+
'.moT rojaM ot lortnoc dnuorG'
50+
))
51+
it('should be pure', async () => {
5052
const scope = { arr: ['a', 'b', 'c'] }
51-
await liquid.parseAndRender('{{ arr | reverse | join: "" }}', scope)
52-
const html = await liquid.parseAndRender('{{ arr | join: "" }}', scope)
53+
await render('{{ arr | reverse | join: "" }}', scope)
54+
const html = await render('{{ arr | join: "" }}', scope)
5355
expect(html).to.equal('abc')
5456
})
5557
})
5658
describe('size', function () {
57-
it('should return string length', async () => {
58-
const html = await liquid.parseAndRender('{{ "Ground control to Major Tom." | size }}')
59-
expect(html).to.equal('28')
60-
})
61-
it('should return array size', async () => {
62-
const html = await liquid.parseAndRender(
63-
'{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array | size }}')
64-
expect(html).to.equal('4')
65-
})
66-
it('should be respected with <string>.size notation', async () => {
67-
const html = await liquid.parseAndRender('{% assign my_string = "Ground control to Major Tom." %}{{ my_string.size }}')
68-
expect(html).to.equal('28')
69-
})
70-
it('should be respected with <array>.size notation', async () => {
71-
const html = await liquid.parseAndRender('{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array.size }}')
72-
expect(html).to.equal('4')
73-
})
74-
it('should return 0 for false', async () => {
75-
const html = await liquid.parseAndRender('{{ false | size }}')
76-
expect(html).to.equal('0')
77-
})
78-
it('should return 0 for nil', async () => {
79-
const html = await liquid.parseAndRender('{{ nil | size }}')
80-
expect(html).to.equal('0')
81-
})
82-
it('should return 0 for undefined', async () => {
83-
const html = await liquid.parseAndRender('{{ foo | size }}')
84-
expect(html).to.equal('0')
85-
})
59+
it('should return string length', () => test(
60+
'{{ "Ground control to Major Tom." | size }}',
61+
'28'
62+
))
63+
it('should return array size', () => test(
64+
'{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array | size }}',
65+
'4'
66+
))
67+
it('should be respected with <string>.size notation', () => test(
68+
'{% assign my_string = "Ground control to Major Tom." %}{{ my_string.size }}',
69+
'28'
70+
))
71+
it('should be respected with <array>.size notation', () => test(
72+
'{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array.size }}',
73+
'4'
74+
))
75+
it('should return 0 for false', () => test('{{ false | size }}', '0'))
76+
it('should return 0 for nil', () => test('{{ nil | size }}', '0'))
77+
it('should return 0 for undefined', () => test('{{ foo | size }}', '0'))
8678
})
8779
describe('first', function () {
88-
it('should support first', async () => {
89-
const html = await liquid.parseAndRender(
90-
'{{arr | first}}',
91-
{ arr: [ 'zebra', 'tiger' ] }
92-
)
93-
expect(html).to.equal('zebra')
94-
})
95-
it('should return empty for nil', async () => {
96-
const html = await liquid.parseAndRender('{{nil | first}}')
97-
expect(html).to.equal('')
98-
})
99-
it('should return empty for undefined', async () => {
100-
const html = await liquid.parseAndRender('{{foo | first}}')
101-
expect(html).to.equal('')
102-
})
103-
it('should return empty for false', async () => {
104-
const html = await liquid.parseAndRender('{{false | first}}')
105-
expect(html).to.equal('')
106-
})
107-
it('should return empty for string', async () => {
108-
const html = await liquid.parseAndRender('{{"zebra" | first}}')
109-
expect(html).to.equal('')
110-
})
80+
it('should support first', () => test(
81+
'{{arr | first}}',
82+
{ arr: [ 'zebra', 'tiger' ] },
83+
'zebra'
84+
))
85+
it('should return empty for nil', () => test('{{nil | first}}', ''))
86+
it('should return empty for undefined', () => test('{{foo | first}}', ''))
87+
it('should return empty for false', () => test('{{false | first}}', ''))
88+
it('should return empty for string', () => test('{{"zebra" | first}}', ''))
11189
})
11290
describe('last', function () {
113-
it('should support last', async () => {
114-
const html = await liquid.parseAndRender(
115-
'{{arr | last}}',
116-
{ arr: [ 'zebra', 'tiger' ] }
117-
)
118-
expect(html).to.equal('tiger')
119-
})
120-
it('should return empty for nil', async () => {
121-
const html = await liquid.parseAndRender('{{nil | last}}')
122-
expect(html).to.equal('')
123-
})
124-
it('should return empty for undefined', async () => {
125-
const html = await liquid.parseAndRender('{{foo | last}}')
126-
expect(html).to.equal('')
127-
})
128-
it('should return empty for false', async () => {
129-
const html = await liquid.parseAndRender('{{false | last}}')
130-
expect(html).to.equal('')
131-
})
132-
it('should return empty for string', async () => {
133-
const html = await liquid.parseAndRender('{{"zebra" | last}}')
134-
expect(html).to.equal('')
135-
})
91+
it('should support last', () => test(
92+
'{{arr | last}}',
93+
{ arr: [ 'zebra', 'tiger' ] },
94+
'tiger'
95+
))
96+
it('should return empty for nil', () => test('{{nil | last}}', ''))
97+
it('should return empty for undefined', () => test('{{foo | last}}', ''))
98+
it('should return empty for false', () => test('{{false | last}}', ''))
99+
it('should return empty for string', () => test('{{"zebra" | last}}', ''))
136100
})
137101
describe('slice', function () {
138102
it('should slice first char by 0', () => test('{{ "Liquid" | slice: 0 }}', 'L'))
@@ -161,44 +125,60 @@ describe('filters/array', function () {
161125
})
162126
})
163127
describe('where', function () {
128+
const products = [
129+
{ title: 'Vacuum', type: 'living room' },
130+
{ title: 'Spatula', type: 'kitchen' },
131+
{ title: 'Television', type: 'living room' },
132+
{ title: 'Garlic press', type: 'kitchen' },
133+
{ title: 'Coffee mug', available: true },
134+
{ title: 'Limited edition sneakers', available: false },
135+
{ title: 'Boring sneakers', available: true }
136+
]
164137
it('should support filter by property value', function () {
165138
return test(`{% assign kitchen_products = products | where: "type", "kitchen" %}
166-
Kitchen products:
167-
{% for product in kitchen_products -%}
168-
- {{ product.title }}
169-
{% endfor %}`, `
170-
Kitchen products:
171-
- Spatula
172-
- Garlic press
173-
`)
139+
Kitchen products:
140+
{% for product in kitchen_products -%}
141+
- {{ product.title }}
142+
{% endfor %}`, { products }, `
143+
Kitchen products:
144+
- Spatula
145+
- Garlic press
146+
`)
174147
})
175148
it('should support filter truthy property', function () {
176149
return test(`{% assign available_products = products | where: "available" %}
177-
Available products:
178-
{% for product in available_products -%}
179-
- {{ product.title }}
180-
{% endfor %}`, `
181-
Available products:
182-
- Coffee mug
183-
- Boring sneakers
184-
`)
150+
Available products:
151+
{% for product in available_products -%}
152+
- {{ product.title }}
153+
{% endfor %}`, { products }, `
154+
Available products:
155+
- Coffee mug
156+
- Boring sneakers
157+
`)
185158
})
186159
it('should support nested property', async function () {
187160
const authors = [
188161
{ name: 'Alice', books: { year: 2019 } },
189162
{ name: 'Bob', books: { year: 2018 } }
190163
]
191-
const html = await liquid.parseAndRender(
164+
return test(
192165
`{% assign recentAuthors = authors | where: 'books.year', 2019 %}
193-
Recent Authors:
194-
{%- for author in recentAuthors %}
195-
- {{author.name}}
196-
{%- endfor %}`,
197-
{ authors }
198-
)
199-
expect(html).to.equal(`
200-
Recent Authors:
201-
- Alice`)
166+
Recent Authors:
167+
{%- for author in recentAuthors %}
168+
- {{author.name}}
169+
{%- endfor %}`,
170+
{ authors }, `
171+
Recent Authors:
172+
- Alice`)
173+
})
174+
it('should apply to string', async () => {
175+
await test('{{"abc" | where: 1, "b" }}', 'abc')
176+
await test('{{"abc" | where: 1, "a" }}', '')
177+
})
178+
it('should normalize non-array input', async () => {
179+
const scope = { obj: { foo: 'FOO' } }
180+
await test('{{obj | where: "foo", "FOO" }}', scope, '[object Object]')
181+
await test('{{obj | where: "foo", "BAR" }}', scope, '')
202182
})
203183
})
204184
})

0 commit comments

Comments
 (0)