diff --git a/.eslintrc.json b/.eslintrc.json
index 2fbd4286b..6d3467339 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -18,6 +18,7 @@
"prefer-const": 2,
"no-unused-vars": "off",
"indent": "off",
+ "no-dupe-class-members": "off",
"@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
diff --git a/demo/nodejs/index.js b/demo/nodejs/index.js
index cf2318906..8c2a4ff4c 100644
--- a/demo/nodejs/index.js
+++ b/demo/nodejs/index.js
@@ -10,9 +10,9 @@ engine.registerTag('header', {
const [key, val] = token.args.split(':')
this[key] = val
},
- render: function (scope, hash) {
- const title = this.liquid.evalValue(this.content, scope)
- return `
${title}
`
+ render: async function (scope, hash, emitter) {
+ const title = await this.liquid.evalValue(this.content, scope)
+ emitter.write(`${title}
`)
}
})
diff --git a/demo/typescript/index.ts b/demo/typescript/index.ts
index a49c3df80..ffb3bde3c 100644
--- a/demo/typescript/index.ts
+++ b/demo/typescript/index.ts
@@ -1,4 +1,4 @@
-import { Liquid, TagToken, Hash, Context } from 'liquidjs'
+import { Liquid, TagToken, Hash, Context, Emitter } from 'liquidjs'
const engine = new Liquid({
root: __dirname,
@@ -10,9 +10,9 @@ engine.registerTag('header', {
const [key, val] = token.args.split(':')
this[key] = val
},
- render: function (context: Context, hash: Hash) {
- const title = this.liquid.evalValue(this['content'], context)
- return `${title}
`
+ render: async function (context: Context, hash: Hash, emitter: Emitter) {
+ const title = await this.liquid.evalValue(this['content'], context)
+ emitter.write(`${title}
`)
}
})
diff --git a/src/builtin/tags/assign.ts b/src/builtin/tags/assign.ts
index 6c40b4f40..e54244727 100644
--- a/src/builtin/tags/assign.ts
+++ b/src/builtin/tags/assign.ts
@@ -12,6 +12,8 @@ export default {
this.value = match[2]
},
render: async function (ctx: Context) {
- ctx.front()[this.key] = await this.liquid.evalValue(this.value, ctx)
+ ctx.front()[this.key] = ctx.sync
+ ? this.liquid.evalValueSync(this.value, ctx)
+ : await this.liquid.evalValue(this.value, ctx)
}
} as ITagImplOptions
diff --git a/src/builtin/tags/block.ts b/src/builtin/tags/block.ts
index 9c78eac4a..b3da8991c 100644
--- a/src/builtin/tags/block.ts
+++ b/src/builtin/tags/block.ts
@@ -17,9 +17,13 @@ export default {
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const blocks = ctx.getRegister('blocks')
const childDefined = blocks[this.block]
+ const r = this.liquid.renderer
const html = childDefined !== undefined
? childDefined
- : await this.liquid.renderer.renderTemplates(this.tpls, ctx)
+ : (ctx.sync
+ ? r.renderTemplatesSync(this.tpls, ctx)
+ : await r.renderTemplates(this.tpls, ctx)
+ )
if (ctx.getRegister('blockMode', BlockMode.OUTPUT) === BlockMode.STORE) {
blocks[this.block] = html
diff --git a/src/builtin/tags/break.ts b/src/builtin/tags/break.ts
index 5f3f49d34..f765bc888 100644
--- a/src/builtin/tags/break.ts
+++ b/src/builtin/tags/break.ts
@@ -1,7 +1,7 @@
-import { RenderBreakError } from '../../util/error'
+import { Emitter, Context, Hash } from '../../types'
export default {
- render: async function () {
- throw new RenderBreakError('break')
+ render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
+ emitter.break = true
}
}
diff --git a/src/builtin/tags/capture.ts b/src/builtin/tags/capture.ts
index 60c7b3455..2354e11cc 100644
--- a/src/builtin/tags/capture.ts
+++ b/src/builtin/tags/capture.ts
@@ -21,7 +21,10 @@ export default {
stream.start()
},
render: async function (ctx: Context) {
- const html = await this.liquid.renderer.renderTemplates(this.templates, ctx)
+ const r = this.liquid.renderer
+ const html = ctx.sync
+ ? r.renderTemplatesSync(this.templates, ctx)
+ : await r.renderTemplates(this.templates, ctx)
ctx.front()[this.variable] = html
}
} as ITagImplOptions
diff --git a/src/builtin/tags/case.ts b/src/builtin/tags/case.ts
index 22cf9ecd2..b38f49825 100644
--- a/src/builtin/tags/case.ts
+++ b/src/builtin/tags/case.ts
@@ -25,15 +25,30 @@ export default {
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
+ const r = this.liquid.renderer
for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i]
- const val = new Expression(branch.val).value(ctx)
- const cond = new Expression(this.cond).value(ctx)
+ const val = await new Expression(branch.val).value(ctx)
+ const cond = await new Expression(this.cond).value(ctx)
if (val === cond) {
- this.liquid.renderer.renderTemplates(branch.templates, ctx, emitter)
+ await r.renderTemplates(branch.templates, ctx, emitter)
return
}
}
- this.liquid.renderer.renderTemplates(this.elseTemplates, ctx, emitter)
+ await r.renderTemplates(this.elseTemplates, ctx, emitter)
+ },
+
+ renderSync: function (ctx: Context, hash: Hash, emitter: Emitter) {
+ const r = this.liquid.renderer
+ for (let i = 0; i < this.cases.length; i++) {
+ const branch = this.cases[i]
+ const val = new Expression(branch.val).valueSync(ctx)
+ const cond = new Expression(this.cond).valueSync(ctx)
+ if (val === cond) {
+ r.renderTemplatesSync(branch.templates, ctx, emitter)
+ return
+ }
+ }
+ r.renderTemplatesSync(this.elseTemplates, ctx, emitter)
}
} as ITagImplOptions
diff --git a/src/builtin/tags/continue.ts b/src/builtin/tags/continue.ts
index 66d5f81cb..6e14bbcd4 100644
--- a/src/builtin/tags/continue.ts
+++ b/src/builtin/tags/continue.ts
@@ -1,7 +1,7 @@
-import { RenderBreakError } from '../../util/error'
+import { Emitter, Context, Hash } from '../../types'
export default {
- render: async function () {
- throw new RenderBreakError('continue')
+ render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
+ emitter.continue = true
}
}
diff --git a/src/builtin/tags/cycle.ts b/src/builtin/tags/cycle.ts
index 0af3518b5..2a7a93b3a 100644
--- a/src/builtin/tags/cycle.ts
+++ b/src/builtin/tags/cycle.ts
@@ -22,7 +22,9 @@ export default {
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
- const group = this.group.value(ctx)
+ const group = ctx.sync
+ ? this.group.valueSync(ctx)
+ : await this.group.value(ctx)
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = ctx.getRegister('cycle')
let idx = groups[fingerprint]
@@ -34,6 +36,9 @@ export default {
const candidate = this.candidates[idx]
idx = (idx + 1) % this.candidates.length
groups[fingerprint] = idx
- emitter.write(new Expression(candidate).value(ctx))
+ const html = ctx.sync
+ ? new Expression(candidate).valueSync(ctx)
+ : await new Expression(candidate).value(ctx)
+ emitter.write(html)
}
} as ITagImplOptions
diff --git a/src/builtin/tags/for.ts b/src/builtin/tags/for.ts
index 116a75dfe..99363196e 100644
--- a/src/builtin/tags/for.ts
+++ b/src/builtin/tags/for.ts
@@ -37,7 +37,10 @@ export default {
stream.start()
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
- let collection = new Expression(this.collection).value(ctx)
+ const r = this.liquid.renderer
+ let collection = ctx.sync
+ ? new Expression(this.collection).valueSync(ctx)
+ : await new Expression(this.collection).value(ctx)
if (!isArray(collection)) {
if (isString(collection) && collection.length > 0) {
@@ -47,7 +50,9 @@ export default {
}
}
if (!isArray(collection) || !collection.length) {
- this.liquid.renderer.renderTemplates(this.elseTemplates, ctx, emitter)
+ ctx.sync
+ ? r.renderTemplatesSync(this.elseTemplates, ctx, emitter)
+ : await r.renderTemplates(this.elseTemplates, ctx, emitter)
return
}
@@ -57,17 +62,19 @@ export default {
collection = collection.slice(offset, offset + limit)
if (this.reversed) collection.reverse()
- const context = { forloop: new ForloopDrop(collection.length) }
- ctx.push(context)
+ const scope = { forloop: new ForloopDrop(collection.length) }
+ ctx.push(scope)
for (const item of collection) {
- context[this.variable] = item
- try {
- await this.liquid.renderer.renderTemplates(this.templates, ctx, emitter)
- } catch (e) {
- if (e.name !== 'RenderBreakError') throw e
- if (e.message === 'break') break
+ scope[this.variable] = item
+ ctx.sync
+ ? r.renderTemplatesSync(this.templates, ctx, emitter)
+ : await r.renderTemplates(this.templates, ctx, emitter)
+ if (emitter.break) {
+ emitter.break = false
+ break
}
- context.forloop.next()
+ emitter.continue = false
+ scope.forloop.next()
}
ctx.pop()
}
diff --git a/src/builtin/tags/if.ts b/src/builtin/tags/if.ts
index ab643b67a..1f086f8d5 100644
--- a/src/builtin/tags/if.ts
+++ b/src/builtin/tags/if.ts
@@ -28,13 +28,28 @@ export default {
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
+ const r = this.liquid.renderer
+
for (const branch of this.branches) {
- const cond = new Expression(branch.cond).value(ctx)
+ const cond = await new Expression(branch.cond).value(ctx)
if (isTruthy(cond)) {
- await this.liquid.renderer.renderTemplates(branch.templates, ctx, emitter)
+ await r.renderTemplates(branch.templates, ctx, emitter)
return
}
}
- await this.liquid.renderer.renderTemplates(this.elseTemplates, ctx, emitter)
+ await r.renderTemplates(this.elseTemplates, ctx, emitter)
+ },
+
+ renderSync: function (ctx: Context, hash: Hash, emitter: Emitter) {
+ const r = this.liquid.renderer
+
+ for (const branch of this.branches) {
+ const cond = new Expression(branch.cond).valueSync(ctx)
+ if (isTruthy(cond)) {
+ r.renderTemplatesSync(branch.templates, ctx, emitter)
+ return
+ }
+ }
+ r.renderTemplatesSync(this.elseTemplates, ctx, emitter)
}
} as ITagImplOptions
diff --git a/src/builtin/tags/include.ts b/src/builtin/tags/include.ts
index 7c5e61455..077cb81be 100644
--- a/src/builtin/tags/include.ts
+++ b/src/builtin/tags/include.ts
@@ -17,14 +17,14 @@ export default {
match = withRE.exec(token.args)
if (match) this.with = match[1]
},
- render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
+ renderSync: function (ctx: Context, hash: Hash, emitter: Emitter) {
let filepath
if (ctx.opts.dynamicPartials) {
if (quotedLine.exec(this.value)) {
const template = this.value.slice(1, -1)
- filepath = await this.liquid.parseAndRender(template, ctx.getAll(), ctx.opts)
+ filepath = this.liquid.parseAndRenderSync(template, ctx.getAll(), ctx.opts)
} else {
- filepath = new Expression(this.value).value(ctx)
+ filepath = new Expression(this.value).valueSync(ctx)
}
} else {
filepath = this.staticValue
@@ -37,7 +37,37 @@ export default {
ctx.setRegister('blocks', {})
ctx.setRegister('blockMode', BlockMode.OUTPUT)
if (this.with) {
- hash[filepath] = new Expression(this.with).evaluate(ctx)
+ hash[filepath] = new Expression(this.with).evaluateSync(ctx)
+ }
+ const templates = this.liquid.getTemplateSync(filepath, ctx.opts)
+ ctx.push(hash)
+ this.liquid.renderer.renderTemplatesSync(templates, ctx, emitter)
+ ctx.pop()
+ ctx.setRegister('blocks', originBlocks)
+ ctx.setRegister('blockMode', originBlockMode)
+ },
+
+ render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
+ let filepath
+ if (ctx.opts.dynamicPartials) {
+ if (quotedLine.exec(this.value)) {
+ const template = this.value.slice(1, -1)
+ filepath = await this.liquid.parseAndRender(template, ctx.getAll(), ctx.opts)
+ } else {
+ filepath = await new Expression(this.value).value(ctx)
+ }
+ } else {
+ filepath = this.staticValue
+ }
+ assert(filepath, `cannot include with empty filename`)
+
+ const originBlocks = ctx.getRegister('blocks')
+ const originBlockMode = ctx.getRegister('blockMode')
+
+ ctx.setRegister('blocks', {})
+ ctx.setRegister('blockMode', BlockMode.OUTPUT)
+ if (this.with) {
+ hash[filepath] = await new Expression(this.with).evaluate(ctx)
}
const templates = await this.liquid.getTemplate(filepath, ctx.opts)
ctx.push(hash)
diff --git a/src/builtin/tags/layout.ts b/src/builtin/tags/layout.ts
index 017b8f0ea..68dacc34e 100644
--- a/src/builtin/tags/layout.ts
+++ b/src/builtin/tags/layout.ts
@@ -21,21 +21,31 @@ export default {
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
const layout = ctx.opts.dynamicPartials
- ? await (new Expression(this.layout).value(ctx))
+ ? (ctx.sync
+ ? new Expression(this.layout).valueSync(ctx)
+ : await new Expression(this.layout).value(ctx)
+ )
: this.staticLayout
assert(layout, `cannot apply layout with empty filename`)
// render the remaining tokens immediately
ctx.setRegister('blockMode', BlockMode.STORE)
const blocks = ctx.getRegister('blocks')
- const html = await this.liquid.renderer.renderTemplates(this.tpls, ctx)
+ const r = this.liquid.renderer
+ const html = ctx.sync
+ ? r.renderTemplatesSync(this.tpls, ctx)
+ : await r.renderTemplates(this.tpls, ctx)
if (blocks[''] === undefined) {
blocks[''] = html
}
- const templates = await this.liquid.getTemplate(layout, ctx.opts)
+ const templates = ctx.sync
+ ? this.liquid.getTemplateSync(layout, ctx.opts)
+ : await this.liquid.getTemplate(layout, ctx.opts)
ctx.push(hash)
ctx.setRegister('blockMode', BlockMode.OUTPUT)
- const partial = await this.liquid.renderer.renderTemplates(templates, ctx)
+ const partial = ctx.sync
+ ? r.renderTemplatesSync(templates, ctx)
+ : await r.renderTemplates(templates, ctx)
ctx.pop()
emitter.write(partial)
}
diff --git a/src/builtin/tags/raw.ts b/src/builtin/tags/raw.ts
index 5390f7236..dd43fd0fe 100644
--- a/src/builtin/tags/raw.ts
+++ b/src/builtin/tags/raw.ts
@@ -15,7 +15,7 @@ export default {
})
stream.start()
},
- render: function (ctx: Context, hash: Hash, emitter: Emitter) {
- emitter.write(this.tokens.map((token: Token) => token.raw).join(''))
+ render: function (ctx: Context) {
+ return this.tokens.map((token: Token) => token.raw).join('')
}
} as ITagImplOptions
diff --git a/src/builtin/tags/tablerow.ts b/src/builtin/tags/tablerow.ts
index 4648ba290..e95492c22 100644
--- a/src/builtin/tags/tablerow.ts
+++ b/src/builtin/tags/tablerow.ts
@@ -29,13 +29,16 @@ export default {
},
render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
- let collection = new Expression(this.collection).value(ctx) || []
+ let collection = ctx.sync
+ ? new Expression(this.collection).valueSync(ctx) || []
+ : await new Expression(this.collection).value(ctx) || []
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
collection = collection.slice(offset, offset + limit)
const cols = hash.cols || collection.length
+ const r = this.liquid.renderer
const tablerowloop = new TablerowloopDrop(collection.length, cols)
const scope = { tablerowloop }
ctx.push(scope)
@@ -47,7 +50,9 @@ export default {
emitter.write(``)
}
emitter.write(`| `)
- 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(' | ')
}
if (collection.length) emitter.write('
')
diff --git a/src/builtin/tags/unless.ts b/src/builtin/tags/unless.ts
index d2418225f..670ec4cdd 100644
--- a/src/builtin/tags/unless.ts
+++ b/src/builtin/tags/unless.ts
@@ -20,10 +20,19 @@ export default {
stream.start()
},
- render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
- const cond = new Expression(this.cond).value(ctx)
+ renderSync: async function (ctx: Context, hash: Hash, emitter: Emitter) {
+ const r = this.liquid.renderer
+ const cond = new Expression(this.cond).valueSync(ctx)
isFalsy(cond)
- ? await this.liquid.renderer.renderTemplates(this.templates, ctx, emitter)
- : await this.liquid.renderer.renderTemplates(this.elseTemplates, ctx, emitter)
+ ? r.renderTemplatesSync(this.templates, ctx, emitter)
+ : r.renderTemplatesSync(this.elseTemplates, ctx, emitter)
+ },
+
+ render: async function (ctx: Context, hash: Hash, emitter: Emitter) {
+ const r = this.liquid.renderer
+ const cond = await new Expression(this.cond).value(ctx)
+ await isFalsy(cond)
+ ? r.renderTemplates(this.templates, ctx, emitter)
+ : r.renderTemplates(this.elseTemplates, ctx, emitter)
}
} as ITagImplOptions
diff --git a/src/context/context.ts b/src/context/context.ts
index 902eaa7eb..5f770e6d5 100644
--- a/src/context/context.ts
+++ b/src/context/context.ts
@@ -9,8 +9,10 @@ export class Context {
private scopes: Scope[] = [{}]
private registers = {}
public environments: Scope
+ public sync: boolean
public opts: NormalizedFullOptions
- public constructor (ctx: object = {}, opts?: NormalizedFullOptions) {
+ public constructor (ctx: object = {}, opts?: NormalizedFullOptions, sync: boolean = false) {
+ this.sync = sync
this.opts = applyDefault(opts)
this.environments = ctx
}
diff --git a/src/fs/browser.ts b/src/fs/browser.ts
index 643f858d5..c94a4c5ae 100644
--- a/src/fs/browser.ts
+++ b/src/fs/browser.ts
@@ -44,8 +44,22 @@ async function readFile (url: string): Promise {
})
}
+function readFileSync (url: string): string {
+ const xhr = new XMLHttpRequest()
+ xhr.open('GET', url, false)
+ xhr.send()
+ if (xhr.status < 200 || xhr.status >= 300) {
+ throw new Error(xhr.statusText)
+ }
+ return xhr.responseText as string
+}
+
async function exists () {
return true
}
-export default { readFile, resolve, exists } as IFS
+function existsSync () {
+ return true
+}
+
+export default { readFile, resolve, exists, existsSync, readFileSync } as IFS
diff --git a/src/fs/ifs.ts b/src/fs/ifs.ts
index 6734e937e..6baefdf58 100644
--- a/src/fs/ifs.ts
+++ b/src/fs/ifs.ts
@@ -1,5 +1,7 @@
export default interface IFS {
exists: (filepath: string) => Promise;
readFile: (filepath: string) => Promise;
+ existsSync: (filepath: string) => boolean;
+ readFileSync: (filepath: string) => string;
resolve: (root: string, file: string, ext: string) => string;
}
diff --git a/src/fs/node.ts b/src/fs/node.ts
index a1b83e2ee..3deda0f2c 100644
--- a/src/fs/node.ts
+++ b/src/fs/node.ts
@@ -1,6 +1,6 @@
import * as _ from '../util/underscore'
import { resolve, extname } from 'path'
-import { stat, readFile } from 'fs'
+import { stat, statSync, readFile, readFileSync } from 'fs'
import IFS from './ifs'
const statAsync = _.promisify(stat)
@@ -13,6 +13,17 @@ const fs: IFS = {
readFile: filepath => {
return readFileAsync(filepath, 'utf8')
},
+ existsSync: (filepath: string) => {
+ try {
+ statSync(filepath)
+ return true
+ } catch (err) {
+ return false
+ }
+ },
+ readFileSync: filepath => {
+ return readFileSync(filepath, 'utf8')
+ },
resolve: (root: string, file: string, ext: string) => {
if (!extname(file)) file += ext
return resolve(root, file)
diff --git a/src/liquid-options.ts b/src/liquid-options.ts
index 22d4ea7bc..0bc1a029f 100644
--- a/src/liquid-options.ts
+++ b/src/liquid-options.ts
@@ -88,7 +88,7 @@ export function applyDefault (options?: NormalizedOptions): NormalizedFullOption
return { ...defaultOptions, ...options }
}
-function normalizeStringArray (value: any): string[] {
+export function normalizeStringArray (value: any): string[] {
if (_.isArray(value)) return value as string[]
if (_.isString(value)) return [value as string]
return []
diff --git a/src/liquid.ts b/src/liquid.ts
index 3f2269060..4b881d7ba 100644
--- a/src/liquid.ts
+++ b/src/liquid.ts
@@ -11,9 +11,12 @@ import { ITagImplOptions } from './template/tag/itag-impl-options'
import { Value } from './template/value'
import builtinTags from './builtin/tags'
import builtinFilters from './builtin/filters'
-import { LiquidOptions, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
+import { LiquidOptions, normalizeStringArray, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
import { FilterImplOptions } from './template/filter/filter-impl-options'
import IFS from './fs/ifs'
+
+type GetTemplateResult = ITemplate[] | undefined
+
export * from './types'
export class Liquid {
@@ -34,47 +37,72 @@ export class Liquid {
_.forOwn(builtinTags, (conf, name) => this.registerTag(name, conf))
_.forOwn(builtinFilters, (handler, name) => this.registerFilter(name, handler))
}
- public parse (html: string, filepath?: string) {
+ public parse (html: string, filepath?: string): ITemplate[] {
const tokens = this.tokenizer.tokenize(html, filepath)
return this.parser.parse(tokens)
}
- public render (tpl: ITemplate[], scope?: object, opts?: LiquidOptions) {
+ public render (tpl: ITemplate[], scope?: object, opts?: LiquidOptions): Promise {
const options = { ...this.options, ...normalize(opts) }
const ctx = new Context(scope, options)
return this.renderer.renderTemplates(tpl, ctx)
}
- public async parseAndRender (html: string, ctx?: object, opts?: LiquidOptions) {
- const tpl = await this.parse(html)
- return this.render(tpl, ctx, opts)
+ public renderSync (tpl: ITemplate[], scope?: object, opts?: LiquidOptions): string {
+ const options = { ...this.options, ...normalize(opts) }
+ const ctx = new Context(scope, options, true)
+ return this.renderer.renderTemplatesSync(tpl, ctx)
}
- public async getTemplate (file: string, opts?: LiquidOptions) {
- const options = normalize(opts)
- const roots = options.root ? [...options.root, ...this.options.root] : this.options.root
- const paths = roots.map(root => this.fs.resolve(root, file, this.options.extname))
+ public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise {
+ const tpl = this.parse(html)
+ return this.render(tpl, scope, opts)
+ }
+ public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): string {
+ const tpl = this.parse(html)
+ return this.renderSync(tpl, scope, opts)
+ }
+ public getTemplateSync (file: string, opts?: LiquidOptions): ITemplate[] {
+ const options = { ...this.options, ...normalize(opts) }
+ const paths = options.root.map(root => this.fs.resolve(root, file, options.extname))
for (const filepath of paths) {
- if (this.options.cache && this.cache[filepath]) return this.cache[filepath]
-
- if (!(await this.fs.exists(filepath))) continue
-
- const value = this.parse(await this.fs.readFile(filepath), filepath)
- if (this.options.cache) this.cache[filepath] = value
- return value
+ const tpl = this.respectCache(filepath, () => {
+ if (!(this.fs.existsSync(filepath))) return
+ return this.parse(this.fs.readFileSync(filepath), filepath)
+ })
+ if (tpl) return tpl
}
- const err = new Error('ENOENT') as any
- err.message = `ENOENT: Failed to lookup "${file}" in "${roots}"`
- err.code = 'ENOENT'
- throw err
+ throw this.lookupError(file, options.root)
+ }
+ public async getTemplate (file: string, opts?: LiquidOptions): Promise {
+ const options = { ...this.options, ...normalize(opts) }
+ const paths = options.root.map(root => this.fs.resolve(root, file, options.extname))
+
+ for (const filepath of paths) {
+ const tpl = await this.respectCache(filepath, async () => {
+ if (!(await this.fs.exists(filepath))) return
+ return this.parse(await this.fs.readFile(filepath), filepath)
+ })
+ if (tpl !== undefined) return tpl
+ }
+ throw this.lookupError(file, options.root)
}
public async renderFile (file: string, ctx?: object, opts?: LiquidOptions) {
- const options = normalize(opts)
- const templates = await this.getTemplate(file, options)
+ const templates = await this.getTemplate(file, opts)
return this.render(templates, ctx, opts)
}
- public evalValue (str: string, ctx: Context) {
+ public renderFileSync (file: string, ctx?: object, opts?: LiquidOptions) {
+ const options = normalize(opts)
+ const templates = this.getTemplateSync(file, options)
+ return this.renderSync(templates, ctx, opts)
+ }
+ public async evalValue (str: string, ctx: Context): Promise {
return new Value(str, this.options.strictFilters).value(ctx)
}
+
+ public evalValueSync (str: string, ctx: Context): any {
+ return new Value(str, this.options.strictFilters).valueSync(ctx)
+ }
+
public registerFilter (name: string, filter: FilterImplOptions) {
return Filter.register(name, filter)
}
@@ -85,10 +113,35 @@ export class Liquid {
return plugin.call(this, Liquid)
}
public express () {
- const self = this
+ const self = this // eslint-disable-line
return function (this: any, filePath: string, ctx: object, cb: (err: Error | null, html?: string) => void) {
- const opts = { root: this.root }
+ const opts = { root: [...normalizeStringArray(this.root), ...self.options.root] }
self.renderFile(filePath, ctx, opts).then(html => cb(null, html), cb)
}
}
-}
+
+ private lookupError (file: string, roots: string[]) {
+ const err = new Error('ENOENT') as any
+ err.message = `ENOENT: Failed to lookup "${file}" in "${roots}"`
+ err.code = 'ENOENT'
+ return err
+ }
+
+ private setCache (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): Promise
+ private respectCache (filepath: string, resolver: () => GetTemplateResult | Promise): GetTemplateResult | Promise {
+ 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)
+ }
+}
\ No newline at end of file
diff --git a/src/render/emitter.ts b/src/render/emitter.ts
index 8e199dd28..6231a6382 100644
--- a/src/render/emitter.ts
+++ b/src/render/emitter.ts
@@ -1,5 +1,7 @@
export class Emitter {
- public html: string = '';
+ public html = '';
+ public break = false;
+ public continue = false;
public write (html: string) {
this.html += html
diff --git a/src/render/expression.ts b/src/render/expression.ts
index 9e3cd35c5..d3f63ccfd 100644
--- a/src/render/expression.ts
+++ b/src/render/expression.ts
@@ -1,38 +1,55 @@
import { assert } from '../util/assert'
-import { isRange, rangeValue } from './range'
+import { isRange, rangeValue, rangeValueSync } from './range'
import { Value } from './value'
import { Context } from '../context/context'
import { toValue } from '../util/underscore'
import { isOperator, precedence, operatorImpls } from './operator'
export class Expression {
- private str: string
+ private operands: any[] = []
+ private postfix: string[]
- public constructor (str: string = '') {
- this.str = str
+ public constructor (str = '') {
+ this.postfix = [...toPostfix(str)]
}
- public evaluate (ctx: Context): any {
+ public async evaluate (ctx: Context): Promise {
assert(ctx, 'unable to evaluate: context not defined')
- const operands = []
- for (const token of toPostfix(this.str)) {
+ for (const token of this.postfix) {
if (isOperator(token)) {
- const r = operands.pop()
- const l = operands.pop()
- const result = operatorImpls[token](l, r)
- operands.push(result)
- continue
- }
- if (isRange(token)) {
- operands.push(rangeValue(token, ctx))
- continue
- }
- operands.push(new Value(token).evaluate(ctx))
+ this.evaluateOnce(token)
+ } else if (isRange(token)) {
+ this.operands.push(await rangeValue(token, ctx))
+ } else this.operands.push(await new Value(token).evaluate(ctx))
}
- return operands[0]
+ return this.operands[0]
}
- public value (ctx: Context): any {
- return toValue(this.evaluate(ctx))
+ 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 {
+ 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)
+ this.operands.push(result)
}
}
diff --git a/src/render/range.ts b/src/render/range.ts
index 7d962ab8f..b3eed3de7 100644
--- a/src/render/range.ts
+++ b/src/render/range.ts
@@ -3,15 +3,24 @@ import { Context } from '../context/context'
import { range } from '../util/underscore'
import { Value } from './value'
-export function isRange (token: string = '') {
+export function isRange (token: string) {
return token[0] === '(' && token[token.length - 1] === ')'
}
-export function rangeValue (token: string = '', ctx: Context) {
+export async function rangeValue (token: string, ctx: Context) {
let match
if ((match = token.match(rangeLine))) {
- const low = new Value(match[1]).value(ctx)
- const high = new Value(match[2]).value(ctx)
+ const low = await new Value(match[1]).value(ctx)
+ const high = await new Value(match[2]).value(ctx)
+ return range(+low, +high + 1)
+ }
+}
+
+export function rangeValueSync (token: string, ctx: Context) {
+ let match
+ if ((match = token.match(rangeLine))) {
+ const low = new Value(match[1]).valueSync(ctx)
+ const high = new Value(match[2]).valueSync(ctx)
return range(+low, +high + 1)
}
}
diff --git a/src/render/render.ts b/src/render/render.ts
index 3afb05429..3f7c9cbcb 100644
--- a/src/render/render.ts
+++ b/src/render/render.ts
@@ -4,13 +4,26 @@ import { ITemplate } from '../template/itemplate'
import { Emitter } from './emitter'
export class Render {
- public async renderTemplates (templates: ITemplate[], ctx: Context, emitter = new Emitter()) {
+ public async renderTemplates (templates: ITemplate[], ctx: Context, emitter = new Emitter()): Promise {
for (const tpl of templates) {
try {
- await tpl.render(ctx, emitter)
+ const html = await tpl.render(ctx, emitter)
+ html && emitter.write(html)
+ if (emitter.break || emitter.continue) break
} catch (e) {
- if (e.name === 'RenderBreakError') throw e
- throw e.name === 'RenderError' ? e : new RenderError(e, tpl)
+ throw RenderError.is(e) ? e : new RenderError(e, tpl)
+ }
+ }
+ return emitter.html
+ }
+ public renderTemplatesSync (templates: ITemplate[], ctx: Context, emitter = new Emitter()): string {
+ for (const tpl of templates) {
+ try {
+ const html = tpl.renderSync(ctx, emitter)
+ html && !(html instanceof Promise) && emitter.write(html)
+ if (emitter.break || emitter.continue) break
+ } catch (e) {
+ throw RenderError.is(e) ? e : new RenderError(e, tpl)
}
}
return emitter.html
diff --git a/src/render/value.ts b/src/render/value.ts
index cbc3ad656..d5d952008 100644
--- a/src/render/value.ts
+++ b/src/render/value.ts
@@ -5,11 +5,15 @@ import { parseLiteral } from '../parser/literal'
export class Value {
private str: string
- public constructor (str: string = '') {
+ public constructor (str: string) {
this.str = str
}
- public evaluate (ctx: Context) {
+ public async evaluate (ctx: Context) {
+ return this.evaluateSync(ctx)
+ }
+
+ public evaluateSync (ctx: Context) {
const literalValue = parseLiteral(this.str)
if (literalValue !== undefined) {
return literalValue
@@ -17,7 +21,11 @@ export class Value {
return ctx.get(this.str)
}
- public value (ctx: Context) {
- return toValue(this.evaluate(ctx))
+ public async value (ctx: Context) {
+ return toValue(await this.evaluate(ctx))
+ }
+
+ public valueSync (ctx: Context) {
+ return toValue(this.evaluateSync(ctx))
}
}
diff --git a/src/template/filter/filter.ts b/src/template/filter/filter.ts
index 818a995e8..e48fdef52 100644
--- a/src/template/filter/filter.ts
+++ b/src/template/filter/filter.ts
@@ -21,11 +21,19 @@ export class Filter {
this.impl = impl || (x => x)
this.args = args
}
- public render (value: any, context: Context) {
+ public async render (value: any, context: Context) {
const argv: any[] = []
for (const arg of this.args) {
- if (isKeyValuePair(arg)) argv.push([arg[0], new Expression(arg[1]).evaluate(context)])
- else argv.push(new Expression(arg).evaluate(context))
+ if (isKeyValuePair(arg)) argv.push([arg[0], await new Expression(arg[1]).evaluate(context)])
+ else argv.push(await new Expression(arg).evaluate(context))
+ }
+ return this.impl.apply({ context }, [value, ...argv])
+ }
+ public renderSync (value: any, context: Context) {
+ const argv: any[] = []
+ for (const arg of this.args) {
+ if (isKeyValuePair(arg)) argv.push([arg[0], new Expression(arg[1]).evaluateSync(context)])
+ else argv.push(new Expression(arg).evaluateSync(context))
}
return this.impl.apply({ context }, [value, ...argv])
}
diff --git a/src/template/html.ts b/src/template/html.ts
index f167fa4fa..326621ee9 100644
--- a/src/template/html.ts
+++ b/src/template/html.ts
@@ -10,7 +10,10 @@ export class HTML extends Template implements ITemplate {
super(token)
this.str = token.value
}
- public render (ctx: Context, emitter: Emitter) {
+ public renderSync (ctx: Context, emitter: Emitter) {
emitter.write(this.str)
}
+ public async render (ctx: Context, emitter: Emitter) {
+ this.renderSync(ctx, emitter)
+ }
}
diff --git a/src/template/itemplate.ts b/src/template/itemplate.ts
index 956014cf9..b14bc7491 100644
--- a/src/template/itemplate.ts
+++ b/src/template/itemplate.ts
@@ -4,5 +4,6 @@ import { Emitter } from '../render/emitter'
export interface ITemplate {
token: Token;
- render(ctx: Context, emitter: Emitter): Promise | void;
+ render(ctx: Context, emitter: Emitter): Promise;
+ renderSync(ctx: Context, emitter: Emitter): any;
}
diff --git a/src/template/output.ts b/src/template/output.ts
index 48d8f6bc2..d4a866611 100644
--- a/src/template/output.ts
+++ b/src/template/output.ts
@@ -12,6 +12,10 @@ export class Output extends Template implements ITemplate {
super(token)
this.value = new Value(token.value, strictFilters)
}
+ public renderSync (ctx: Context, emitter: Emitter) {
+ const val = this.value.valueSync(ctx)
+ emitter.write(stringify(toValue(val)))
+ }
public async render (ctx: Context, emitter: Emitter) {
const val = await this.value.value(ctx)
emitter.write(stringify(toValue(val)))
diff --git a/src/template/tag/hash.ts b/src/template/tag/hash.ts
index 07cd5f342..2dedfc3ef 100644
--- a/src/template/tag/hash.ts
+++ b/src/template/tag/hash.ts
@@ -10,14 +10,28 @@ import { Context } from '../../context/context'
*/
export class Hash {
[key: string]: any
- public static async create (markup: string, ctx: Context) {
+ private static parse (markup: string) {
const instance = new Hash()
let match
hashCapture.lastIndex = 0
while ((match = hashCapture.exec(markup))) {
const k = match[1]
const v = match[2]
- instance[k] = new Expression(v).evaluate(ctx)
+ instance[k] = v
+ }
+ return instance
+ }
+ public static createSync (markup: string, ctx: Context) {
+ const instance = Hash.parse(markup)
+ for (const key of Object.keys(instance)) {
+ instance[key] = new Expression(instance[key]).evaluateSync(ctx)
+ }
+ return instance
+ }
+ public static async create (markup: string, ctx: Context) {
+ const instance = Hash.parse(markup)
+ for (const key of Object.keys(instance)) {
+ instance[key] = await new Expression(instance[key]).evaluate(ctx)
}
return instance
}
diff --git a/src/template/tag/itag-impl-options.ts b/src/template/tag/itag-impl-options.ts
index 5cf66d183..a0699dbad 100644
--- a/src/template/tag/itag-impl-options.ts
+++ b/src/template/tag/itag-impl-options.ts
@@ -8,4 +8,5 @@ import { Emitter } from '../../render/emitter'
export interface ITagImplOptions {
parse?: (this: ITagImpl, token: TagToken, remainingTokens: Token[]) => void;
render: (this: ITagImpl, ctx: Context, hash: Hash, emitter: Emitter) => void;
+ renderSync?: (this: ITagImpl, ctx: Context, hash: Hash, emitter: Emitter) => void;
}
diff --git a/src/template/tag/tag.ts b/src/template/tag/tag.ts
index 320b59f34..dc0f674d5 100644
--- a/src/template/tag/tag.ts
+++ b/src/template/tag/tag.ts
@@ -23,10 +23,16 @@ export class Tag extends Template implements ITemplate {
this.impl.parse(token, tokens)
}
}
+ public renderSync (ctx: Context, emitter: Emitter) {
+ const hash = Hash.createSync(this.token.args, ctx)
+ const impl = this.impl
+ if (isFunction(impl.renderSync)) return impl.renderSync(ctx, hash, emitter)
+ if (isFunction(impl.render)) return impl.render(ctx, hash, emitter)
+ }
public async render (ctx: Context, emitter: Emitter) {
const hash = await Hash.create(this.token.args, ctx)
const impl = this.impl
- if (isFunction(impl.render)) await impl.render(ctx, hash, emitter)
+ if (isFunction(impl.render)) return impl.render(ctx, hash, emitter)
}
public static register (name: string, tag: ITagImplOptions) {
Tag.impls[name] = tag
diff --git a/src/template/value.ts b/src/template/value.ts
index ca54f4f32..a353bfe19 100644
--- a/src/template/value.ts
+++ b/src/template/value.ts
@@ -47,10 +47,17 @@ export class Value {
}
this.filters.push(new Filter(name, args, this.strictFilters))
}
- public value (ctx: Context) {
- let val = new Expression(this.initial).evaluate(ctx)
+ public async value (ctx: Context) {
+ let val = await new Expression(this.initial).evaluate(ctx)
for (const filter of this.filters) {
- val = filter.render(val, ctx)
+ val = await filter.render(val, ctx)
+ }
+ return val
+ }
+ public valueSync (ctx: Context) {
+ let val = new Expression(this.initial).evaluateSync(ctx)
+ for (const filter of this.filters) {
+ val = filter.renderSync(val, ctx)
}
return val
}
diff --git a/src/types.ts b/src/types.ts
index 8a6ba69a7..cfa3605e4 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,4 +1,4 @@
-export { ParseError, TokenizationError, RenderBreakError, AssertionError } from './util/error'
+export { ParseError, TokenizationError, AssertionError } from './util/error'
export { Drop } from './drop/drop'
export { Emitter } from './render/emitter'
export { Expression } from './render/expression'
diff --git a/src/util/error.ts b/src/util/error.ts
index 3a7ac8f9b..f0fec86a1 100644
--- a/src/util/error.ts
+++ b/src/util/error.ts
@@ -43,14 +43,8 @@ export class RenderError extends LiquidError {
this.message = err.message
super.update()
}
-}
-
-export class RenderBreakError extends Error {
- public resolvedHTML: string = ''
- public constructor (message: string) {
- super(message)
- this.name = 'RenderBreakError'
- this.message = message + ''
+ public static is (obj: any): obj is RenderError {
+ return obj instanceof RenderError
}
}
diff --git a/test/e2e/eval-value-sync.ts b/test/e2e/eval-value-sync.ts
new file mode 100644
index 000000000..030117d9f
--- /dev/null
+++ b/test/e2e/eval-value-sync.ts
@@ -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/)
+ })
+})
diff --git a/test/e2e/eval-value.ts b/test/e2e/eval-value.ts
index 1b000a47b..c46e9d4a4 100644
--- a/test/e2e/eval-value.ts
+++ b/test/e2e/eval-value.ts
@@ -6,6 +6,6 @@ describe('.evalValue()', function () {
beforeEach(() => { engine = new Liquid() })
it('should throw when scope undefined', async function () {
- return expect(() => engine.evalValue('{{"foo"}}', null as any)).to.throw(/context not defined/)
+ return expect(engine.evalValue('{{"foo"}}', null as any)).to.be.rejectedWith(/context not defined/)
})
})
diff --git a/test/integration/builtin/tags/assign.ts b/test/integration/builtin/tags/assign.ts
index 9d8f1fcb6..60115971b 100644
--- a/test/integration/builtin/tags/assign.ts
+++ b/test/integration/builtin/tags/assign.ts
@@ -81,4 +81,9 @@ describe('tags/assign', function () {
return expect(html).to.equal('12 2')
})
})
+ it('should support sync', function () {
+ const src = '{% assign foo="bar" %}{{foo}}'
+ const html = liquid.parseAndRenderSync(src)
+ return expect(html).to.equal('bar')
+ })
})
diff --git a/test/integration/builtin/tags/capture.ts b/test/integration/builtin/tags/capture.ts
index 1c51b8eb4..4aed916ed 100644
--- a/test/integration/builtin/tags/capture.ts
+++ b/test/integration/builtin/tags/capture.ts
@@ -32,4 +32,9 @@ describe('tags/capture', function () {
return expect(liquid.parseAndRender(src))
.to.be.rejectedWith(/tag .* not closed/)
})
+ it('should support sync', function () {
+ const src = '{% capture f %}{{"a" | capitalize}}{%endcapture%}{{f}}'
+ const html = liquid.parseAndRenderSync(src)
+ return expect(html).to.equal('A')
+ })
})
diff --git a/test/integration/builtin/tags/case.ts b/test/integration/builtin/tags/case.ts
index 11abdf8b4..6044eebc2 100644
--- a/test/integration/builtin/tags/case.ts
+++ b/test/integration/builtin/tags/case.ts
@@ -48,4 +48,20 @@ describe('tags/case', function () {
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('d')
})
+ describe('sync support', function () {
+ it('should hit the specified case', function () {
+ const src = '{% case "foo"%}' +
+ '{% when "foo" %}foo{% when "bar"%}bar' +
+ '{%endcase%}'
+ const html = liquid.parseAndRenderSync(src)
+ return expect(html).to.equal('foo')
+ })
+ it('should support else branch', function () {
+ const src = '{% case "a" %}' +
+ '{% when "b" %}b{% when "c"%}c{%else %}d' +
+ '{%endcase%}'
+ const html = liquid.parseAndRenderSync(src)
+ return expect(html).to.equal('d')
+ })
+ })
})
diff --git a/test/integration/builtin/tags/comment.ts b/test/integration/builtin/tags/comment.ts
index 5895f4db8..db652ecc7 100644
--- a/test/integration/builtin/tags/comment.ts
+++ b/test/integration/builtin/tags/comment.ts
@@ -31,4 +31,11 @@ describe('tags/comment', function () {
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
+ describe('sync support', function () {
+ it('should ignore plain string', function () {
+ const src = 'My name is {% comment %}super{% endcomment %} Shopify.'
+ const html = liquid.parseAndRenderSync(src)
+ return expect(html).to.equal('My name is Shopify.')
+ })
+ })
})
diff --git a/test/integration/builtin/tags/cycle.ts b/test/integration/builtin/tags/cycle.ts
index 6335006a1..082ddc381 100644
--- a/test/integration/builtin/tags/cycle.ts
+++ b/test/integration/builtin/tags/cycle.ts
@@ -35,4 +35,9 @@ describe('tags/cycle', function () {
const html = await liquid.parseAndRender(src, ctx)
return expect(html).to.equal('121')
})
+ it('should support sync', function () {
+ const src = "{% cycle '1', '2', '3' %}"
+ const html = liquid.parseAndRenderSync(src + src + src + src)
+ return expect(html).to.equal('1231')
+ })
})
diff --git a/test/integration/builtin/tags/for.ts b/test/integration/builtin/tags/for.ts
index af5956aee..7e22bdd10 100644
--- a/test/integration/builtin/tags/for.ts
+++ b/test/integration/builtin/tags/for.ts
@@ -6,13 +6,13 @@ import { Scope } from '../../../../src/context/scope'
use(chaiAsPromised)
describe('tags/for', function () {
- let liquid: Liquid, ctx: Scope
+ let liquid: Liquid, scope: Scope
before(function () {
liquid = new Liquid()
liquid.registerTag('throwingTag', {
render: function () { throw new Error('intended render error') }
})
- ctx = {
+ scope = {
one: 1,
// eslint-disable-next-line
strObj: new String(''),
@@ -25,30 +25,30 @@ describe('tags/for', function () {
})
it('should support array', async function () {
const src = '{%for c in alpha%}{{c}}{%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('abc')
})
it('should support object', async function () {
const src = '{%for item in obj%}{{item[0]}},{{item[1]}}-{%else%}b{%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('foo,bar-coo,haa-')
})
it('should output forloop', async function () {
const src = '{%for i in (1..1)%}{{forloop}}{%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('{"i":0,"length":1}')
})
describe('illegal', function () {
it('should reject when for not closed', function () {
const src = '{%for c in alpha%}{{c}}'
- return expect(liquid.parseAndRender(src, ctx))
+ return expect(liquid.parseAndRender(src, scope))
.to.be.rejectedWith(/tag .* not closed/)
})
it('should reject when inner templates rejected', function () {
const src = '{%for c in alpha%}{%throwingTag%}{%endfor%}'
- return expect(liquid.parseAndRender(src, ctx))
+ return expect(liquid.parseAndRender(src, scope))
.to.be.rejectedWith(/intended render error/)
})
})
@@ -56,38 +56,38 @@ describe('tags/for', function () {
describe('else', function () {
it('should goto else for empty array', async function () {
const src = '{%for c in emptyArray%}a{%else%}b{%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
it('should treat non-empty string as one single element', async function () {
const src = '{%for c in "abc"%}x{{c}}{%else%}y{%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('xabc')
})
it('should goto else for empty string', async function () {
const src = '{%for c in ""%}a{%else%}b{%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
it('should goto else for empty string object', async function () {
// it should be false although `new String` is none-conform
const src = '{%for c in strObj%}a{%else%}b{%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
it('should goto else for empty object', async function () {
const src = '{%for c in emptyObj%}a{%else%}b{%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
it('should goto else for null-prototyped object', async function () {
const src = '{%for c in nullProtoObj%}a{%else%}b{%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
})
@@ -102,7 +102,7 @@ describe('tags/for', function () {
const dst = 'true.1.0.false.3.3.2a\n' +
'false.2.1.false.3.2.1b\n' +
'false.3.2.true.3.1.0c\n'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal(dst)
})
@@ -111,7 +111,7 @@ describe('tags/for', function () {
const src = '{% for i in (1..5) %}' +
'{% if i == 4 %}continue{% continue %}{% endif %}{{i}}' +
'{% endfor %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123continue5')
})
it('should output contents before continue', async function () {
@@ -119,7 +119,7 @@ describe('tags/for', function () {
'{% if i == 4 %}continue{% continue %}{% endif %}' +
'{{ i }}' +
'{% endfor %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123continue5')
})
})
@@ -129,7 +129,7 @@ describe('tags/for', function () {
'{% if i == 4 %}{% break %}{% endif %}' +
'{{ i }}' +
'{% endfor %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123')
})
it('should output contents before break', async function () {
@@ -137,7 +137,7 @@ describe('tags/for', function () {
'{% if i == 4 %}breaking{% break %}{% endif %}' +
'{{ i }}' +
'{% endfor %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123breaking')
})
})
@@ -145,22 +145,22 @@ describe('tags/for', function () {
describe('limit', function () {
it('should support for with limit', async function () {
const src = '{% for i in (1..5) limit:2 %}{{ i }}{% endfor %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('12')
})
it('should set forloop.last properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.last}} {%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('false true ')
})
it('should set forloop.first properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.first}} {%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('true false ')
})
it('should set forloop.length properly', function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.length}} {%endfor%}'
- return expect(liquid.parseAndRender(src, ctx))
+ return expect(liquid.parseAndRender(src, scope))
.to.eventually.equal('2 2 ')
})
})
@@ -168,27 +168,27 @@ describe('tags/for', function () {
describe('offset', function () {
it('should support offset with limit', async function () {
const src = '{% for i in (1..10) limit:2 offset:5%}{{ i }}{% endfor %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('67')
})
it('should set index properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.index}} {%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('1 2 ')
})
it('should set index0 properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.index0}} {%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('0 1 ')
})
it('should set rindex properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.rindex}} {%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('2 1 ')
})
it('should set rindex0 properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.rindex0}} {%endfor%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('1 0 ')
})
})
@@ -196,20 +196,47 @@ describe('tags/for', function () {
describe('reverse', function () {
it('should support for reversed in the last position', async function () {
const src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('21')
})
it('should support for reversed in the first position', async function () {
const src = '{% for i in (1..5) reversed limit:2 %}{{ i }}{% endfor %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('21')
})
it('should support for reversed in the middle position', async function () {
const src = '{% for i in (1..5) offset:2 reversed limit:4 %}{{ i }}{% endfor %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src)
return expect(html).to.equal('543')
})
})
+ describe('sync', function () {
+ it('should support sync', function () {
+ const src = '{% for i in (1..5) %}{{i}}{%endfor%}'
+ const html = liquid.parseAndRenderSync(src)
+ return expect(html).to.equal('12345')
+ })
+ it('should output contents before break', function () {
+ const src = '{% for i in (1..5) %}' +
+ '{% if i == 4 %}breaking{% break %}{% endif %}' +
+ '{{ i }}' +
+ '{% endfor %}'
+ const html = liquid.parseAndRenderSync(src, scope)
+ return expect(html).to.equal('123breaking')
+ })
+ it('should support for with continue', function () {
+ const src = '{% for i in (1..5) %}' +
+ '{% if i == 4 %}continue{% continue %}{% endif %}{{i}}' +
+ '{% endfor %}'
+ const html = liquid.parseAndRenderSync(src, scope)
+ return expect(html).to.equal('123continue5')
+ })
+ it('should goto else for empty array', async function () {
+ const src = '{%for c in emptyArray%}a{%else%}b{%endfor%}'
+ const html = await liquid.parseAndRenderSync(src, scope)
+ return expect(html).to.equal('b')
+ })
+ })
})
diff --git a/test/integration/builtin/tags/if.ts b/test/integration/builtin/tags/if.ts
index 8fb747d57..65cf09625 100644
--- a/test/integration/builtin/tags/if.ts
+++ b/test/integration/builtin/tags/if.ts
@@ -3,7 +3,7 @@ import { expect } from 'chai'
describe('tags/if', function () {
const liquid = new Liquid()
- const ctx = {
+ const scope = {
one: 1,
two: 2,
emptyString: '',
@@ -12,52 +12,52 @@ describe('tags/if', function () {
it('should throw if not closed', function () {
const src = '{% if false%}yes'
- return expect(liquid.parseAndRender(src, ctx))
+ return expect(liquid.parseAndRender(src, scope))
.to.be.rejectedWith(/tag {% if false%} not closed/)
})
it('should support nested', async function () {
const src = '{%if false%}{%if true%}{%else%}a{%endif%}{%endif%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('')
})
describe('single value as condition', function () {
it('should support boolean', async function () {
const src = '{% if false %}1{%elsif true%}2{%else%}3{%endif%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('2')
})
it('should treat Array truthy', async function () {
const src = '{%if emptyArray%}a{%endif%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('a')
})
it('should return true if empty string', async function () {
const src = '{%if emptyString%}a{%endif%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('a')
})
})
describe('expression as condition', function () {
it('should support ==', async function () {
const src = '{% if 2 == 3 %}yes{%else%}no{%endif%}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should support >=', async function () {
const src = '{% if 1 >= 2 and one 10', async function () {
const src = '{% if null > 10 %}yes{% else %}no{% endif %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for null <= 10', async function () {
const src = '{% if null <= 10 %}yes{% else %}no{% endif %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for null >= 10', async function () {
const src = '{% if null >= 10 %}yes{% else %}no{% endif %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for 10 < null', async function () {
const src = '{% if 10 < null %}yes{% else %}no{% endif %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for 10 > null', async function () {
const src = '{% if 10 > null %}yes{% else %}no{% endif %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for 10 <= null', async function () {
const src = '{% if 10 <= null %}yes{% else %}no{% endif %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for 10 >= null', async function () {
const src = '{% if 10 >= null %}yes{% else %}no{% endif %}'
- const html = await liquid.parseAndRender(src, ctx)
+ const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
})
+ it('should support sync', function () {
+ const src = '{%if true%}true{%else%}false{%endif%}'
+ const html = liquid.parseAndRenderSync(src, scope)
+ return expect(html).to.equal('true')
+ })
})
diff --git a/test/integration/builtin/tags/include.ts b/test/integration/builtin/tags/include.ts
index 1e1184feb..94709fafb 100644
--- a/test/integration/builtin/tags/include.ts
+++ b/test/integration/builtin/tags/include.ts
@@ -171,4 +171,39 @@ describe('tags/include', function () {
return expect(html).to.equal('Xchild with redY')
})
})
+ describe('sync support', function () {
+ it('should support quoted string', function () {
+ mock({
+ '/current.html': 'bar{% include "bar/foo.html" %}bar',
+ '/bar/foo.html': 'foo'
+ })
+ const html = liquid.renderFileSync('/current.html')
+ return expect(html).to.equal('barfoobar')
+ })
+ it('should support template string', function () {
+ mock({
+ '/current.html': 'bar{% include name" %}bar',
+ '/bar/foo.html': 'foo'
+ })
+ const html = liquid.renderFileSync('/current.html', { name: '/bar/foo.html' })
+ return expect(html).to.equal('barfoobar')
+ })
+ it('should support include: with', function () {
+ mock({
+ '/with.html': '{% include "color" with "red", shape: "rect" %}',
+ '/color.html': 'color:{{color}}, shape:{{shape}}'
+ })
+ const html = liquid.renderFileSync('with.html')
+ return expect(html).to.equal('color:red, shape:rect')
+ })
+ it('should support filename with extention', function () {
+ mock({
+ '/parent.html': 'X{% include child.html color:"red" %}Y',
+ '/child.html': 'child with {{color}}'
+ })
+ const staticLiquid = new Liquid({ dynamicPartials: false, root: '/' })
+ const html = staticLiquid.renderFileSync('parent.html')
+ return expect(html).to.equal('Xchild with redY')
+ })
+ })
})
diff --git a/test/integration/builtin/tags/layout.ts b/test/integration/builtin/tags/layout.ts
index 6f0052cf5..de6eae0eb 100644
--- a/test/integration/builtin/tags/layout.ts
+++ b/test/integration/builtin/tags/layout.ts
@@ -133,4 +133,13 @@ describe('tags/layout', function () {
return expect(html).to.equal('blackA')
})
})
+ it('should support sync', function () {
+ mock({
+ '/grand.html': 'X{%block a%}G{%endblock%}Y',
+ '/parent.html': '{%layout "grand" %}{%block a%}P{%endblock%}',
+ '/main.html': '{%layout "parent"%}{%block a%}A{%endblock%}'
+ })
+ const html = liquid.renderFileSync('/main.html')
+ return expect(html).to.equal('XAY')
+ })
})
diff --git a/test/integration/builtin/tags/raw.ts b/test/integration/builtin/tags/raw.ts
index 832840498..3d62b837c 100644
--- a/test/integration/builtin/tags/raw.ts
+++ b/test/integration/builtin/tags/raw.ts
@@ -3,20 +3,24 @@ import { expect } from 'chai'
describe('tags/raw', function () {
const liquid = new Liquid()
- it('should support raw 1', async function () {
+ it('should throw when not closed', async function () {
const p = liquid.parseAndRender('{% raw%}')
return expect(p).be.rejectedWith(/{% raw%} not closed/)
})
- it('should support raw 2', async function () {
+ it('should output filters as it is', async function () {
const src = '{% raw %}{{ 5 | plus: 6 }}{% endraw %} is equal to 11.'
const dst = '{{ 5 | plus: 6 }} is equal to 11.'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal(dst)
})
- it('should support raw 3', async function () {
+ it('should preserve blank characters', async function () {
const src = '{% raw %}\n{{ foo}} \n{% endraw %}'
const dst = '\n{{ foo}} \n'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal(dst)
})
+ it('should support sync', function () {
+ const html = liquid.parseAndRenderSync('{% raw %}{{foo}}{% endraw %}')
+ return expect(html).to.equal('{{foo}}')
+ })
})
diff --git a/test/integration/builtin/tags/tablerow.ts b/test/integration/builtin/tags/tablerow.ts
index abbe61d59..eff3538a3 100644
--- a/test/integration/builtin/tags/tablerow.ts
+++ b/test/integration/builtin/tags/tablerow.ts
@@ -107,4 +107,18 @@ describe('tags/tablerow', function () {
return expect(html).to.equal(dst)
})
})
+ describe('sync support', function () {
+ it('should support tablerow', function () {
+ const src = '{% tablerow i in (1..3)%}{{ i }}{% endtablerow %}'
+ const dst = '| 1 | 2 | 3 |
'
+ 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)
+ })
+ })
})
diff --git a/test/integration/builtin/tags/unless.ts b/test/integration/builtin/tags/unless.ts
index 4f7b68cfe..ca0b1a5d1 100644
--- a/test/integration/builtin/tags/unless.ts
+++ b/test/integration/builtin/tags/unless.ts
@@ -34,4 +34,16 @@ describe('tags/unless', function () {
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
+ describe('sync support', function () {
+ it('should render else when predicate yields true', function () {
+ const src = '{% unless 0 %}yes{%else%}no{%endunless%}'
+ const html = liquid.parseAndRenderSync(src)
+ expect(html).to.equal('no')
+ })
+ it('should render unless when predicate yields false', function () {
+ const src = '{% unless false %}yes{%else%}no{%endunless%}'
+ const html = liquid.parseAndRenderSync(src)
+ expect(html).to.equal('yes')
+ })
+ })
})
diff --git a/test/integration/drop/drop.ts b/test/integration/drop/drop.ts
index 0b5d7988a..bd81ba21b 100644
--- a/test/integration/drop/drop.ts
+++ b/test/integration/drop/drop.ts
@@ -49,6 +49,10 @@ describe('drop/drop', function () {
const html = await liquid.parseAndRender(`{{obj.name}}`, { obj: new PromiseDrop() })
expect(html).to.equal('NAME')
})
+ it('should resolve before calling filters', async function () {
+ const html = await liquid.parseAndRender(`{{obj.name | downcase}}`, { obj: new PromiseDrop() })
+ expect(html).to.equal('name')
+ })
it('should support promise returned by liquidMethodMissing', async function () {
const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new PromiseDrop() })
expect(html).to.equal('FOO')
diff --git a/test/integration/liquid/cache.ts b/test/integration/liquid/cache.ts
index 6e6356ff5..ff66238ad 100644
--- a/test/integration/liquid/cache.ts
+++ b/test/integration/liquid/cache.ts
@@ -3,36 +3,87 @@ import { Liquid } from '../../../src/liquid'
import { mock, restore } from '../../stub/mockfs'
describe('LiquidOptions#cache', function () {
- let engine: Liquid
- beforeEach(function () {
- engine = new Liquid({
- root: '/root/',
- extname: '.html'
- })
- mock({ '/root/files/foo.html': 'foo' })
- })
afterEach(restore)
- it('should be disabled by default', function () {
- return engine.renderFile('files/foo')
- .then(x => expect(x).to.equal('foo'))
- .then(() => mock({
- '/root/files/foo.html': 'bar'
- }))
- .then(() => engine.renderFile('files/foo'))
- .then(x => expect(x).to.equal('bar'))
- })
- it('should respect cache=true option', function () {
- engine = new Liquid({
- root: '/root/',
- extname: '.html',
- cache: true
+
+ describe('#renderFile', function () {
+ it('should be disabled by default', async function () {
+ const engine = new Liquid({
+ root: '/root/',
+ extname: '.html'
+ })
+ mock({ '/root/files/foo.html': 'foo' })
+ const x = await engine.renderFile('files/foo')
+ expect(x).to.equal('foo')
+ mock({ '/root/files/foo.html': 'bar' })
+ const y = await engine.renderFile('files/foo')
+ expect(y).to.equal('bar')
+ })
+ it('should respect cache=true option', async function () {
+ const engine = new Liquid({
+ root: '/root/',
+ extname: '.html',
+ cache: true
+ })
+ mock({ '/root/files/foo.html': 'foo' })
+ const x = await engine.renderFile('files/foo')
+ expect(x).to.equal('foo')
+ mock({ '/root/files/foo.html': 'bar' })
+ const y = await engine.renderFile('files/foo')
+ expect(y).to.equal('foo')
+ })
+ it('should not cache not exist file', async function () {
+ const engine = new Liquid({
+ root: '/root/',
+ extname: '.html',
+ cache: true
+ })
+ try { await engine.renderFile('foo') } catch (err) {}
+
+ mock({ '/root/foo.html': 'foo' })
+ const y = await engine.renderFile('foo')
+ expect(y).to.equal('foo')
+ })
+ })
+
+ describe('#renderFileSync', function () {
+ it('should be disabled by default', function () {
+ const engine = new Liquid({
+ root: '/root/',
+ extname: '.html'
+ })
+ mock({ '/root/foo.html': 'foo' })
+ const x = engine.renderFileSync('foo')
+ expect(x).to.equal('foo')
+
+ mock({ '/root/foo.html': 'bar' })
+ const y = engine.renderFileSync('foo')
+ expect(y).to.equal('bar')
+ })
+ it('should respect cache=true option', function () {
+ const engine = new Liquid({
+ root: '/root/',
+ extname: '.html',
+ cache: true
+ })
+ mock({ '/root/foo.html': 'foo' })
+ const x = engine.renderFileSync('foo')
+ expect(x).to.equal('foo')
+
+ mock({ '/root/foo.html': 'bar' })
+ const y = engine.renderFileSync('foo')
+ expect(y).to.equal('foo')
+ })
+ it('should not cache not exist file', async function () {
+ const engine = new Liquid({
+ root: '/root/',
+ extname: '.html',
+ cache: true
+ })
+ try { engine.renderFileSync('foo') } catch (err) {}
+
+ mock({ '/root/foo.html': 'foo' })
+ const y = await engine.renderFile('foo')
+ expect(y).to.equal('foo')
})
- return engine.renderFile('files/foo')
- .then(x => expect(x).to.equal('foo'))
- .then(() => mock({
- '/root/files/foo.html': 'bar'
- }))
- .then(() => engine.renderFile('files/foo'))
- .then(x => expect(x).to.equal('foo'))
})
})
diff --git a/test/integration/liquid/liquid.ts b/test/integration/liquid/liquid.ts
index 6bd48fbd4..e79e29b4e 100644
--- a/test/integration/liquid/liquid.ts
+++ b/test/integration/liquid/liquid.ts
@@ -53,14 +53,14 @@ describe('Liquid', function () {
})
after(restore)
it('should render single template', function (done) {
- render.call({ root: '.' }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
+ render.call({ root: '/root' }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
if (err) return done(err)
expect(result).to.equal('foo')
done()
})
})
it('should render single template with Array-typed root', function (done) {
- render.call({ root: ['.'] }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
+ render.call({ root: ['/root'] }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
if (err) return done(err)
expect(result).to.equal('foo')
done()
diff --git a/test/integration/liquid/register-filters.ts b/test/integration/liquid/register-filters.ts
index a7b6be01a..447cff36b 100644
--- a/test/integration/liquid/register-filters.ts
+++ b/test/integration/liquid/register-filters.ts
@@ -1,17 +1,24 @@
-import { test, liquid } from '../../stub/render'
+import { expect } from 'chai'
+import { Liquid } from '../../../src/liquid'
describe('liquid#registerFilter()', function () {
+ const liquid = new Liquid()
+
describe('object arguments', function () {
- liquid.registerFilter('obj_test', function () {
- return JSON.stringify(arguments)
+ liquid.registerFilter('obj_test', function (...args) {
+ return JSON.stringify(args)
+ })
+ it('should support object', async () => {
+ const src = `{{ "a" | obj_test: k1: "v1", k2: foo }}`,
+ const dst = '["a",["k1","v1"],["k2","bar"]]'
+ const html = await liquid.parseAndRender(src, { foo: 'bar' })
+ return expect(html).to.equal(dst)
+ })
+ it('should support mixed object', async () => {
+ const src = `{{ "a" | obj_test: "something", k1: "v1", k2: foo }}`,
+ const dst = '["a","something",["k1","v1"],["k2","bar"]]'
+ const html = await liquid.parseAndRender(src, { foo: 'bar' })
+ return expect(html).to.equal(dst)
})
- it('should support object', () => test(
- `{{ "a" | obj_test: k1: "v1", k2: foo }}`,
- '{"0":"a","1":["k1","v1"],"2":["k2","bar"]}'
- ))
- it('should support mixed object', () => test(
- `{{ "a" | obj_test: "something", k1: "v1", k2: foo }}`,
- '{"0":"a","1":"something","2":["k1","v1"],"3":["k2","bar"]}'
- ))
})
})
diff --git a/test/integration/util/error.ts b/test/integration/util/error.ts
index 0a554df3b..039cfce32 100644
--- a/test/integration/util/error.ts
+++ b/test/integration/util/error.ts
@@ -1,4 +1,5 @@
import { expect } from 'chai'
+import { RenderError } from '../../../src/util/error'
import { Liquid } from '../../../src/liquid'
import * as path from 'path'
import { mock, restore } from '../../stub/mockfs'
@@ -264,4 +265,46 @@ describe('error', function () {
expect(err.stack).to.match(/at .*:\d+:\d+\)/)
})
})
+ describe('sync support', function () {
+ let engine
+ beforeEach(function () {
+ engine = new Liquid({
+ root: '/'
+ })
+ engine.registerTag('throwingTag', {
+ render: function () {
+ throw new Error('intended render error')
+ }
+ })
+ })
+ it('should throw RenderError when tag throws', function () {
+ const src = '{%throwingTag%}'
+ expect(() => engine.parseAndRenderSync(src))
+ .to.throw(RenderError, /intended render error/)
+ })
+ it('should contain original error info for {% include %}', function () {
+ const origin = ['1st', '2nd', '3rd', 'X{%throwingTag%} Y', '5th', '6th', '7th']
+ mock({
+ '/throwing-tag.html': origin.join('\n')
+ })
+ const html = '{%include "throwing-tag.html"%}'
+ const message = [
+ ' 2| 2nd',
+ ' 3| 3rd',
+ '>> 4| X{%throwingTag%} Y',
+ ' 5| 5th',
+ ' 6| 6th',
+ ' 7| 7th',
+ 'RenderError'
+ ]
+ try {
+ engine.parseAndRenderSync(html)
+ throw new Error('expected throw')
+ } catch (err) {
+ expect(err.message).to.equal(`intended render error, file:${path.resolve('/throwing-tag.html')}, line:4, col:2`)
+ expect(err.stack).to.contain(message.join('\n'))
+ expect(err.name).to.equal('RenderError')
+ }
+ })
+ })
})
diff --git a/test/stub/mockfs.ts b/test/stub/mockfs.ts
index e29d7d50c..2a75bd8ef 100644
--- a/test/stub/mockfs.ts
+++ b/test/stub/mockfs.ts
@@ -8,8 +8,7 @@ interface FileDescriptor {
}
let files: { [path: string]: FileDescriptor } = {}
-const readFile = fs.readFile
-const exists = fs.exists
+const { readFile, exists, readFileSync, existsSync } = fs
export function mock (options: { [path: string]: (string | FileDescriptor) }) {
forOwn(options, (val, key) => {
@@ -18,19 +17,26 @@ export function mock (options: { [path: string]: (string | FileDescriptor) }) {
: val as FileDescriptor
})
fs.readFile = async function (path) {
+ return fs.readFileSync(path)
+ }
+ fs.readFileSync = function (path) {
const file = files[path]
if (file === undefined) throw new Error('ENOENT')
if (file.mode === '0000') throw new Error('EACCES')
return file.content
}
-
fs.exists = async function (path: string) {
+ return fs.existsSync(path)
+ }
+ fs.existsSync = function (path: string) {
return !!files[path]
}
}
export function restore () {
files = {}
+ fs.readFileSync = readFileSync
+ fs.existsSync = existsSync
fs.readFile = readFile
fs.exists = exists
}
diff --git a/test/stub/render.ts b/test/stub/render.ts
index b9f81a672..10d7d72d2 100644
--- a/test/stub/render.ts
+++ b/test/stub/render.ts
@@ -8,7 +8,7 @@ export const ctx = {
foo: 'bar',
arr: [-2, 'a'],
obj: { foo: 'bar' },
- func: function () {},
+ func: function () {}, // eslint-disable-line
posts: [{ category: 'foo' }, { category: 'bar' }],
products: [
{ title: 'Vacuum', type: 'living room' },
diff --git a/test/unit/fs/browser.ts b/test/unit/fs/browser.ts
index 4e383e2a5..6b65856c0 100644
--- a/test/unit/fs/browser.ts
+++ b/test/unit/fs/browser.ts
@@ -59,6 +59,12 @@ describe('fs/browser', function () {
})
})
+ describe('#existsSync()', () => {
+ it('should always return true', function () {
+ expect(fs.existsSync('/foo/bar')).to.equal(true)
+ })
+ })
+
describe('#readFile()', () => {
let server: sinon.SinonFakeServer
beforeEach(() => {
@@ -87,4 +93,29 @@ describe('fs/browser', function () {
return result
})
})
+
+ describe('#readFileSync()', () => {
+ let server: sinon.SinonFakeServer
+ beforeEach(() => {
+ server = sinon.fakeServer.create()
+ server.autoRespond = true
+ server.respondWith(
+ 'GET', 'https://example.com/views/hello.html',
+ [200, { 'Content-Type': 'text/plain' }, 'hello {{name}}']
+ );
+ (global as any).XMLHttpRequest = sinon.useFakeXMLHttpRequest()
+ })
+ afterEach(() => {
+ server.restore()
+ delete (global as any).XMLHttpRequest
+ })
+ it('should get corresponding text', function () {
+ const html = fs.readFileSync('https://example.com/views/hello.html')
+ return expect(html).to.equal('hello {{name}}')
+ })
+ it('should throw 404', () => {
+ return expect(() => fs.readFileSync('https://example.com/not/exist.html'))
+ .to.throw('Not Found')
+ })
+ })
})
diff --git a/test/unit/fs/node.ts b/test/unit/fs/node.ts
index d0feaa8dd..574686402 100644
--- a/test/unit/fs/node.ts
+++ b/test/unit/fs/node.ts
@@ -6,7 +6,7 @@ import * as chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised)
describe('fs', function () {
- describe('#resolve()', function () {
+ describe('.resolve()', function () {
it('should resolve based on root', async function () {
const filepath = fs.resolve('/foo', 'bar.html', '.liquid')
const expected = path.resolve('/foo/bar.html')
@@ -18,23 +18,40 @@ describe('fs', function () {
return expect(filepath).to.equal(expected)
})
})
- describe('#exists', () => {
+ describe('.existsSync', () => {
+ it('should resolve as false if not exists', () => {
+ expect(fs.existsSync('/foo/bar')).to.be.false
+ })
+ it('should resolve as true if exists', () => {
+ expect(fs.existsSync(__filename)).to.be.true
+ })
+ })
+ describe('.exists', () => {
it('should resolve as false if not exists', async () => {
const result = await fs.exists('/foo/bar')
- return expect(result).to.be.false
+ expect(result).to.be.false
})
it('should resolve as true if exists', async () => {
const result = await fs.exists(__filename)
- return expect(result).to.be.true
+ expect(result).to.be.true
})
})
- describe('#readFile', function () {
+ describe('.readFileSync', function () {
+ it('should throw when not exist', function () {
+ return expect(() => fs.readFileSync('/foo/bar')).to.throw('ENOENT')
+ })
+ it('should read content if exists', function () {
+ const content = fs.readFileSync(__filename)
+ expect(content).to.contain('should read content if exists')
+ })
+ })
+ describe('.readFile', function () {
it('should throw when not exist', function () {
return expect(fs.readFile('/foo/bar')).to.rejectedWith('ENOENT')
})
it('should read content if exists', async function () {
const content = await fs.readFile(__filename)
- return expect(content).to.contain('should read content if exists')
+ expect(content).to.contain('should read content if exists')
})
})
})
diff --git a/test/unit/render/expression.ts b/test/unit/render/expression.ts
index adbbf6dc0..2293b3f43 100644
--- a/test/unit/render/expression.ts
+++ b/test/unit/render/expression.ts
@@ -17,51 +17,56 @@ describe('Expression', function () {
})
it('should throw when context not defined', async function () {
- return expect(() => new Expression().value()).to.throw(/context not defined/)
+ return expect(new Expression().value()).to.be.rejectedWith(/context not defined/)
})
it('should eval simple expression', async function () {
- expect(new Expression('1 < 2').value(ctx)).to.equal(true)
- expect(new Expression('2 <= 2').value(ctx)).to.equal(true)
- expect(new Expression('one <= two').value(ctx)).to.equal(true)
- expect(new Expression('x contains "x"').value(ctx)).to.equal(false)
- expect(new Expression('x contains "X"').value(ctx)).to.equal(true)
- expect(new Expression('1 contains "x"').value(ctx)).to.equal(false)
- expect(new Expression('y contains "x"').value(ctx)).to.equal(false)
- expect(new Expression('z contains "x"').value(ctx)).to.equal(false)
- expect(new Expression('(1..5) contains 3').value(ctx)).to.equal(true)
- expect(new Expression('(1..5) contains 6').value(ctx)).to.equal(false)
- expect(new Expression('"<=" == "<="').value(ctx)).to.equal(true)
+ expect(await new Expression('1 < 2').value(ctx)).to.equal(true)
+ expect(await new Expression('1 < 2').value(ctx)).to.equal(true)
+ expect(await new Expression('2 <= 2').value(ctx)).to.equal(true)
+ expect(await new Expression('one <= two').value(ctx)).to.equal(true)
+ expect(await new Expression('x contains "x"').value(ctx)).to.equal(false)
+ expect(await new Expression('x contains "X"').value(ctx)).to.equal(true)
+ expect(await new Expression('1 contains "x"').value(ctx)).to.equal(false)
+ expect(await new Expression('y contains "x"').value(ctx)).to.equal(false)
+ expect(await new Expression('z contains "x"').value(ctx)).to.equal(false)
+ expect(await new Expression('(1..5) contains 3').value(ctx)).to.equal(true)
+ expect(await new Expression('(1..5) contains 6').value(ctx)).to.equal(false)
+ expect(await new Expression('"<=" == "<="').value(ctx)).to.equal(true)
})
describe('complex expression', function () {
it('should support value or value', async function () {
- expect(new Expression('false or true').value(ctx)).to.equal(true)
+ expect(await new Expression('false or true').value(ctx)).to.equal(true)
})
it('should support < and contains', async function () {
- expect(new Expression('1 < 2 and x contains "x"').value(ctx)).to.equal(false)
+ expect(await new Expression('1 < 2 and x contains "x"').value(ctx)).to.equal(false)
})
it('should support < or contains', async function () {
- expect(new Expression('1 < 2 or x contains "x"').value(ctx)).to.equal(true)
+ expect(await new Expression('1 < 2 or x contains "x"').value(ctx)).to.equal(true)
})
it('should support value and !=', async function () {
- expect(new Expression('empty and empty != ""').value(ctx)).to.equal(false)
+ expect(await new Expression('empty and empty != ""').value(ctx)).to.equal(false)
})
it('should recognize quoted value', async function () {
- expect(new Expression('">"').value(ctx)).to.equal('>')
+ expect(await new Expression('">"').value(ctx)).to.equal('>')
})
- it('should evaluate from right to left', function () {
- expect(new Expression('true or false and false').value(ctx)).to.equal(true)
- expect(new Expression('true and false and false or true').value(ctx)).to.equal(false)
+ it('should evaluate from right to left', async function () {
+ expect(await new Expression('true or false and false').value(ctx)).to.equal(true)
+ expect(await new Expression('true and false and false or true').value(ctx)).to.equal(false)
})
- it('should recognize property access', function () {
+ it('should recognize property access', async function () {
const ctx = new Context({ obj: { foo: true } })
- expect(new Expression('obj["foo"] and true').value(ctx)).to.equal(true)
+ expect(await new Expression('obj["foo"] and true').value(ctx)).to.equal(true)
})
})
it('should eval range expression', async function () {
- expect(new Expression('(2..4)').value(ctx)).to.deep.equal([2, 3, 4])
- expect(new Expression('(two..4)').value(ctx)).to.deep.equal([2, 3, 4])
+ expect(await new Expression('(2..4)').value(ctx)).to.deep.equal([2, 3, 4])
+ expect(await new Expression('(two..4)').value(ctx)).to.deep.equal([2, 3, 4])
+ })
+
+ it('should support sync', function () {
+ expect(new Expression('empty and empty != ""').valueSync(ctx)).to.equal(false)
})
})
diff --git a/test/unit/render/value.ts b/test/unit/render/value.ts
index 96c9034b9..5fa7c4e92 100644
--- a/test/unit/render/value.ts
+++ b/test/unit/render/value.ts
@@ -5,20 +5,20 @@ import { expect } from 'chai'
describe('Value', function () {
it('should eval number variable', async function () {
const ctx = new Context({ one: 1 })
- expect(new Value('one').value(ctx)).to.equal(1)
+ expect(new Value('one').valueSync(ctx)).to.equal(1)
})
it('question mark should be valid variable name', async function () {
const ctx = new Context({ 'has_value?': true })
- expect(new Value('has_value?').value(ctx)).to.equal(true)
+ expect(new Value('has_value?').valueSync(ctx)).to.equal(true)
})
it('should eval string variable', async function () {
const ctx = new Context({ x: 'XXX' })
- expect(new Value('x').value(ctx)).to.equal('XXX')
+ expect(new Value('x').valueSync(ctx)).to.equal('XXX')
})
it('should eval null literal', async function () {
- expect(new Value('null').value({})).to.be.null
+ expect(new Value('null').valueSync({})).to.be.null
})
it('should eval nil literal', async function () {
- expect(new Value('nil').value({})).to.be.null
+ expect(new Value('nil').valueSync({})).to.be.null
})
})
diff --git a/test/unit/template/filter/filter.ts b/test/unit/template/filter/filter.ts
index 0c1f6fb50..a57b8fb79 100644
--- a/test/unit/template/filter/filter.ts
+++ b/test/unit/template/filter/filter.ts
@@ -60,4 +60,14 @@ describe('filter', function () {
new Filter('/', [], false)
}).to.not.throw()
})
+
+ it('should support sync', function () {
+ Filter.register('add', (a, b) => a + b)
+ expect(new Filter('add', ['2'], false).renderSync(3, ctx)).to.equal(5)
+ })
+
+ it('should support key value pairs', function () {
+ Filter.register('add', (a, b) => b[0] + ':' + (a + b[1]))
+ expect(new Filter('add', [['num', '2']], false).renderSync(3, ctx)).to.equal('num:5')
+ })
})
diff --git a/test/unit/template/hash.ts b/test/unit/template/hash.ts
new file mode 100644
index 000000000..94b15e6f4
--- /dev/null
+++ b/test/unit/template/hash.ts
@@ -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)
+ })
+})
diff --git a/test/unit/template/tag.ts b/test/unit/template/tag.ts
index 2925c4365..211150ee2 100644
--- a/test/unit/template/tag.ts
+++ b/test/unit/template/tag.ts
@@ -10,7 +10,7 @@ chai.use(sinonChai)
const expect = chai.expect
const liquid = new Liquid()
-describe('tag', function () {
+describe('Tag', function () {
let ctx: Context
const emitter = { write: (html: string) => (emitter.html += html), html: '' }
before(function () {