feat: promise in expression & nested property, #533 #276

This commit is contained in:
Harttle
2022-08-27 23:19:16 +08:00
committed by Harttle
parent 0997530bab
commit bbf00f37bf
19 changed files with 256 additions and 20537 deletions
+1
View File
@@ -3,3 +3,4 @@ dist
demo demo
coverage coverage
docs docs
private.*
+10
View File
@@ -7,13 +7,18 @@
"node": true "node": true
}, },
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json"
},
"plugins": [ "plugins": [
"deprecation",
"mocha", "mocha",
"standard", "standard",
"@typescript-eslint", "@typescript-eslint",
"promise" "promise"
], ],
"rules": { "rules": {
"deprecation/deprecation": "error",
"no-var": 2, "no-var": 2,
"prefer-const": 2, "prefer-const": 2,
"no-unused-vars": "off", "no-unused-vars": "off",
@@ -36,5 +41,10 @@
"rules": { "rules": {
"@typescript-eslint/no-var-requires": "off" "@typescript-eslint/no-var-requires": "off"
} }
}, {
"files": ["test/**/*.ts"],
"rules": {
"deprecation/deprecation": "off"
}
}] }]
} }
+92 -20474
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -84,6 +84,7 @@
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-standard": "^12.0.0", "eslint-config-standard": "^12.0.0",
"eslint-plugin-deprecation": "^1.3.2",
"eslint-plugin-import": "^2.15.0", "eslint-plugin-import": "^2.15.0",
"eslint-plugin-mocha": "^5.3.0", "eslint-plugin-mocha": "^5.3.0",
"eslint-plugin-node": "^8.0.1", "eslint-plugin-node": "^8.0.1",
+29 -18
View File
@@ -10,14 +10,19 @@ export const last = argumentsToValue((v: any) => isArray(v) ? arrayLast(v) : '')
export const first = argumentsToValue((v: any) => isArray(v) ? v[0] : '') export const first = argumentsToValue((v: any) => isArray(v) ? v[0] : '')
export const reverse = argumentsToValue((v: any[]) => [...toArray(v)].reverse()) export const reverse = argumentsToValue((v: any[]) => [...toArray(v)].reverse())
export function sort<T> (this: FilterImpl, arr: T[], property?: string) { export function * sort<T> (this: FilterImpl, arr: T[], property?: string): IterableIterator<unknown> {
arr = toValue(arr) const values: [T, string | number][] = []
const getValue = (obj: Scope) => property ? this.context.getFromScope(obj, stringify(property).split('.')) : obj for (const item of toArray(toValue(arr))) {
return [...toArray(arr)].sort((lhs, rhs) => { values.push([
lhs = getValue(lhs) item,
rhs = getValue(rhs) property ? yield this.context._getFromScope(item, stringify(property).split('.')) : item
return lhs < rhs ? -1 : (lhs > rhs ? 1 : 0) ])
}) }
return values.sort((lhs, rhs) => {
const lvalue = lhs[1]
const rvalue = rhs[1]
return lvalue < rvalue ? -1 : (lvalue > rvalue ? 1 : 0)
}).map(tuple => tuple[0])
} }
export function sortNatural<T> (input: T[], property?: string) { export function sortNatural<T> (input: T[], property?: string) {
@@ -31,9 +36,12 @@ export function sortNatural<T> (input: T[], property?: string) {
export const size = (v: string | any[]) => (v && v.length) || 0 export const size = (v: string | any[]) => (v && v.length) || 0
export function map (this: FilterImpl, arr: Scope[], property: string) { export function * map (this: FilterImpl, arr: Scope[], property: string): IterableIterator<unknown> {
arr = toValue(arr) const results = []
return toArray(arr).map(obj => this.context.getFromScope(obj, stringify(property).split('.'))) for (const item of toArray(toValue(arr))) {
results.push(yield this.context._getFromScope(item, stringify(property).split('.')))
}
return results
} }
export function compact<T> (this: FilterImpl, arr: T[]) { export function compact<T> (this: FilterImpl, arr: T[]) {
@@ -55,13 +63,16 @@ export function slice<T> (v: T[] | string, begin: number, length = 1): T[] | str
return v.slice(begin, begin + length) return v.slice(begin, begin + length)
} }
export function where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): T[] { export function * where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): IterableIterator<unknown> {
arr = toValue(arr) const values: unknown[] = []
return toArray(arr).filter(obj => { arr = toArray(toValue(arr))
const value = this.context.getFromScope(obj, stringify(property).split('.')) for (const item of arr) {
if (expected === undefined) return isTruthy(value, this.context) values.push(yield this.context._getFromScope(item, stringify(property).split('.')))
if (isComparable(expected)) return expected.equals(value) }
return value === expected return arr.filter((_, i) => {
if (expected === undefined) return isTruthy(values[i], this.context)
if (isComparable(expected)) return expected.equals(values[i])
return values[i] === expected
}) })
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import { toValue, evalToken, Value, Emitter, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types' import { toValue, _evalToken, Value, Emitter, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types'
import { Tokenizer } from '../../parser/tokenizer' import { Tokenizer } from '../../parser/tokenizer'
export default { export default {
@@ -37,7 +37,7 @@ export default {
const r = this.liquid.renderer const r = this.liquid.renderer
const cond = toValue(yield this.cond.value(ctx, ctx.opts.lenientIf)) const cond = toValue(yield this.cond.value(ctx, ctx.opts.lenientIf))
for (const branch of this.cases) { for (const branch of this.cases) {
const val = evalToken(branch.val, ctx, ctx.opts.lenientIf) const val = yield _evalToken(branch.val, ctx, ctx.opts.lenientIf)
if (val === cond) { if (val === cond) {
yield r.renderTemplates(branch.templates, ctx, emitter) yield r.renderTemplates(branch.templates, ctx, emitter)
return return
+4 -4
View File
@@ -1,5 +1,5 @@
import { assert } from '../../util/assert' import { assert } from '../../util/assert'
import { evalToken, Emitter, TagToken, Context, TagImplOptions } from '../../types' import { _evalToken, Emitter, TagToken, Context, TagImplOptions } from '../../types'
import { Tokenizer } from '../../parser/tokenizer' import { Tokenizer } from '../../parser/tokenizer'
export default { export default {
@@ -25,8 +25,8 @@ export default {
assert(this.candidates.length, () => `empty candidates: ${tagToken.getText()}`) assert(this.candidates.length, () => `empty candidates: ${tagToken.getText()}`)
}, },
render: function (ctx: Context, emitter: Emitter) { render: function * (ctx: Context, emitter: Emitter) {
const group = evalToken(this.group, ctx) const group = yield _evalToken(this.group, ctx)
const fingerprint = `cycle:${group}:` + this.candidates.join(',') const fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = ctx.getRegister('cycle') const groups = ctx.getRegister('cycle')
let idx = groups[fingerprint] let idx = groups[fingerprint]
@@ -38,7 +38,7 @@ export default {
const candidate = this.candidates[idx] const candidate = this.candidates[idx]
idx = (idx + 1) % this.candidates.length idx = (idx + 1) % this.candidates.length
groups[fingerprint] = idx groups[fingerprint] = idx
const html = evalToken(candidate, ctx) const html = yield _evalToken(candidate, ctx)
emitter.write(html) emitter.write(html)
} }
} as TagImplOptions } as TagImplOptions
+2 -2
View File
@@ -1,4 +1,4 @@
import { assert, Tokenizer, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types' import { assert, Tokenizer, _evalToken, Emitter, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types'
import { toEnumerable } from '../../util/collection' import { toEnumerable } from '../../util/collection'
import { ForloopDrop } from '../../drop/forloop-drop' import { ForloopDrop } from '../../drop/forloop-drop'
import { Hash, HashValue } from '../../template/tag/hash' import { Hash, HashValue } from '../../template/tag/hash'
@@ -40,7 +40,7 @@ export default {
}, },
render: function * (ctx: Context, emitter: Emitter): Generator<unknown, void | string, HashValue | Template[]> { render: function * (ctx: Context, emitter: Emitter): Generator<unknown, void | string, HashValue | Template[]> {
const r = this.liquid.renderer const r = this.liquid.renderer
let collection = toEnumerable(yield evalToken(this.collection, ctx)) let collection = toEnumerable(yield _evalToken(this.collection, ctx))
if (!collection.length) { if (!collection.length) {
yield r.renderTemplates(this.elseTemplates, ctx, emitter) yield r.renderTemplates(this.elseTemplates, ctx, emitter)
+2 -2
View File
@@ -1,4 +1,4 @@
import { assert, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types' import { assert, Tokenizer, _evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
import BlockMode from '../../context/block-mode' import BlockMode from '../../context/block-mode'
import { parseFilePath, renderFilePath } from './render' import { parseFilePath, renderFilePath } from './render'
@@ -32,7 +32,7 @@ export default {
ctx.setRegister('blocks', {}) ctx.setRegister('blocks', {})
ctx.setRegister('blockMode', BlockMode.OUTPUT) ctx.setRegister('blockMode', BlockMode.OUTPUT)
const scope = yield hash.render(ctx) const scope = yield hash.render(ctx)
if (withVar) scope[filepath] = evalToken(withVar, ctx) if (withVar) scope[filepath] = yield _evalToken(withVar, ctx)
const templates = yield liquid._parsePartialFile(filepath, ctx.sync, this['currentFile']) const templates = yield liquid._parsePartialFile(filepath, ctx.sync, this['currentFile'])
ctx.push(ctx.opts.jekyllInclude ? { include: scope } : scope) ctx.push(ctx.opts.jekyllInclude ? { include: scope } : scope)
yield renderer.renderTemplates(templates, ctx, emitter) yield renderer.renderTemplates(templates, ctx, emitter)
+5 -5
View File
@@ -3,7 +3,7 @@ import { assert } from '../../util/assert'
import { ForloopDrop } from '../../drop/forloop-drop' import { ForloopDrop } from '../../drop/forloop-drop'
import { toEnumerable } from '../../util/collection' import { toEnumerable } from '../../util/collection'
import { Liquid } from '../../liquid' import { Liquid } from '../../liquid'
import { Token, Template, evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types' import { Token, Template, evalQuotedToken, TypeGuards, Tokenizer, _evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
export default { export default {
parseFilePath, parseFilePath,
@@ -56,12 +56,12 @@ export default {
__assign(scope, yield hash.render(ctx)) __assign(scope, yield hash.render(ctx))
if (this['with']) { if (this['with']) {
const { value, alias } = this['with'] const { value, alias } = this['with']
scope[alias || filepath] = evalToken(value, ctx) scope[alias || filepath] = yield _evalToken(value, ctx)
} }
if (this['for']) { if (this['for']) {
const { value, alias } = this['for'] const { value, alias } = this['for']
let collection = evalToken(value, ctx) let collection = yield _evalToken(value, ctx)
collection = toEnumerable(collection) collection = toEnumerable(collection)
scope['forloop'] = new ForloopDrop(collection.length, value.getText(), alias) scope['forloop'] = new ForloopDrop(collection.length, value.getText(), alias)
for (const item of collection) { for (const item of collection) {
@@ -108,8 +108,8 @@ function optimize (templates: Template[]): string | Template[] {
return templates return templates
} }
export function renderFilePath (file: ParsedFileName, ctx: Context, liquid: Liquid) { export function * renderFilePath (file: ParsedFileName, ctx: Context, liquid: Liquid): IterableIterator<unknown> {
if (typeof file === 'string') return file if (typeof file === 'string') return file
if (Array.isArray(file)) return liquid.renderer.renderTemplates(file, ctx) if (Array.isArray(file)) return liquid.renderer.renderTemplates(file, ctx)
return evalToken(file, ctx) return yield _evalToken(file, ctx)
} }
+2 -2
View File
@@ -1,5 +1,5 @@
import { toEnumerable } from '../../util/collection' import { toEnumerable } from '../../util/collection'
import { assert, evalToken, Emitter, Hash, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types' import { assert, _evalToken, Emitter, Hash, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types'
import { TablerowloopDrop } from '../../drop/tablerowloop-drop' import { TablerowloopDrop } from '../../drop/tablerowloop-drop'
import { Tokenizer } from '../../parser/tokenizer' import { Tokenizer } from '../../parser/tokenizer'
@@ -31,7 +31,7 @@ export default {
}, },
render: function * (ctx: Context, emitter: Emitter) { render: function * (ctx: Context, emitter: Emitter) {
let collection = toEnumerable(yield evalToken(this.collection, ctx)) let collection = toEnumerable(yield _evalToken(this.collection, ctx))
const hash = yield this.hash.render(ctx) const hash = yield this.hash.render(ctx)
const offset = hash.offset || 0 const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit const limit = (hash.limit === undefined) ? collection.length : hash.limit
+21 -8
View File
@@ -4,6 +4,7 @@ import { NormalizedFullOptions, defaultOptions, RenderOptions } from '../liquid-
import { Scope } from './scope' import { Scope } from './scope'
import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore' import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore'
import { InternalUndefinedVariableError } from '../util/error' import { InternalUndefinedVariableError } from '../util/error'
import { toValueSync } from '../util/async'
type PropertyKey = string | number; type PropertyKey = string | number;
@@ -56,19 +57,31 @@ export class Context {
return [this.globals, this.environments, ...this.scopes] return [this.globals, this.environments, ...this.scopes]
.reduce((ctx, val) => __assign(ctx, val), {}) .reduce((ctx, val) => __assign(ctx, val), {})
} }
public get (paths: PropertyKey[]) { /**
const scope = this.findScope(paths[0]) * @deprecated use `_get()` instead
return this.getFromScope(scope, paths) */
public get (paths: PropertyKey[]): unknown {
return toValueSync(this._get(paths))
} }
public getFromScope (scope: object, paths: PropertyKey[] | string) { public * _get (paths: PropertyKey[]): IterableIterator<unknown> {
const scope = this.findScope(paths[0])
return yield this._getFromScope(scope, paths)
}
/**
* @deprecated use `_get()` instead
*/
public getFromScope (scope: unknown, paths: PropertyKey[] | string): IterableIterator<unknown> {
return toValueSync(this._getFromScope(scope, paths))
}
public * _getFromScope (scope: unknown, paths: PropertyKey[] | string): IterableIterator<unknown> {
if (isString(paths)) paths = paths.split('.') if (isString(paths)) paths = paths.split('.')
return paths.reduce((scope, path, i) => { for (let i = 0; i < paths.length; i++) {
scope = readProperty(scope, path, this.opts.ownPropertyOnly) scope = yield readProperty(scope as object, paths[i], this.opts.ownPropertyOnly)
if (isNil(scope) && this.strictVariables) { if (isNil(scope) && this.strictVariables) {
throw new InternalUndefinedVariableError((paths as string[]).slice(0, i + 1).join!('.')) throw new InternalUndefinedVariableError((paths as string[]).slice(0, i + 1).join!('.'))
} }
return scope }
}, scope) return scope
} }
public push (ctx: object) { public push (ctx: object) {
return this.scopes.push(ctx) return this.scopes.push(ctx)
+21 -10
View File
@@ -13,6 +13,7 @@ import { Context } from '../context/context'
import { range } from '../util/underscore' import { range } from '../util/underscore'
import { Operators } from '../render/operator' import { Operators } from '../render/operator'
import { UndefinedVariableError } from '../util/error' import { UndefinedVariableError } from '../util/error'
import { toValueSync } from '../util/async'
export class Expression { export class Expression {
private postfix: Token[] private postfix: Token[]
@@ -30,26 +31,36 @@ export class Expression {
const result = yield evalOperatorToken(ctx.opts.operators, token, l, r, ctx) const result = yield evalOperatorToken(ctx.opts.operators, token, l, r, ctx)
operands.push(result) operands.push(result)
} else { } else {
operands.push(yield evalToken(token, ctx, lenient && this.postfix.length === 1)) operands.push(yield _evalToken(token, ctx, lenient && this.postfix.length === 1))
} }
} }
return operands[0] return operands[0]
} }
} }
export function evalToken (token: Token | undefined, ctx: Context, lenient = false): any { /**
if (TypeGuards.isPropertyAccessToken(token)) return evalPropertyAccessToken(token, ctx, lenient) * @deprecated use `_evalToken` instead
if (TypeGuards.isRangeToken(token)) return evalRangeToken(token, ctx) */
export function * evalToken (token: Token | undefined, ctx: Context, lenient = false) {
return toValueSync(_evalToken(token, ctx, lenient))
}
export function * _evalToken (token: Token | undefined, ctx: Context, lenient = false): IterableIterator<unknown> {
if (TypeGuards.isPropertyAccessToken(token)) return yield evalPropertyAccessToken(token, ctx, lenient)
if (TypeGuards.isRangeToken(token)) return yield evalRangeToken(token, ctx)
if (TypeGuards.isLiteralToken(token)) return evalLiteralToken(token) if (TypeGuards.isLiteralToken(token)) return evalLiteralToken(token)
if (TypeGuards.isNumberToken(token)) return evalNumberToken(token) if (TypeGuards.isNumberToken(token)) return evalNumberToken(token)
if (TypeGuards.isWordToken(token)) return token.getText() if (TypeGuards.isWordToken(token)) return token.getText()
if (TypeGuards.isQuotedToken(token)) return evalQuotedToken(token) if (TypeGuards.isQuotedToken(token)) return evalQuotedToken(token)
} }
function evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean) { function * evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean): IterableIterator<unknown> {
const props: string[] = token.props.map(prop => evalToken(prop, ctx, false)) const props: string[] = []
for (const prop of token.props) {
props.push((yield _evalToken(prop, ctx, false)) as unknown as string)
}
try { try {
return ctx.get([token.propertyName, ...props]) return yield ctx._get([token.propertyName, ...props])
} catch (e) { } catch (e) {
if (lenient && (e as Error).name === 'InternalUndefinedVariableError') return null if (lenient && (e as Error).name === 'InternalUndefinedVariableError') return null
throw (new UndefinedVariableError(e as Error, token)) throw (new UndefinedVariableError(e as Error, token))
@@ -74,9 +85,9 @@ function evalLiteralToken (token: LiteralToken) {
return literalValues[token.literal] return literalValues[token.literal]
} }
function evalRangeToken (token: RangeToken, ctx: Context) { function * evalRangeToken (token: RangeToken, ctx: Context) {
const low: number = evalToken(token.lhs, ctx) const low: number = yield _evalToken(token.lhs, ctx)
const high: number = evalToken(token.rhs, ctx) const high: number = yield _evalToken(token.rhs, ctx)
return range(+low, +high + 1) return range(+low, +high + 1)
} }
+4 -4
View File
@@ -1,4 +1,4 @@
import { evalToken } from '../../render/expression' import { _evalToken } from '../../render/expression'
import { Context } from '../../context/context' import { Context } from '../../context/context'
import { identify } from '../../util/underscore' import { identify } from '../../util/underscore'
import { FilterImplOptions } from './filter-impl-options' import { FilterImplOptions } from './filter-impl-options'
@@ -17,11 +17,11 @@ export class Filter {
this.args = args this.args = args
this.liquid = liquid this.liquid = liquid
} }
public render (value: any, context: Context) { public * render (value: any, context: Context): IterableIterator<unknown> {
const argv: any[] = [] const argv: any[] = []
for (const arg of this.args as FilterArg[]) { for (const arg of this.args as FilterArg[]) {
if (isKeyValuePair(arg)) argv.push([arg[0], evalToken(arg[1], context)]) if (isKeyValuePair(arg)) argv.push([arg[0], yield _evalToken(arg[1], context)])
else argv.push(evalToken(arg, context)) else argv.push(yield _evalToken(arg, context))
} }
return this.impl.apply({ context, liquid: this.liquid }, [value, ...argv]) return this.impl.apply({ context, liquid: this.liquid }, [value, ...argv])
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import { evalToken } from '../../render/expression' import { _evalToken } from '../../render/expression'
import { Context } from '../../context/context' import { Context } from '../../context/context'
import { Tokenizer } from '../../parser/tokenizer' import { Tokenizer } from '../../parser/tokenizer'
@@ -25,7 +25,7 @@ export class Hash {
* render (ctx: Context): Generator<unknown, HashValue, unknown> { * render (ctx: Context): Generator<unknown, HashValue, unknown> {
const hash = {} const hash = {}
for (const key of Object.keys(this.hash)) { for (const key of Object.keys(this.hash)) {
hash[key] = this.hash[key] === undefined ? true : yield evalToken(this.hash[key], ctx) hash[key] = this.hash[key] === undefined ? true : yield _evalToken(this.hash[key], ctx)
} }
return hash return hash
} }
+2 -1
View File
@@ -20,7 +20,8 @@ export { TopLevelToken } from './tokens/toplevel-token'
export { Tokenizer } from './parser/tokenizer' export { Tokenizer } from './parser/tokenizer'
export { Hash } from './template/tag/hash' export { Hash } from './template/tag/hash'
export { Value } from './template/value' export { Value } from './template/value'
export { evalToken, evalQuotedToken } from './render/expression' // eslint-disable-next-line deprecation/deprecation
export { _evalToken, evalToken, evalQuotedToken } from './render/expression'
export { toPromise, toThenable, toValueSync } from './util/async' export { toPromise, toThenable, toValueSync } from './util/async'
export { defaultOperators, Operators } from './render/operator' export { defaultOperators, Operators } from './render/operator'
export { createTrie, Trie } from './util/operator-trie' export { createTrie, Trie } from './util/operator-trie'
+2 -2
View File
@@ -109,7 +109,7 @@ const formatCodes = {
M: (d: LiquidDate) => d.getMinutes(), M: (d: LiquidDate) => d.getMinutes(),
N: (d: LiquidDate, opts: FormatOptions) => { N: (d: LiquidDate, opts: FormatOptions) => {
const width = Number(opts.width) || 9 const width = Number(opts.width) || 9
const str = String(d.getMilliseconds()).substr(0, width) const str = String(d.getMilliseconds()).slice(0, width)
return padEnd(str, width, '0') return padEnd(str, width, '0')
}, },
p: (d: LiquidDate) => (d.getHours() < 12 ? 'AM' : 'PM'), p: (d: LiquidDate) => (d.getHours() < 12 ? 'AM' : 'PM'),
@@ -123,7 +123,7 @@ const formatCodes = {
W: (d: LiquidDate) => getWeekOfYear(d, 1), W: (d: LiquidDate) => getWeekOfYear(d, 1),
x: (d: LiquidDate) => d.toLocaleDateString(), x: (d: LiquidDate) => d.toLocaleDateString(),
X: (d: LiquidDate) => d.toLocaleTimeString(), X: (d: LiquidDate) => d.toLocaleTimeString(),
y: (d: LiquidDate) => d.getFullYear().toString().substring(2, 4), y: (d: LiquidDate) => d.getFullYear().toString().slice(2, 4),
Y: (d: LiquidDate) => d.getFullYear(), Y: (d: LiquidDate) => d.getFullYear(),
z: (d: LiquidDate, opts: FormatOptions) => { z: (d: LiquidDate, opts: FormatOptions) => {
const nOffset = Math.abs(d.getTimezoneOffset()) const nOffset = Math.abs(d.getTimezoneOffset())
+1 -1
View File
@@ -150,7 +150,7 @@ export function changeCase (str: string): string {
} }
export function ellipsis (str: string, N: number): string { export function ellipsis (str: string, N: number): string {
return str.length > N ? str.substr(0, N - 3) + '...' : str return str.length > N ? str.slice(0, N - 3) + '...' : str
} }
// compare string in case-insensitive way, undefined values to the tail // compare string in case-insensitive way, undefined values to the tail
+53
View File
@@ -276,4 +276,57 @@ describe('Issues', function () {
const result = liquid.evalValueSync('a > b', { a: 1, b: 2 }) const result = liquid.evalValueSync('a > b', { a: 1, b: 2 })
expect(result).to.equal(false) expect(result).to.equal(false)
}) })
it('#276 Promise support in expressions', async () => {
const liquid = new Liquid()
const tpl = '{%if name == "alice" %}true{%endif%}'
const ctx = { name: Promise.resolve('alice') }
const html = await liquid.parseAndRender(tpl, ctx)
expect(html).to.equal('true')
})
it('#533 Nested Promise support for scope object', async () => {
const liquid = new Liquid()
const context = {
a: 1,
b: Promise.resolve(1),
async c () { return 1 },
d: { d: 1 },
e: { e: Promise.resolve(1) },
f: {
async f () { return 1 }
},
g: Promise.resolve({ g: 1 }),
async h () {
return { h: 1 }
},
i: Promise.resolve({
i: Promise.resolve(1)
}),
j: Promise.resolve({
async j () {
return 1
}
})
}
expect(await liquid.evalValue('a == 1', context)).to.equal(true)
expect(await liquid.evalValue('b == 1', context)).to.equal(true)
expect(await liquid.evalValue('c == 1', context)).to.equal(true)
expect(await liquid.evalValue('d.d == 1', context)).to.equal(true)
expect(await liquid.evalValue('e.e == 1', context)).to.equal(true)
expect(await liquid.evalValue('f.f == 1', context)).to.equal(true)
expect(await liquid.evalValue('g.g == 1', context)).to.equal(true)
expect(await liquid.evalValue('h.h == 1', context)).to.equal(true)
expect(await liquid.evalValue('i.i == 1', context)).to.equal(true)
expect(await liquid.evalValue('j.j == 1', context)).to.equal(true)
expect(await liquid.parseAndRender('{{a}}', context)).to.equal('1')
expect(await liquid.parseAndRender('{{b}}', context)).to.equal('1')
expect(await liquid.parseAndRender('{{c}}', context)).to.equal('1')
expect(await liquid.parseAndRender('{{d.d}}', context)).to.equal('1')
expect(await liquid.parseAndRender('{{e.e}}', context)).to.equal('1')
expect(await liquid.parseAndRender('{{f.f}}', context)).to.equal('1')
expect(await liquid.parseAndRender('{{g.g}}', context)).to.equal('1')
expect(await liquid.parseAndRender('{{h.h}}', context)).to.equal('1')
expect(await liquid.parseAndRender('{{i.i}}', context)).to.equal('1')
expect(await liquid.parseAndRender('{{j.j}}', context)).to.equal('1')
})
}) })