mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
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:
@@ -4,6 +4,7 @@ const { join } = require('path')
|
||||
|
||||
const liquid = new Liquid({
|
||||
root: join(__dirname, '../templates'),
|
||||
cache: true,
|
||||
extname: '.liquid'
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Contribution Guideline
|
||||
|
||||
## Star on Github 👉 [][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
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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
|
||||
|
||||
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 机器人依赖这个标识自动发布。
|
||||
|
||||
**向后兼容**:请考虑向后(之前的旧的版本)兼容。LiquidJS 被用于很多层的软件,包括底层库、编译器、站点生成器、 Web 服务器。对多数最终用户来说,驱动或请求整个系统做一次主版本升级是很难办到的。
|
||||
|
||||
## 成为赞助者!
|
||||
|
||||
LiquidJS 是开源的、免费的,并且 **没有** 商业支持,也 **没有** 任何广告。如果你喜欢 LiquidJS 或你的公司在使用 LiquidJS,请考虑通过 [Open Collective][oc] 或 [Patreon][pt] 赞助,作为感谢你的名字和头像(或 Logo)会展示在这里和 [Github README][liquidjs]。
|
||||
|
||||
+1
-6
@@ -13,13 +13,8 @@
|
||||
"scripts": {
|
||||
"lint": "eslint \"**/*.ts\" .",
|
||||
"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\"",
|
||||
"benchmark:prepare": "cd benchmark && npm ci",
|
||||
"benchmark": "cd benchmark && npm start",
|
||||
"benchmark:engines": "cd benchmark && npm run engines",
|
||||
"benchmark": "cd benchmark && npm ci && npm start",
|
||||
"build": "npm run build:dist && npm run build:docs",
|
||||
"build:dist": "rm -rf dist && rollup -c rollup.config.ts && ls -lh dist",
|
||||
"build:docs": "bin/build-docs.sh"
|
||||
|
||||
@@ -37,7 +37,7 @@ export default {
|
||||
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
||||
const scope = yield hash.render(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)
|
||||
yield renderer.renderTemplates(templates, ctx, emitter)
|
||||
ctx.pop()
|
||||
|
||||
@@ -26,7 +26,7 @@ export default {
|
||||
: evalToken(this.file, ctx))
|
||||
: file.getText()
|
||||
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
|
||||
ctx.setRegister('blockMode', BlockMode.STORE)
|
||||
|
||||
@@ -64,12 +64,12 @@ export default {
|
||||
scope['forloop'] = new ForloopDrop(collection.length)
|
||||
for (const item of collection) {
|
||||
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)
|
||||
scope.forloop.next()
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
+57
-55
@@ -24,6 +24,7 @@ export class Liquid {
|
||||
public parser: Parser
|
||||
public filters: FilterMap
|
||||
public tags: TagMap
|
||||
private parseFileImpl: (file: string, sync?: boolean) => Iterator<Template[]>
|
||||
|
||||
public constructor (opts: LiquidOptions = {}) {
|
||||
this.options = applyDefault(normalize(opts))
|
||||
@@ -31,6 +32,7 @@ export class Liquid {
|
||||
this.renderer = new Render()
|
||||
this.filters = new FilterMap(this.options.strictFilters, this)
|
||||
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(builtinFilters, (handler: FilterImplOptions, name: string) => this.registerFilter(snakeCase(name), handler))
|
||||
@@ -41,64 +43,61 @@ export class Liquid {
|
||||
return this.parser.parse(tokens)
|
||||
}
|
||||
|
||||
public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> {
|
||||
const options = { ...this.options, ...normalize(opts) }
|
||||
const ctx = new Context(scope, options, sync)
|
||||
const emitter = new Emitter(options.keepOutputType)
|
||||
public _render (tpl: Template[], scope?: object, sync?: boolean): IterableIterator<any> {
|
||||
const ctx = new Context(scope, this.options, sync)
|
||||
const emitter = new Emitter(this.options.keepOutputType)
|
||||
return this.renderer.renderTemplates(tpl, ctx, emitter)
|
||||
}
|
||||
public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise<any> {
|
||||
return toPromise(this._render(tpl, scope, opts, false))
|
||||
public async render (tpl: Template[], scope?: object): Promise<any> {
|
||||
return toPromise(this._render(tpl, scope, false))
|
||||
}
|
||||
public renderSync (tpl: Template[], scope?: object, opts?: LiquidOptions): any {
|
||||
return toValue(this._render(tpl, scope, opts, true))
|
||||
public renderSync (tpl: Template[], scope?: object): any {
|
||||
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)
|
||||
return this._render(tpl, scope, opts, sync)
|
||||
return this._render(tpl, scope, sync)
|
||||
}
|
||||
public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<any> {
|
||||
return toPromise(this._parseAndRender(html, scope, opts, false))
|
||||
public async parseAndRender (html: string, scope?: object): Promise<any> {
|
||||
return toPromise(this._parseAndRender(html, scope, false))
|
||||
}
|
||||
public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): any {
|
||||
return toValue(this._parseAndRender(html, scope, opts, true))
|
||||
public parseAndRenderSync (html: string, scope?: object): any {
|
||||
return toValue(this._parseAndRender(html, scope, true))
|
||||
}
|
||||
|
||||
public * _parseFile (file: string, opts?: LiquidOptions, sync?: boolean) {
|
||||
const options = { ...this.options, ...normalize(opts) }
|
||||
const paths = options.root.map(root => options.fs.resolve(root, file, options.extname))
|
||||
if (options.fs.fallback !== undefined) {
|
||||
const filepath = options.fs.fallback(file)
|
||||
if (filepath !== undefined) paths.push(filepath)
|
||||
}
|
||||
|
||||
for (const filepath of paths) {
|
||||
const { cache } = options
|
||||
if (cache) {
|
||||
const tpls = yield cache.read(filepath)
|
||||
private * _parseFileCached (file: string, sync?: boolean) {
|
||||
const cache = this.options.cache!
|
||||
let tpls = yield cache.read(file)
|
||||
if (tpls) return tpls
|
||||
|
||||
tpls = yield this._parseFile(file, sync)
|
||||
cache.write(file, tpls)
|
||||
return tpls
|
||||
}
|
||||
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)
|
||||
if (cache) cache.write(filepath, tpl)
|
||||
private * _parseFile (file: string, sync?: boolean) {
|
||||
const { fs, root } = this.options
|
||||
|
||||
for (const filepath of this.lookupFiles(file, this.options)) {
|
||||
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
|
||||
}
|
||||
throw this.lookupError(file, options.root)
|
||||
throw this.lookupError(file, root)
|
||||
}
|
||||
public async parseFile (file: string, opts?: LiquidOptions): Promise<Template[]> {
|
||||
return toPromise(this._parseFile(file, opts, false))
|
||||
public async parseFile (file: string): Promise<Template[]> {
|
||||
return toPromise(this.parseFileImpl(file, false))
|
||||
}
|
||||
public parseFileSync (file: string, opts?: LiquidOptions): Template[] {
|
||||
return toValue(this._parseFile(file, opts, true))
|
||||
public parseFileSync (file: string): Template[] {
|
||||
return toValue(this.parseFileImpl(file, true))
|
||||
}
|
||||
public async renderFile (file: string, ctx?: object, opts?: LiquidOptions) {
|
||||
const templates = await this.parseFile(file, opts)
|
||||
return this.render(templates, ctx, opts)
|
||||
public async renderFile (file: string, ctx?: object) {
|
||||
const templates = await this.parseFile(file)
|
||||
return this.render(templates, ctx)
|
||||
}
|
||||
public renderFileSync (file: string, ctx?: object, opts?: LiquidOptions) {
|
||||
const templates = this.parseFileSync(file, opts)
|
||||
return this.renderSync(templates, ctx, opts)
|
||||
public renderFileSync (file: string, ctx?: object) {
|
||||
const templates = this.parseFileSync(file)
|
||||
return this.renderSync(templates, ctx)
|
||||
}
|
||||
|
||||
public _evalValue (str: string, ctx: Context): IterableIterator<any> {
|
||||
@@ -123,9 +122,25 @@ export class Liquid {
|
||||
}
|
||||
public express () {
|
||||
const self = this // eslint-disable-line
|
||||
let firstCall = true
|
||||
|
||||
return function (this: any, filePath: string, ctx: object, callback: (err: Error | null, rendered: string) => void) {
|
||||
const opts = { root: [...normalizeStringArray(this.root), ...self.options.root] }
|
||||
self.renderFile(filePath, ctx, opts).then(html => callback(null, html) as any, callback as any)
|
||||
if (firstCall) {
|
||||
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'
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,15 +76,12 @@ describe('tags/render', function () {
|
||||
})
|
||||
|
||||
it('should be able to access globals', async function () {
|
||||
liquid = new Liquid({ root: '/', extname: '.html', globals: { name: 'Harttle' } })
|
||||
mock({
|
||||
'/hash.html': 'InParent: {{name}} {% render "user.html" %}',
|
||||
'/user.html': 'InChild: {{name}}'
|
||||
})
|
||||
const html = await liquid.renderFile('hash.html', {
|
||||
name: 'harttle'
|
||||
}, {
|
||||
globals: { name: 'Harttle' }
|
||||
})
|
||||
const html = await liquid.renderFile('hash', { name: 'harttle' })
|
||||
expect(html).to.equal('InParent: harttle InChild: Harttle')
|
||||
})
|
||||
|
||||
|
||||
@@ -132,34 +132,6 @@ describe('LiquidOptions#cache', function () {
|
||||
const y = await engine.renderFile('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 () {
|
||||
@@ -202,33 +174,5 @@ describe('LiquidOptions#cache', function () {
|
||||
const y = await engine.renderFile('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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('Liquid', function () {
|
||||
root: ['/root/'],
|
||||
extname: '.html'
|
||||
})
|
||||
const tpls = await engine.getTemplate('mocha')
|
||||
const tpls = await engine.parseFileSync('mocha')
|
||||
expect(tpls.length).to.gte(1)
|
||||
expect(tpls[0].token.getText()).to.contain('module.exports')
|
||||
})
|
||||
@@ -126,7 +126,7 @@ describe('Liquid', function () {
|
||||
root: ['/boo', '/root/'],
|
||||
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\/"/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,53 +20,61 @@ describe('LiquidOptions#strict*', function () {
|
||||
})
|
||||
it('should throw when strictVariables true', function () {
|
||||
const tpl = engine.parse('before{{notdefined}}after')
|
||||
const opts = {
|
||||
engine = new Liquid({
|
||||
root: '/root/',
|
||||
extname: '.html',
|
||||
strictVariables: true
|
||||
}
|
||||
return expect(engine.render(tpl, ctx, opts)).to
|
||||
})
|
||||
return expect(engine.render(tpl, ctx)).to
|
||||
.be.rejectedWith(/undefined variable: notdefined/)
|
||||
})
|
||||
it('should pass strictVariables to render by parseAndRender', function () {
|
||||
const html = 'before{{notdefined}}after'
|
||||
const opts = {
|
||||
engine = new Liquid({
|
||||
root: '/root/',
|
||||
extname: '.html',
|
||||
strictVariables: true
|
||||
}
|
||||
return expect(engine.parseAndRender(html, ctx, opts)).to
|
||||
})
|
||||
return expect(engine.parseAndRender(html, ctx)).to
|
||||
.be.rejectedWith(/undefined variable: notdefined/)
|
||||
})
|
||||
describe('with strictVariables and lenientIf', function () {
|
||||
const strictLenientOpts = {
|
||||
beforeEach(() => {
|
||||
engine = new Liquid({
|
||||
root: '/root/',
|
||||
extname: '.html',
|
||||
strictVariables: true,
|
||||
lenientIf: true
|
||||
}
|
||||
})
|
||||
})
|
||||
it('should not throw in `if` with a single variable', async function () {
|
||||
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')
|
||||
})
|
||||
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 html = await engine.render(tpl, { 'defined3': 'bla' }, strictLenientOpts)
|
||||
const html = await engine.render(tpl, { 'defined3': 'bla' })
|
||||
return expect(html).to.equal('bla')
|
||||
})
|
||||
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 html = await engine.render(tpl, ctx, strictLenientOpts)
|
||||
const html = await engine.render(tpl, ctx)
|
||||
return expect(html).to.equal('beforeXafter')
|
||||
})
|
||||
it('should still throw with an undefined variable in a compound `if` expression', function () {
|
||||
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/)
|
||||
})
|
||||
it('should allow an undefined variable when before the `default` filter', async function () {
|
||||
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')
|
||||
})
|
||||
it('should not allow undefined variable even if `lenientIf` set', async function () {
|
||||
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
@@ -4,8 +4,8 @@ import { expect } from 'chai'
|
||||
|
||||
export const liquid = new Liquid()
|
||||
|
||||
export function render (src: string, ctx?: object, opts?: LiquidOptions) {
|
||||
return liquid.parseAndRender(src, ctx, opts)
|
||||
export function render (src: string, ctx?: object) {
|
||||
return liquid.parseAndRender(src, ctx)
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -210,8 +210,8 @@ describe('util/strftime', function () {
|
||||
expect(t(now, '%#P')).to.equal('PM')
|
||||
})
|
||||
it('should support : flag', () => {
|
||||
const date = new Date('2016-01-04T13:15:23.000Z')
|
||||
date.getTimezoneOffset = () => -480 // suppose we're in +8:00
|
||||
const date = new Date('2016-01-04T13:15:23.000Z');
|
||||
(timezoneOffset as any) = -480 // suppose we're in +8:00
|
||||
expect(t(date, '%:z')).to.equal('+08:00')
|
||||
expect(t(date, '%z')).to.equal('+0800')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user