Skip to content

Commit a19feea

Browse files
jg-rpharttle
authored andcommitted
feat: iteration protocols
1 parent e87f068 commit a19feea

File tree

5 files changed

+100
-2
lines changed

5 files changed

+100
-2
lines changed

src/util/collection.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { isNil, isString, isObject, isArray } from './underscore'
1+
import { isNil, isString, isObject, isArray, isIterable } from './underscore'
22

33
export function toEnumerable (val: any) {
44
if (isArray(val)) return val
55
if (isString(val) && val.length > 0) return [val]
6+
if (isIterable(val)) return Array.from(val)
67
if (isObject(val)) return Object.keys(val).map((key) => [key, val[key]])
78
return []
89
}

src/util/underscore.ts

+4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export function isArray (value: any): value is any[] {
6060
return toString.call(value) === '[object Array]'
6161
}
6262

63+
export function isIterable (value: any): value is Iterable<any> {
64+
return isObject(value) && Symbol.iterator in value
65+
}
66+
6367
/*
6468
* Iterates over own enumerable string keyed properties of an object and invokes iteratee for each property.
6569
* The iteratee is invoked with three arguments: (value, key, object).

test/integration/builtin/tags/for.ts

+63-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Liquid } from '../../../../src/liquid'
1+
import { Drop, Liquid } from '../../../../src/liquid'
22
import { expect, use } from 'chai'
33
import * as chaiAsPromised from 'chai-as-promised'
44
import { Scope } from '../../../../src/context/scope'
@@ -304,4 +304,66 @@ describe('tags/for', function () {
304304
return expect(html).to.equal('b')
305305
})
306306
})
307+
308+
describe('iterables', function () {
309+
class MockIterable {
310+
* [Symbol.iterator] () {
311+
yield 'a'
312+
yield 'b'
313+
yield 'c'
314+
}
315+
}
316+
317+
class MockEmptyIterable {
318+
* [Symbol.iterator] () {}
319+
}
320+
321+
class MockIterableDrop extends Drop {
322+
* [Symbol.iterator] () {
323+
yield 'a'
324+
yield 'b'
325+
yield 'c'
326+
}
327+
328+
public valueOf (): string {
329+
return 'MockIterableDrop'
330+
}
331+
}
332+
333+
it('should loop over iterable objects', function () {
334+
const src = '{% for i in someIterable %}{{i}}{%endfor%}'
335+
const html = liquid.parseAndRenderSync(src, { someIterable: new MockIterable() })
336+
return expect(html).to.equal('abc')
337+
})
338+
it('should loop over iterable drops', function () {
339+
const src = '{{ someDrop }}: {% for i in someDrop %}{{i}}{%endfor%}'
340+
const html = liquid.parseAndRenderSync(src, { someDrop: new MockIterableDrop() })
341+
return expect(html).to.equal('MockIterableDrop: abc')
342+
})
343+
it('should loop over iterable objects with a limit', function () {
344+
const src = '{% for i in someIterable limit:2 %}{{i}}{%endfor%}'
345+
const html = liquid.parseAndRenderSync(src, { someIterable: new MockIterable() })
346+
return expect(html).to.equal('ab')
347+
})
348+
it('should loop over iterable objects with an offset', function () {
349+
const src = '{% for i in someIterable offset:1 %}{{i}}{%endfor%}'
350+
const html = liquid.parseAndRenderSync(src, { someIterable: new MockIterable() })
351+
return expect(html).to.equal('bc')
352+
})
353+
it('should loop over iterable objects in reverse', function () {
354+
const src = '{% for i in someIterable reversed %}{{i}}{%endfor%}'
355+
const html = liquid.parseAndRenderSync(src, { someIterable: new MockIterable() })
356+
return expect(html).to.equal('cba')
357+
})
358+
it('should go to else for an empty iterable', function () {
359+
const src = '{% for i in emptyIterable reversed %}{{i}}{%else%}EMPTY{%endfor%}'
360+
const html = liquid.parseAndRenderSync(src, { emptyIterable: new MockEmptyIterable() })
361+
return expect(html).to.equal('EMPTY')
362+
})
363+
it('should support iterable names', function () {
364+
const src = '{% for i in someDrop %}{{forloop.name}} {%else%}EMPTY{%endfor%}'
365+
const html = liquid.parseAndRenderSync(src, { someDrop: new MockIterableDrop() })
366+
return expect(html).to.equal('i-someDrop i-someDrop i-someDrop ')
367+
})
368+
})
307369
})

test/integration/builtin/tags/render.ts

+14
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,20 @@ describe('tags/render', function () {
155155
const html = await liquid.renderFile('index.html', { colors: ['red', 'green'] })
156156
expect(html).to.equal('1: red\n2: green\n')
157157
})
158+
it('should support for <iterable> as', async function () {
159+
class MockIterable {
160+
* [Symbol.iterator] () {
161+
yield 'red'
162+
yield 'green'
163+
}
164+
}
165+
mock({
166+
'/index.html': '{% render "item" for colors as color %}',
167+
'/item.html': '{{forloop.index}}: {{color}}\n'
168+
})
169+
const html = await liquid.renderFile('index.html', { colors: new MockIterable() })
170+
expect(html).to.equal('1: red\n2: green\n')
171+
})
158172
it('should support for <non-array> as', async function () {
159173
mock({
160174
'/index.html': '{% render "item" for "green" as color %}',

test/integration/builtin/tags/tablerow.ts

+17
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@ describe('tags/tablerow', function () {
2424
return expect(html).to.equal(dst)
2525
})
2626

27+
it('should support iterables', async function () {
28+
class MockIterable {
29+
* [Symbol.iterator] () {
30+
yield 1
31+
yield 2
32+
yield 3
33+
}
34+
}
35+
const src = '{% tablerow i in someIterable %}{{ i }}{% endtablerow %}'
36+
const ctx = {
37+
someIterable: new MockIterable()
38+
}
39+
const dst = '<tr class="row1"><td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>'
40+
const html = await liquid.parseAndRender(src, ctx)
41+
return expect(html).to.equal(dst)
42+
})
43+
2744
it('should support cols', async function () {
2845
const src = '{% tablerow i in alpha cols:2 %}{{ i }}{% endtablerow %}'
2946
const ctx = {

0 commit comments

Comments
 (0)