diff --git a/src/builtin/filters/string.ts b/src/builtin/filters/string.ts index 1acd264b7f..24ee445c9f 100644 --- a/src/builtin/filters/string.ts +++ b/src/builtin/filters/string.ts @@ -1,5 +1,3 @@ -import { FilterImpl } from '../../template/filter/filter-impl' - export default { 'append': (v: string, arg: string) => v + arg, 'prepend': (v: string, arg: string) => arg + v, @@ -27,4 +25,4 @@ export default { if (arr.length >= l) ret += o return ret } -} as {[key: string]: FilterImpl} +} \ No newline at end of file diff --git a/src/builtin/tags/assign.ts b/src/builtin/tags/assign.ts index 0587dac4f9..441b45fab7 100644 --- a/src/builtin/tags/assign.ts +++ b/src/builtin/tags/assign.ts @@ -14,6 +14,6 @@ export default { this.value = match[2] }, render: async function (ctx: Context) { - ctx.scopes[0][this.key] = await this.liquid.evalValue(this.value, ctx) + ctx.front()[this.key] = await this.liquid.evalValue(this.value, ctx) } } as ITagImplOptions diff --git a/src/builtin/tags/block.ts b/src/builtin/tags/block.ts index d01f4d5358..891c58ce66 100644 --- a/src/builtin/tags/block.ts +++ b/src/builtin/tags/block.ts @@ -20,13 +20,14 @@ export default { stream.start() }, render: async function (ctx: Context) { - const childDefined = ctx.blocks[this.block] + const blocks = ctx.getRegister('blocks') + const childDefined = blocks[this.block] const html = childDefined !== undefined ? childDefined : await this.liquid.renderer.renderTemplates(this.tpls, ctx) - if (ctx.blockMode === BlockMode.STORE) { - ctx.blocks[this.block] = html + if (ctx.getRegister('blockMode', BlockMode.OUTPUT) === BlockMode.STORE) { + blocks[this.block] = html return '' } return html diff --git a/src/builtin/tags/capture.ts b/src/builtin/tags/capture.ts index 993ff1d0aa..e7c49803e1 100644 --- a/src/builtin/tags/capture.ts +++ b/src/builtin/tags/capture.ts @@ -25,6 +25,6 @@ export default { }, render: async function (ctx: Context) { const html = await this.liquid.renderer.renderTemplates(this.templates, ctx) - ctx.scopes[0][this.variable] = html + ctx.front()[this.variable] = html } } as ITagImplOptions diff --git a/src/builtin/tags/cycle.ts b/src/builtin/tags/cycle.ts index aec848aaa9..1095a57583 100644 --- a/src/builtin/tags/cycle.ts +++ b/src/builtin/tags/cycle.ts @@ -27,7 +27,7 @@ export default { render: async function (ctx: Context) { const group = await evalValue(this.group, ctx) const fingerprint = `cycle:${group}:` + this.candidates.join(',') - const groups = ctx.groups + const groups = ctx.getRegister('cycle') let idx = groups[fingerprint] if (idx === undefined) { diff --git a/src/builtin/tags/include.ts b/src/builtin/tags/include.ts index a7936e7539..0413b833bd 100644 --- a/src/builtin/tags/include.ts +++ b/src/builtin/tags/include.ts @@ -41,20 +41,20 @@ export default { } assert(filepath, `cannot include with empty filename`) - const originBlocks = ctx.blocks - const originBlockMode = ctx.blockMode + const originBlocks = ctx.getRegister('blocks') + const originBlockMode = ctx.getRegister('blockMode') - ctx.blocks = {} - ctx.blockMode = BlockMode.OUTPUT + ctx.setRegister('blocks', {}) + ctx.setRegister('blockMode', BlockMode.OUTPUT) if (this.with) { hash[filepath] = await evalValue(this.with, ctx) } const templates = await this.liquid.getTemplate(filepath, ctx.opts) ctx.push(hash) const html = await this.liquid.renderer.renderTemplates(templates, ctx) - ctx.pop(hash) - ctx.blocks = originBlocks - ctx.blockMode = originBlockMode + ctx.pop() + ctx.setRegister('blocks', originBlocks) + ctx.setRegister('blockMode', originBlockMode) return html } } diff --git a/src/builtin/tags/layout.ts b/src/builtin/tags/layout.ts index 5533759485..c5b40b406e 100644 --- a/src/builtin/tags/layout.ts +++ b/src/builtin/tags/layout.ts @@ -31,16 +31,17 @@ export default { assert(layout, `cannot apply layout with empty filename`) // render the remaining tokens immediately - ctx.blockMode = BlockMode.STORE + ctx.setRegister('blockMode', BlockMode.STORE) + const blocks = ctx.getRegister('blocks') const html = await this.liquid.renderer.renderTemplates(this.tpls, ctx) - if (ctx.blocks[''] === undefined) { - ctx.blocks[''] = html + if (blocks[''] === undefined) { + blocks[''] = html } const templates = await this.liquid.getTemplate(layout, ctx.opts) ctx.push(hash) - ctx.blockMode = BlockMode.OUTPUT + ctx.setRegister('blockMode', BlockMode.OUTPUT) const partial = await this.liquid.renderer.renderTemplates(templates, ctx) - ctx.pop(hash) + ctx.pop() return partial } } as ITagImplOptions diff --git a/src/builtin/tags/tablerow.ts b/src/builtin/tags/tablerow.ts index 23452a11f0..d7562b8728 100644 --- a/src/builtin/tags/tablerow.ts +++ b/src/builtin/tags/tablerow.ts @@ -59,7 +59,7 @@ export default { html += '' } if (collection.length) html += '' - ctx.pop(scope) + ctx.pop() return html } } as ITagImplOptions diff --git a/src/context/context.ts b/src/context/context.ts index eb481085e9..651c5a1f15 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -3,20 +3,23 @@ import { Drop } from '../drop/drop' import { __assign } from 'tslib' import assert from '../util/assert' import { NormalizedFullOptions, applyDefault } from '../liquid-options' -import BlockMode from './block-mode' import { Scope } from './scope' export default class Context { opts: NormalizedFullOptions - scopes: Array = [{}] environments: Scope - blocks: object = {} - groups: {[key: string]: number} = {} - blockMode: BlockMode = BlockMode.OUTPUT + private scopes: Array = [{}] + private registers = {} constructor (ctx: object = {}, opts?: NormalizedFullOptions) { this.opts = applyDefault(opts) this.environments = ctx } + getRegister(key: string, defaultValue = {}) { + return this.registers[key] = this.registers[key] || defaultValue + } + setRegister(key: string, value: any) { + return this.registers[key] = value + } getAll () { return [this.environments, ...this.scopes] .reduce((ctx, val) => __assign(ctx, val), {}) @@ -25,7 +28,7 @@ export default class Context { const paths = await this.parseProp(path) let ctx = this.findScope(paths[0]) || this.environments for (const path of paths) { - ctx = this.readProperty(ctx, path) + ctx = readProperty(ctx, path) if (_.isNil(ctx) && this.opts.strictVariables) { throw new TypeError(`undefined variable: ${path}`) } @@ -35,15 +38,11 @@ export default class Context { push (ctx: object) { return this.scopes.push(ctx) } - pop (ctx?: object): object | undefined { - if (!arguments.length) { - return this.scopes.pop() - } - const i = this.scopes.findIndex(scope => scope === ctx) - if (i === -1) { - throw new TypeError('scope not found, cannot pop') - } - return this.scopes.splice(i, 1)[0] + pop () { + return this.scopes.pop() + } + front () { + return this.scopes[0] } private findScope (key: string) { for (let i = this.scopes.length - 1; i >= 0; i--) { @@ -54,16 +53,6 @@ export default class Context { } return null } - private readProperty (obj: Scope, key: string) { - if (_.isNil(obj)) return obj - obj = _.toLiquid(obj) - if (obj instanceof Drop) { - if (_.isFunction(obj[key])) return obj[key]() - if (obj.hasOwnProperty(key)) return obj[key] - return obj.liquidMethodMissing(key) - } - return key === 'size' ? readSize(obj) : obj[key] - } /* * Parse property access sequence from access string @@ -124,6 +113,17 @@ export default class Context { } } +function readProperty (obj: Scope, key: string) { + if (_.isNil(obj)) return obj + obj = _.toLiquid(obj) + if (obj instanceof Drop) { + if (_.isFunction(obj[key])) return obj[key]() + if (obj.hasOwnProperty(key)) return obj[key] + return obj.liquidMethodMissing(key) + } + return key === 'size' ? readSize(obj) : obj[key] +} + function readSize (obj: Scope) { if (!_.isNil(obj['size'])) return obj['size'] if (_.isArray(obj) || _.isString(obj)) return obj.length diff --git a/src/liquid.ts b/src/liquid.ts index 06c03f64e4..1dfcfe3036 100644 --- a/src/liquid.ts +++ b/src/liquid.ts @@ -14,7 +14,7 @@ import { isTruthy, isFalsy, evalExp, evalValue } from './render/syntax' import builtinTags from './builtin/tags' import builtinFilters from './builtin/filters' import { LiquidOptions, NormalizedFullOptions, applyDefault, normalize } from './liquid-options' -import { FilterImpl } from './template/filter/filter-impl' +import { FilterImplOptions } from './template/filter/filter-impl-options' export default class Liquid { public options: NormalizedFullOptions @@ -72,7 +72,7 @@ export default class Liquid { evalValue (str: string, ctx: Context) { return new Value(str, this.options.strictFilters).value(ctx) } - registerFilter (name: string, filter: FilterImpl) { + registerFilter (name: string, filter: FilterImplOptions) { return Filter.register(name, filter) } registerTag (name: string, tag: ITagImplOptions) { diff --git a/src/template/filter/filter-impl-options.ts b/src/template/filter/filter-impl-options.ts new file mode 100644 index 0000000000..3690c0ea90 --- /dev/null +++ b/src/template/filter/filter-impl-options.ts @@ -0,0 +1,3 @@ +import { FilterImpl } from "./filter-impl"; + +export type FilterImplOptions = (this: FilterImpl, value: any, ...args: any[]) => any \ No newline at end of file diff --git a/src/template/filter/filter-impl.ts b/src/template/filter/filter-impl.ts index 1e8a3a5663..257075ff3f 100644 --- a/src/template/filter/filter-impl.ts +++ b/src/template/filter/filter-impl.ts @@ -1 +1,5 @@ -export type FilterImpl = (value: any, ...args: any[]) => any +import Context from "../../context/context"; + +export interface FilterImpl { + context: Context +} \ No newline at end of file diff --git a/src/template/filter/filter.ts b/src/template/filter/filter.ts index e08fbb8944..70a60a6089 100644 --- a/src/template/filter/filter.ts +++ b/src/template/filter/filter.ts @@ -1,15 +1,15 @@ import { evalValue } from '../../render/syntax' import Context from '../../context/context' import { isArray } from '../../util/underscore' -import { FilterImpl } from './filter-impl' +import { FilterImplOptions } from './filter-impl-options' export type FilterArgs = Array export class Filter { name: string - impl: FilterImpl + impl: FilterImplOptions args: FilterArgs - private static impls: {[key: string]: FilterImpl} = {} + private static impls: {[key: string]: FilterImplOptions} = {} constructor (name: string, args: FilterArgs, strictFilters: boolean) { const impl = Filter.impls[name] @@ -19,15 +19,15 @@ export class Filter { this.impl = impl || (x => x) this.args = args } - async render (value: any, ctx: Context) { + async render (value: any, context: Context) { const argv: any[] = [] for (const arg of this.args) { - if (isArray(arg)) argv.push([arg[0], await evalValue(arg[1], ctx)]) - else argv.push(await evalValue(arg, ctx)) + if (isArray(arg)) argv.push([arg[0], await evalValue(arg[1], context)]) + else argv.push(await evalValue(arg, context)) } - return this.impl.apply(null, [value, ...argv]) + return this.impl.apply({ context }, [value, ...argv]) } - static register (name: string, filter: FilterImpl) { + static register (name: string, filter: FilterImplOptions) { Filter.impls[name] = filter } static clear () { diff --git a/test/unit/context/context.ts b/test/unit/context/context.ts index 1f21773a8e..1468ee6034 100644 --- a/test/unit/context/context.ts +++ b/test/unit/context/context.ts @@ -137,15 +137,15 @@ describe('scope', function () { return expect(ctx.get('notdefined')).to.be.rejectedWith(/undefined variable: notdefined/) }) it('should throw when deep variable not exist', async function () { - ctx.scopes.push({ 'foo': 'FOO' }) + ctx.push({ foo: 'FOO' }) return expect(ctx.get('foo.bar.not.defined')).to.be.rejectedWith(/undefined variable: bar/) }) it('should throw when itself not defined', async function () { - ctx.scopes.push({ 'foo': 'FOO' }) + ctx.push({ foo: 'FOO' }) return expect(ctx.get('foo.BAR')).to.be.rejectedWith(/undefined variable: BAR/) }) it('should find variable in parent scope', async function () { - ctx.scopes.push({ 'foo': 'foo' }) + ctx.push({ 'foo': 'foo' }) ctx.push({ 'bar': 'bar' }) @@ -161,7 +161,7 @@ describe('scope', function () { describe('.push()', function () { it('should push scope', async function () { - ctx.scopes.push({ 'bar': 'bar' }) + ctx.push({ 'bar': 'bar' }) ctx.push({ foo: 'foo' }) @@ -169,7 +169,7 @@ describe('scope', function () { expect(await ctx.get('bar')).to.equal('bar') }) it('should hide deep properties by push', async function () { - ctx.scopes.push({ 'bar': { bar: 'bar' } }) + ctx.push({ bar: { bar: 'bar' } }) ctx.push({ bar: { foo: 'foo' } }) expect(await ctx.get('bar.foo')).to.equal('foo') expect(await ctx.get('bar.bar')).to.equal(undefined) @@ -184,25 +184,4 @@ describe('scope', function () { expect(await ctx.get('foo')).to.equal('zoo') }) }) - it('should pop specified scope', async function () { - const scope1 = { - foo: 'foo' - } - const scope2 = { - bar: 'bar' - } - ctx.push(scope1) - ctx.push(scope2) - expect(await ctx.get('foo')).to.equal('foo') - expect(await ctx.get('bar')).to.equal('bar') - ctx.pop(scope1) - expect(await ctx.get('foo')).to.equal('zoo') - expect(await ctx.get('bar')).to.equal('bar') - }) - it('should throw when specified scope not found', function () { - const scope1 = { - foo: 'foo' - } - expect(() => ctx.pop(scope1)).to.throw('scope not found, cannot pop') - }) }) diff --git a/test/unit/template/filter/filter.ts b/test/unit/template/filter/filter.ts index db05f5e3b6..94948f2e29 100644 --- a/test/unit/template/filter/filter.ts +++ b/test/unit/template/filter/filter.ts @@ -22,12 +22,18 @@ describe('filter', function () { expect(await new Filter('undefined', [], false).render('foo', ctx)).to.equal('foo') }) - it('should call filter impl with corrct arguments', async function () { + it('should call filter impl with correct arguments', async function () { const spy = sinon.spy() Filter.register('foo', spy) await new Filter('foo', ['33'], false).render('foo', ctx) expect(spy).to.have.been.calledWith('foo', 33) }) + it('should call filter impl with correct this arg', async function () { + const spy = sinon.spy() + Filter.register('foo', spy) + await new Filter('foo', ['33'], false).render('foo', ctx) + expect(spy).to.have.been.calledOn(sinon.match.has('context', ctx)) + }) it('should render a simple filter', async function () { Filter.register('upcase', x => x.toUpperCase()) expect(await new Filter('upcase', [], false).render('foo', ctx)).to.equal('FOO')