From 6801552fe6829770cbbfdda051731c8b466ed9ec Mon Sep 17 00:00:00 2001 From: Harttle Date: Sat, 11 Dec 2021 12:35:36 +0800 Subject: [PATCH] feat: customize globals & strictVariables when calling render, see #432 --- src/builtin/tags/render.ts | 2 +- src/context/context.ts | 18 +++++++++---- src/liquid-options.ts | 15 +++++++++++ src/liquid.ts | 42 +++++++++++++++---------------- src/types.ts | 1 + test/integration/liquid/liquid.ts | 9 +++++++ 6 files changed, 60 insertions(+), 27 deletions(-) diff --git a/src/builtin/tags/render.ts b/src/builtin/tags/render.ts index 06ab13df24..28bcbb5b41 100644 --- a/src/builtin/tags/render.ts +++ b/src/builtin/tags/render.ts @@ -51,7 +51,7 @@ export default { const filepath = yield this.renderFilePath(this['file'], ctx, liquid) assert(filepath, () => `illegal filename "${filepath}"`) - const childCtx = new Context({}, ctx.opts, ctx.sync) + const childCtx = new Context({}, ctx.opts, { sync: ctx.sync, globals: ctx.globals, strictVariables: ctx.strictVariables }) const scope = childCtx.bottom() __assign(scope, yield hash.render(ctx)) if (this['with']) { diff --git a/src/context/context.ts b/src/context/context.ts index bbce304a7b..fb14df5c42 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -1,6 +1,6 @@ import { Drop } from '../drop/drop' import { __assign } from 'tslib' -import { NormalizedFullOptions, defaultOptions } from '../liquid-options' +import { NormalizedFullOptions, defaultOptions, RenderOptions } from '../liquid-options' import { Scope } from './scope' import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore' import { InternalUndefinedVariableError } from '../util/error' @@ -23,12 +23,20 @@ export class Context { */ public globals: Scope public sync: boolean + /** + * The normalized liquid options object + */ public opts: NormalizedFullOptions - public constructor (env: object = {}, opts: NormalizedFullOptions = defaultOptions, sync = false) { - this.sync = sync + /** + * Throw when accessing undefined variable? + */ + public strictVariables: boolean; + public constructor (env: object = {}, opts: NormalizedFullOptions = defaultOptions, renderOptions: RenderOptions = {}) { + this.sync = !!renderOptions.sync this.opts = opts - this.globals = opts.globals + this.globals = renderOptions.globals ?? opts.globals this.environments = env + this.strictVariables = renderOptions.strictVariables ?? this.opts.strictVariables } public getRegister (key: string) { return (this.registers[key] = this.registers[key] || {}) @@ -54,7 +62,7 @@ export class Context { if (typeof paths === 'string') paths = paths.split('.') return paths.reduce((scope, path) => { scope = readProperty(scope, path) - if (isNil(scope) && this.opts.strictVariables) { + if (isNil(scope) && this.strictVariables) { throw new InternalUndefinedVariableError(path) } return scope diff --git a/src/liquid-options.ts b/src/liquid-options.ts index dd2cc377fe..0603e3e23d 100644 --- a/src/liquid-options.ts +++ b/src/liquid-options.ts @@ -65,6 +65,21 @@ export interface LiquidOptions { orderedFilterParameters?: boolean; } +export interface RenderOptions { + /** + * This call is sync or async? It's used by Liquid internal methods, you'll not need this. + */ + sync?: boolean; + /** + * Same as `globals` on LiquidOptions, but only for current render() call + */ + globals?: object; + /** + * Same as `strictVariables` on LiquidOptions, but only for current render() call + */ + strictVariables?: boolean; +} + interface NormalizedOptions extends LiquidOptions { root?: string[]; partials?: string[]; diff --git a/src/liquid.ts b/src/liquid.ts index db6aaeb3e1..f08bbe043b 100644 --- a/src/liquid.ts +++ b/src/liquid.ts @@ -10,7 +10,7 @@ import builtinTags from './builtin/tags' import * as builtinFilters from './builtin/filters' import { TagMap } from './template/tag/tag-map' import { FilterMap } from './template/filter/filter-map' -import { LiquidOptions, normalizeDirectoryList, NormalizedFullOptions, normalize } from './liquid-options' +import { LiquidOptions, normalizeDirectoryList, NormalizedFullOptions, normalize, RenderOptions } from './liquid-options' import { FilterImplOptions } from './template/filter/filter-impl-options' import { toPromise, toValue } from './util/async' @@ -39,30 +39,30 @@ export class Liquid { return this.parser.parse(html, filepath) } - public _render (tpl: Template[], scope?: object, sync?: boolean): IterableIterator { - const ctx = new Context(scope, this.options, sync) + public _render (tpl: Template[], scope: object | undefined, renderOptions: RenderOptions): IterableIterator { + const ctx = new Context(scope, this.options, renderOptions) return this.renderer.renderTemplates(tpl, ctx) } - public async render (tpl: Template[], scope?: object): Promise { - return toPromise(this._render(tpl, scope, false)) + public async render (tpl: Template[], scope?: object, renderOptions?: RenderOptions): Promise { + return toPromise(this._render(tpl, scope, { ...renderOptions, sync: false })) } - public renderSync (tpl: Template[], scope?: object): any { - return toValue(this._render(tpl, scope, true)) + public renderSync (tpl: Template[], scope?: object, renderOptions?: RenderOptions): any { + return toValue(this._render(tpl, scope, { ...renderOptions, sync: true })) } - public renderToNodeStream (tpl: Template[], scope?: object): NodeJS.ReadableStream { - const ctx = new Context(scope, this.options) + public renderToNodeStream (tpl: Template[], scope?: object, renderOptions: RenderOptions = {}): NodeJS.ReadableStream { + const ctx = new Context(scope, this.options, renderOptions) return this.renderer.renderTemplatesToNodeStream(tpl, ctx) } - public _parseAndRender (html: string, scope?: object, sync?: boolean): IterableIterator { + public _parseAndRender (html: string, scope: object | undefined, renderOptions: RenderOptions): IterableIterator { const tpl = this.parse(html) - return this._render(tpl, scope, sync) + return this._render(tpl, scope, renderOptions) } - public async parseAndRender (html: string, scope?: object): Promise { - return toPromise(this._parseAndRender(html, scope, false)) + public async parseAndRender (html: string, scope?: object, renderOptions?: RenderOptions): Promise { + return toPromise(this._parseAndRender(html, scope, { ...renderOptions, sync: false })) } - public parseAndRenderSync (html: string, scope?: object): any { - return toValue(this._parseAndRender(html, scope, true)) + public parseAndRenderSync (html: string, scope?: object, renderOptions?: RenderOptions): any { + return toValue(this._parseAndRender(html, scope, { ...renderOptions, sync: true })) } public _parsePartialFile (file: string, sync?: boolean, currentFile?: string) { @@ -77,17 +77,17 @@ export class Liquid { public parseFileSync (file: string): Template[] { return toValue(this.parser.parseFile(file, true)) } - public async renderFile (file: string, ctx?: object) { + public async renderFile (file: string, ctx?: object, renderOptions?: RenderOptions) { const templates = await this.parseFile(file) - return this.render(templates, ctx) + return this.render(templates, ctx, renderOptions) } - public renderFileSync (file: string, ctx?: object) { + public renderFileSync (file: string, ctx?: object, renderOptions?: RenderOptions) { const templates = this.parseFileSync(file) - return this.renderSync(templates, ctx) + return this.renderSync(templates, ctx, renderOptions) } - public async renderFileToNodeStream (file: string, scope?: object) { + public async renderFileToNodeStream (file: string, scope?: object, renderOptions?: RenderOptions) { const templates = await this.parseFile(file) - return this.renderToNodeStream(templates, scope) + return this.renderToNodeStream(templates, scope, renderOptions) } public _evalValue (str: string, ctx: Context): IterableIterator { diff --git a/src/types.ts b/src/types.ts index dbe2b1deb8..6c07b9cc3a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import * as TypeGuards from './util/type-guards' export { TypeGuards } export { ParseError, TokenizationError, AssertionError } from './util/error' diff --git a/test/integration/liquid/liquid.ts b/test/integration/liquid/liquid.ts index 5a5418d421..05c95b8836 100644 --- a/test/integration/liquid/liquid.ts +++ b/test/integration/liquid/liquid.ts @@ -43,6 +43,15 @@ describe('Liquid', function () { const html = await engine.parseAndRender(src, {}) return expect(html).to.equal('12') }) + it('should support `globals` render option', async function () { + const src = '{{ foo }}' + const html = await engine.parseAndRender(src, {}, { globals: { foo: 'FOO' } }) + return expect(html).to.equal('FOO') + }) + it('should support `strictVariables` render option', function () { + const src = '{{ foo }}' + return expect(engine.parseAndRender(src, {}, { strictVariables: true })).rejectedWith(/undefined variable/) + }) }) describe('#express()', function () { const liquid = new Liquid({ root: '/root' })