mirror of
https://github.com/harttle/liquidjs.git
synced 2026-10-03 00:55:13 -07:00
* feat: WIP async Drop valueOf resolution (#948) Incomplete/abandoned implementation of async Drop.valueOf() awaited across the render pipeline. Kept for reference only; known to be inconsistent and not production-ready. Co-authored-by: Cursor <[email protected]> * feat!: resolve async Drop.valueOf() for filters and tags Co-authored-by: Cursor <[email protected]> * fix: keep Drops on filter and tag inputs Filters and tag arguments still receive the Drop. Await valueOf() at output, conditions, and operators, and restore the existing render/include Drop tests. Co-authored-by: Cursor <[email protected]> --------- Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Liquid, Drop } from '../../../src'
|
||||
import { mock, restore } from '../../stub/mockfs'
|
||||
|
||||
describe('drop/drop', function () {
|
||||
let liquid: Liquid
|
||||
@@ -71,6 +72,19 @@ describe('drop/drop', function () {
|
||||
const html = await liquid.parseAndRender(tpl, { drop: new CustomDrop() })
|
||||
expect(html).toBe('foobar: foo;bar;')
|
||||
})
|
||||
it('should support promise returned by valueOf', async () => {
|
||||
class AsyncValueOfDrop extends Drop {
|
||||
id = '42'
|
||||
async valueOf () {
|
||||
return 'serialized'
|
||||
}
|
||||
}
|
||||
const html = await liquid.parseAndRender(
|
||||
`{{o.id}}:{{o}}:{% if o %}yes{% endif %}:{% unless o %}no{% endunless %}`,
|
||||
{ o: new AsyncValueOfDrop() }
|
||||
)
|
||||
expect(html).toBe('42:serialized:yes:')
|
||||
})
|
||||
it('should support valueOf in == expression', async () => {
|
||||
class AddressDrop extends Drop {
|
||||
valueOf () {
|
||||
@@ -119,4 +133,192 @@ describe('drop/drop', function () {
|
||||
const html = await liquid.parseAndRender(`{{obj.number}} {{obj.string}} {{obj.boolean}} {{obj.array | first}} {{obj.object.foo}} {{obj.drop.getName}}`, { obj: new DynamicTypeDrop() })
|
||||
expect(html).toBe('42 foo true 1 bar GET NAME')
|
||||
})
|
||||
|
||||
describe('async valueOf', function () {
|
||||
beforeEach(() => {
|
||||
liquid = new Liquid({ root: '/', extname: '.html' })
|
||||
})
|
||||
afterEach(restore)
|
||||
|
||||
class AsyncStringDrop extends Drop {
|
||||
constructor (private value: string) { super() }
|
||||
async valueOf () {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
|
||||
class AsyncFalsyDrop extends Drop {
|
||||
async valueOf () {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class AsyncArrayDrop extends Drop {
|
||||
constructor (private value: string[]) { super() }
|
||||
async valueOf () {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
|
||||
class AsyncNumberDrop extends Drop {
|
||||
constructor (private value: number) { super() }
|
||||
async valueOf () {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
|
||||
describe('output & filters', function () {
|
||||
it('output {{ drop }}', async function () {
|
||||
const html = await liquid.parseAndRender(`{{a}}`, { a: new AsyncStringDrop('x') })
|
||||
expect(html).toBe('x')
|
||||
})
|
||||
it('where_exp resolves async properties', async function () {
|
||||
const items = [{ v: new AsyncStringDrop('x'), id: '1' }, { v: new AsyncStringDrop('y'), id: '2' }]
|
||||
const html = await liquid.parseAndRender(`{% assign r = items | where_exp: 'item', 'item.v == "y"' %}{{ r[0].id }}`, { items })
|
||||
expect(html).toBe('2')
|
||||
})
|
||||
it('where by async property', async function () {
|
||||
const items = [{ v: new AsyncStringDrop('x'), id: '1' }, { v: new AsyncStringDrop('y'), id: '2' }]
|
||||
const html = await liquid.parseAndRender(`{% assign r = items | where: 'v', 'y' %}{{ r[0].id }}`, { items })
|
||||
expect(html).toBe('2')
|
||||
})
|
||||
it('find by async property', async function () {
|
||||
const items = [{ v: new AsyncStringDrop('x'), id: '1' }, { v: new AsyncStringDrop('y'), id: '2' }]
|
||||
const html = await liquid.parseAndRender(`{% assign f = items | find: 'v', 'y' %}{{ f.id }}`, { items })
|
||||
expect(html).toBe('2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('branch conditions', function () {
|
||||
it('if truthy', async function () {
|
||||
const html = await liquid.parseAndRender(`{% if a %}yes{% else %}no{% endif %}`, { a: new AsyncStringDrop('x') })
|
||||
expect(html).toBe('yes')
|
||||
})
|
||||
it('if falsy', async function () {
|
||||
const html = await liquid.parseAndRender(`{% if a %}yes{% else %}no{% endif %}`, { a: new AsyncFalsyDrop() })
|
||||
expect(html).toBe('no')
|
||||
})
|
||||
it('unless', async function () {
|
||||
const html = await liquid.parseAndRender(`{% unless a %}yes{% else %}no{% endunless %}`, { a: new AsyncFalsyDrop() })
|
||||
expect(html).toBe('yes')
|
||||
})
|
||||
it('elsif', async function () {
|
||||
const html = await liquid.parseAndRender(
|
||||
`{% if false %}a{% elsif b %}b{% else %}c{% endif %}`,
|
||||
{ b: new AsyncStringDrop('x') }
|
||||
)
|
||||
expect(html).toBe('b')
|
||||
})
|
||||
it('case/when', async function () {
|
||||
const html = await liquid.parseAndRender(
|
||||
`{% case a %}{% when "x" %}X{% when "y" %}Y{% else %}Z{% endcase %}`,
|
||||
{ a: new AsyncStringDrop('y') }
|
||||
)
|
||||
expect(html).toBe('Y')
|
||||
})
|
||||
it('case when empty uses Comparable on the resolved value', async function () {
|
||||
const html = await liquid.parseAndRender(
|
||||
`{% case a %}{% when empty %}E{% else %}X{% endcase %}`,
|
||||
{ a: new AsyncArrayDrop([]) }
|
||||
)
|
||||
expect(html).toBe('E')
|
||||
})
|
||||
})
|
||||
|
||||
describe('operators', function () {
|
||||
it('== / !=', async function () {
|
||||
const html = await liquid.parseAndRender(
|
||||
`{% if a == b %}eq{% endif %}/{% if a != c %}ne{% endif %}`,
|
||||
{ a: new AsyncStringDrop('t'), b: new AsyncStringDrop('t'), c: new AsyncStringDrop('u') }
|
||||
)
|
||||
expect(html).toBe('eq/ne')
|
||||
})
|
||||
it('< / >', async function () {
|
||||
const html = await liquid.parseAndRender(
|
||||
`{% if a < b %}lt{% endif %}/{% if b > a %}gt{% endif %}`,
|
||||
{ a: new AsyncNumberDrop(1), b: new AsyncNumberDrop(2) }
|
||||
)
|
||||
expect(html).toBe('lt/gt')
|
||||
})
|
||||
it('contains', async function () {
|
||||
const html = await liquid.parseAndRender(
|
||||
`{% if a contains "od" %}yes{% endif %}`,
|
||||
{ a: new AsyncStringDrop('product') }
|
||||
)
|
||||
expect(html).toBe('yes')
|
||||
})
|
||||
it('and / or', async function () {
|
||||
const html = await liquid.parseAndRender(
|
||||
`{% if a and b %}both{% endif %}/{% if f or a %}either{% endif %}`,
|
||||
{ a: new AsyncStringDrop('x'), b: new AsyncStringDrop('y'), f: new AsyncFalsyDrop() }
|
||||
)
|
||||
expect(html).toBe('both/either')
|
||||
})
|
||||
it('not (unless)', async function () {
|
||||
const html = await liquid.parseAndRender(
|
||||
`{% unless a %}yes{% else %}no{% endunless %}`,
|
||||
{ a: new AsyncFalsyDrop() }
|
||||
)
|
||||
expect(html).toBe('yes')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pass-through tags', function () {
|
||||
it('assign then output', async function () {
|
||||
const html = await liquid.parseAndRender(`{% assign b = a %}{{ b }}`, { a: new AsyncStringDrop('z') })
|
||||
expect(html).toBe('z')
|
||||
})
|
||||
it('assign keeps the drop so properties stay readable', async function () {
|
||||
class NamedDrop extends Drop {
|
||||
id = '42'
|
||||
async valueOf () { return 'serialized' }
|
||||
}
|
||||
const html = await liquid.parseAndRender(`{% assign b = a %}{{ b.id }}:{{ b }}`, { a: new NamedDrop() })
|
||||
expect(html).toBe('42:serialized')
|
||||
})
|
||||
it('supports async Comparable.equals', async function () {
|
||||
class EqDrop extends Drop {
|
||||
constructor (private n: string) { super() }
|
||||
async equals (rhs: unknown) {
|
||||
const other = rhs instanceof Drop ? await rhs.valueOf() : rhs
|
||||
return (await this.valueOf()) === other
|
||||
}
|
||||
async valueOf () { return this.n }
|
||||
gt () { return false }
|
||||
geq () { return false }
|
||||
lt () { return false }
|
||||
leq () { return false }
|
||||
}
|
||||
const html = await liquid.parseAndRender(
|
||||
`{% if a == b %}eq{% endif %}/{% if a == c %}bad{% else %}ne{% endif %}`,
|
||||
{ a: new EqDrop('t'), b: new EqDrop('t'), c: new EqDrop('u') }
|
||||
)
|
||||
expect(html).toBe('eq/ne')
|
||||
})
|
||||
it('echo', async function () {
|
||||
const html = await liquid.parseAndRender(`{% echo a %}`, { a: new AsyncStringDrop('z') })
|
||||
expect(html).toBe('z')
|
||||
})
|
||||
it('render with', async function () {
|
||||
mock({
|
||||
'/c.html': '{{ color }}'
|
||||
})
|
||||
const html = await liquid.parseAndRender(
|
||||
`{% render "c" with a as color %}`,
|
||||
{ a: new AsyncStringDrop('red') }
|
||||
)
|
||||
expect(html).toBe('red')
|
||||
})
|
||||
it('include with', async function () {
|
||||
mock({
|
||||
'/color.html': '{{ color }}'
|
||||
})
|
||||
const html = await liquid.parseAndRender(
|
||||
`{% include "color" with a %}`,
|
||||
{ a: new AsyncStringDrop('red') }
|
||||
)
|
||||
expect(html).toBe('red')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -65,6 +65,16 @@ describe('tags/if', function () {
|
||||
const html = await liquid.parseAndRender(src, scope)
|
||||
return expect(html).toBe('yes')
|
||||
})
|
||||
it('should keep comparable operands uncoerced', async function () {
|
||||
const src = '{% if empty == empty %}yes{%else%}no{%endif%}'
|
||||
const html = await liquid.parseAndRender(src, scope)
|
||||
return expect(html).toBe('no')
|
||||
})
|
||||
it('should keep comparable operands uncoerced in elsif', async function () {
|
||||
const src = '{% if false %}a{% elsif empty == empty %}b{% else %}c{% endif %}'
|
||||
const html = await liquid.parseAndRender(src, scope)
|
||||
return expect(html).toBe('c')
|
||||
})
|
||||
it('should support value and expression', async function () {
|
||||
const src = `X{%if version and version != '' %}x{{version}}y{%endif%}Y`
|
||||
const scope = { 'version': '' }
|
||||
|
||||
@@ -142,6 +142,21 @@ describe('tags/include', function () {
|
||||
const html = await liquid.renderFile('with.html', { color: new ColorDrop() })
|
||||
expect(html).toBe('ColorDrop')
|
||||
})
|
||||
it('should await async valueOf when the included Drop is output', async function () {
|
||||
class ColorDrop extends Drop {
|
||||
label = 'blue'
|
||||
public async valueOf () {
|
||||
return 'red!'
|
||||
}
|
||||
}
|
||||
liquid.registerFilter('name', x => x.constructor.name)
|
||||
mock({
|
||||
'/with.html': '{% include "color" with color %}',
|
||||
'/color.html': '{{color.label}}:{{color}}:{{color | name}}'
|
||||
})
|
||||
const html = await liquid.renderFile('with.html', { color: new ColorDrop() })
|
||||
expect(html).toBe('blue:red!:ColorDrop')
|
||||
})
|
||||
|
||||
it('should support nested includes', async function () {
|
||||
mock({
|
||||
|
||||
@@ -226,6 +226,21 @@ describe('tags/render', function () {
|
||||
const html = await liquid.renderFile('with.html', { color: new ColorDrop() })
|
||||
expect(html).toBe('ColorDrop')
|
||||
})
|
||||
it('should await async valueOf when the rendered Drop is output', async function () {
|
||||
class ColorDrop extends Drop {
|
||||
label = 'blue'
|
||||
public async valueOf () {
|
||||
return 'red!'
|
||||
}
|
||||
}
|
||||
liquid.registerFilter('name', x => x.constructor.name)
|
||||
mock({
|
||||
'/with.html': '{% render "color" with color %}',
|
||||
'/color.html': '{{color.label}}:{{color}}:{{color | name}}'
|
||||
})
|
||||
const html = await liquid.renderFile('with.html', { color: new ColorDrop() })
|
||||
expect(html).toBe('blue:red!:ColorDrop')
|
||||
})
|
||||
|
||||
it('should support nested renders', async function () {
|
||||
mock({
|
||||
|
||||
@@ -35,6 +35,11 @@ describe('tags/unless', function () {
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).toBe('')
|
||||
})
|
||||
it('should keep comparable operands uncoerced', async function () {
|
||||
const src = '{% unless empty == empty %}yes{%else%}no{%endunless%}'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).toBe('yes')
|
||||
})
|
||||
|
||||
it('should output unless contents in order', async function () {
|
||||
const src = `
|
||||
|
||||
Reference in New Issue
Block a user