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, "prefer-const": 2,
"no-unused-vars": "off", "no-unused-vars": "off",
"indent": "off", "indent": "off",
"no-dupe-class-members": "off",
"@typescript-eslint/indent": ["error", 2], "@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
+3 -3
View File
@@ -10,9 +10,9 @@ engine.registerTag('header', {
const [key, val] = token.args.split(':') const [key, val] = token.args.split(':')
this[key] = val this[key] = val
}, },
render: function (scope, hash) { render: async function (scope, hash, emitter) {
const title = this.liquid.evalValue(this.content, scope) const title = await this.liquid.evalValue(this.content, scope)
return `<h1>${title}</h1>` 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({ const engine = new Liquid({
root: __dirname, root: __dirname,
@@ -10,9 +10,9 @@ engine.registerTag('header', {
const [key, val] = token.args.split(':') const [key, val] = token.args.split(':')
this[key] = val this[key] = val
}, },
render: function (context: Context, hash: Hash) { render: async function (context: Context, hash: Hash, emitter: Emitter) {
const title = this.liquid.evalValue(this['content'], context) const title = await this.liquid.evalValue(this['content'], context)
return `<h1>${title}</h1>` emitter.write(`<h1>${title}</h1>`)
} }
}) })
+3 -1
View File
@@ -12,6 +12,8 @@ export default {
this.value = match[2] this.value = match[2]
}, },
render: async function (ctx: Context) { 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 } as ITagImplOptions
+5 -1
View File
@@ -17,9 +17,13 @@ export default {
render: async function (ctx: Context, hash: Hash, emitter: Emitter) { render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const blocks = ctx.getRegister('blocks') const blocks = ctx.getRegister('blocks')
const childDefined = blocks[this.block] const childDefined = blocks[this.block]
const r = this.liquid.renderer
const html = childDefined !== undefined const html = childDefined !== undefined
? childDefined ? 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) { if (ctx.getRegister('blockMode', BlockMode.OUTPUT) === BlockMode.STORE) {
blocks[this.block] = html 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 { export default {
render: async function () { render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
throw new RenderBreakError('break') emitter.break = true
} }
} }
+4 -1
View File
@@ -21,7 +21,10 @@ export default {
stream.start() stream.start()
}, },
render: async function (ctx: Context) { 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 ctx.front()[this.variable] = html
} }
} as ITagImplOptions } as ITagImplOptions
+19 -4
View File
@@ -25,15 +25,30 @@ export default {
}, },
render: async function (ctx: Context, hash: Hash, emitter: Emitter) { render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
for (let i = 0; i < this.cases.length; i++) { for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i] const branch = this.cases[i]
const val = new Expression(branch.val).value(ctx) const val = await new Expression(branch.val).value(ctx)
const cond = new Expression(this.cond).value(ctx) const cond = await new Expression(this.cond).value(ctx)
if (val === cond) { if (val === cond) {
this.liquid.renderer.renderTemplates(branch.templates, ctx, emitter) await r.renderTemplates(branch.templates, ctx, emitter)
return 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 } as ITagImplOptions
+3 -3
View File
@@ -1,7 +1,7 @@
import { RenderBreakError } from '../../util/error' import { Emitter, Context, Hash } from '../../types'
export default { export default {
render: async function () { render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
throw new RenderBreakError('continue') emitter.continue = true
} }
} }
+7 -2
View File
@@ -22,7 +22,9 @@ export default {
}, },
render: async function (ctx: Context, hash: Hash, emitter: Emitter) { 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 fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = ctx.getRegister('cycle') const groups = ctx.getRegister('cycle')
let idx = groups[fingerprint] let idx = groups[fingerprint]
@@ -34,6 +36,9 @@ export default {
const candidate = this.candidates[idx] const candidate = this.candidates[idx]
idx = (idx + 1) % this.candidates.length idx = (idx + 1) % this.candidates.length
groups[fingerprint] = idx groups[fingerprint] = idx
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 } as ITagImplOptions
+18 -11
View File
@@ -37,7 +37,10 @@ export default {
stream.start() stream.start()
}, },
render: async function (ctx: Context, hash: Hash, emitter: Emitter) { 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 (!isArray(collection)) {
if (isString(collection) && collection.length > 0) { if (isString(collection) && collection.length > 0) {
@@ -47,7 +50,9 @@ export default {
} }
} }
if (!isArray(collection) || !collection.length) { 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 return
} }
@@ -57,17 +62,19 @@ export default {
collection = collection.slice(offset, offset + limit) collection = collection.slice(offset, offset + limit)
if (this.reversed) collection.reverse() if (this.reversed) collection.reverse()
const context = { forloop: new ForloopDrop(collection.length) } const scope = { forloop: new ForloopDrop(collection.length) }
ctx.push(context) ctx.push(scope)
for (const item of collection) { for (const item of collection) {
context[this.variable] = item scope[this.variable] = item
try { ctx.sync
await this.liquid.renderer.renderTemplates(this.templates, ctx, emitter) ? r.renderTemplatesSync(this.templates, ctx, emitter)
} catch (e) { : await r.renderTemplates(this.templates, ctx, emitter)
if (e.name !== 'RenderBreakError') throw e if (emitter.break) {
if (e.message === 'break') break emitter.break = false
break
} }
context.forloop.next() emitter.continue = false
scope.forloop.next()
} }
ctx.pop() ctx.pop()
} }
+18 -3
View File
@@ -28,13 +28,28 @@ export default {
}, },
render: async function (ctx: Context, hash: Hash, emitter: Emitter) { render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const r = this.liquid.renderer
for (const branch of this.branches) { 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)) { if (isTruthy(cond)) {
await this.liquid.renderer.renderTemplates(branch.templates, ctx, emitter) await r.renderTemplates(branch.templates, ctx, emitter)
return 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 } as ITagImplOptions
+34 -4
View File
@@ -17,14 +17,14 @@ export default {
match = withRE.exec(token.args) match = withRE.exec(token.args)
if (match) this.with = match[1] 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 let filepath
if (ctx.opts.dynamicPartials) { if (ctx.opts.dynamicPartials) {
if (quotedLine.exec(this.value)) { if (quotedLine.exec(this.value)) {
const template = this.value.slice(1, -1) 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 { } else {
filepath = new Expression(this.value).value(ctx) filepath = new Expression(this.value).valueSync(ctx)
} }
} else { } else {
filepath = this.staticValue filepath = this.staticValue
@@ -37,7 +37,37 @@ export default {
ctx.setRegister('blocks', {}) ctx.setRegister('blocks', {})
ctx.setRegister('blockMode', BlockMode.OUTPUT) ctx.setRegister('blockMode', BlockMode.OUTPUT)
if (this.with) { 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) const templates = await this.liquid.getTemplate(filepath, ctx.opts)
ctx.push(hash) ctx.push(hash)
+14 -4
View File
@@ -21,21 +21,31 @@ export default {
}, },
render: async function (ctx: Context, hash: Hash, emitter: Emitter) { render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const layout = ctx.opts.dynamicPartials 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 : this.staticLayout
assert(layout, `cannot apply layout with empty filename`) assert(layout, `cannot apply layout with empty filename`)
// render the remaining tokens immediately // render the remaining tokens immediately
ctx.setRegister('blockMode', BlockMode.STORE) ctx.setRegister('blockMode', BlockMode.STORE)
const blocks = ctx.getRegister('blocks') 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) { if (blocks[''] === undefined) {
blocks[''] = html 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.push(hash)
ctx.setRegister('blockMode', BlockMode.OUTPUT) 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() ctx.pop()
emitter.write(partial) emitter.write(partial)
} }
+2 -2
View File
@@ -15,7 +15,7 @@ export default {
}) })
stream.start() stream.start()
}, },
render: function (ctx: Context, hash: Hash, emitter: Emitter) { render: function (ctx: Context) {
emitter.write(this.tokens.map((token: Token) => token.raw).join('')) return this.tokens.map((token: Token) => token.raw).join('')
} }
} as ITagImplOptions } as ITagImplOptions
+7 -2
View File
@@ -29,13 +29,16 @@ export default {
}, },
render: async function (ctx: Context, hash: Hash, emitter: Emitter) { 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 offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit const limit = (hash.limit === undefined) ? collection.length : hash.limit
collection = collection.slice(offset, offset + limit) collection = collection.slice(offset, offset + limit)
const cols = hash.cols || collection.length const cols = hash.cols || collection.length
const r = this.liquid.renderer
const tablerowloop = new TablerowloopDrop(collection.length, cols) const tablerowloop = new TablerowloopDrop(collection.length, cols)
const scope = { tablerowloop } const scope = { tablerowloop }
ctx.push(scope) ctx.push(scope)
@@ -47,7 +50,9 @@ export default {
emitter.write(`<tr class="row${tablerowloop.row()}">`) emitter.write(`<tr class="row${tablerowloop.row()}">`)
} }
emitter.write(`<td class="col${tablerowloop.col()}">`) 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>') emitter.write('</td>')
} }
if (collection.length) emitter.write('</tr>') if (collection.length) emitter.write('</tr>')
+13 -4
View File
@@ -20,10 +20,19 @@ export default {
stream.start() stream.start()
}, },
render: async function (ctx: Context, hash: Hash, emitter: Emitter) { renderSync: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const cond = new Expression(this.cond).value(ctx) const r = this.liquid.renderer
const cond = new Expression(this.cond).valueSync(ctx)
isFalsy(cond) isFalsy(cond)
? await this.liquid.renderer.renderTemplates(this.templates, ctx, emitter) ? r.renderTemplatesSync(this.templates, ctx, emitter)
: await this.liquid.renderer.renderTemplates(this.elseTemplates, 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 } as ITagImplOptions
+3 -1
View File
@@ -9,8 +9,10 @@ export class Context {
private scopes: Scope[] = [{}] private scopes: Scope[] = [{}]
private registers = {} private registers = {}
public environments: Scope public environments: Scope
public sync: boolean
public opts: NormalizedFullOptions 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.opts = applyDefault(opts)
this.environments = ctx 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 () { async function exists () {
return true 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 { export default interface IFS {
exists: (filepath: string) => Promise<boolean>; exists: (filepath: string) => Promise<boolean>;
readFile: (filepath: string) => Promise<string>; readFile: (filepath: string) => Promise<string>;
existsSync: (filepath: string) => boolean;
readFileSync: (filepath: string) => string;
resolve: (root: string, file: string, ext: string) => string; resolve: (root: string, file: string, ext: string) => string;
} }
+12 -1
View File
@@ -1,6 +1,6 @@
import * as _ from '../util/underscore' import * as _ from '../util/underscore'
import { resolve, extname } from 'path' import { resolve, extname } from 'path'
import { stat, readFile } from 'fs' import { stat, statSync, readFile, readFileSync } from 'fs'
import IFS from './ifs' import IFS from './ifs'
const statAsync = _.promisify(stat) const statAsync = _.promisify(stat)
@@ -13,6 +13,17 @@ const fs: IFS = {
readFile: filepath => { readFile: filepath => {
return readFileAsync(filepath, 'utf8') 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) => { resolve: (root: string, file: string, ext: string) => {
if (!extname(file)) file += ext if (!extname(file)) file += ext
return resolve(root, file) return resolve(root, file)
+1 -1
View File
@@ -88,7 +88,7 @@ export function applyDefault (options?: NormalizedOptions): NormalizedFullOption
return { ...defaultOptions, ...options } return { ...defaultOptions, ...options }
} }
function normalizeStringArray (value: any): string[] { export function normalizeStringArray (value: any): string[] {
if (_.isArray(value)) return value as string[] if (_.isArray(value)) return value as string[]
if (_.isString(value)) return [value as string] if (_.isString(value)) return [value as string]
return [] return []
+79 -26
View File
@@ -11,9 +11,12 @@ import { ITagImplOptions } from './template/tag/itag-impl-options'
import { Value } from './template/value' import { Value } from './template/value'
import builtinTags from './builtin/tags' import builtinTags from './builtin/tags'
import builtinFilters from './builtin/filters' 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 { FilterImplOptions } from './template/filter/filter-impl-options'
import IFS from './fs/ifs' import IFS from './fs/ifs'
type GetTemplateResult = ITemplate[] | undefined
export * from './types' export * from './types'
export class Liquid { export class Liquid {
@@ -34,47 +37,72 @@ export class Liquid {
_.forOwn(builtinTags, (conf, name) => this.registerTag(name, conf)) _.forOwn(builtinTags, (conf, name) => this.registerTag(name, conf))
_.forOwn(builtinFilters, (handler, name) => this.registerFilter(name, handler)) _.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) const tokens = this.tokenizer.tokenize(html, filepath)
return this.parser.parse(tokens) 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 options = { ...this.options, ...normalize(opts) }
const ctx = new Context(scope, options) const ctx = new Context(scope, options)
return this.renderer.renderTemplates(tpl, ctx) return this.renderer.renderTemplates(tpl, ctx)
} }
public async parseAndRender (html: string, ctx?: object, opts?: LiquidOptions) { public renderSync (tpl: ITemplate[], scope?: object, opts?: LiquidOptions): string {
const tpl = await this.parse(html) const options = { ...this.options, ...normalize(opts) }
return this.render(tpl, ctx, opts) const ctx = new Context(scope, options, true)
return this.renderer.renderTemplatesSync(tpl, ctx)
} }
public async getTemplate (file: string, opts?: LiquidOptions) { public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<string> {
const options = normalize(opts) const tpl = this.parse(html)
const roots = options.root ? [...options.root, ...this.options.root] : this.options.root return this.render(tpl, scope, opts)
const paths = roots.map(root => this.fs.resolve(root, file, this.options.extname)) }
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) { for (const filepath of paths) {
if (this.options.cache && this.cache[filepath]) return this.cache[filepath] const tpl = this.respectCache(filepath, () => {
if (!(this.fs.existsSync(filepath))) return
if (!(await this.fs.exists(filepath))) continue return this.parse(this.fs.readFileSync(filepath), filepath)
})
const value = this.parse(await this.fs.readFile(filepath), filepath) if (tpl) return tpl
if (this.options.cache) this.cache[filepath] = value
return value
} }
const err = new Error('ENOENT') as any throw this.lookupError(file, options.root)
err.message = `ENOENT: Failed to lookup "${file}" in "${roots}"` }
err.code = 'ENOENT' public async getTemplate (file: string, opts?: LiquidOptions): Promise<ITemplate[]> {
throw err 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) { public async renderFile (file: string, ctx?: object, opts?: LiquidOptions) {
const options = normalize(opts) const templates = await this.getTemplate(file, opts)
const templates = await this.getTemplate(file, options)
return this.render(templates, ctx, 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) 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) { public registerFilter (name: string, filter: FilterImplOptions) {
return Filter.register(name, filter) return Filter.register(name, filter)
} }
@@ -85,10 +113,35 @@ export class Liquid {
return plugin.call(this, Liquid) return plugin.call(this, Liquid)
} }
public express () { 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) { 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) 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 { export class Emitter {
public html: string = ''; public html = '';
public break = false;
public continue = false;
public write (html: string) { public write (html: string) {
this.html += html this.html += html
+39 -22
View File
@@ -1,38 +1,55 @@
import { assert } from '../util/assert' import { assert } from '../util/assert'
import { isRange, rangeValue } from './range' import { isRange, rangeValue, rangeValueSync } from './range'
import { Value } from './value' import { Value } from './value'
import { Context } from '../context/context' import { Context } from '../context/context'
import { toValue } from '../util/underscore' import { toValue } from '../util/underscore'
import { isOperator, precedence, operatorImpls } from './operator' import { isOperator, precedence, operatorImpls } from './operator'
export class Expression { export class Expression {
private str: string private operands: any[] = []
private postfix: string[]
public constructor (str: string = '') { public constructor (str = '') {
this.str = 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') assert(ctx, 'unable to evaluate: context not defined')
const operands = [] for (const token of this.postfix) {
for (const token of toPostfix(this.str)) {
if (isOperator(token)) { if (isOperator(token)) {
const r = operands.pop() this.evaluateOnce(token)
const l = operands.pop() } 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) const result = operatorImpls[token](l, r)
operands.push(result) this.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))
} }
} }
+13 -4
View File
@@ -3,15 +3,24 @@ import { Context } from '../context/context'
import { range } from '../util/underscore' import { range } from '../util/underscore'
import { Value } from './value' import { Value } from './value'
export function isRange (token: string = '') { export function isRange (token: string) {
return token[0] === '(' && token[token.length - 1] === ')' return token[0] === '(' && token[token.length - 1] === ')'
} }
export function rangeValue (token: string = '', ctx: Context) { export async function rangeValue (token: string, ctx: Context) {
let match let match
if ((match = token.match(rangeLine))) { if ((match = token.match(rangeLine))) {
const low = new Value(match[1]).value(ctx) const low = await new Value(match[1]).value(ctx)
const high = new Value(match[2]).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) return range(+low, +high + 1)
} }
} }
+17 -4
View File
@@ -4,13 +4,26 @@ import { ITemplate } from '../template/itemplate'
import { Emitter } from './emitter' import { Emitter } from './emitter'
export class Render { 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) { for (const tpl of templates) {
try { 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) { } catch (e) {
if (e.name === 'RenderBreakError') throw e throw RenderError.is(e) ? e : new RenderError(e, tpl)
throw e.name === 'RenderError' ? 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 return emitter.html
+12 -4
View File
@@ -5,11 +5,15 @@ import { parseLiteral } from '../parser/literal'
export class Value { export class Value {
private str: string private str: string
public constructor (str: string = '') { public constructor (str: string) {
this.str = str 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) const literalValue = parseLiteral(this.str)
if (literalValue !== undefined) { if (literalValue !== undefined) {
return literalValue return literalValue
@@ -17,7 +21,11 @@ export class Value {
return ctx.get(this.str) return ctx.get(this.str)
} }
public value (ctx: Context) { public async value (ctx: Context) {
return toValue(this.evaluate(ctx)) 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.impl = impl || (x => x)
this.args = args this.args = args
} }
public render (value: any, context: Context) { public async render (value: any, context: Context) {
const argv: any[] = [] const argv: any[] = []
for (const arg of this.args) { for (const arg of this.args) {
if (isKeyValuePair(arg)) argv.push([arg[0], new Expression(arg[1]).evaluate(context)]) if (isKeyValuePair(arg)) argv.push([arg[0], await new Expression(arg[1]).evaluate(context)])
else argv.push(new Expression(arg).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]) 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) super(token)
this.str = token.value this.str = token.value
} }
public render (ctx: Context, emitter: Emitter) { public renderSync (ctx: Context, emitter: Emitter) {
emitter.write(this.str) 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 { export interface ITemplate {
token: Token; 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) super(token)
this.value = new Value(token.value, strictFilters) 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) { public async render (ctx: Context, emitter: Emitter) {
const val = await this.value.value(ctx) const val = await this.value.value(ctx)
emitter.write(stringify(toValue(val))) emitter.write(stringify(toValue(val)))
+16 -2
View File
@@ -10,14 +10,28 @@ import { Context } from '../../context/context'
*/ */
export class Hash { export class Hash {
[key: string]: any [key: string]: any
public static async create (markup: string, ctx: Context) { private static parse (markup: string) {
const instance = new Hash() const instance = new Hash()
let match let match
hashCapture.lastIndex = 0 hashCapture.lastIndex = 0
while ((match = hashCapture.exec(markup))) { while ((match = hashCapture.exec(markup))) {
const k = match[1] const k = match[1]
const v = match[2] 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 return instance
} }
+1
View File
@@ -8,4 +8,5 @@ import { Emitter } from '../../render/emitter'
export interface ITagImplOptions { export interface ITagImplOptions {
parse?: (this: ITagImpl, token: TagToken, remainingTokens: Token[]) => void; parse?: (this: ITagImpl, token: TagToken, remainingTokens: Token[]) => void;
render: (this: ITagImpl, ctx: Context, hash: Hash, emitter: Emitter) => 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) 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) { public async render (ctx: Context, emitter: Emitter) {
const hash = await Hash.create(this.token.args, ctx) const hash = await Hash.create(this.token.args, ctx)
const impl = this.impl 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) { public static register (name: string, tag: ITagImplOptions) {
Tag.impls[name] = tag Tag.impls[name] = tag
+10 -3
View File
@@ -47,10 +47,17 @@ export class Value {
} }
this.filters.push(new Filter(name, args, this.strictFilters)) this.filters.push(new Filter(name, args, this.strictFilters))
} }
public value (ctx: Context) { public async value (ctx: Context) {
let val = new Expression(this.initial).evaluate(ctx) let val = await new Expression(this.initial).evaluate(ctx)
for (const filter of this.filters) { 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 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 { Drop } from './drop/drop'
export { Emitter } from './render/emitter' export { Emitter } from './render/emitter'
export { Expression } from './render/expression' export { Expression } from './render/expression'
+2 -8
View File
@@ -43,14 +43,8 @@ export class RenderError extends LiquidError {
this.message = err.message this.message = err.message
super.update() super.update()
} }
} public static is (obj: any): obj is RenderError {
return obj instanceof RenderError
export class RenderBreakError extends Error {
public resolvedHTML: string = ''
public constructor (message: string) {
super(message)
this.name = 'RenderBreakError'
this.message = message + ''
} }
} }
+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() }) beforeEach(() => { engine = new Liquid() })
it('should throw when scope undefined', async function () { 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') 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)) return expect(liquid.parseAndRender(src))
.to.be.rejectedWith(/tag .* not closed/) .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) const html = await liquid.parseAndRender(src)
return expect(html).to.equal('d') 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) const html = await liquid.parseAndRender(src)
return expect(html).to.equal('') 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) const html = await liquid.parseAndRender(src, ctx)
return expect(html).to.equal('121') 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) use(chaiAsPromised)
describe('tags/for', function () { describe('tags/for', function () {
let liquid: Liquid, ctx: Scope let liquid: Liquid, scope: Scope
before(function () { before(function () {
liquid = new Liquid() liquid = new Liquid()
liquid.registerTag('throwingTag', { liquid.registerTag('throwingTag', {
render: function () { throw new Error('intended render error') } render: function () { throw new Error('intended render error') }
}) })
ctx = { scope = {
one: 1, one: 1,
// eslint-disable-next-line // eslint-disable-next-line
strObj: new String(''), strObj: new String(''),
@@ -25,30 +25,30 @@ describe('tags/for', function () {
}) })
it('should support array', async function () { it('should support array', async function () {
const src = '{%for c in alpha%}{{c}}{%endfor%}' 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') return expect(html).to.equal('abc')
}) })
it('should support object', async function () { it('should support object', async function () {
const src = '{%for item in obj%}{{item[0]}},{{item[1]}}-{%else%}b{%endfor%}' 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-') return expect(html).to.equal('foo,bar-coo,haa-')
}) })
it('should output forloop', async function () { it('should output forloop', async function () {
const src = '{%for i in (1..1)%}{{forloop}}{%endfor%}' 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}') return expect(html).to.equal('{"i":0,"length":1}')
}) })
describe('illegal', function () { describe('illegal', function () {
it('should reject when for not closed', function () { it('should reject when for not closed', function () {
const src = '{%for c in alpha%}{{c}}' 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/) .to.be.rejectedWith(/tag .* not closed/)
}) })
it('should reject when inner templates rejected', function () { it('should reject when inner templates rejected', function () {
const src = '{%for c in alpha%}{%throwingTag%}{%endfor%}' 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/) .to.be.rejectedWith(/intended render error/)
}) })
}) })
@@ -56,38 +56,38 @@ describe('tags/for', function () {
describe('else', function () { describe('else', function () {
it('should goto else for empty array', async function () { it('should goto else for empty array', async function () {
const src = '{%for c in emptyArray%}a{%else%}b{%endfor%}' 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') return expect(html).to.equal('b')
}) })
it('should treat non-empty string as one single element', async function () { it('should treat non-empty string as one single element', async function () {
const src = '{%for c in "abc"%}x{{c}}{%else%}y{%endfor%}' 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') return expect(html).to.equal('xabc')
}) })
it('should goto else for empty string', async function () { it('should goto else for empty string', async function () {
const src = '{%for c in ""%}a{%else%}b{%endfor%}' 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') return expect(html).to.equal('b')
}) })
it('should goto else for empty string object', async function () { it('should goto else for empty string object', async function () {
// it should be false although `new String` is none-conform // it should be false although `new String` is none-conform
const src = '{%for c in strObj%}a{%else%}b{%endfor%}' 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') return expect(html).to.equal('b')
}) })
it('should goto else for empty object', async function () { it('should goto else for empty object', async function () {
const src = '{%for c in emptyObj%}a{%else%}b{%endfor%}' 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') return expect(html).to.equal('b')
}) })
it('should goto else for null-prototyped object', async function () { it('should goto else for null-prototyped object', async function () {
const src = '{%for c in nullProtoObj%}a{%else%}b{%endfor%}' 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') return expect(html).to.equal('b')
}) })
}) })
@@ -102,7 +102,7 @@ describe('tags/for', function () {
const dst = 'true.1.0.false.3.3.2a\n' + const dst = 'true.1.0.false.3.3.2a\n' +
'false.2.1.false.3.2.1b\n' + 'false.2.1.false.3.2.1b\n' +
'false.3.2.true.3.1.0c\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) return expect(html).to.equal(dst)
}) })
@@ -111,7 +111,7 @@ describe('tags/for', function () {
const src = '{% for i in (1..5) %}' + const src = '{% for i in (1..5) %}' +
'{% if i == 4 %}continue{% continue %}{% endif %}{{i}}' + '{% if i == 4 %}continue{% continue %}{% endif %}{{i}}' +
'{% endfor %}' '{% endfor %}'
const html = await liquid.parseAndRender(src, ctx) const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123continue5') return expect(html).to.equal('123continue5')
}) })
it('should output contents before continue', async function () { it('should output contents before continue', async function () {
@@ -119,7 +119,7 @@ describe('tags/for', function () {
'{% if i == 4 %}continue{% continue %}{% endif %}' + '{% if i == 4 %}continue{% continue %}{% endif %}' +
'{{ i }}' + '{{ i }}' +
'{% endfor %}' '{% endfor %}'
const html = await liquid.parseAndRender(src, ctx) const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123continue5') return expect(html).to.equal('123continue5')
}) })
}) })
@@ -129,7 +129,7 @@ describe('tags/for', function () {
'{% if i == 4 %}{% break %}{% endif %}' + '{% if i == 4 %}{% break %}{% endif %}' +
'{{ i }}' + '{{ i }}' +
'{% endfor %}' '{% endfor %}'
const html = await liquid.parseAndRender(src, ctx) const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123') return expect(html).to.equal('123')
}) })
it('should output contents before break', async function () { it('should output contents before break', async function () {
@@ -137,7 +137,7 @@ describe('tags/for', function () {
'{% if i == 4 %}breaking{% break %}{% endif %}' + '{% if i == 4 %}breaking{% break %}{% endif %}' +
'{{ i }}' + '{{ i }}' +
'{% endfor %}' '{% endfor %}'
const html = await liquid.parseAndRender(src, ctx) const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123breaking') return expect(html).to.equal('123breaking')
}) })
}) })
@@ -145,22 +145,22 @@ describe('tags/for', function () {
describe('limit', function () { describe('limit', function () {
it('should support for with limit', async function () { it('should support for with limit', async function () {
const src = '{% for i in (1..5) limit:2 %}{{ i }}{% endfor %}' 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') return expect(html).to.equal('12')
}) })
it('should set forloop.last properly', async function () { it('should set forloop.last properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.last}} {%endfor%}' 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 ') return expect(html).to.equal('false true ')
}) })
it('should set forloop.first properly', async function () { it('should set forloop.first properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.first}} {%endfor%}' 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 ') return expect(html).to.equal('true false ')
}) })
it('should set forloop.length properly', function () { it('should set forloop.length properly', function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.length}} {%endfor%}' 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 ') .to.eventually.equal('2 2 ')
}) })
}) })
@@ -168,27 +168,27 @@ describe('tags/for', function () {
describe('offset', function () { describe('offset', function () {
it('should support offset with limit', async function () { it('should support offset with limit', async function () {
const src = '{% for i in (1..10) limit:2 offset:5%}{{ i }}{% endfor %}' 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') return expect(html).to.equal('67')
}) })
it('should set index properly', async function () { it('should set index properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.index}} {%endfor%}' 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 ') return expect(html).to.equal('1 2 ')
}) })
it('should set index0 properly', async function () { it('should set index0 properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.index0}} {%endfor%}' 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 ') return expect(html).to.equal('0 1 ')
}) })
it('should set rindex properly', async function () { it('should set rindex properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.rindex}} {%endfor%}' 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 ') return expect(html).to.equal('2 1 ')
}) })
it('should set rindex0 properly', async function () { it('should set rindex0 properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.rindex0}} {%endfor%}' 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 ') return expect(html).to.equal('1 0 ')
}) })
}) })
@@ -196,20 +196,47 @@ describe('tags/for', function () {
describe('reverse', function () { describe('reverse', function () {
it('should support for reversed in the last position', async 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 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') return expect(html).to.equal('21')
}) })
it('should support for reversed in the first position', async function () { it('should support for reversed in the first position', async function () {
const src = '{% for i in (1..5) reversed limit:2 %}{{ i }}{% endfor %}' 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') return expect(html).to.equal('21')
}) })
it('should support for reversed in the middle position', async function () { 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 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') 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 () { describe('tags/if', function () {
const liquid = new Liquid() const liquid = new Liquid()
const ctx = { const scope = {
one: 1, one: 1,
two: 2, two: 2,
emptyString: '', emptyString: '',
@@ -12,52 +12,52 @@ describe('tags/if', function () {
it('should throw if not closed', function () { it('should throw if not closed', function () {
const src = '{% if false%}yes' 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/) .to.be.rejectedWith(/tag {% if false%} not closed/)
}) })
it('should support nested', async function () { it('should support nested', async function () {
const src = '{%if false%}{%if true%}{%else%}a{%endif%}{%endif%}' 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('') return expect(html).to.equal('')
}) })
describe('single value as condition', function () { describe('single value as condition', function () {
it('should support boolean', async function () { it('should support boolean', async function () {
const src = '{% if false %}1{%elsif true%}2{%else%}3{%endif%}' 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') return expect(html).to.equal('2')
}) })
it('should treat Array truthy', async function () { it('should treat Array truthy', async function () {
const src = '{%if emptyArray%}a{%endif%}' 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') return expect(html).to.equal('a')
}) })
it('should return true if empty string', async function () { it('should return true if empty string', async function () {
const src = '{%if emptyString%}a{%endif%}' 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') return expect(html).to.equal('a')
}) })
}) })
describe('expression as condition', function () { describe('expression as condition', function () {
it('should support ==', async function () { it('should support ==', async function () {
const src = '{% if 2 == 3 %}yes{%else%}no{%endif%}' 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') return expect(html).to.equal('no')
}) })
it('should support >=', async function () { it('should support >=', async function () {
const src = '{% if 1 >= 2 and one<two %}a{%endif%}' 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('') return expect(html).to.equal('')
}) })
it('should support !=', async function () { it('should support !=', async function () {
const src = '{% if one != two %}yes{%else%}no{%endif%}' 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') return expect(html).to.equal('yes')
}) })
it('should support value and expression', async function () { it('should support value and expression', async function () {
const src = `X{%if version and version != '' %}x{{version}}y{%endif%}Y` const src = `X{%if version and version != '' %}x{{version}}y{%endif%}Y`
const ctx = { 'version': '' } const scope = { 'version': '' }
const html = await liquid.parseAndRender(src, ctx) const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('XY') return expect(html).to.equal('XY')
}) })
it('should evaluate right to left', async function () { it('should evaluate right to left', async function () {
@@ -69,50 +69,55 @@ describe('tags/if', function () {
describe('comparasion to null', function () { describe('comparasion to null', function () {
it('should evaluate false for null < 10', async function () { it('should evaluate false for null < 10', async function () {
const src = '{% if null < 10 %}yes{% else %}no{% endif %}' 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') return expect(html).to.equal('no')
}) })
it('should evaluate false for null > 10', async function () { it('should evaluate false for null > 10', async function () {
const src = '{% if null > 10 %}yes{% else %}no{% endif %}' 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') return expect(html).to.equal('no')
}) })
it('should evaluate false for null <= 10', async function () { it('should evaluate false for null <= 10', async function () {
const src = '{% if null <= 10 %}yes{% else %}no{% endif %}' 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') return expect(html).to.equal('no')
}) })
it('should evaluate false for null >= 10', async function () { it('should evaluate false for null >= 10', async function () {
const src = '{% if null >= 10 %}yes{% else %}no{% endif %}' 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') return expect(html).to.equal('no')
}) })
it('should evaluate false for 10 < null', async function () { it('should evaluate false for 10 < null', async function () {
const src = '{% if 10 < null %}yes{% else %}no{% endif %}' 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') return expect(html).to.equal('no')
}) })
it('should evaluate false for 10 > null', async function () { it('should evaluate false for 10 > null', async function () {
const src = '{% if 10 > null %}yes{% else %}no{% endif %}' 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') return expect(html).to.equal('no')
}) })
it('should evaluate false for 10 <= null', async function () { it('should evaluate false for 10 <= null', async function () {
const src = '{% if 10 <= null %}yes{% else %}no{% endif %}' 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') return expect(html).to.equal('no')
}) })
it('should evaluate false for 10 >= null', async function () { it('should evaluate false for 10 >= null', async function () {
const src = '{% if 10 >= null %}yes{% else %}no{% endif %}' 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') 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') 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') 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 () { describe('tags/raw', function () {
const liquid = new Liquid() const liquid = new Liquid()
it('should support raw 1', async function () { it('should throw when not closed', async function () {
const p = liquid.parseAndRender('{% raw%}') const p = liquid.parseAndRender('{% raw%}')
return expect(p).be.rejectedWith(/{% raw%} not closed/) 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 src = '{% raw %}{{ 5 | plus: 6 }}{% endraw %} is equal to 11.'
const dst = '{{ 5 | plus: 6 }} is equal to 11.' const dst = '{{ 5 | plus: 6 }} is equal to 11.'
const html = await liquid.parseAndRender(src) const html = await liquid.parseAndRender(src)
return expect(html).to.equal(dst) 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 src = '{% raw %}\n{{ foo}} \n{% endraw %}'
const dst = '\n{{ foo}} \n' const dst = '\n{{ foo}} \n'
const html = await liquid.parseAndRender(src) const html = await liquid.parseAndRender(src)
return expect(html).to.equal(dst) 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) 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) const html = await liquid.parseAndRender(src)
return expect(html).to.equal('') 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() }) const html = await liquid.parseAndRender(`{{obj.name}}`, { obj: new PromiseDrop() })
expect(html).to.equal('NAME') 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 () { it('should support promise returned by liquidMethodMissing', async function () {
const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new PromiseDrop() }) const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new PromiseDrop() })
expect(html).to.equal('FOO') expect(html).to.equal('FOO')
+73 -22
View File
@@ -3,36 +3,87 @@ import { Liquid } from '../../../src/liquid'
import { mock, restore } from '../../stub/mockfs' import { mock, restore } from '../../stub/mockfs'
describe('LiquidOptions#cache', function () { describe('LiquidOptions#cache', function () {
let engine: Liquid afterEach(restore)
beforeEach(function () {
engine = new Liquid({ describe('#renderFile', function () {
it('should be disabled by default', async function () {
const engine = new Liquid({
root: '/root/', root: '/root/',
extname: '.html' extname: '.html'
}) })
mock({ '/root/files/foo.html': '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('bar')
}) })
afterEach(restore) it('should respect cache=true option', async function () {
it('should be disabled by default', function () { const engine = new Liquid({
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({
root: '/root/', root: '/root/',
extname: '.html', extname: '.html',
cache: true cache: true
}) })
return engine.renderFile('files/foo') mock({ '/root/files/foo.html': 'foo' })
.then(x => expect(x).to.equal('foo')) const x = await engine.renderFile('files/foo')
.then(() => mock({ expect(x).to.equal('foo')
'/root/files/foo.html': 'bar' mock({ '/root/files/foo.html': 'bar' })
})) const y = await engine.renderFile('files/foo')
.then(() => engine.renderFile('files/foo')) expect(y).to.equal('foo')
.then(x => expect(x).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) after(restore)
it('should render single template', function (done) { 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) if (err) return done(err)
expect(result).to.equal('foo') expect(result).to.equal('foo')
done() done()
}) })
}) })
it('should render single template with Array-typed root', function (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) if (err) return done(err)
expect(result).to.equal('foo') expect(result).to.equal('foo')
done() 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 () { describe('liquid#registerFilter()', function () {
const liquid = new Liquid()
describe('object arguments', function () { describe('object arguments', function () {
liquid.registerFilter('obj_test', function () { liquid.registerFilter('obj_test', function (...args) {
return JSON.stringify(arguments) 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 { expect } from 'chai'
import { RenderError } from '../../../src/util/error'
import { Liquid } from '../../../src/liquid' import { Liquid } from '../../../src/liquid'
import * as path from 'path' import * as path from 'path'
import { mock, restore } from '../../stub/mockfs' import { mock, restore } from '../../stub/mockfs'
@@ -264,4 +265,46 @@ describe('error', function () {
expect(err.stack).to.match(/at .*:\d+:\d+\)/) 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 } = {} let files: { [path: string]: FileDescriptor } = {}
const readFile = fs.readFile const { readFile, exists, readFileSync, existsSync } = fs
const exists = fs.exists
export function mock (options: { [path: string]: (string | FileDescriptor) }) { export function mock (options: { [path: string]: (string | FileDescriptor) }) {
forOwn(options, (val, key) => { forOwn(options, (val, key) => {
@@ -18,19 +17,26 @@ export function mock (options: { [path: string]: (string | FileDescriptor) }) {
: val as FileDescriptor : val as FileDescriptor
}) })
fs.readFile = async function (path) { fs.readFile = async function (path) {
return fs.readFileSync(path)
}
fs.readFileSync = function (path) {
const file = files[path] const file = files[path]
if (file === undefined) throw new Error('ENOENT') if (file === undefined) throw new Error('ENOENT')
if (file.mode === '0000') throw new Error('EACCES') if (file.mode === '0000') throw new Error('EACCES')
return file.content return file.content
} }
fs.exists = async function (path: string) { fs.exists = async function (path: string) {
return fs.existsSync(path)
}
fs.existsSync = function (path: string) {
return !!files[path] return !!files[path]
} }
} }
export function restore () { export function restore () {
files = {} files = {}
fs.readFileSync = readFileSync
fs.existsSync = existsSync
fs.readFile = readFile fs.readFile = readFile
fs.exists = exists fs.exists = exists
} }
+1 -1
View File
@@ -8,7 +8,7 @@ export const ctx = {
foo: 'bar', foo: 'bar',
arr: [-2, 'a'], arr: [-2, 'a'],
obj: { foo: 'bar' }, obj: { foo: 'bar' },
func: function () {}, func: function () {}, // eslint-disable-line
posts: [{ category: 'foo' }, { category: 'bar' }], posts: [{ category: 'foo' }, { category: 'bar' }],
products: [ products: [
{ title: 'Vacuum', type: 'living room' }, { 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()', () => { describe('#readFile()', () => {
let server: sinon.SinonFakeServer let server: sinon.SinonFakeServer
beforeEach(() => { beforeEach(() => {
@@ -87,4 +93,29 @@ describe('fs/browser', function () {
return result 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) use(chaiAsPromised)
describe('fs', function () { describe('fs', function () {
describe('#resolve()', function () { describe('.resolve()', function () {
it('should resolve based on root', async function () { it('should resolve based on root', async function () {
const filepath = fs.resolve('/foo', 'bar.html', '.liquid') const filepath = fs.resolve('/foo', 'bar.html', '.liquid')
const expected = path.resolve('/foo/bar.html') const expected = path.resolve('/foo/bar.html')
@@ -18,23 +18,40 @@ describe('fs', function () {
return expect(filepath).to.equal(expected) 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 () => { it('should resolve as false if not exists', async () => {
const result = await fs.exists('/foo/bar') 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 () => { it('should resolve as true if exists', async () => {
const result = await fs.exists(__filename) 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 () { it('should throw when not exist', function () {
return expect(fs.readFile('/foo/bar')).to.rejectedWith('ENOENT') return expect(fs.readFile('/foo/bar')).to.rejectedWith('ENOENT')
}) })
it('should read content if exists', async function () { it('should read content if exists', async function () {
const content = await fs.readFile(__filename) 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 () { 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 () { it('should eval simple expression', async function () {
expect(new Expression('1 < 2').value(ctx)).to.equal(true) expect(await new Expression('1 < 2').value(ctx)).to.equal(true)
expect(new Expression('2 <= 2').value(ctx)).to.equal(true) expect(await new Expression('1 < 2').value(ctx)).to.equal(true)
expect(new Expression('one <= two').value(ctx)).to.equal(true) expect(await new Expression('2 <= 2').value(ctx)).to.equal(true)
expect(new Expression('x contains "x"').value(ctx)).to.equal(false) expect(await new Expression('one <= two').value(ctx)).to.equal(true)
expect(new Expression('x contains "X"').value(ctx)).to.equal(true) expect(await new Expression('x contains "x"').value(ctx)).to.equal(false)
expect(new Expression('1 contains "x"').value(ctx)).to.equal(false) expect(await new Expression('x contains "X"').value(ctx)).to.equal(true)
expect(new Expression('y contains "x"').value(ctx)).to.equal(false) expect(await new Expression('1 contains "x"').value(ctx)).to.equal(false)
expect(new Expression('z contains "x"').value(ctx)).to.equal(false) expect(await new Expression('y contains "x"').value(ctx)).to.equal(false)
expect(new Expression('(1..5) contains 3').value(ctx)).to.equal(true) expect(await new Expression('z contains "x"').value(ctx)).to.equal(false)
expect(new Expression('(1..5) contains 6').value(ctx)).to.equal(false) expect(await new Expression('(1..5) contains 3').value(ctx)).to.equal(true)
expect(new Expression('"<=" == "<="').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 () { describe('complex expression', function () {
it('should support value or value', async 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 () { 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 () { 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 () { 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 () { 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 () { it('should evaluate from right to left', async function () {
expect(new Expression('true or false and false').value(ctx)).to.equal(true) expect(await 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) 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 } }) 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 () { it('should eval range expression', async function () {
expect(new Expression('(2..4)').value(ctx)).to.deep.equal([2, 3, 4]) expect(await 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('(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 () { describe('Value', function () {
it('should eval number variable', async function () { it('should eval number variable', async function () {
const ctx = new Context({ one: 1 }) 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 () { it('question mark should be valid variable name', async function () {
const ctx = new Context({ 'has_value?': true }) 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 () { it('should eval string variable', async function () {
const ctx = new Context({ x: 'XXX' }) 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 () { 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 () { 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) new Filter('/', [], false)
}).to.not.throw() }).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 expect = chai.expect
const liquid = new Liquid() const liquid = new Liquid()
describe('tag', function () { describe('Tag', function () {
let ctx: Context let ctx: Context
const emitter = { write: (html: string) => (emitter.html += html), html: '' } const emitter = { write: (html: string) => (emitter.html += html), html: '' }
before(function () { before(function () {