mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
feat: renderSync, parseAndRenderSync and renderFileSync, see #48
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>`)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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>`)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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/)
|
||||
})
|
||||
})
|
||||
@@ -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/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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.')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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}}')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"]}'
|
||||
))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
@@ -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' },
|
||||
|
||||
@@ -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
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,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
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user