perf: improve performance by 4x by simplified parseFile

- previously deprecated `getTemplate()` and `getTemplateSync()` not no longer supported
- `opts` no longer support dynamic set in `parseFile()`, `renderFile()` arguments
This commit is contained in:
harttle
2021-09-27 23:04:41 +08:00
parent 6b9f872bcc
commit 24f5346084
14 changed files with 103 additions and 151 deletions
+1
View File
@@ -4,6 +4,7 @@ const { join } = require('path')
const liquid = new Liquid({ const liquid = new Liquid({
root: join(__dirname, '../templates'), root: join(__dirname, '../templates'),
cache: true,
extname: '.liquid' extname: '.liquid'
}) })
@@ -4,7 +4,7 @@ title: Contribution Guideline
## Star on Github 👉 [![harttle/liquidjs](https://img.shields.io/github/stars/harttle/liquidjs?style=flat-square)][liquidjs] ## Star on Github 👉 [![harttle/liquidjs](https://img.shields.io/github/stars/harttle/liquidjs?style=flat-square)][liquidjs]
Staring us is the most important and easiest way to support us: boost its rank and expose it to more people, which in turn makes it better. Staring LiquidJS is the most important and easiest way to support us: boost its rank and expose it to more people, which in turn makes it better.
## Show Me Your Code ## Show Me Your Code
@@ -22,6 +22,8 @@ npm test
**Commit Message**: Please align to [the Angular Commit Message Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits), especially note the [type identifier](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type), on which semantic-release bot depends. **Commit Message**: Please align to [the Angular Commit Message Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits), especially note the [type identifier](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type), on which semantic-release bot depends.
**Backward-Compatibility**: please be backward-compatible. LiquidJS is used by multiple layers of softwares, including underlying libraries, compilers, site generators and Web servers. It's not easy to do a major upgrade for most of them.
## Financial Support ## Financial Support
LiquidJS is Open Source and Free and **without** capitalists support and **without** any ADs. To help it live and thrive, consider contribute on [Open Collective][oc] or [Patreon][pt]. To acknowledge your contribution, your name and avatar will be listed here and on [Github README][liquidjs]. LiquidJS is Open Source and Free and **without** capitalists support and **without** any ADs. To help it live and thrive, consider contribute on [Open Collective][oc] or [Patreon][pt]. To acknowledge your contribution, your name and avatar will be listed here and on [Github README][liquidjs].
@@ -22,6 +22,8 @@ npm test
**提交消息**:请遵守 [Angular 提交消息规范](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits),尤其注意 [type 标识](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type)semantic-release 机器人依赖这个标识自动发布。 **提交消息**:请遵守 [Angular 提交消息规范](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits),尤其注意 [type 标识](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type)semantic-release 机器人依赖这个标识自动发布。
**向后兼容**:请考虑向后(之前的旧的版本)兼容。LiquidJS 被用于很多层的软件,包括底层库、编译器、站点生成器、 Web 服务器。对多数最终用户来说,驱动或请求整个系统做一次主版本升级是很难办到的。
## 成为赞助者! ## 成为赞助者!
LiquidJS 是开源的、免费的,并且 **没有** 商业支持,也 **没有** 任何广告。如果你喜欢 LiquidJS 或你的公司在使用 LiquidJS,请考虑通过 [Open Collective][oc] 或 [Patreon][pt] 赞助,作为感谢你的名字和头像(或 Logo)会展示在这里和 [Github README][liquidjs]。 LiquidJS 是开源的、免费的,并且 **没有** 商业支持,也 **没有** 任何广告。如果你喜欢 LiquidJS 或你的公司在使用 LiquidJS,请考虑通过 [Open Collective][oc] 或 [Patreon][pt] 赞助,作为感谢你的名字和头像(或 Logo)会展示在这里和 [Github README][liquidjs]。
+1 -6
View File
@@ -13,13 +13,8 @@
"scripts": { "scripts": {
"lint": "eslint \"**/*.ts\" .", "lint": "eslint \"**/*.ts\" .",
"check": "npm test && npm run lint", "check": "npm test && npm run lint",
"unit": "mocha \"test/unit/**/*.ts\"",
"integration": "mocha \"test/integration/**/*.ts\"",
"e2e": "mocha \"test/e2e/**/*.ts\"",
"test": "nyc mocha \"test/**/*.ts\"", "test": "nyc mocha \"test/**/*.ts\"",
"benchmark:prepare": "cd benchmark && npm ci", "benchmark": "cd benchmark && npm ci && npm start",
"benchmark": "cd benchmark && npm start",
"benchmark:engines": "cd benchmark && npm run engines",
"build": "npm run build:dist && npm run build:docs", "build": "npm run build:dist && npm run build:docs",
"build:dist": "rm -rf dist && rollup -c rollup.config.ts && ls -lh dist", "build:dist": "rm -rf dist && rollup -c rollup.config.ts && ls -lh dist",
"build:docs": "bin/build-docs.sh" "build:docs": "bin/build-docs.sh"
+1 -1
View File
@@ -37,7 +37,7 @@ export default {
ctx.setRegister('blockMode', BlockMode.OUTPUT) ctx.setRegister('blockMode', BlockMode.OUTPUT)
const scope = yield hash.render(ctx) const scope = yield hash.render(ctx)
if (withVar) scope[filepath] = evalToken(withVar, ctx) if (withVar) scope[filepath] = evalToken(withVar, ctx)
const templates = yield liquid._parseFile(filepath, ctx.opts, ctx.sync) const templates = yield liquid.parseFileImpl(filepath, ctx.sync)
ctx.push(scope) ctx.push(scope)
yield renderer.renderTemplates(templates, ctx, emitter) yield renderer.renderTemplates(templates, ctx, emitter)
ctx.pop() ctx.pop()
+1 -1
View File
@@ -26,7 +26,7 @@ export default {
: evalToken(this.file, ctx)) : evalToken(this.file, ctx))
: file.getText() : file.getText()
assert(filepath, () => `file "${file.getText()}"("${filepath}") not available`) assert(filepath, () => `file "${file.getText()}"("${filepath}") not available`)
const templates = yield liquid._parseFile(filepath, ctx.opts, ctx.sync) const templates = yield liquid.parseFileImpl(filepath, ctx.sync)
// render remaining contents and store rendered results // render remaining contents and store rendered results
ctx.setRegister('blockMode', BlockMode.STORE) ctx.setRegister('blockMode', BlockMode.STORE)
+2 -2
View File
@@ -64,12 +64,12 @@ export default {
scope['forloop'] = new ForloopDrop(collection.length) scope['forloop'] = new ForloopDrop(collection.length)
for (const item of collection) { for (const item of collection) {
scope[alias] = item scope[alias] = item
const templates = yield liquid._parseFile(filepath, childCtx.opts, childCtx.sync) const templates = yield liquid.parseFileImpl(filepath, childCtx.sync)
yield renderer.renderTemplates(templates, childCtx, emitter) yield renderer.renderTemplates(templates, childCtx, emitter)
scope.forloop.next() scope.forloop.next()
} }
} else { } else {
const templates = yield liquid._parseFile(filepath, childCtx.opts, childCtx.sync) const templates = yield liquid.parseFileImpl(filepath, childCtx.sync)
yield renderer.renderTemplates(templates, childCtx, emitter) yield renderer.renderTemplates(templates, childCtx, emitter)
} }
} }
+58 -56
View File
@@ -24,6 +24,7 @@ export class Liquid {
public parser: Parser public parser: Parser
public filters: FilterMap public filters: FilterMap
public tags: TagMap public tags: TagMap
private parseFileImpl: (file: string, sync?: boolean) => Iterator<Template[]>
public constructor (opts: LiquidOptions = {}) { public constructor (opts: LiquidOptions = {}) {
this.options = applyDefault(normalize(opts)) this.options = applyDefault(normalize(opts))
@@ -31,6 +32,7 @@ export class Liquid {
this.renderer = new Render() this.renderer = new Render()
this.filters = new FilterMap(this.options.strictFilters, this) this.filters = new FilterMap(this.options.strictFilters, this)
this.tags = new TagMap() this.tags = new TagMap()
this.parseFileImpl = this.options.cache ? this._parseFileCached : this._parseFile
forOwn(builtinTags, (conf: TagImplOptions, name: string) => this.registerTag(snakeCase(name), conf)) forOwn(builtinTags, (conf: TagImplOptions, name: string) => this.registerTag(snakeCase(name), conf))
forOwn(builtinFilters, (handler: FilterImplOptions, name: string) => this.registerFilter(snakeCase(name), handler)) forOwn(builtinFilters, (handler: FilterImplOptions, name: string) => this.registerFilter(snakeCase(name), handler))
@@ -41,64 +43,61 @@ export class Liquid {
return this.parser.parse(tokens) return this.parser.parse(tokens)
} }
public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> { public _render (tpl: Template[], scope?: object, sync?: boolean): IterableIterator<any> {
const options = { ...this.options, ...normalize(opts) } const ctx = new Context(scope, this.options, sync)
const ctx = new Context(scope, options, sync) const emitter = new Emitter(this.options.keepOutputType)
const emitter = new Emitter(options.keepOutputType)
return this.renderer.renderTemplates(tpl, ctx, emitter) return this.renderer.renderTemplates(tpl, ctx, emitter)
} }
public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise<any> { public async render (tpl: Template[], scope?: object): Promise<any> {
return toPromise(this._render(tpl, scope, opts, false)) return toPromise(this._render(tpl, scope, false))
} }
public renderSync (tpl: Template[], scope?: object, opts?: LiquidOptions): any { public renderSync (tpl: Template[], scope?: object): any {
return toValue(this._render(tpl, scope, opts, true)) return toValue(this._render(tpl, scope, true))
} }
public _parseAndRender (html: string, scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> { public _parseAndRender (html: string, scope?: object, sync?: boolean): IterableIterator<any> {
const tpl = this.parse(html) const tpl = this.parse(html)
return this._render(tpl, scope, opts, sync) return this._render(tpl, scope, sync)
} }
public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<any> { public async parseAndRender (html: string, scope?: object): Promise<any> {
return toPromise(this._parseAndRender(html, scope, opts, false)) return toPromise(this._parseAndRender(html, scope, false))
} }
public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): any { public parseAndRenderSync (html: string, scope?: object): any {
return toValue(this._parseAndRender(html, scope, opts, true)) return toValue(this._parseAndRender(html, scope, true))
} }
public * _parseFile (file: string, opts?: LiquidOptions, sync?: boolean) { private * _parseFileCached (file: string, sync?: boolean) {
const options = { ...this.options, ...normalize(opts) } const cache = this.options.cache!
const paths = options.root.map(root => options.fs.resolve(root, file, options.extname)) let tpls = yield cache.read(file)
if (options.fs.fallback !== undefined) { if (tpls) return tpls
const filepath = options.fs.fallback(file)
if (filepath !== undefined) paths.push(filepath)
}
for (const filepath of paths) { tpls = yield this._parseFile(file, sync)
const { cache } = options cache.write(file, tpls)
if (cache) { return tpls
const tpls = yield cache.read(filepath) }
if (tpls) return tpls private * _parseFile (file: string, sync?: boolean) {
} const { fs, root } = this.options
if (!(sync ? options.fs.existsSync(filepath) : yield options.fs.exists(filepath))) continue
const tpl = this.parse(sync ? options.fs.readFileSync(filepath) : yield options.fs.readFile(filepath), filepath) for (const filepath of this.lookupFiles(file, this.options)) {
if (cache) cache.write(filepath, tpl) if (!(sync ? fs.existsSync(filepath) : yield fs.exists(filepath))) continue
const tpl = this.parse(sync ? fs.readFileSync(filepath) : yield fs.readFile(filepath), filepath)
return tpl return tpl
} }
throw this.lookupError(file, options.root) throw this.lookupError(file, root)
} }
public async parseFile (file: string, opts?: LiquidOptions): Promise<Template[]> { public async parseFile (file: string): Promise<Template[]> {
return toPromise(this._parseFile(file, opts, false)) return toPromise(this.parseFileImpl(file, false))
} }
public parseFileSync (file: string, opts?: LiquidOptions): Template[] { public parseFileSync (file: string): Template[] {
return toValue(this._parseFile(file, opts, true)) return toValue(this.parseFileImpl(file, true))
} }
public async renderFile (file: string, ctx?: object, opts?: LiquidOptions) { public async renderFile (file: string, ctx?: object) {
const templates = await this.parseFile(file, opts) const templates = await this.parseFile(file)
return this.render(templates, ctx, opts) return this.render(templates, ctx)
} }
public renderFileSync (file: string, ctx?: object, opts?: LiquidOptions) { public renderFileSync (file: string, ctx?: object) {
const templates = this.parseFileSync(file, opts) const templates = this.parseFileSync(file)
return this.renderSync(templates, ctx, opts) return this.renderSync(templates, ctx)
} }
public _evalValue (str: string, ctx: Context): IterableIterator<any> { public _evalValue (str: string, ctx: Context): IterableIterator<any> {
@@ -123,9 +122,25 @@ export class Liquid {
} }
public express () { public express () {
const self = this // eslint-disable-line const self = this // eslint-disable-line
let firstCall = true
return function (this: any, filePath: string, ctx: object, callback: (err: Error | null, rendered: string) => void) { return function (this: any, filePath: string, ctx: object, callback: (err: Error | null, rendered: string) => void) {
const opts = { root: [...normalizeStringArray(this.root), ...self.options.root] } if (firstCall) {
self.renderFile(filePath, ctx, opts).then(html => callback(null, html) as any, callback as any) firstCall = false
self.options.root.unshift(...normalizeStringArray(this.root))
}
self.renderFile(filePath, ctx).then(html => callback(null, html) as any, callback as any)
}
}
private * lookupFiles (file: string, options: NormalizedFullOptions) {
const { root, fs, extname } = options
for (const dir of root) {
yield fs.resolve(dir, file, extname)
}
if (fs.fallback !== undefined) {
const filepath = fs.fallback(file)
if (filepath !== undefined) yield filepath
} }
} }
@@ -135,17 +150,4 @@ export class Liquid {
err.code = 'ENOENT' err.code = 'ENOENT'
return err return err
} }
/**
* @deprecated use parseFile instead
*/
public async getTemplate (file: string, opts?: LiquidOptions): Promise<Template[]> {
return this.parseFile(file, opts)
}
/**
* @deprecated use parseFileSync instead
*/
public getTemplateSync (file: string, opts?: LiquidOptions): Template[] {
return this.parseFileSync(file, opts)
}
} }
+2 -5
View File
@@ -76,15 +76,12 @@ describe('tags/render', function () {
}) })
it('should be able to access globals', async function () { it('should be able to access globals', async function () {
liquid = new Liquid({ root: '/', extname: '.html', globals: { name: 'Harttle' } })
mock({ mock({
'/hash.html': 'InParent: {{name}} {% render "user.html" %}', '/hash.html': 'InParent: {{name}} {% render "user.html" %}',
'/user.html': 'InChild: {{name}}' '/user.html': 'InChild: {{name}}'
}) })
const html = await liquid.renderFile('hash.html', { const html = await liquid.renderFile('hash', { name: 'harttle' })
name: 'harttle'
}, {
globals: { name: 'Harttle' }
})
expect(html).to.equal('InParent: harttle InChild: Harttle') expect(html).to.equal('InParent: harttle InChild: Harttle')
}) })
-56
View File
@@ -132,34 +132,6 @@ describe('LiquidOptions#cache', function () {
const y = await engine.renderFile('foo') const y = await engine.renderFile('foo')
expect(y).to.equal('foo') expect(y).to.equal('foo')
}) })
it('should respect passed in cache=false 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')
const z = await engine.renderFile('files/foo', undefined, { cache: false })
expect(z).to.equal('bar')
})
it('should use cache when passing in other options', 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', undefined, { greedy: true })
expect(y).to.equal('foo')
})
}) })
describe('#renderFileSync', function () { describe('#renderFileSync', function () {
@@ -202,33 +174,5 @@ describe('LiquidOptions#cache', function () {
const y = await engine.renderFile('foo') const y = await engine.renderFile('foo')
expect(y).to.equal('foo') expect(y).to.equal('foo')
}) })
it('should respect passed in cache=false option', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
mock({ '/root/files/foo.html': 'foo' })
const x = engine.renderFileSync('files/foo')
expect(x).to.equal('foo')
mock({ '/root/files/foo.html': 'bar' })
const y = engine.renderFileSync('files/foo')
expect(y).to.equal('foo')
const z = engine.renderFileSync('files/foo', undefined, { cache: false })
expect(z).to.equal('bar')
})
it('should use cache when passing in other options', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
mock({ '/root/files/foo.html': 'foo' })
const x = engine.renderFileSync('files/foo')
expect(x).to.equal('foo')
mock({ '/root/files/foo.html': 'bar' })
const y = engine.renderFileSync('files/foo', undefined, { greedy: true })
expect(y).to.equal('foo')
})
}) })
}) })
+2 -2
View File
@@ -91,7 +91,7 @@ describe('Liquid', function () {
root: ['/root/'], root: ['/root/'],
extname: '.html' extname: '.html'
}) })
const tpls = await engine.getTemplate('mocha') const tpls = await engine.parseFileSync('mocha')
expect(tpls.length).to.gte(1) expect(tpls.length).to.gte(1)
expect(tpls[0].token.getText()).to.contain('module.exports') expect(tpls[0].token.getText()).to.contain('module.exports')
}) })
@@ -126,7 +126,7 @@ describe('Liquid', function () {
root: ['/boo', '/root/'], root: ['/boo', '/root/'],
extname: '.html' extname: '.html'
}) })
return expect(() => engine.getTemplateSync('/not/exist.html')) return expect(() => engine.parseFileSync('/not/exist.html'))
.to.throw(/Failed to lookup "\/not\/exist.html" in "\/boo,\/root\/"/) .to.throw(/Failed to lookup "\/not\/exist.html" in "\/boo,\/root\/"/)
}) })
}) })
+24 -16
View File
@@ -20,53 +20,61 @@ describe('LiquidOptions#strict*', function () {
}) })
it('should throw when strictVariables true', function () { it('should throw when strictVariables true', function () {
const tpl = engine.parse('before{{notdefined}}after') const tpl = engine.parse('before{{notdefined}}after')
const opts = { engine = new Liquid({
root: '/root/',
extname: '.html',
strictVariables: true strictVariables: true
} })
return expect(engine.render(tpl, ctx, opts)).to return expect(engine.render(tpl, ctx)).to
.be.rejectedWith(/undefined variable: notdefined/) .be.rejectedWith(/undefined variable: notdefined/)
}) })
it('should pass strictVariables to render by parseAndRender', function () { it('should pass strictVariables to render by parseAndRender', function () {
const html = 'before{{notdefined}}after' const html = 'before{{notdefined}}after'
const opts = { engine = new Liquid({
root: '/root/',
extname: '.html',
strictVariables: true strictVariables: true
} })
return expect(engine.parseAndRender(html, ctx, opts)).to return expect(engine.parseAndRender(html, ctx)).to
.be.rejectedWith(/undefined variable: notdefined/) .be.rejectedWith(/undefined variable: notdefined/)
}) })
describe('with strictVariables and lenientIf', function () { describe('with strictVariables and lenientIf', function () {
const strictLenientOpts = { beforeEach(() => {
strictVariables: true, engine = new Liquid({
lenientIf: true root: '/root/',
} extname: '.html',
strictVariables: true,
lenientIf: true
})
})
it('should not throw in `if` with a single variable', async function () { it('should not throw in `if` with a single variable', async function () {
const tpl = engine.parse('before{% if notdefined %}{{notdefined}}{% endif %}after') const tpl = engine.parse('before{% if notdefined %}{{notdefined}}{% endif %}after')
const html = await engine.render(tpl, ctx, strictLenientOpts) const html = await engine.render(tpl, ctx)
return expect(html).to.equal('beforeafter') return expect(html).to.equal('beforeafter')
}) })
it('should support elsif with undefined variables', async function () { it('should support elsif with undefined variables', async function () {
const tpl = engine.parse('{% if notdefined1 %}a{% elsif notdefined2 %}b{% elsif defined3 %}{{defined3}}{% else %}d{% endif %}') const tpl = engine.parse('{% if notdefined1 %}a{% elsif notdefined2 %}b{% elsif defined3 %}{{defined3}}{% else %}d{% endif %}')
const html = await engine.render(tpl, { 'defined3': 'bla' }, strictLenientOpts) const html = await engine.render(tpl, { 'defined3': 'bla' })
return expect(html).to.equal('bla') return expect(html).to.equal('bla')
}) })
it('should not throw in `unless` with a single variable', async function () { it('should not throw in `unless` with a single variable', async function () {
const tpl = engine.parse('before{% unless notdefined %}X{% else %}{{notdefined}}{% endunless %}after') const tpl = engine.parse('before{% unless notdefined %}X{% else %}{{notdefined}}{% endunless %}after')
const html = await engine.render(tpl, ctx, strictLenientOpts) const html = await engine.render(tpl, ctx)
return expect(html).to.equal('beforeXafter') return expect(html).to.equal('beforeXafter')
}) })
it('should still throw with an undefined variable in a compound `if` expression', function () { it('should still throw with an undefined variable in a compound `if` expression', function () {
const tpl = engine.parse('{% if notdefined == 15 %}a{% endif %}') const tpl = engine.parse('{% if notdefined == 15 %}a{% endif %}')
const fhtml = engine.render(tpl, ctx, strictLenientOpts) const fhtml = engine.render(tpl, ctx)
return expect(fhtml).to.be.rejectedWith(/undefined variable: notdefined/) return expect(fhtml).to.be.rejectedWith(/undefined variable: notdefined/)
}) })
it('should allow an undefined variable when before the `default` filter', async function () { it('should allow an undefined variable when before the `default` filter', async function () {
const tpl = engine.parse('{{notdefined | default: "a" | tolower}}') const tpl = engine.parse('{{notdefined | default: "a" | tolower}}')
const html = await engine.render(tpl, ctx, strictLenientOpts) const html = await engine.render(tpl, ctx)
return expect(html).to.equal('a') return expect(html).to.equal('a')
}) })
it('should not allow undefined variable even if `lenientIf` set', async function () { it('should not allow undefined variable even if `lenientIf` set', async function () {
const tpl = engine.parse('{{notdefined | tolower}}') const tpl = engine.parse('{{notdefined | tolower}}')
return expect(() => engine.renderSync(tpl, ctx, strictLenientOpts)).to.throw('undefined variable: notdefined') return expect(() => engine.renderSync(tpl, ctx)).to.throw('undefined variable: notdefined')
}) })
}) })
}) })
+4 -3
View File
@@ -4,8 +4,8 @@ import { expect } from 'chai'
export const liquid = new Liquid() export const liquid = new Liquid()
export function render (src: string, ctx?: object, opts?: LiquidOptions) { export function render (src: string, ctx?: object) {
return liquid.parseAndRender(src, ctx, opts) return liquid.parseAndRender(src, ctx)
} }
export async function test (src: string, ctx: object | string, dst?: string, opts?: LiquidOptions) { export async function test (src: string, ctx: object | string, dst?: string, opts?: LiquidOptions) {
@@ -13,5 +13,6 @@ export async function test (src: string, ctx: object | string, dst?: string, opt
dst = ctx as string dst = ctx as string
ctx = {} ctx = {}
} }
return expect(await render(src, ctx as object, opts)).to.equal(dst) const engine = opts ? new Liquid(opts) : liquid
return expect(await engine.parseAndRender(src, ctx as object)).to.equal(dst)
} }
+2 -2
View File
@@ -210,8 +210,8 @@ describe('util/strftime', function () {
expect(t(now, '%#P')).to.equal('PM') expect(t(now, '%#P')).to.equal('PM')
}) })
it('should support : flag', () => { it('should support : flag', () => {
const date = new Date('2016-01-04T13:15:23.000Z') const date = new Date('2016-01-04T13:15:23.000Z');
date.getTimezoneOffset = () => -480 // suppose we're in +8:00 (timezoneOffset as any) = -480 // suppose we're in +8:00
expect(t(date, '%:z')).to.equal('+08:00') expect(t(date, '%:z')).to.equal('+08:00')
expect(t(date, '%z')).to.equal('+0800') expect(t(date, '%z')).to.equal('+0800')
}) })