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:
@@ -38,7 +38,7 @@ The following modified template also contains 3 values to random from, but they'
|
|||||||
It can be tricky to parse all these cases manually, but there's a [Tokenizer][Tokenizer] class in LiquidJS you can make use of.
|
It can be tricky to parse all these cases manually, but there's a [Tokenizer][Tokenizer] class in LiquidJS you can make use of.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const { Liquid, Tokenizer, evalToken } = require('liquidjs')
|
const { Liquid, Tokenizer, evalTokenValue } = require('liquidjs')
|
||||||
engine.registerTag('random', {
|
engine.registerTag('random', {
|
||||||
parse(tagToken) {
|
parse(tagToken) {
|
||||||
const tokenizer = new Tokenizer(tagToken.args)
|
const tokenizer = new Tokenizer(tagToken.args)
|
||||||
@@ -52,7 +52,7 @@ engine.registerTag('random', {
|
|||||||
const index = Math.floor(this.items.length * Math.random())
|
const index = Math.floor(this.items.length * Math.random())
|
||||||
const token = this.items[index]
|
const token = this.items[index]
|
||||||
// in LiquidJS, we use yield to wait for async call
|
// in LiquidJS, we use yield to wait for async call
|
||||||
const value = yield evalToken(token, context)
|
const value = yield evalTokenValue(token, context)
|
||||||
emitter.write(value)
|
emitter.write(value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { isNil, isString, toValue } from '../util'
|
|||||||
import { EmptyDrop } from '../drop'
|
import { EmptyDrop } from '../drop'
|
||||||
|
|
||||||
export class BlankDrop extends EmptyDrop {
|
export class BlankDrop extends EmptyDrop {
|
||||||
public equals (value: any) {
|
public * equals (value: any): Generator<unknown, boolean, any> {
|
||||||
if (value === false) return true
|
if (value === false) return true
|
||||||
if (isNil(toValue(value))) return true
|
if (isNil(yield toValue(value))) return true
|
||||||
if (isString(value)) return /^\s*$/.test(value)
|
if (isString(value)) return /^\s*$/.test(value)
|
||||||
return super.equals(value)
|
return yield super.equals(value)
|
||||||
}
|
}
|
||||||
static is (value: unknown) {
|
static is (value: unknown) {
|
||||||
return value instanceof BlankDrop
|
return value instanceof BlankDrop
|
||||||
|
|||||||
+12
-5
@@ -1,11 +1,18 @@
|
|||||||
import { isFunction } from '../util'
|
import { isFunction } from '../util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comparison methods may return a boolean, a promise, or a generator.
|
||||||
|
* Operators yield the result, so sync implementations stay valid and async
|
||||||
|
* `valueOf()` can be awaited with `yield`.
|
||||||
|
*/
|
||||||
|
export type ComparableResult = boolean | Promise<boolean> | Generator<unknown, boolean, any>
|
||||||
|
|
||||||
export interface Comparable {
|
export interface Comparable {
|
||||||
equals: (rhs: any) => boolean;
|
equals: (rhs: any) => ComparableResult;
|
||||||
gt: (rhs: any) => boolean;
|
gt: (rhs: any) => ComparableResult;
|
||||||
geq: (rhs: any) => boolean;
|
geq: (rhs: any) => ComparableResult;
|
||||||
lt: (rhs: any) => boolean;
|
lt: (rhs: any) => ComparableResult;
|
||||||
leq: (rhs: any) => boolean;
|
leq: (rhs: any) => ComparableResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isComparable (arg: any): arg is Comparable {
|
export function isComparable (arg: any): arg is Comparable {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Comparable } from './comparable'
|
|||||||
import { isObject, isString, isArray, toValue } from '../util'
|
import { isObject, isString, isArray, toValue } from '../util'
|
||||||
|
|
||||||
export class EmptyDrop extends Drop implements Comparable {
|
export class EmptyDrop extends Drop implements Comparable {
|
||||||
public equals (value: any) {
|
public * equals (value: any): Generator<unknown, boolean, any> {
|
||||||
if (value instanceof EmptyDrop) return false
|
if (value instanceof EmptyDrop) return false
|
||||||
value = toValue(value)
|
value = yield toValue(value)
|
||||||
if (isString(value) || isArray(value)) return value.length === 0
|
if (isString(value) || isArray(value)) return value.length === 0
|
||||||
if (isObject(value)) return Object.keys(value).length === 0
|
if (isObject(value)) return Object.keys(value).length === 0
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Comparable } from './comparable'
|
|||||||
import { isNil, toValue } from '../util'
|
import { isNil, toValue } from '../util'
|
||||||
|
|
||||||
export class NullDrop extends Drop implements Comparable {
|
export class NullDrop extends Drop implements Comparable {
|
||||||
public equals (value: any) {
|
public * equals (value: any): Generator<unknown, boolean, any> {
|
||||||
return isNil(toValue(value))
|
return isNil(yield toValue(value))
|
||||||
}
|
}
|
||||||
public gt () {
|
public gt () {
|
||||||
return false
|
return false
|
||||||
|
|||||||
+11
-12
@@ -121,26 +121,26 @@ export function slice<T> (this: FilterImpl, v: T[] | string, begin: number, leng
|
|||||||
: String.prototype.slice.call(v, begin, begin + length)
|
: String.prototype.slice.call(v, begin, begin + length)
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectedMatcher (this: FilterImpl, expected: any): (v: any) => boolean {
|
function * matches (this: FilterImpl, value: unknown, expected: unknown): Generator<unknown, boolean, any> {
|
||||||
if (this.context.opts.jekyllWhere) {
|
if (this.context.opts.jekyllWhere) {
|
||||||
return (v: any) => EmptyDrop.is(expected) ? equals(v, expected) : (isArray(v) ? arrayIncludes(v, expected) : equals(v, expected))
|
if (EmptyDrop.is(expected)) return yield equals(value, expected)
|
||||||
} else if (expected === undefined) {
|
if (isArray(value)) return yield arrayIncludes(value, expected)
|
||||||
return (v: any) => isTruthy(v, this.context)
|
return yield equals(value, expected)
|
||||||
} else {
|
|
||||||
return (v: any) => equals(v, expected)
|
|
||||||
}
|
}
|
||||||
|
if (expected === undefined) return isTruthy(value, this.context)
|
||||||
|
return yield equals(value, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
function * filter<T extends object> (this: FilterImpl, include: boolean, arr: T[], property: string, expected: any): IterableIterator<unknown> {
|
function * filter<T extends object> (this: FilterImpl, include: boolean, arr: T[], property: string, expected: any): IterableIterator<unknown> {
|
||||||
const values: unknown[] = []
|
|
||||||
arr = toArray(arr)
|
arr = toArray(arr)
|
||||||
this.context.memoryLimit.use(arr.length)
|
this.context.memoryLimit.use(arr.length)
|
||||||
const token = new Tokenizer(stringify(property)).readScopeValue()
|
const token = new Tokenizer(stringify(property)).readScopeValue()
|
||||||
|
const result: T[] = []
|
||||||
for (const item of arr) {
|
for (const item of arr) {
|
||||||
values.push(yield evalToken(token, this.context.spawn(item)))
|
const value = yield evalToken(token, this.context.spawn(item))
|
||||||
|
if ((yield matches.call(this, value, expected)) === include) result.push(item)
|
||||||
}
|
}
|
||||||
const matcher = expectedMatcher.call(this, expected)
|
return result
|
||||||
return Array.prototype.filter.call(arr, (_, i) => matcher(values[i]) === include)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function * filter_exp<T extends object> (this: FilterImpl, include: boolean, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
|
function * filter_exp<T extends object> (this: FilterImpl, include: boolean, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
|
||||||
@@ -204,10 +204,9 @@ export function * group_by_exp<T extends object> (this: FilterImpl, arr: T[], it
|
|||||||
function * search<T extends object> (this: FilterImpl, arr: T[], property: string, expected: string): IterableIterator<unknown> {
|
function * search<T extends object> (this: FilterImpl, arr: T[], property: string, expected: string): IterableIterator<unknown> {
|
||||||
const token = new Tokenizer(stringify(property)).readScopeValue()
|
const token = new Tokenizer(stringify(property)).readScopeValue()
|
||||||
const array = toArray(arr)
|
const array = toArray(arr)
|
||||||
const matcher = expectedMatcher.call(this, expected)
|
|
||||||
for (let index = 0; index < array.length; index++) {
|
for (let index = 0; index < array.length; index++) {
|
||||||
const value = yield evalToken(token, this.context.spawn(array[index]))
|
const value = yield evalToken(token, this.context.spawn(array[index]))
|
||||||
if (matcher(value)) return [index, array[index]]
|
if (yield matches.call(this, value, expected)) return [index, array[index]]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -4,9 +4,9 @@ export * as TypeGuards from './util/type-guards'
|
|||||||
export { toValue, createTrie, Trie, toPromise, toValueSync, assert, LiquidError, ParseError, RenderError, UndefinedVariableError, TokenizationError, AssertionError } from './util'
|
export { toValue, createTrie, Trie, toPromise, toValueSync, assert, LiquidError, ParseError, RenderError, UndefinedVariableError, TokenizationError, AssertionError } from './util'
|
||||||
export type { LiquidErrors } from './util/error'
|
export type { LiquidErrors } from './util/error'
|
||||||
export { Drop } from './drop'
|
export { Drop } from './drop'
|
||||||
export type { Comparable } from './drop'
|
export type { Comparable, ComparableResult } from './drop'
|
||||||
export { Emitter } from './emitters'
|
export { Emitter } from './emitters'
|
||||||
export { defaultOperators, Operators, evalToken, evalQuotedToken, Expression, isFalsy, isTruthy } from './render'
|
export { defaultOperators, Operators, evalToken, evalTokenValue, evalQuotedToken, Expression, isFalsy, isTruthy } from './render'
|
||||||
export { Context, Scope } from './context'
|
export { Context, Scope } from './context'
|
||||||
export { Value, Hash, Template, FilterImplOptions, Tag, Filter, Output, Variable, VariableLocation, VariableSegments, Variables, StaticAnalysis, StaticAnalysisOptions, analyze, analyzeSync, Arguments, PartialScope } from './template'
|
export { Value, Hash, Template, FilterImplOptions, Tag, Filter, Output, Variable, VariableLocation, VariableSegments, Variables, StaticAnalysis, StaticAnalysisOptions, analyze, analyzeSync, Arguments, PartialScope } from './template'
|
||||||
export type { TagRenderReturn } from './template'
|
export type { TagRenderReturn } from './template'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { QuotedToken, RangeToken, OperatorToken, Token, PropertyAccessToken, OperatorType, operatorTypes, FilteredValueToken } from '../tokens'
|
import { QuotedToken, RangeToken, OperatorToken, Token, PropertyAccessToken, OperatorType, operatorTypes, FilteredValueToken } from '../tokens'
|
||||||
import { isRangeToken, isPropertyAccessToken, isFilteredValueToken, UndefinedVariableError, range, isOperatorToken, assert } from '../util'
|
import { isRangeToken, isPropertyAccessToken, isFilteredValueToken, UndefinedVariableError, range, isOperatorToken, assert, toValue } from '../util'
|
||||||
import type { Context } from '../context'
|
import type { Context } from '../context'
|
||||||
import type { UnaryOperatorHandler } from '../render'
|
import type { UnaryOperatorHandler } from '../render'
|
||||||
import { Drop } from '../drop'
|
import { Drop } from '../drop'
|
||||||
@@ -36,6 +36,11 @@ export class Expression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate `token`, preserving Drops.
|
||||||
|
* Operators use this so `Comparable` drops (`empty`, `nil`, `blank`) and custom drops
|
||||||
|
* are compared as drops rather than as their `valueOf()` result.
|
||||||
|
*/
|
||||||
export function * evalToken (token: Token | undefined, ctx: Context, lenient = false): IterableIterator<unknown> {
|
export function * evalToken (token: Token | undefined, ctx: Context, lenient = false): IterableIterator<unknown> {
|
||||||
if (!token) return
|
if (!token) return
|
||||||
if ('content' in token) return token.content
|
if ('content' in token) return token.content
|
||||||
@@ -59,10 +64,18 @@ function * evalFilteredValueToken (token: FilteredValueToken, ctx: Context, leni
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate `token` to the value filters and tags consume.
|
||||||
|
* Awaits a promise returned by `Drop.valueOf()`.
|
||||||
|
*/
|
||||||
|
export function * evalTokenValue (token: Token | undefined, ctx: Context, lenient = false): IterableIterator<unknown> {
|
||||||
|
return yield toValue(yield evalToken(token, ctx, lenient))
|
||||||
|
}
|
||||||
|
|
||||||
function * evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean): IterableIterator<unknown> {
|
function * evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean): IterableIterator<unknown> {
|
||||||
const props: (string | number | Drop)[] = []
|
const props: (string | number | Drop)[] = []
|
||||||
for (const prop of token.props) {
|
for (const prop of token.props) {
|
||||||
props.push((yield evalToken(prop, ctx, false)) as unknown as string | number | Drop)
|
props.push((yield evalTokenValue(prop, ctx, false)) as unknown as string | number | Drop)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (token.variable) {
|
if (token.variable) {
|
||||||
@@ -81,9 +94,9 @@ export function evalQuotedToken (token: QuotedToken) {
|
|||||||
return token.content
|
return token.content
|
||||||
}
|
}
|
||||||
|
|
||||||
function * evalRangeToken (token: RangeToken, ctx: Context) {
|
function * evalRangeToken (token: RangeToken, ctx: Context): IterableIterator<unknown> {
|
||||||
const low: number = yield evalToken(token.lhs, ctx)
|
const low = (yield evalTokenValue(token.lhs, ctx)) as unknown as number
|
||||||
const high: number = yield evalToken(token.rhs, ctx)
|
const high = (yield evalTokenValue(token.rhs, ctx)) as unknown as number
|
||||||
ctx.memoryLimit.use(high - low + 1)
|
ctx.memoryLimit.use(high - low + 1)
|
||||||
return range(+low, +high + 1)
|
return range(+low, +high + 1)
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-37
@@ -4,61 +4,74 @@ import { toValue } from '../util'
|
|||||||
import { isFalsy, isTruthy } from '../render/boolean'
|
import { isFalsy, isTruthy } from '../render/boolean'
|
||||||
import { isArray, isFunction } from '../util/underscore'
|
import { isArray, isFunction } from '../util/underscore'
|
||||||
|
|
||||||
export type UnaryOperatorHandler = (operand: any, ctx: Context) => boolean;
|
/**
|
||||||
export type BinaryOperatorHandler = (lhs: any, rhs: any, ctx: Context) => boolean;
|
* A handler may return a `boolean` (legacy interface), a `Promise`, or a generator
|
||||||
|
* that `yield`s to await async values. The render driver resolves all three, so
|
||||||
|
* operators written against the legacy `=> boolean` signature keep working.
|
||||||
|
*/
|
||||||
|
export type OperatorGenerator = Generator<unknown, boolean, any>
|
||||||
|
export type OperatorResult = boolean | Promise<boolean> | OperatorGenerator
|
||||||
|
export type UnaryOperatorHandler = (operand: any, ctx: Context) => OperatorResult;
|
||||||
|
export type BinaryOperatorHandler = (lhs: any, rhs: any, ctx: Context) => OperatorResult;
|
||||||
export type OperatorHandler = UnaryOperatorHandler | BinaryOperatorHandler;
|
export type OperatorHandler = UnaryOperatorHandler | BinaryOperatorHandler;
|
||||||
export type Operators = Record<string, OperatorHandler>
|
export type Operators = Record<string, OperatorHandler>
|
||||||
|
|
||||||
export const defaultOperators: Operators = {
|
export const defaultOperators: Operators = {
|
||||||
'==': equals,
|
'==': function * (l: any, r: any): OperatorGenerator { return yield equals(l, r) },
|
||||||
'!=': (l: any, r: any) => !equals(l, r),
|
'!=': function * (l: any, r: any): OperatorGenerator { return !(yield equals(l, r)) },
|
||||||
'>': (l: any, r: any) => {
|
'>': function * (l: any, r: any): OperatorGenerator {
|
||||||
if (isComparable(l)) return l.gt(r)
|
if (isComparable(l)) return yield l.gt(r)
|
||||||
if (isComparable(r)) return r.lt(l)
|
if (isComparable(r)) return yield r.lt(l)
|
||||||
return toValue(l) > toValue(r)
|
return (yield toValue(l)) > (yield toValue(r))
|
||||||
},
|
},
|
||||||
'<': (l: any, r: any) => {
|
'<': function * (l: any, r: any): OperatorGenerator {
|
||||||
if (isComparable(l)) return l.lt(r)
|
if (isComparable(l)) return yield l.lt(r)
|
||||||
if (isComparable(r)) return r.gt(l)
|
if (isComparable(r)) return yield r.gt(l)
|
||||||
return toValue(l) < toValue(r)
|
return (yield toValue(l)) < (yield toValue(r))
|
||||||
},
|
},
|
||||||
'>=': (l: any, r: any) => {
|
'>=': function * (l: any, r: any): OperatorGenerator {
|
||||||
if (isComparable(l)) return l.geq(r)
|
if (isComparable(l)) return yield l.geq(r)
|
||||||
if (isComparable(r)) return r.leq(l)
|
if (isComparable(r)) return yield r.leq(l)
|
||||||
return toValue(l) >= toValue(r)
|
return (yield toValue(l)) >= (yield toValue(r))
|
||||||
},
|
},
|
||||||
'<=': (l: any, r: any) => {
|
'<=': function * (l: any, r: any): OperatorGenerator {
|
||||||
if (isComparable(l)) return l.leq(r)
|
if (isComparable(l)) return yield l.leq(r)
|
||||||
if (isComparable(r)) return r.geq(l)
|
if (isComparable(r)) return yield r.geq(l)
|
||||||
return toValue(l) <= toValue(r)
|
return (yield toValue(l)) <= (yield toValue(r))
|
||||||
},
|
},
|
||||||
'contains': (l: any, r: any) => {
|
'contains': function * (l: any, r: any): OperatorGenerator {
|
||||||
l = toValue(l)
|
l = yield toValue(l)
|
||||||
if (isArray(l)) return l.some((i) => equals(i, r))
|
if (isArray(l)) return yield arrayIncludes(l, r)
|
||||||
if (isFunction(l?.indexOf)) return l.indexOf(toValue(r)) > -1
|
if (isFunction(l?.indexOf)) return l.indexOf(yield toValue(r)) > -1
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
'not': (v: any, ctx: Context) => isFalsy(toValue(v), ctx),
|
'not': function * (v: any, ctx: Context): OperatorGenerator { return isFalsy(yield toValue(v), ctx) },
|
||||||
'and': (l: any, r: any, ctx: Context) => isTruthy(toValue(l), ctx) && isTruthy(toValue(r), ctx),
|
'and': function * (l: any, r: any, ctx: Context): OperatorGenerator { return isTruthy(yield toValue(l), ctx) && isTruthy(yield toValue(r), ctx) },
|
||||||
'or': (l: any, r: any, ctx: Context) => isTruthy(toValue(l), ctx) || isTruthy(toValue(r), ctx)
|
'or': function * (l: any, r: any, ctx: Context): OperatorGenerator { return isTruthy(yield toValue(l), ctx) || isTruthy(yield toValue(r), ctx) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function equals (lhs: any, rhs: any): boolean {
|
export function * equals (lhs: any, rhs: any): Generator<unknown, boolean, any> {
|
||||||
if (isComparable(lhs)) return lhs.equals(rhs)
|
if (isComparable(lhs)) return yield lhs.equals(rhs)
|
||||||
if (isComparable(rhs)) return rhs.equals(lhs)
|
if (isComparable(rhs)) return yield rhs.equals(lhs)
|
||||||
lhs = toValue(lhs)
|
lhs = yield toValue(lhs)
|
||||||
rhs = toValue(rhs)
|
rhs = yield toValue(rhs)
|
||||||
if (isArray(lhs)) {
|
if (isArray(lhs)) {
|
||||||
return isArray(rhs) && arrayEquals(lhs, rhs)
|
return isArray(rhs) && (yield arrayEquals(lhs, rhs))
|
||||||
}
|
}
|
||||||
return lhs === rhs
|
return lhs === rhs
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayEquals (lhs: any[], rhs: any[]): boolean {
|
function * arrayEquals (lhs: any[], rhs: any[]): Generator<unknown, boolean, any> {
|
||||||
if (lhs.length !== rhs.length) return false
|
if (lhs.length !== rhs.length) return false
|
||||||
return !lhs.some((value, i) => !equals(value, rhs[i]))
|
for (let i = 0; i < lhs.length; i++) {
|
||||||
|
if (!(yield equals(lhs[i], rhs[i]))) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function arrayIncludes (arr: any[], item: any): boolean {
|
export function * arrayIncludes (arr: any[], item: any): Generator<unknown, boolean, any> {
|
||||||
return arr.some(value => equals(value, item))
|
for (const value of arr) {
|
||||||
|
if (yield equals(value, item)) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -56,12 +56,12 @@ export default class extends Tag {
|
|||||||
|
|
||||||
* render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
|
* render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
|
||||||
const r = this.liquid.renderer
|
const r = this.liquid.renderer
|
||||||
const target = toValue(yield this.value.value(ctx, ctx.opts.lenientIf))
|
const target = yield toValue(yield this.value.value(ctx, ctx.opts.lenientIf))
|
||||||
let branchHit = false
|
let branchHit = false
|
||||||
for (const branch of this.branches) {
|
for (const branch of this.branches) {
|
||||||
for (const valueToken of branch.values) {
|
for (const valueToken of branch.values) {
|
||||||
const value = yield evalToken(valueToken, ctx, ctx.opts.lenientIf)
|
const value = yield evalToken(valueToken, ctx, ctx.opts.lenientIf)
|
||||||
if (equals(target, value)) {
|
if (yield equals(target, value)) {
|
||||||
yield r.renderTemplates(branch.templates, ctx, emitter)
|
yield r.renderTemplates(branch.templates, ctx, emitter)
|
||||||
branchHit = true
|
branchHit = true
|
||||||
break
|
break
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
import { Liquid, TopLevelToken, Emitter, Value, TagToken, Context, Tag } from '..'
|
import { Liquid, TopLevelToken, Emitter, Value, TagToken, Context, Tag } from '..'
|
||||||
import { Arguments } from '../template'
|
import { Arguments } from '../template'
|
||||||
|
import { toValue } from '../util'
|
||||||
|
|
||||||
export default class extends Tag {
|
export default class extends Tag {
|
||||||
private value?: Value
|
private value?: Value
|
||||||
@@ -13,7 +14,7 @@ export default class extends Tag {
|
|||||||
}
|
}
|
||||||
* render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
|
* render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
|
||||||
if (!this.value) return
|
if (!this.value) return
|
||||||
const val = yield this.value.value(ctx, false)
|
const val = yield toValue(yield this.value.value(ctx, false))
|
||||||
emitter.write(val)
|
emitter.write(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { Liquid, Tag, Value, Emitter, isTruthy, TagToken, TopLevelToken, Context, Template } from '..'
|
import { Liquid, Tag, Value, Emitter, isTruthy, TagToken, TopLevelToken, Context, Template } from '..'
|
||||||
import { Parser } from '../parser'
|
import { Parser } from '../parser'
|
||||||
import { Arguments } from '../template'
|
import { Arguments } from '../template'
|
||||||
import { assert, assertEmpty } from '../util'
|
import { assert, assertEmpty, toValue } from '../util'
|
||||||
|
|
||||||
export default class extends Tag {
|
export default class extends Tag {
|
||||||
branches: { value: Value, templates: Template[] }[] = []
|
branches: { value: Value, templates: Template[] }[] = []
|
||||||
@@ -37,7 +37,7 @@ export default class extends Tag {
|
|||||||
const r = this.liquid.renderer
|
const r = this.liquid.renderer
|
||||||
|
|
||||||
for (const { value, templates } of this.branches) {
|
for (const { value, templates } of this.branches) {
|
||||||
const v = yield value.value(ctx, ctx.opts.lenientIf)
|
const v = yield toValue(yield value.value(ctx, ctx.opts.lenientIf))
|
||||||
if (isTruthy(v, ctx)) {
|
if (isTruthy(v, ctx)) {
|
||||||
yield r.renderTemplates(templates, ctx, emitter)
|
yield r.renderTemplates(templates, ctx, emitter)
|
||||||
return
|
return
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
import { Liquid, Tag, Value, TopLevelToken, Template, Emitter, isTruthy, isFalsy, Context, TagToken } from '..'
|
import { Liquid, Tag, Value, TopLevelToken, Template, Emitter, isTruthy, isFalsy, Context, TagToken } from '..'
|
||||||
import { Parser } from '../parser'
|
import { Parser } from '../parser'
|
||||||
import { Arguments } from '../template'
|
import { Arguments } from '../template'
|
||||||
|
import { toValue } from '../util'
|
||||||
|
|
||||||
export default class extends Tag {
|
export default class extends Tag {
|
||||||
branches: { value: Value, test: (val: any, ctx: Context) => boolean, templates: Template[] }[] = []
|
branches: { value: Value, test: (val: any, ctx: Context) => boolean, templates: Template[] }[] = []
|
||||||
@@ -44,7 +45,7 @@ export default class extends Tag {
|
|||||||
const r = this.liquid.renderer
|
const r = this.liquid.renderer
|
||||||
|
|
||||||
for (const { value, test, templates } of this.branches) {
|
for (const { value, test, templates } of this.branches) {
|
||||||
const v = yield value.value(ctx, ctx.opts.lenientIf)
|
const v = yield toValue(yield value.value(ctx, ctx.opts.lenientIf))
|
||||||
if (test(v, ctx)) {
|
if (test(v, ctx)) {
|
||||||
yield r.renderTemplates(templates, ctx, emitter)
|
yield r.renderTemplates(templates, ctx, emitter)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Tokenizer } from '../parser'
|
|||||||
import { Liquid } from '../liquid'
|
import { Liquid } from '../liquid'
|
||||||
import { Filter } from './filter'
|
import { Filter } from './filter'
|
||||||
import { FilterToken } from '../tokens'
|
import { FilterToken } from '../tokens'
|
||||||
|
import { toValue } from '../util'
|
||||||
|
|
||||||
export class Output extends TemplateImpl<OutputToken> implements Template {
|
export class Output extends TemplateImpl<OutputToken> implements Template {
|
||||||
value: Value
|
value: Value
|
||||||
@@ -22,7 +23,7 @@ export class Output extends TemplateImpl<OutputToken> implements Template {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public * render (ctx: Context, emitter: Emitter): IterableIterator<unknown> {
|
public * render (ctx: Context, emitter: Emitter): IterableIterator<unknown> {
|
||||||
const val = yield this.value.value(ctx, false)
|
const val = yield toValue(yield this.value.value(ctx, false))
|
||||||
emitter.write(val)
|
emitter.write(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Liquid, Drop } from '../../../src'
|
import { Liquid, Drop } from '../../../src'
|
||||||
|
import { mock, restore } from '../../stub/mockfs'
|
||||||
|
|
||||||
describe('drop/drop', function () {
|
describe('drop/drop', function () {
|
||||||
let liquid: Liquid
|
let liquid: Liquid
|
||||||
@@ -71,6 +72,19 @@ describe('drop/drop', function () {
|
|||||||
const html = await liquid.parseAndRender(tpl, { drop: new CustomDrop() })
|
const html = await liquid.parseAndRender(tpl, { drop: new CustomDrop() })
|
||||||
expect(html).toBe('foobar: foo;bar;')
|
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 () => {
|
it('should support valueOf in == expression', async () => {
|
||||||
class AddressDrop extends Drop {
|
class AddressDrop extends Drop {
|
||||||
valueOf () {
|
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() })
|
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')
|
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)
|
const html = await liquid.parseAndRender(src, scope)
|
||||||
return expect(html).toBe('yes')
|
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 () {
|
it('should support value and expression', async function () {
|
||||||
const src = `X{%if version and version != '' %}x{{version}}y{%endif%}Y`
|
const src = `X{%if version and version != '' %}x{{version}}y{%endif%}Y`
|
||||||
const scope = { 'version': '' }
|
const scope = { 'version': '' }
|
||||||
|
|||||||
@@ -142,6 +142,21 @@ describe('tags/include', function () {
|
|||||||
const html = await liquid.renderFile('with.html', { color: new ColorDrop() })
|
const html = await liquid.renderFile('with.html', { color: new ColorDrop() })
|
||||||
expect(html).toBe('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 () {
|
it('should support nested includes', async function () {
|
||||||
mock({
|
mock({
|
||||||
|
|||||||
@@ -226,6 +226,21 @@ describe('tags/render', function () {
|
|||||||
const html = await liquid.renderFile('with.html', { color: new ColorDrop() })
|
const html = await liquid.renderFile('with.html', { color: new ColorDrop() })
|
||||||
expect(html).toBe('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 () {
|
it('should support nested renders', async function () {
|
||||||
mock({
|
mock({
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ describe('tags/unless', function () {
|
|||||||
const html = await liquid.parseAndRender(src)
|
const html = await liquid.parseAndRender(src)
|
||||||
return expect(html).toBe('')
|
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 () {
|
it('should output unless contents in order', async function () {
|
||||||
const src = `
|
const src = `
|
||||||
|
|||||||
Reference in New Issue
Block a user