feat: renderSync, parseAndRenderSync and renderFileSync, see #48

This commit is contained in:
harttle
2019-08-26 10:15:43 -05:00
committed by Jun Yang
parent 8028f82499
commit 7fb01ad69a
66 changed files with 896 additions and 272 deletions
+1
View File
@@ -18,6 +18,7 @@
"prefer-const": 2,
"no-unused-vars": "off",
"indent": "off",
"no-dupe-class-members": "off",
"@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
+3 -3
View File
@@ -10,9 +10,9 @@ engine.registerTag('header', {
const [key, val] = token.args.split(':')
this[key] = val
},
render: function (scope, hash) {
const title = this.liquid.evalValue(this.content, scope)
return `<h1>${title}</h1>`
render: async function (scope, hash, emitter) {
const title = await this.liquid.evalValue(this.content, scope)
emitter.write(`<h1>${title}</h1>`)
}
})
+4 -4
View File
@@ -1,4 +1,4 @@
import { Liquid, TagToken, Hash, Context } from 'liquidjs'
import { Liquid, TagToken, Hash, Context, Emitter } from 'liquidjs'
const engine = new Liquid({
root: __dirname,
@@ -10,9 +10,9 @@ engine.registerTag('header', {
const [key, val] = token.args.split(':')
this[key] = val
},
render: function (context: Context, hash: Hash) {
const title = this.liquid.evalValue(this['content'], context)
return `<h1>${title}</h1>`
render: async function (context: Context, hash: Hash, emitter: Emitter) {
const title = await this.liquid.evalValue(this['content'], context)
emitter.write(`<h1>${title}</h1>`)
}
})
+3 -1
View File
@@ -12,6 +12,8 @@ export default {
this.value = match[2]
},
render: async function (ctx: Context) {
ctx.front()[this.key] = await this.liquid.evalValue(this.value, ctx)
ctx.front()[this.key] = ctx.sync
? this.liquid.evalValueSync(this.value, ctx)
: await this.liquid.evalValue(this.value, ctx)
}
} as ITagImplOptions
+5 -1
View File
@@ -17,9 +17,13 @@ export default {
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const blocks = ctx.getRegister('blocks')
const childDefined = blocks[this.block]
const r = this.liquid.renderer
const html = childDefined !== undefined
? childDefined
: await this.liquid.renderer.renderTemplates(this.tpls, ctx)
: (ctx.sync
? r.renderTemplatesSync(this.tpls, ctx)
: await r.renderTemplates(this.tpls, ctx)
)
if (ctx.getRegister('blockMode', BlockMode.OUTPUT) === BlockMode.STORE) {
blocks[this.block] = html
+3 -3
View File
@@ -1,7 +1,7 @@
import { RenderBreakError } from '../../util/error'
import { Emitter, Context, Hash } from '../../types'
export default {
render: async function () {
throw new RenderBreakError('break')
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
emitter.break = true
}
}
+4 -1
View File
@@ -21,7 +21,10 @@ export default {
stream.start()
},
render: async function (ctx: Context) {
const html = await this.liquid.renderer.renderTemplates(this.templates, ctx)
const r = this.liquid.renderer
const html = ctx.sync
? r.renderTemplatesSync(this.templates, ctx)
: await r.renderTemplates(this.templates, ctx)
ctx.front()[this.variable] = html
}
} as ITagImplOptions
+19 -4
View File
@@ -25,15 +25,30 @@ export default {
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i]
const val = new Expression(branch.val).value(ctx)
const cond = new Expression(this.cond).value(ctx)
const val = await new Expression(branch.val).value(ctx)
const cond = await new Expression(this.cond).value(ctx)
if (val === cond) {
this.liquid.renderer.renderTemplates(branch.templates, ctx, emitter)
await r.renderTemplates(branch.templates, ctx, emitter)
return
}
}
this.liquid.renderer.renderTemplates(this.elseTemplates, ctx, emitter)
await r.renderTemplates(this.elseTemplates, ctx, emitter)
},
renderSync: function (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i]
const val = new Expression(branch.val).valueSync(ctx)
const cond = new Expression(this.cond).valueSync(ctx)
if (val === cond) {
r.renderTemplatesSync(branch.templates, ctx, emitter)
return
}
}
r.renderTemplatesSync(this.elseTemplates, ctx, emitter)
}
} as ITagImplOptions
+3 -3
View File
@@ -1,7 +1,7 @@
import { RenderBreakError } from '../../util/error'
import { Emitter, Context, Hash } from '../../types'
export default {
render: async function () {
throw new RenderBreakError('continue')
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
emitter.continue = true
}
}
+7 -2
View File
@@ -22,7 +22,9 @@ export default {
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const group = this.group.value(ctx)
const group = ctx.sync
? this.group.valueSync(ctx)
: await this.group.value(ctx)
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = ctx.getRegister('cycle')
let idx = groups[fingerprint]
@@ -34,6 +36,9 @@ export default {
const candidate = this.candidates[idx]
idx = (idx + 1) % this.candidates.length
groups[fingerprint] = idx
emitter.write(new Expression(candidate).value(ctx))
const html = ctx.sync
? new Expression(candidate).valueSync(ctx)
: await new Expression(candidate).value(ctx)
emitter.write(html)
}
} as ITagImplOptions
+18 -11
View File
@@ -37,7 +37,10 @@ export default {
stream.start()
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
let collection = new Expression(this.collection).value(ctx)
const r = this.liquid.renderer
let collection = ctx.sync
? new Expression(this.collection).valueSync(ctx)
: await new Expression(this.collection).value(ctx)
if (!isArray(collection)) {
if (isString(collection) && collection.length > 0) {
@@ -47,7 +50,9 @@ export default {
}
}
if (!isArray(collection) || !collection.length) {
this.liquid.renderer.renderTemplates(this.elseTemplates, ctx, emitter)
ctx.sync
? r.renderTemplatesSync(this.elseTemplates, ctx, emitter)
: await r.renderTemplates(this.elseTemplates, ctx, emitter)
return
}
@@ -57,17 +62,19 @@ export default {
collection = collection.slice(offset, offset + limit)
if (this.reversed) collection.reverse()
const context = { forloop: new ForloopDrop(collection.length) }
ctx.push(context)
const scope = { forloop: new ForloopDrop(collection.length) }
ctx.push(scope)
for (const item of collection) {
context[this.variable] = item
try {
await this.liquid.renderer.renderTemplates(this.templates, ctx, emitter)
} catch (e) {
if (e.name !== 'RenderBreakError') throw e
if (e.message === 'break') break
scope[this.variable] = item
ctx.sync
? r.renderTemplatesSync(this.templates, ctx, emitter)
: await r.renderTemplates(this.templates, ctx, emitter)
if (emitter.break) {
emitter.break = false
break
}
context.forloop.next()
emitter.continue = false
scope.forloop.next()
}
ctx.pop()
}
+18 -3
View File
@@ -28,13 +28,28 @@ export default {
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
for (const branch of this.branches) {
const cond = new Expression(branch.cond).value(ctx)
const cond = await new Expression(branch.cond).value(ctx)
if (isTruthy(cond)) {
await this.liquid.renderer.renderTemplates(branch.templates, ctx, emitter)
await r.renderTemplates(branch.templates, ctx, emitter)
return
}
}
await this.liquid.renderer.renderTemplates(this.elseTemplates, ctx, emitter)
await r.renderTemplates(this.elseTemplates, ctx, emitter)
},
renderSync: function (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
for (const branch of this.branches) {
const cond = new Expression(branch.cond).valueSync(ctx)
if (isTruthy(cond)) {
r.renderTemplatesSync(branch.templates, ctx, emitter)
return
}
}
r.renderTemplatesSync(this.elseTemplates, ctx, emitter)
}
} as ITagImplOptions
+34 -4
View File
@@ -17,14 +17,14 @@ export default {
match = withRE.exec(token.args)
if (match) this.with = match[1]
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
renderSync: function (ctx: Context, hash: Hash, emitter: Emitter) {
let filepath
if (ctx.opts.dynamicPartials) {
if (quotedLine.exec(this.value)) {
const template = this.value.slice(1, -1)
filepath = await this.liquid.parseAndRender(template, ctx.getAll(), ctx.opts)
filepath = this.liquid.parseAndRenderSync(template, ctx.getAll(), ctx.opts)
} else {
filepath = new Expression(this.value).value(ctx)
filepath = new Expression(this.value).valueSync(ctx)
}
} else {
filepath = this.staticValue
@@ -37,7 +37,37 @@ export default {
ctx.setRegister('blocks', {})
ctx.setRegister('blockMode', BlockMode.OUTPUT)
if (this.with) {
hash[filepath] = new Expression(this.with).evaluate(ctx)
hash[filepath] = new Expression(this.with).evaluateSync(ctx)
}
const templates = this.liquid.getTemplateSync(filepath, ctx.opts)
ctx.push(hash)
this.liquid.renderer.renderTemplatesSync(templates, ctx, emitter)
ctx.pop()
ctx.setRegister('blocks', originBlocks)
ctx.setRegister('blockMode', originBlockMode)
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
let filepath
if (ctx.opts.dynamicPartials) {
if (quotedLine.exec(this.value)) {
const template = this.value.slice(1, -1)
filepath = await this.liquid.parseAndRender(template, ctx.getAll(), ctx.opts)
} else {
filepath = await new Expression(this.value).value(ctx)
}
} else {
filepath = this.staticValue
}
assert(filepath, `cannot include with empty filename`)
const originBlocks = ctx.getRegister('blocks')
const originBlockMode = ctx.getRegister('blockMode')
ctx.setRegister('blocks', {})
ctx.setRegister('blockMode', BlockMode.OUTPUT)
if (this.with) {
hash[filepath] = await new Expression(this.with).evaluate(ctx)
}
const templates = await this.liquid.getTemplate(filepath, ctx.opts)
ctx.push(hash)
+14 -4
View File
@@ -21,21 +21,31 @@ export default {
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const layout = ctx.opts.dynamicPartials
? await (new Expression(this.layout).value(ctx))
? (ctx.sync
? new Expression(this.layout).valueSync(ctx)
: await new Expression(this.layout).value(ctx)
)
: this.staticLayout
assert(layout, `cannot apply layout with empty filename`)
// render the remaining tokens immediately
ctx.setRegister('blockMode', BlockMode.STORE)
const blocks = ctx.getRegister('blocks')
const html = await this.liquid.renderer.renderTemplates(this.tpls, ctx)
const r = this.liquid.renderer
const html = ctx.sync
? r.renderTemplatesSync(this.tpls, ctx)
: await r.renderTemplates(this.tpls, ctx)
if (blocks[''] === undefined) {
blocks[''] = html
}
const templates = await this.liquid.getTemplate(layout, ctx.opts)
const templates = ctx.sync
? this.liquid.getTemplateSync(layout, ctx.opts)
: await this.liquid.getTemplate(layout, ctx.opts)
ctx.push(hash)
ctx.setRegister('blockMode', BlockMode.OUTPUT)
const partial = await this.liquid.renderer.renderTemplates(templates, ctx)
const partial = ctx.sync
? r.renderTemplatesSync(templates, ctx)
: await r.renderTemplates(templates, ctx)
ctx.pop()
emitter.write(partial)
}
+2 -2
View File
@@ -15,7 +15,7 @@ export default {
})
stream.start()
},
render: function (ctx: Context, hash: Hash, emitter: Emitter) {
emitter.write(this.tokens.map((token: Token) => token.raw).join(''))
render: function (ctx: Context) {
return this.tokens.map((token: Token) => token.raw).join('')
}
} as ITagImplOptions
+7 -2
View File
@@ -29,13 +29,16 @@ export default {
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
let collection = new Expression(this.collection).value(ctx) || []
let collection = ctx.sync
? new Expression(this.collection).valueSync(ctx) || []
: await new Expression(this.collection).value(ctx) || []
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
collection = collection.slice(offset, offset + limit)
const cols = hash.cols || collection.length
const r = this.liquid.renderer
const tablerowloop = new TablerowloopDrop(collection.length, cols)
const scope = { tablerowloop }
ctx.push(scope)
@@ -47,7 +50,9 @@ export default {
emitter.write(`<tr class="row${tablerowloop.row()}">`)
}
emitter.write(`<td class="col${tablerowloop.col()}">`)
await this.liquid.renderer.renderTemplates(this.templates, ctx, emitter)
ctx.sync
? r.renderTemplatesSync(this.templates, ctx, emitter)
: await r.renderTemplates(this.templates, ctx, emitter)
emitter.write('</td>')
}
if (collection.length) emitter.write('</tr>')
+13 -4
View File
@@ -20,10 +20,19 @@ export default {
stream.start()
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const cond = new Expression(this.cond).value(ctx)
renderSync: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
const cond = new Expression(this.cond).valueSync(ctx)
isFalsy(cond)
? await this.liquid.renderer.renderTemplates(this.templates, ctx, emitter)
: await this.liquid.renderer.renderTemplates(this.elseTemplates, ctx, emitter)
? r.renderTemplatesSync(this.templates, ctx, emitter)
: r.renderTemplatesSync(this.elseTemplates, ctx, emitter)
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
const cond = await new Expression(this.cond).value(ctx)
await isFalsy(cond)
? r.renderTemplates(this.templates, ctx, emitter)
: r.renderTemplates(this.elseTemplates, ctx, emitter)
}
} as ITagImplOptions
+3 -1
View File
@@ -9,8 +9,10 @@ export class Context {
private scopes: Scope[] = [{}]
private registers = {}
public environments: Scope
public sync: boolean
public opts: NormalizedFullOptions
public constructor (ctx: object = {}, opts?: NormalizedFullOptions) {
public constructor (ctx: object = {}, opts?: NormalizedFullOptions, sync: boolean = false) {
this.sync = sync
this.opts = applyDefault(opts)
this.environments = ctx
}
+15 -1
View File
@@ -44,8 +44,22 @@ async function readFile (url: string): Promise<string> {
})
}
function readFileSync (url: string): string {
const xhr = new XMLHttpRequest()
xhr.open('GET', url, false)
xhr.send()
if (xhr.status < 200 || xhr.status >= 300) {
throw new Error(xhr.statusText)
}
return xhr.responseText as string
}
async function exists () {
return true
}
export default { readFile, resolve, exists } as IFS
function existsSync () {
return true
}
export default { readFile, resolve, exists, existsSync, readFileSync } as IFS
+2
View File
@@ -1,5 +1,7 @@
export default interface IFS {
exists: (filepath: string) => Promise<boolean>;
readFile: (filepath: string) => Promise<string>;
existsSync: (filepath: string) => boolean;
readFileSync: (filepath: string) => string;
resolve: (root: string, file: string, ext: string) => string;
}
+12 -1
View File
@@ -1,6 +1,6 @@
import * as _ from '../util/underscore'
import { resolve, extname } from 'path'
import { stat, readFile } from 'fs'
import { stat, statSync, readFile, readFileSync } from 'fs'
import IFS from './ifs'
const statAsync = _.promisify(stat)
@@ -13,6 +13,17 @@ const fs: IFS = {
readFile: filepath => {
return readFileAsync(filepath, 'utf8')
},
existsSync: (filepath: string) => {
try {
statSync(filepath)
return true
} catch (err) {
return false
}
},
readFileSync: filepath => {
return readFileSync(filepath, 'utf8')
},
resolve: (root: string, file: string, ext: string) => {
if (!extname(file)) file += ext
return resolve(root, file)
+1 -1
View File
@@ -88,7 +88,7 @@ export function applyDefault (options?: NormalizedOptions): NormalizedFullOption
return { ...defaultOptions, ...options }
}
function normalizeStringArray (value: any): string[] {
export function normalizeStringArray (value: any): string[] {
if (_.isArray(value)) return value as string[]
if (_.isString(value)) return [value as string]
return []
+79 -26
View File
@@ -11,9 +11,12 @@ import { ITagImplOptions } from './template/tag/itag-impl-options'
import { Value } from './template/value'
import builtinTags from './builtin/tags'
import builtinFilters from './builtin/filters'
import { LiquidOptions, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
import { LiquidOptions, normalizeStringArray, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
import { FilterImplOptions } from './template/filter/filter-impl-options'
import IFS from './fs/ifs'
type GetTemplateResult = ITemplate[] | undefined
export * from './types'
export class Liquid {
@@ -34,47 +37,72 @@ export class Liquid {
_.forOwn(builtinTags, (conf, name) => this.registerTag(name, conf))
_.forOwn(builtinFilters, (handler, name) => this.registerFilter(name, handler))
}
public parse (html: string, filepath?: string) {
public parse (html: string, filepath?: string): ITemplate[] {
const tokens = this.tokenizer.tokenize(html, filepath)
return this.parser.parse(tokens)
}
public render (tpl: ITemplate[], scope?: object, opts?: LiquidOptions) {
public render (tpl: ITemplate[], scope?: object, opts?: LiquidOptions): Promise<string> {
const options = { ...this.options, ...normalize(opts) }
const ctx = new Context(scope, options)
return this.renderer.renderTemplates(tpl, ctx)
}
public async parseAndRender (html: string, ctx?: object, opts?: LiquidOptions) {
const tpl = await this.parse(html)
return this.render(tpl, ctx, opts)
public renderSync (tpl: ITemplate[], scope?: object, opts?: LiquidOptions): string {
const options = { ...this.options, ...normalize(opts) }
const ctx = new Context(scope, options, true)
return this.renderer.renderTemplatesSync(tpl, ctx)
}
public async getTemplate (file: string, opts?: LiquidOptions) {
const options = normalize(opts)
const roots = options.root ? [...options.root, ...this.options.root] : this.options.root
const paths = roots.map(root => this.fs.resolve(root, file, this.options.extname))
public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<string> {
const tpl = this.parse(html)
return this.render(tpl, scope, opts)
}
public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): string {
const tpl = this.parse(html)
return this.renderSync(tpl, scope, opts)
}
public getTemplateSync (file: string, opts?: LiquidOptions): ITemplate[] {
const options = { ...this.options, ...normalize(opts) }
const paths = options.root.map(root => this.fs.resolve(root, file, options.extname))
for (const filepath of paths) {
if (this.options.cache && this.cache[filepath]) return this.cache[filepath]
if (!(await this.fs.exists(filepath))) continue
const value = this.parse(await this.fs.readFile(filepath), filepath)
if (this.options.cache) this.cache[filepath] = value
return value
const tpl = this.respectCache(filepath, () => {
if (!(this.fs.existsSync(filepath))) return
return this.parse(this.fs.readFileSync(filepath), filepath)
})
if (tpl) return tpl
}
const err = new Error('ENOENT') as any
err.message = `ENOENT: Failed to lookup "${file}" in "${roots}"`
err.code = 'ENOENT'
throw err
throw this.lookupError(file, options.root)
}
public async getTemplate (file: string, opts?: LiquidOptions): Promise<ITemplate[]> {
const options = { ...this.options, ...normalize(opts) }
const paths = options.root.map(root => this.fs.resolve(root, file, options.extname))
for (const filepath of paths) {
const tpl = await this.respectCache(filepath, async () => {
if (!(await this.fs.exists(filepath))) return
return this.parse(await this.fs.readFile(filepath), filepath)
})
if (tpl !== undefined) return tpl
}
throw this.lookupError(file, options.root)
}
public async renderFile (file: string, ctx?: object, opts?: LiquidOptions) {
const options = normalize(opts)
const templates = await this.getTemplate(file, options)
const templates = await this.getTemplate(file, opts)
return this.render(templates, ctx, opts)
}
public evalValue (str: string, ctx: Context) {
public renderFileSync (file: string, ctx?: object, opts?: LiquidOptions) {
const options = normalize(opts)
const templates = this.getTemplateSync(file, options)
return this.renderSync(templates, ctx, opts)
}
public async evalValue (str: string, ctx: Context): Promise<any> {
return new Value(str, this.options.strictFilters).value(ctx)
}
public evalValueSync (str: string, ctx: Context): any {
return new Value(str, this.options.strictFilters).valueSync(ctx)
}
public registerFilter (name: string, filter: FilterImplOptions) {
return Filter.register(name, filter)
}
@@ -85,10 +113,35 @@ export class Liquid {
return plugin.call(this, Liquid)
}
public express () {
const self = this
const self = this // eslint-disable-line
return function (this: any, filePath: string, ctx: object, cb: (err: Error | null, html?: string) => void) {
const opts = { root: this.root }
const opts = { root: [...normalizeStringArray(this.root), ...self.options.root] }
self.renderFile(filePath, ctx, opts).then(html => cb(null, html), cb)
}
}
private lookupError (file: string, roots: string[]) {
const err = new Error('ENOENT') as any
err.message = `ENOENT: Failed to lookup "${file}" in "${roots}"`
err.code = 'ENOENT'
return err
}
private setCache<T extends GetTemplateResult> (filepath: string, tpl: T): T {
if (tpl === undefined) return tpl
this.cache[filepath] = tpl
return tpl
}
private respectCache (filepath: string, resolver: () => GetTemplateResult): GetTemplateResult
private respectCache (filepath: string, resolver: () => Promise<GetTemplateResult>): Promise<GetTemplateResult>
private respectCache (filepath: string, resolver: () => GetTemplateResult | Promise<GetTemplateResult>): GetTemplateResult | Promise<GetTemplateResult> {
if (!this.options.cache) return resolver()
if (this.cache[filepath]) return this.cache[filepath]
const tpl = resolver()
if (tpl instanceof Promise) {
return tpl.then(c => this.setCache(filepath, c))
}
return this.setCache(filepath, tpl)
}
}
+3 -1
View File
@@ -1,5 +1,7 @@
export class Emitter {
public html: string = '';
public html = '';
public break = false;
public continue = false;
public write (html: string) {
this.html += html
+39 -22
View File
@@ -1,38 +1,55 @@
import { assert } from '../util/assert'
import { isRange, rangeValue } from './range'
import { isRange, rangeValue, rangeValueSync } from './range'
import { Value } from './value'
import { Context } from '../context/context'
import { toValue } from '../util/underscore'
import { isOperator, precedence, operatorImpls } from './operator'
export class Expression {
private str: string
private operands: any[] = []
private postfix: string[]
public constructor (str: string = '') {
this.str = str
public constructor (str = '') {
this.postfix = [...toPostfix(str)]
}
public evaluate (ctx: Context): any {
public async evaluate (ctx: Context): Promise<any> {
assert(ctx, 'unable to evaluate: context not defined')
const operands = []
for (const token of toPostfix(this.str)) {
for (const token of this.postfix) {
if (isOperator(token)) {
const r = operands.pop()
const l = operands.pop()
this.evaluateOnce(token)
} else if (isRange(token)) {
this.operands.push(await rangeValue(token, ctx))
} else this.operands.push(await new Value(token).evaluate(ctx))
}
return this.operands[0]
}
public evaluateSync (ctx: Context): any {
assert(ctx, 'unable to evaluate: context not defined')
for (const token of this.postfix) {
if (isOperator(token)) {
this.evaluateOnce(token)
} else if (isRange(token)) {
this.operands.push(rangeValueSync(token, ctx))
} else {
const val = new Value(token).evaluateSync(ctx)
this.operands.push(val)
}
}
return this.operands[0]
}
public async value (ctx: Context): Promise<any> {
return toValue(await this.evaluate(ctx))
}
public valueSync (ctx: Context): any {
return toValue(this.evaluateSync(ctx))
}
private evaluateOnce (token: string) {
const r = this.operands.pop()
const l = this.operands.pop()
const result = operatorImpls[token](l, r)
operands.push(result)
continue
}
if (isRange(token)) {
operands.push(rangeValue(token, ctx))
continue
}
operands.push(new Value(token).evaluate(ctx))
}
return operands[0]
}
public value (ctx: Context): any {
return toValue(this.evaluate(ctx))
this.operands.push(result)
}
}
+13 -4
View File
@@ -3,15 +3,24 @@ import { Context } from '../context/context'
import { range } from '../util/underscore'
import { Value } from './value'
export function isRange (token: string = '') {
export function isRange (token: string) {
return token[0] === '(' && token[token.length - 1] === ')'
}
export function rangeValue (token: string = '', ctx: Context) {
export async function rangeValue (token: string, ctx: Context) {
let match
if ((match = token.match(rangeLine))) {
const low = new Value(match[1]).value(ctx)
const high = new Value(match[2]).value(ctx)
const low = await new Value(match[1]).value(ctx)
const high = await new Value(match[2]).value(ctx)
return range(+low, +high + 1)
}
}
export function rangeValueSync (token: string, ctx: Context) {
let match
if ((match = token.match(rangeLine))) {
const low = new Value(match[1]).valueSync(ctx)
const high = new Value(match[2]).valueSync(ctx)
return range(+low, +high + 1)
}
}
+17 -4
View File
@@ -4,13 +4,26 @@ import { ITemplate } from '../template/itemplate'
import { Emitter } from './emitter'
export class Render {
public async renderTemplates (templates: ITemplate[], ctx: Context, emitter = new Emitter()) {
public async renderTemplates (templates: ITemplate[], ctx: Context, emitter = new Emitter()): Promise<string> {
for (const tpl of templates) {
try {
await tpl.render(ctx, emitter)
const html = await tpl.render(ctx, emitter)
html && emitter.write(html)
if (emitter.break || emitter.continue) break
} catch (e) {
if (e.name === 'RenderBreakError') throw e
throw e.name === 'RenderError' ? e : new RenderError(e, tpl)
throw RenderError.is(e) ? e : new RenderError(e, tpl)
}
}
return emitter.html
}
public renderTemplatesSync (templates: ITemplate[], ctx: Context, emitter = new Emitter()): string {
for (const tpl of templates) {
try {
const html = tpl.renderSync(ctx, emitter)
html && !(html instanceof Promise) && emitter.write(html)
if (emitter.break || emitter.continue) break
} catch (e) {
throw RenderError.is(e) ? e : new RenderError(e, tpl)
}
}
return emitter.html
+12 -4
View File
@@ -5,11 +5,15 @@ import { parseLiteral } from '../parser/literal'
export class Value {
private str: string
public constructor (str: string = '') {
public constructor (str: string) {
this.str = str
}
public evaluate (ctx: Context) {
public async evaluate (ctx: Context) {
return this.evaluateSync(ctx)
}
public evaluateSync (ctx: Context) {
const literalValue = parseLiteral(this.str)
if (literalValue !== undefined) {
return literalValue
@@ -17,7 +21,11 @@ export class Value {
return ctx.get(this.str)
}
public value (ctx: Context) {
return toValue(this.evaluate(ctx))
public async value (ctx: Context) {
return toValue(await this.evaluate(ctx))
}
public valueSync (ctx: Context) {
return toValue(this.evaluateSync(ctx))
}
}
+11 -3
View File
@@ -21,11 +21,19 @@ export class Filter {
this.impl = impl || (x => x)
this.args = args
}
public render (value: any, context: Context) {
public async render (value: any, context: Context) {
const argv: any[] = []
for (const arg of this.args) {
if (isKeyValuePair(arg)) argv.push([arg[0], new Expression(arg[1]).evaluate(context)])
else argv.push(new Expression(arg).evaluate(context))
if (isKeyValuePair(arg)) argv.push([arg[0], await new Expression(arg[1]).evaluate(context)])
else argv.push(await new Expression(arg).evaluate(context))
}
return this.impl.apply({ context }, [value, ...argv])
}
public renderSync (value: any, context: Context) {
const argv: any[] = []
for (const arg of this.args) {
if (isKeyValuePair(arg)) argv.push([arg[0], new Expression(arg[1]).evaluateSync(context)])
else argv.push(new Expression(arg).evaluateSync(context))
}
return this.impl.apply({ context }, [value, ...argv])
}
+4 -1
View File
@@ -10,7 +10,10 @@ export class HTML extends Template<HTMLToken> implements ITemplate {
super(token)
this.str = token.value
}
public render (ctx: Context, emitter: Emitter) {
public renderSync (ctx: Context, emitter: Emitter) {
emitter.write(this.str)
}
public async render (ctx: Context, emitter: Emitter) {
this.renderSync(ctx, emitter)
}
}
+2 -1
View File
@@ -4,5 +4,6 @@ import { Emitter } from '../render/emitter'
export interface ITemplate {
token: Token;
render(ctx: Context, emitter: Emitter): Promise<void> | void;
render(ctx: Context, emitter: Emitter): Promise<any>;
renderSync(ctx: Context, emitter: Emitter): any;
}
+4
View File
@@ -12,6 +12,10 @@ export class Output extends Template<OutputToken> implements ITemplate {
super(token)
this.value = new Value(token.value, strictFilters)
}
public renderSync (ctx: Context, emitter: Emitter) {
const val = this.value.valueSync(ctx)
emitter.write(stringify(toValue(val)))
}
public async render (ctx: Context, emitter: Emitter) {
const val = await this.value.value(ctx)
emitter.write(stringify(toValue(val)))
+16 -2
View File
@@ -10,14 +10,28 @@ import { Context } from '../../context/context'
*/
export class Hash {
[key: string]: any
public static async create (markup: string, ctx: Context) {
private static parse (markup: string) {
const instance = new Hash()
let match
hashCapture.lastIndex = 0
while ((match = hashCapture.exec(markup))) {
const k = match[1]
const v = match[2]
instance[k] = new Expression(v).evaluate(ctx)
instance[k] = v
}
return instance
}
public static createSync (markup: string, ctx: Context) {
const instance = Hash.parse(markup)
for (const key of Object.keys(instance)) {
instance[key] = new Expression(instance[key]).evaluateSync(ctx)
}
return instance
}
public static async create (markup: string, ctx: Context) {
const instance = Hash.parse(markup)
for (const key of Object.keys(instance)) {
instance[key] = await new Expression(instance[key]).evaluate(ctx)
}
return instance
}
+1
View File
@@ -8,4 +8,5 @@ import { Emitter } from '../../render/emitter'
export interface ITagImplOptions {
parse?: (this: ITagImpl, token: TagToken, remainingTokens: Token[]) => void;
render: (this: ITagImpl, ctx: Context, hash: Hash, emitter: Emitter) => void;
renderSync?: (this: ITagImpl, ctx: Context, hash: Hash, emitter: Emitter) => void;
}
+7 -1
View File
@@ -23,10 +23,16 @@ export class Tag extends Template<TagToken> implements ITemplate {
this.impl.parse(token, tokens)
}
}
public renderSync (ctx: Context, emitter: Emitter) {
const hash = Hash.createSync(this.token.args, ctx)
const impl = this.impl
if (isFunction(impl.renderSync)) return impl.renderSync(ctx, hash, emitter)
if (isFunction(impl.render)) return impl.render(ctx, hash, emitter)
}
public async render (ctx: Context, emitter: Emitter) {
const hash = await Hash.create(this.token.args, ctx)
const impl = this.impl
if (isFunction(impl.render)) await impl.render(ctx, hash, emitter)
if (isFunction(impl.render)) return impl.render(ctx, hash, emitter)
}
public static register (name: string, tag: ITagImplOptions) {
Tag.impls[name] = tag
+10 -3
View File
@@ -47,10 +47,17 @@ export class Value {
}
this.filters.push(new Filter(name, args, this.strictFilters))
}
public value (ctx: Context) {
let val = new Expression(this.initial).evaluate(ctx)
public async value (ctx: Context) {
let val = await new Expression(this.initial).evaluate(ctx)
for (const filter of this.filters) {
val = filter.render(val, ctx)
val = await filter.render(val, ctx)
}
return val
}
public valueSync (ctx: Context) {
let val = new Expression(this.initial).evaluateSync(ctx)
for (const filter of this.filters) {
val = filter.renderSync(val, ctx)
}
return val
}
+1 -1
View File
@@ -1,4 +1,4 @@
export { ParseError, TokenizationError, RenderBreakError, AssertionError } from './util/error'
export { ParseError, TokenizationError, AssertionError } from './util/error'
export { Drop } from './drop/drop'
export { Emitter } from './render/emitter'
export { Expression } from './render/expression'
+2 -8
View File
@@ -43,14 +43,8 @@ export class RenderError extends LiquidError {
this.message = err.message
super.update()
}
}
export class RenderBreakError extends Error {
public resolvedHTML: string = ''
public constructor (message: string) {
super(message)
this.name = 'RenderBreakError'
this.message = message + ''
public static is (obj: any): obj is RenderError {
return obj instanceof RenderError
}
}
+11
View File
@@ -0,0 +1,11 @@
import { expect } from 'chai'
import { Liquid } from '../..'
describe('#evalValueSync()', function () {
var engine: Liquid
beforeEach(() => { engine = new Liquid() })
it('should throw when scope undefined', async function () {
return expect(() => engine.evalValueSync('{{"foo"}}', null as any)).to.throw(/context not defined/)
})
})
+1 -1
View File
@@ -6,6 +6,6 @@ describe('.evalValue()', function () {
beforeEach(() => { engine = new Liquid() })
it('should throw when scope undefined', async function () {
return expect(() => engine.evalValue('{{"foo"}}', null as any)).to.throw(/context not defined/)
return expect(engine.evalValue('{{"foo"}}', null as any)).to.be.rejectedWith(/context not defined/)
})
})
+5
View File
@@ -81,4 +81,9 @@ describe('tags/assign', function () {
return expect(html).to.equal('12 2')
})
})
it('should support sync', function () {
const src = '{% assign foo="bar" %}{{foo}}'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('bar')
})
})
+5
View File
@@ -32,4 +32,9 @@ describe('tags/capture', function () {
return expect(liquid.parseAndRender(src))
.to.be.rejectedWith(/tag .* not closed/)
})
it('should support sync', function () {
const src = '{% capture f %}{{"a" | capitalize}}{%endcapture%}{{f}}'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('A')
})
})
+16
View File
@@ -48,4 +48,20 @@ describe('tags/case', function () {
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('d')
})
describe('sync support', function () {
it('should hit the specified case', function () {
const src = '{% case "foo"%}' +
'{% when "foo" %}foo{% when "bar"%}bar' +
'{%endcase%}'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('foo')
})
it('should support else branch', function () {
const src = '{% case "a" %}' +
'{% when "b" %}b{% when "c"%}c{%else %}d' +
'{%endcase%}'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('d')
})
})
})
+7
View File
@@ -31,4 +31,11 @@ describe('tags/comment', function () {
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
describe('sync support', function () {
it('should ignore plain string', function () {
const src = 'My name is {% comment %}super{% endcomment %} Shopify.'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('My name is Shopify.')
})
})
})
+5
View File
@@ -35,4 +35,9 @@ describe('tags/cycle', function () {
const html = await liquid.parseAndRender(src, ctx)
return expect(html).to.equal('121')
})
it('should support sync', function () {
const src = "{% cycle '1', '2', '3' %}"
const html = liquid.parseAndRenderSync(src + src + src + src)
return expect(html).to.equal('1231')
})
})
+57 -30
View File
@@ -6,13 +6,13 @@ import { Scope } from '../../../../src/context/scope'
use(chaiAsPromised)
describe('tags/for', function () {
let liquid: Liquid, ctx: Scope
let liquid: Liquid, scope: Scope
before(function () {
liquid = new Liquid()
liquid.registerTag('throwingTag', {
render: function () { throw new Error('intended render error') }
})
ctx = {
scope = {
one: 1,
// eslint-disable-next-line
strObj: new String(''),
@@ -25,30 +25,30 @@ describe('tags/for', function () {
})
it('should support array', async function () {
const src = '{%for c in alpha%}{{c}}{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('abc')
})
it('should support object', async function () {
const src = '{%for item in obj%}{{item[0]}},{{item[1]}}-{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('foo,bar-coo,haa-')
})
it('should output forloop', async function () {
const src = '{%for i in (1..1)%}{{forloop}}{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('{"i":0,"length":1}')
})
describe('illegal', function () {
it('should reject when for not closed', function () {
const src = '{%for c in alpha%}{{c}}'
return expect(liquid.parseAndRender(src, ctx))
return expect(liquid.parseAndRender(src, scope))
.to.be.rejectedWith(/tag .* not closed/)
})
it('should reject when inner templates rejected', function () {
const src = '{%for c in alpha%}{%throwingTag%}{%endfor%}'
return expect(liquid.parseAndRender(src, ctx))
return expect(liquid.parseAndRender(src, scope))
.to.be.rejectedWith(/intended render error/)
})
})
@@ -56,38 +56,38 @@ describe('tags/for', function () {
describe('else', function () {
it('should goto else for empty array', async function () {
const src = '{%for c in emptyArray%}a{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
it('should treat non-empty string as one single element', async function () {
const src = '{%for c in "abc"%}x{{c}}{%else%}y{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('xabc')
})
it('should goto else for empty string', async function () {
const src = '{%for c in ""%}a{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
it('should goto else for empty string object', async function () {
// it should be false although `new String` is none-conform
const src = '{%for c in strObj%}a{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
it('should goto else for empty object', async function () {
const src = '{%for c in emptyObj%}a{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
it('should goto else for null-prototyped object', async function () {
const src = '{%for c in nullProtoObj%}a{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
})
@@ -102,7 +102,7 @@ describe('tags/for', function () {
const dst = 'true.1.0.false.3.3.2a\n' +
'false.2.1.false.3.2.1b\n' +
'false.3.2.true.3.1.0c\n'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal(dst)
})
@@ -111,7 +111,7 @@ describe('tags/for', function () {
const src = '{% for i in (1..5) %}' +
'{% if i == 4 %}continue{% continue %}{% endif %}{{i}}' +
'{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123continue5')
})
it('should output contents before continue', async function () {
@@ -119,7 +119,7 @@ describe('tags/for', function () {
'{% if i == 4 %}continue{% continue %}{% endif %}' +
'{{ i }}' +
'{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123continue5')
})
})
@@ -129,7 +129,7 @@ describe('tags/for', function () {
'{% if i == 4 %}{% break %}{% endif %}' +
'{{ i }}' +
'{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123')
})
it('should output contents before break', async function () {
@@ -137,7 +137,7 @@ describe('tags/for', function () {
'{% if i == 4 %}breaking{% break %}{% endif %}' +
'{{ i }}' +
'{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123breaking')
})
})
@@ -145,22 +145,22 @@ describe('tags/for', function () {
describe('limit', function () {
it('should support for with limit', async function () {
const src = '{% for i in (1..5) limit:2 %}{{ i }}{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('12')
})
it('should set forloop.last properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.last}} {%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('false true ')
})
it('should set forloop.first properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.first}} {%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('true false ')
})
it('should set forloop.length properly', function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.length}} {%endfor%}'
return expect(liquid.parseAndRender(src, ctx))
return expect(liquid.parseAndRender(src, scope))
.to.eventually.equal('2 2 ')
})
})
@@ -168,27 +168,27 @@ describe('tags/for', function () {
describe('offset', function () {
it('should support offset with limit', async function () {
const src = '{% for i in (1..10) limit:2 offset:5%}{{ i }}{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('67')
})
it('should set index properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.index}} {%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('1 2 ')
})
it('should set index0 properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.index0}} {%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('0 1 ')
})
it('should set rindex properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.rindex}} {%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('2 1 ')
})
it('should set rindex0 properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.rindex0}} {%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('1 0 ')
})
})
@@ -196,20 +196,47 @@ describe('tags/for', function () {
describe('reverse', function () {
it('should support for reversed in the last position', async function () {
const src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('21')
})
it('should support for reversed in the first position', async function () {
const src = '{% for i in (1..5) reversed limit:2 %}{{ i }}{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('21')
})
it('should support for reversed in the middle position', async function () {
const src = '{% for i in (1..5) offset:2 reversed limit:4 %}{{ i }}{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('543')
})
})
describe('sync', function () {
it('should support sync', function () {
const src = '{% for i in (1..5) %}{{i}}{%endfor%}'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('12345')
})
it('should output contents before break', function () {
const src = '{% for i in (1..5) %}' +
'{% if i == 4 %}breaking{% break %}{% endif %}' +
'{{ i }}' +
'{% endfor %}'
const html = liquid.parseAndRenderSync(src, scope)
return expect(html).to.equal('123breaking')
})
it('should support for with continue', function () {
const src = '{% for i in (1..5) %}' +
'{% if i == 4 %}continue{% continue %}{% endif %}{{i}}' +
'{% endfor %}'
const html = liquid.parseAndRenderSync(src, scope)
return expect(html).to.equal('123continue5')
})
it('should goto else for empty array', async function () {
const src = '{%for c in emptyArray%}a{%else%}b{%endfor%}'
const html = await liquid.parseAndRenderSync(src, scope)
return expect(html).to.equal('b')
})
})
})
+24 -19
View File
@@ -3,7 +3,7 @@ import { expect } from 'chai'
describe('tags/if', function () {
const liquid = new Liquid()
const ctx = {
const scope = {
one: 1,
two: 2,
emptyString: '',
@@ -12,52 +12,52 @@ describe('tags/if', function () {
it('should throw if not closed', function () {
const src = '{% if false%}yes'
return expect(liquid.parseAndRender(src, ctx))
return expect(liquid.parseAndRender(src, scope))
.to.be.rejectedWith(/tag {% if false%} not closed/)
})
it('should support nested', async function () {
const src = '{%if false%}{%if true%}{%else%}a{%endif%}{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('')
})
describe('single value as condition', function () {
it('should support boolean', async function () {
const src = '{% if false %}1{%elsif true%}2{%else%}3{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('2')
})
it('should treat Array truthy', async function () {
const src = '{%if emptyArray%}a{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('a')
})
it('should return true if empty string', async function () {
const src = '{%if emptyString%}a{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('a')
})
})
describe('expression as condition', function () {
it('should support ==', async function () {
const src = '{% if 2 == 3 %}yes{%else%}no{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should support >=', async function () {
const src = '{% if 1 >= 2 and one<two %}a{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('')
})
it('should support !=', async function () {
const src = '{% if one != two %}yes{%else%}no{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('yes')
})
it('should support value and expression', async function () {
const src = `X{%if version and version != '' %}x{{version}}y{%endif%}Y`
const ctx = { 'version': '' }
const html = await liquid.parseAndRender(src, ctx)
const scope = { 'version': '' }
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('XY')
})
it('should evaluate right to left', async function () {
@@ -69,50 +69,55 @@ describe('tags/if', function () {
describe('comparasion to null', function () {
it('should evaluate false for null < 10', async function () {
const src = '{% if null < 10 %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for null > 10', async function () {
const src = '{% if null > 10 %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for null <= 10', async function () {
const src = '{% if null <= 10 %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for null >= 10', async function () {
const src = '{% if null >= 10 %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for 10 < null', async function () {
const src = '{% if 10 < null %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for 10 > null', async function () {
const src = '{% if 10 > null %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for 10 <= null', async function () {
const src = '{% if 10 <= null %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for 10 >= null', async function () {
const src = '{% if 10 >= null %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
})
it('should support sync', function () {
const src = '{%if true%}true{%else%}false{%endif%}'
const html = liquid.parseAndRenderSync(src, scope)
return expect(html).to.equal('true')
})
})
+35
View File
@@ -171,4 +171,39 @@ describe('tags/include', function () {
return expect(html).to.equal('Xchild with redY')
})
})
describe('sync support', function () {
it('should support quoted string', function () {
mock({
'/current.html': 'bar{% include "bar/foo.html" %}bar',
'/bar/foo.html': 'foo'
})
const html = liquid.renderFileSync('/current.html')
return expect(html).to.equal('barfoobar')
})
it('should support template string', function () {
mock({
'/current.html': 'bar{% include name" %}bar',
'/bar/foo.html': 'foo'
})
const html = liquid.renderFileSync('/current.html', { name: '/bar/foo.html' })
return expect(html).to.equal('barfoobar')
})
it('should support include: with', function () {
mock({
'/with.html': '{% include "color" with "red", shape: "rect" %}',
'/color.html': 'color:{{color}}, shape:{{shape}}'
})
const html = liquid.renderFileSync('with.html')
return expect(html).to.equal('color:red, shape:rect')
})
it('should support filename with extention', function () {
mock({
'/parent.html': 'X{% include child.html color:"red" %}Y',
'/child.html': 'child with {{color}}'
})
const staticLiquid = new Liquid({ dynamicPartials: false, root: '/' })
const html = staticLiquid.renderFileSync('parent.html')
return expect(html).to.equal('Xchild with redY')
})
})
})
+9
View File
@@ -133,4 +133,13 @@ describe('tags/layout', function () {
return expect(html).to.equal('blackA')
})
})
it('should support sync', function () {
mock({
'/grand.html': 'X{%block a%}G{%endblock%}Y',
'/parent.html': '{%layout "grand" %}{%block a%}P{%endblock%}',
'/main.html': '{%layout "parent"%}{%block a%}A{%endblock%}'
})
const html = liquid.renderFileSync('/main.html')
return expect(html).to.equal('XAY')
})
})
+7 -3
View File
@@ -3,20 +3,24 @@ import { expect } from 'chai'
describe('tags/raw', function () {
const liquid = new Liquid()
it('should support raw 1', async function () {
it('should throw when not closed', async function () {
const p = liquid.parseAndRender('{% raw%}')
return expect(p).be.rejectedWith(/{% raw%} not closed/)
})
it('should support raw 2', async function () {
it('should output filters as it is', async function () {
const src = '{% raw %}{{ 5 | plus: 6 }}{% endraw %} is equal to 11.'
const dst = '{{ 5 | plus: 6 }} is equal to 11.'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal(dst)
})
it('should support raw 3', async function () {
it('should preserve blank characters', async function () {
const src = '{% raw %}\n{{ foo}} \n{% endraw %}'
const dst = '\n{{ foo}} \n'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal(dst)
})
it('should support sync', function () {
const html = liquid.parseAndRenderSync('{% raw %}{{foo}}{% endraw %}')
return expect(html).to.equal('{{foo}}')
})
})
+14
View File
@@ -107,4 +107,18 @@ describe('tags/tablerow', function () {
return expect(html).to.equal(dst)
})
})
describe('sync support', function () {
it('should support tablerow', function () {
const src = '{% tablerow i in (1..3)%}{{ i }}{% endtablerow %}'
const dst = '<tr class="row1"><td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>'
const html = liquid.parseAndRenderSync(src)
expect(html).to.equal(dst)
})
it('should support empty tablerow', function () {
const src = '{% tablerow i in "" cols:2 %}{{ i }}{% endtablerow %}'
const dst = ''
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal(dst)
})
})
})
+12
View File
@@ -34,4 +34,16 @@ describe('tags/unless', function () {
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
describe('sync support', function () {
it('should render else when predicate yields true', function () {
const src = '{% unless 0 %}yes{%else%}no{%endunless%}'
const html = liquid.parseAndRenderSync(src)
expect(html).to.equal('no')
})
it('should render unless when predicate yields false', function () {
const src = '{% unless false %}yes{%else%}no{%endunless%}'
const html = liquid.parseAndRenderSync(src)
expect(html).to.equal('yes')
})
})
})
+4
View File
@@ -49,6 +49,10 @@ describe('drop/drop', function () {
const html = await liquid.parseAndRender(`{{obj.name}}`, { obj: new PromiseDrop() })
expect(html).to.equal('NAME')
})
it('should resolve before calling filters', async function () {
const html = await liquid.parseAndRender(`{{obj.name | downcase}}`, { obj: new PromiseDrop() })
expect(html).to.equal('name')
})
it('should support promise returned by liquidMethodMissing', async function () {
const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new PromiseDrop() })
expect(html).to.equal('FOO')
+73 -22
View File
@@ -3,36 +3,87 @@ import { Liquid } from '../../../src/liquid'
import { mock, restore } from '../../stub/mockfs'
describe('LiquidOptions#cache', function () {
let engine: Liquid
beforeEach(function () {
engine = new Liquid({
afterEach(restore)
describe('#renderFile', function () {
it('should be disabled by default', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html'
})
mock({ '/root/files/foo.html': 'foo' })
const x = await engine.renderFile('files/foo')
expect(x).to.equal('foo')
mock({ '/root/files/foo.html': 'bar' })
const y = await engine.renderFile('files/foo')
expect(y).to.equal('bar')
})
afterEach(restore)
it('should be disabled by default', function () {
return engine.renderFile('files/foo')
.then(x => expect(x).to.equal('foo'))
.then(() => mock({
'/root/files/foo.html': 'bar'
}))
.then(() => engine.renderFile('files/foo'))
.then(x => expect(x).to.equal('bar'))
})
it('should respect cache=true option', function () {
engine = new Liquid({
it('should respect cache=true option', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
return engine.renderFile('files/foo')
.then(x => expect(x).to.equal('foo'))
.then(() => mock({
'/root/files/foo.html': 'bar'
}))
.then(() => engine.renderFile('files/foo'))
.then(x => expect(x).to.equal('foo'))
mock({ '/root/files/foo.html': 'foo' })
const x = await engine.renderFile('files/foo')
expect(x).to.equal('foo')
mock({ '/root/files/foo.html': 'bar' })
const y = await engine.renderFile('files/foo')
expect(y).to.equal('foo')
})
it('should not cache not exist file', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
try { await engine.renderFile('foo') } catch (err) {}
mock({ '/root/foo.html': 'foo' })
const y = await engine.renderFile('foo')
expect(y).to.equal('foo')
})
})
describe('#renderFileSync', function () {
it('should be disabled by default', function () {
const engine = new Liquid({
root: '/root/',
extname: '.html'
})
mock({ '/root/foo.html': 'foo' })
const x = engine.renderFileSync('foo')
expect(x).to.equal('foo')
mock({ '/root/foo.html': 'bar' })
const y = engine.renderFileSync('foo')
expect(y).to.equal('bar')
})
it('should respect cache=true option', function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
mock({ '/root/foo.html': 'foo' })
const x = engine.renderFileSync('foo')
expect(x).to.equal('foo')
mock({ '/root/foo.html': 'bar' })
const y = engine.renderFileSync('foo')
expect(y).to.equal('foo')
})
it('should not cache not exist file', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
try { engine.renderFileSync('foo') } catch (err) {}
mock({ '/root/foo.html': 'foo' })
const y = await engine.renderFile('foo')
expect(y).to.equal('foo')
})
})
})
+2 -2
View File
@@ -53,14 +53,14 @@ describe('Liquid', function () {
})
after(restore)
it('should render single template', function (done) {
render.call({ root: '.' }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
render.call({ root: '/root' }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
if (err) return done(err)
expect(result).to.equal('foo')
done()
})
})
it('should render single template with Array-typed root', function (done) {
render.call({ root: ['.'] }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
render.call({ root: ['/root'] }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
if (err) return done(err)
expect(result).to.equal('foo')
done()
+18 -11
View File
@@ -1,17 +1,24 @@
import { test, liquid } from '../../stub/render'
import { expect } from 'chai'
import { Liquid } from '../../../src/liquid'
describe('liquid#registerFilter()', function () {
const liquid = new Liquid()
describe('object arguments', function () {
liquid.registerFilter('obj_test', function () {
return JSON.stringify(arguments)
liquid.registerFilter('obj_test', function (...args) {
return JSON.stringify(args)
})
it('should support object', async () => {
const src = `{{ "a" | obj_test: k1: "v1", k2: foo }}`,
const dst = '["a",["k1","v1"],["k2","bar"]]'
const html = await liquid.parseAndRender(src, { foo: 'bar' })
return expect(html).to.equal(dst)
})
it('should support mixed object', async () => {
const src = `{{ "a" | obj_test: "something", k1: "v1", k2: foo }}`,
const dst = '["a","something",["k1","v1"],["k2","bar"]]'
const html = await liquid.parseAndRender(src, { foo: 'bar' })
return expect(html).to.equal(dst)
})
it('should support object', () => test(
`{{ "a" | obj_test: k1: "v1", k2: foo }}`,
'{"0":"a","1":["k1","v1"],"2":["k2","bar"]}'
))
it('should support mixed object', () => test(
`{{ "a" | obj_test: "something", k1: "v1", k2: foo }}`,
'{"0":"a","1":"something","2":["k1","v1"],"3":["k2","bar"]}'
))
})
})
+43
View File
@@ -1,4 +1,5 @@
import { expect } from 'chai'
import { RenderError } from '../../../src/util/error'
import { Liquid } from '../../../src/liquid'
import * as path from 'path'
import { mock, restore } from '../../stub/mockfs'
@@ -264,4 +265,46 @@ describe('error', function () {
expect(err.stack).to.match(/at .*:\d+:\d+\)/)
})
})
describe('sync support', function () {
let engine
beforeEach(function () {
engine = new Liquid({
root: '/'
})
engine.registerTag('throwingTag', {
render: function () {
throw new Error('intended render error')
}
})
})
it('should throw RenderError when tag throws', function () {
const src = '{%throwingTag%}'
expect(() => engine.parseAndRenderSync(src))
.to.throw(RenderError, /intended render error/)
})
it('should contain original error info for {% include %}', function () {
const origin = ['1st', '2nd', '3rd', 'X{%throwingTag%} Y', '5th', '6th', '7th']
mock({
'/throwing-tag.html': origin.join('\n')
})
const html = '{%include "throwing-tag.html"%}'
const message = [
' 2| 2nd',
' 3| 3rd',
'>> 4| X{%throwingTag%} Y',
' 5| 5th',
' 6| 6th',
' 7| 7th',
'RenderError'
]
try {
engine.parseAndRenderSync(html)
throw new Error('expected throw')
} catch (err) {
expect(err.message).to.equal(`intended render error, file:${path.resolve('/throwing-tag.html')}, line:4, col:2`)
expect(err.stack).to.contain(message.join('\n'))
expect(err.name).to.equal('RenderError')
}
})
})
})
+9 -3
View File
@@ -8,8 +8,7 @@ interface FileDescriptor {
}
let files: { [path: string]: FileDescriptor } = {}
const readFile = fs.readFile
const exists = fs.exists
const { readFile, exists, readFileSync, existsSync } = fs
export function mock (options: { [path: string]: (string | FileDescriptor) }) {
forOwn(options, (val, key) => {
@@ -18,19 +17,26 @@ export function mock (options: { [path: string]: (string | FileDescriptor) }) {
: val as FileDescriptor
})
fs.readFile = async function (path) {
return fs.readFileSync(path)
}
fs.readFileSync = function (path) {
const file = files[path]
if (file === undefined) throw new Error('ENOENT')
if (file.mode === '0000') throw new Error('EACCES')
return file.content
}
fs.exists = async function (path: string) {
return fs.existsSync(path)
}
fs.existsSync = function (path: string) {
return !!files[path]
}
}
export function restore () {
files = {}
fs.readFileSync = readFileSync
fs.existsSync = existsSync
fs.readFile = readFile
fs.exists = exists
}
+1 -1
View File
@@ -8,7 +8,7 @@ export const ctx = {
foo: 'bar',
arr: [-2, 'a'],
obj: { foo: 'bar' },
func: function () {},
func: function () {}, // eslint-disable-line
posts: [{ category: 'foo' }, { category: 'bar' }],
products: [
{ title: 'Vacuum', type: 'living room' },
+31
View File
@@ -59,6 +59,12 @@ describe('fs/browser', function () {
})
})
describe('#existsSync()', () => {
it('should always return true', function () {
expect(fs.existsSync('/foo/bar')).to.equal(true)
})
})
describe('#readFile()', () => {
let server: sinon.SinonFakeServer
beforeEach(() => {
@@ -87,4 +93,29 @@ describe('fs/browser', function () {
return result
})
})
describe('#readFileSync()', () => {
let server: sinon.SinonFakeServer
beforeEach(() => {
server = sinon.fakeServer.create()
server.autoRespond = true
server.respondWith(
'GET', 'https://example.com/views/hello.html',
[200, { 'Content-Type': 'text/plain' }, 'hello {{name}}']
);
(global as any).XMLHttpRequest = sinon.useFakeXMLHttpRequest()
})
afterEach(() => {
server.restore()
delete (global as any).XMLHttpRequest
})
it('should get corresponding text', function () {
const html = fs.readFileSync('https://example.com/views/hello.html')
return expect(html).to.equal('hello {{name}}')
})
it('should throw 404', () => {
return expect(() => fs.readFileSync('https://example.com/not/exist.html'))
.to.throw('Not Found')
})
})
})
+23 -6
View File
@@ -6,7 +6,7 @@ import * as chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised)
describe('fs', function () {
describe('#resolve()', function () {
describe('.resolve()', function () {
it('should resolve based on root', async function () {
const filepath = fs.resolve('/foo', 'bar.html', '.liquid')
const expected = path.resolve('/foo/bar.html')
@@ -18,23 +18,40 @@ describe('fs', function () {
return expect(filepath).to.equal(expected)
})
})
describe('#exists', () => {
describe('.existsSync', () => {
it('should resolve as false if not exists', () => {
expect(fs.existsSync('/foo/bar')).to.be.false
})
it('should resolve as true if exists', () => {
expect(fs.existsSync(__filename)).to.be.true
})
})
describe('.exists', () => {
it('should resolve as false if not exists', async () => {
const result = await fs.exists('/foo/bar')
return expect(result).to.be.false
expect(result).to.be.false
})
it('should resolve as true if exists', async () => {
const result = await fs.exists(__filename)
return expect(result).to.be.true
expect(result).to.be.true
})
})
describe('#readFile', function () {
describe('.readFileSync', function () {
it('should throw when not exist', function () {
return expect(() => fs.readFileSync('/foo/bar')).to.throw('ENOENT')
})
it('should read content if exists', function () {
const content = fs.readFileSync(__filename)
expect(content).to.contain('should read content if exists')
})
})
describe('.readFile', function () {
it('should throw when not exist', function () {
return expect(fs.readFile('/foo/bar')).to.rejectedWith('ENOENT')
})
it('should read content if exists', async function () {
const content = await fs.readFile(__filename)
return expect(content).to.contain('should read content if exists')
expect(content).to.contain('should read content if exists')
})
})
})
+29 -24
View File
@@ -17,51 +17,56 @@ describe('Expression', function () {
})
it('should throw when context not defined', async function () {
return expect(() => new Expression().value()).to.throw(/context not defined/)
return expect(new Expression().value()).to.be.rejectedWith(/context not defined/)
})
it('should eval simple expression', async function () {
expect(new Expression('1 < 2').value(ctx)).to.equal(true)
expect(new Expression('2 <= 2').value(ctx)).to.equal(true)
expect(new Expression('one <= two').value(ctx)).to.equal(true)
expect(new Expression('x contains "x"').value(ctx)).to.equal(false)
expect(new Expression('x contains "X"').value(ctx)).to.equal(true)
expect(new Expression('1 contains "x"').value(ctx)).to.equal(false)
expect(new Expression('y contains "x"').value(ctx)).to.equal(false)
expect(new Expression('z contains "x"').value(ctx)).to.equal(false)
expect(new Expression('(1..5) contains 3').value(ctx)).to.equal(true)
expect(new Expression('(1..5) contains 6').value(ctx)).to.equal(false)
expect(new Expression('"<=" == "<="').value(ctx)).to.equal(true)
expect(await new Expression('1 < 2').value(ctx)).to.equal(true)
expect(await new Expression('1 < 2').value(ctx)).to.equal(true)
expect(await new Expression('2 <= 2').value(ctx)).to.equal(true)
expect(await new Expression('one <= two').value(ctx)).to.equal(true)
expect(await new Expression('x contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('x contains "X"').value(ctx)).to.equal(true)
expect(await new Expression('1 contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('y contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('z contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('(1..5) contains 3').value(ctx)).to.equal(true)
expect(await new Expression('(1..5) contains 6').value(ctx)).to.equal(false)
expect(await new Expression('"<=" == "<="').value(ctx)).to.equal(true)
})
describe('complex expression', function () {
it('should support value or value', async function () {
expect(new Expression('false or true').value(ctx)).to.equal(true)
expect(await new Expression('false or true').value(ctx)).to.equal(true)
})
it('should support < and contains', async function () {
expect(new Expression('1 < 2 and x contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('1 < 2 and x contains "x"').value(ctx)).to.equal(false)
})
it('should support < or contains', async function () {
expect(new Expression('1 < 2 or x contains "x"').value(ctx)).to.equal(true)
expect(await new Expression('1 < 2 or x contains "x"').value(ctx)).to.equal(true)
})
it('should support value and !=', async function () {
expect(new Expression('empty and empty != ""').value(ctx)).to.equal(false)
expect(await new Expression('empty and empty != ""').value(ctx)).to.equal(false)
})
it('should recognize quoted value', async function () {
expect(new Expression('">"').value(ctx)).to.equal('>')
expect(await new Expression('">"').value(ctx)).to.equal('>')
})
it('should evaluate from right to left', function () {
expect(new Expression('true or false and false').value(ctx)).to.equal(true)
expect(new Expression('true and false and false or true').value(ctx)).to.equal(false)
it('should evaluate from right to left', async function () {
expect(await new Expression('true or false and false').value(ctx)).to.equal(true)
expect(await new Expression('true and false and false or true').value(ctx)).to.equal(false)
})
it('should recognize property access', function () {
it('should recognize property access', async function () {
const ctx = new Context({ obj: { foo: true } })
expect(new Expression('obj["foo"] and true').value(ctx)).to.equal(true)
expect(await new Expression('obj["foo"] and true').value(ctx)).to.equal(true)
})
})
it('should eval range expression', async function () {
expect(new Expression('(2..4)').value(ctx)).to.deep.equal([2, 3, 4])
expect(new Expression('(two..4)').value(ctx)).to.deep.equal([2, 3, 4])
expect(await new Expression('(2..4)').value(ctx)).to.deep.equal([2, 3, 4])
expect(await new Expression('(two..4)').value(ctx)).to.deep.equal([2, 3, 4])
})
it('should support sync', function () {
expect(new Expression('empty and empty != ""').valueSync(ctx)).to.equal(false)
})
})
+5 -5
View File
@@ -5,20 +5,20 @@ import { expect } from 'chai'
describe('Value', function () {
it('should eval number variable', async function () {
const ctx = new Context({ one: 1 })
expect(new Value('one').value(ctx)).to.equal(1)
expect(new Value('one').valueSync(ctx)).to.equal(1)
})
it('question mark should be valid variable name', async function () {
const ctx = new Context({ 'has_value?': true })
expect(new Value('has_value?').value(ctx)).to.equal(true)
expect(new Value('has_value?').valueSync(ctx)).to.equal(true)
})
it('should eval string variable', async function () {
const ctx = new Context({ x: 'XXX' })
expect(new Value('x').value(ctx)).to.equal('XXX')
expect(new Value('x').valueSync(ctx)).to.equal('XXX')
})
it('should eval null literal', async function () {
expect(new Value('null').value({})).to.be.null
expect(new Value('null').valueSync({})).to.be.null
})
it('should eval nil literal', async function () {
expect(new Value('nil').value({})).to.be.null
expect(new Value('nil').valueSync({})).to.be.null
})
})
+10
View File
@@ -60,4 +60,14 @@ describe('filter', function () {
new Filter('/', [], false)
}).to.not.throw()
})
it('should support sync', function () {
Filter.register('add', (a, b) => a + b)
expect(new Filter('add', ['2'], false).renderSync(3, ctx)).to.equal(5)
})
it('should support key value pairs', function () {
Filter.register('add', (a, b) => b[0] + ':' + (a + b[1]))
expect(new Filter('add', [['num', '2']], false).renderSync(3, ctx)).to.equal('num:5')
})
})
+20
View File
@@ -0,0 +1,20 @@
import * as chai from 'chai'
import { Hash } from '../../../src/template/tag/hash'
import { Context } from '../../../src/context/context'
const expect = chai.expect
describe('Hash', function () {
it('should parse variable', async function () {
const hash = await Hash.create('num:foo', new Context({ foo: 3 }))
expect(hash.num).to.equal(3)
})
it('should parse literals', async function () {
const hash = await Hash.create('num:3', new Context())
expect(hash.num).to.equal(3)
})
it('should support sync', function () {
const hash = Hash.createSync('num:3', new Context())
expect(hash.num).to.equal(3)
})
})
+1 -1
View File
@@ -10,7 +10,7 @@ chai.use(sinonChai)
const expect = chai.expect
const liquid = new Liquid()
describe('tag', function () {
describe('Tag', function () {
let ctx: Context
const emitter = { write: (html: string) => (emitter.html += html), html: '' }
before(function () {