feat: pass context to filters

This commit is contained in:
harttle
2019-04-17 10:24:55 +08:00
parent 6fb4796202
commit 00bc1efe6a
15 changed files with 77 additions and 85 deletions
+1 -3
View File
@@ -1,5 +1,3 @@
import { FilterImpl } from '../../template/filter/filter-impl'
export default { export default {
'append': (v: string, arg: string) => v + arg, 'append': (v: string, arg: string) => v + arg,
'prepend': (v: string, arg: string) => arg + v, 'prepend': (v: string, arg: string) => arg + v,
@@ -27,4 +25,4 @@ export default {
if (arr.length >= l) ret += o if (arr.length >= l) ret += o
return ret return ret
} }
} as {[key: string]: FilterImpl} }
+1 -1
View File
@@ -14,6 +14,6 @@ export default {
this.value = match[2] this.value = match[2]
}, },
render: async function (ctx: Context) { 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 } as ITagImplOptions
+4 -3
View File
@@ -20,13 +20,14 @@ export default {
stream.start() stream.start()
}, },
render: async function (ctx: Context) { 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 const html = childDefined !== undefined
? childDefined ? childDefined
: await this.liquid.renderer.renderTemplates(this.tpls, ctx) : await this.liquid.renderer.renderTemplates(this.tpls, ctx)
if (ctx.blockMode === BlockMode.STORE) { if (ctx.getRegister('blockMode', BlockMode.OUTPUT) === BlockMode.STORE) {
ctx.blocks[this.block] = html blocks[this.block] = html
return '' return ''
} }
return html return html
+1 -1
View File
@@ -25,6 +25,6 @@ export default {
}, },
render: async function (ctx: Context) { render: async function (ctx: Context) {
const html = await this.liquid.renderer.renderTemplates(this.templates, ctx) const html = await this.liquid.renderer.renderTemplates(this.templates, ctx)
ctx.scopes[0][this.variable] = html ctx.front()[this.variable] = html
} }
} as ITagImplOptions } as ITagImplOptions
+1 -1
View File
@@ -27,7 +27,7 @@ export default <ITagImplOptions>{
render: async function (ctx: Context) { render: async function (ctx: Context) {
const group = await evalValue(this.group, ctx) const group = await evalValue(this.group, ctx)
const fingerprint = `cycle:${group}:` + this.candidates.join(',') const fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = ctx.groups const groups = ctx.getRegister('cycle')
let idx = groups[fingerprint] let idx = groups[fingerprint]
if (idx === undefined) { if (idx === undefined) {
+7 -7
View File
@@ -41,20 +41,20 @@ export default <ITagImplOptions>{
} }
assert(filepath, `cannot include with empty filename`) assert(filepath, `cannot include with empty filename`)
const originBlocks = ctx.blocks const originBlocks = ctx.getRegister('blocks')
const originBlockMode = ctx.blockMode const originBlockMode = ctx.getRegister('blockMode')
ctx.blocks = {} ctx.setRegister('blocks', {})
ctx.blockMode = BlockMode.OUTPUT ctx.setRegister('blockMode', BlockMode.OUTPUT)
if (this.with) { if (this.with) {
hash[filepath] = await evalValue(this.with, ctx) hash[filepath] = await evalValue(this.with, ctx)
} }
const templates = await this.liquid.getTemplate(filepath, ctx.opts) const templates = await this.liquid.getTemplate(filepath, ctx.opts)
ctx.push(hash) ctx.push(hash)
const html = await this.liquid.renderer.renderTemplates(templates, ctx) const html = await this.liquid.renderer.renderTemplates(templates, ctx)
ctx.pop(hash) ctx.pop()
ctx.blocks = originBlocks ctx.setRegister('blocks', originBlocks)
ctx.blockMode = originBlockMode ctx.setRegister('blockMode', originBlockMode)
return html return html
} }
} }
+6 -5
View File
@@ -31,16 +31,17 @@ export default {
assert(layout, `cannot apply layout with empty filename`) assert(layout, `cannot apply layout with empty filename`)
// render the remaining tokens immediately // 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) const html = await this.liquid.renderer.renderTemplates(this.tpls, ctx)
if (ctx.blocks[''] === undefined) { if (blocks[''] === undefined) {
ctx.blocks[''] = html blocks[''] = html
} }
const templates = await this.liquid.getTemplate(layout, ctx.opts) const templates = await this.liquid.getTemplate(layout, ctx.opts)
ctx.push(hash) ctx.push(hash)
ctx.blockMode = BlockMode.OUTPUT ctx.setRegister('blockMode', BlockMode.OUTPUT)
const partial = await this.liquid.renderer.renderTemplates(templates, ctx) const partial = await this.liquid.renderer.renderTemplates(templates, ctx)
ctx.pop(hash) ctx.pop()
return partial return partial
} }
} as ITagImplOptions } as ITagImplOptions
+1 -1
View File
@@ -59,7 +59,7 @@ export default {
html += '</td>' html += '</td>'
} }
if (collection.length) html += '</tr>' if (collection.length) html += '</tr>'
ctx.pop(scope) ctx.pop()
return html return html
} }
} as ITagImplOptions } as ITagImplOptions
+25 -25
View File
@@ -3,20 +3,23 @@ import { Drop } from '../drop/drop'
import { __assign } from 'tslib' import { __assign } from 'tslib'
import assert from '../util/assert' import assert from '../util/assert'
import { NormalizedFullOptions, applyDefault } from '../liquid-options' import { NormalizedFullOptions, applyDefault } from '../liquid-options'
import BlockMode from './block-mode'
import { Scope } from './scope' import { Scope } from './scope'
export default class Context { export default class Context {
opts: NormalizedFullOptions opts: NormalizedFullOptions
scopes: Array<Scope> = [{}]
environments: Scope environments: Scope
blocks: object = {} private scopes: Array<Scope> = [{}]
groups: {[key: string]: number} = {} private registers = {}
blockMode: BlockMode = BlockMode.OUTPUT
constructor (ctx: object = {}, opts?: NormalizedFullOptions) { constructor (ctx: object = {}, opts?: NormalizedFullOptions) {
this.opts = applyDefault(opts) this.opts = applyDefault(opts)
this.environments = ctx 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 () { getAll () {
return [this.environments, ...this.scopes] return [this.environments, ...this.scopes]
.reduce((ctx, val) => __assign(ctx, val), {}) .reduce((ctx, val) => __assign(ctx, val), {})
@@ -25,7 +28,7 @@ export default class Context {
const paths = await this.parseProp(path) const paths = await this.parseProp(path)
let ctx = this.findScope(paths[0]) || this.environments let ctx = this.findScope(paths[0]) || this.environments
for (const path of paths) { for (const path of paths) {
ctx = this.readProperty(ctx, path) ctx = readProperty(ctx, path)
if (_.isNil(ctx) && this.opts.strictVariables) { if (_.isNil(ctx) && this.opts.strictVariables) {
throw new TypeError(`undefined variable: ${path}`) throw new TypeError(`undefined variable: ${path}`)
} }
@@ -35,15 +38,11 @@ export default class Context {
push (ctx: object) { push (ctx: object) {
return this.scopes.push(ctx) return this.scopes.push(ctx)
} }
pop (ctx?: object): object | undefined { pop () {
if (!arguments.length) { return this.scopes.pop()
return this.scopes.pop() }
} front () {
const i = this.scopes.findIndex(scope => scope === ctx) return this.scopes[0]
if (i === -1) {
throw new TypeError('scope not found, cannot pop')
}
return this.scopes.splice(i, 1)[0]
} }
private findScope (key: string) { private findScope (key: string) {
for (let i = this.scopes.length - 1; i >= 0; i--) { for (let i = this.scopes.length - 1; i >= 0; i--) {
@@ -54,16 +53,6 @@ export default class Context {
} }
return null 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 * 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) { function readSize (obj: Scope) {
if (!_.isNil(obj['size'])) return obj['size'] if (!_.isNil(obj['size'])) return obj['size']
if (_.isArray(obj) || _.isString(obj)) return obj.length if (_.isArray(obj) || _.isString(obj)) return obj.length
+2 -2
View File
@@ -14,7 +14,7 @@ import { isTruthy, isFalsy, evalExp, evalValue } from './render/syntax'
import builtinTags from './builtin/tags' import builtinTags from './builtin/tags'
import builtinFilters from './builtin/filters' import builtinFilters from './builtin/filters'
import { LiquidOptions, NormalizedFullOptions, applyDefault, normalize } from './liquid-options' 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 { export default class Liquid {
public options: NormalizedFullOptions public options: NormalizedFullOptions
@@ -72,7 +72,7 @@ export default class Liquid {
evalValue (str: string, ctx: Context) { evalValue (str: string, ctx: Context) {
return new Value(str, this.options.strictFilters).value(ctx) return new Value(str, this.options.strictFilters).value(ctx)
} }
registerFilter (name: string, filter: FilterImpl) { registerFilter (name: string, filter: FilterImplOptions) {
return Filter.register(name, filter) return Filter.register(name, filter)
} }
registerTag (name: string, tag: ITagImplOptions) { registerTag (name: string, tag: ITagImplOptions) {
@@ -0,0 +1,3 @@
import { FilterImpl } from "./filter-impl";
export type FilterImplOptions = (this: FilterImpl, value: any, ...args: any[]) => any
+5 -1
View File
@@ -1 +1,5 @@
export type FilterImpl = (value: any, ...args: any[]) => any import Context from "../../context/context";
export interface FilterImpl {
context: Context
}
+8 -8
View File
@@ -1,15 +1,15 @@
import { evalValue } from '../../render/syntax' import { evalValue } from '../../render/syntax'
import Context from '../../context/context' import Context from '../../context/context'
import { isArray } from '../../util/underscore' import { isArray } from '../../util/underscore'
import { FilterImpl } from './filter-impl' import { FilterImplOptions } from './filter-impl-options'
export type FilterArgs = Array<string|[string?, string?]> export type FilterArgs = Array<string|[string?, string?]>
export class Filter { export class Filter {
name: string name: string
impl: FilterImpl impl: FilterImplOptions
args: FilterArgs args: FilterArgs
private static impls: {[key: string]: FilterImpl} = {} private static impls: {[key: string]: FilterImplOptions} = {}
constructor (name: string, args: FilterArgs, strictFilters: boolean) { constructor (name: string, args: FilterArgs, strictFilters: boolean) {
const impl = Filter.impls[name] const impl = Filter.impls[name]
@@ -19,15 +19,15 @@ export class Filter {
this.impl = impl || (x => x) this.impl = impl || (x => x)
this.args = args this.args = args
} }
async render (value: any, ctx: Context) { async render (value: any, context: Context) {
const argv: any[] = [] const argv: any[] = []
for (const arg of this.args) { for (const arg of this.args) {
if (isArray(arg)) argv.push([arg[0], await evalValue(arg[1], ctx)]) if (isArray(arg)) argv.push([arg[0], await evalValue(arg[1], context)])
else argv.push(await evalValue(arg, ctx)) 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 Filter.impls[name] = filter
} }
static clear () { static clear () {
+5 -26
View File
@@ -137,15 +137,15 @@ describe('scope', function () {
return expect(ctx.get('notdefined')).to.be.rejectedWith(/undefined variable: notdefined/) return expect(ctx.get('notdefined')).to.be.rejectedWith(/undefined variable: notdefined/)
}) })
it('should throw when deep variable not exist', async function () { 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/) return expect(ctx.get('foo.bar.not.defined')).to.be.rejectedWith(/undefined variable: bar/)
}) })
it('should throw when itself not defined', async function () { 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/) return expect(ctx.get('foo.BAR')).to.be.rejectedWith(/undefined variable: BAR/)
}) })
it('should find variable in parent scope', async function () { it('should find variable in parent scope', async function () {
ctx.scopes.push({ 'foo': 'foo' }) ctx.push({ 'foo': 'foo' })
ctx.push({ ctx.push({
'bar': 'bar' 'bar': 'bar'
}) })
@@ -161,7 +161,7 @@ describe('scope', function () {
describe('.push()', function () { describe('.push()', function () {
it('should push scope', async function () { it('should push scope', async function () {
ctx.scopes.push({ 'bar': 'bar' }) ctx.push({ 'bar': 'bar' })
ctx.push({ ctx.push({
foo: 'foo' foo: 'foo'
}) })
@@ -169,7 +169,7 @@ describe('scope', function () {
expect(await ctx.get('bar')).to.equal('bar') expect(await ctx.get('bar')).to.equal('bar')
}) })
it('should hide deep properties by push', async function () { 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' } }) ctx.push({ bar: { foo: 'foo' } })
expect(await ctx.get('bar.foo')).to.equal('foo') expect(await ctx.get('bar.foo')).to.equal('foo')
expect(await ctx.get('bar.bar')).to.equal(undefined) expect(await ctx.get('bar.bar')).to.equal(undefined)
@@ -184,25 +184,4 @@ describe('scope', function () {
expect(await ctx.get('foo')).to.equal('zoo') 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')
})
}) })
+7 -1
View File
@@ -22,12 +22,18 @@ describe('filter', function () {
expect(await new Filter('undefined', [], false).render('foo', ctx)).to.equal('foo') 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() const spy = sinon.spy()
Filter.register('foo', spy) Filter.register('foo', spy)
await new Filter('foo', ['33'], false).render('foo', ctx) await new Filter('foo', ['33'], false).render('foo', ctx)
expect(spy).to.have.been.calledWith('foo', 33) 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 () { it('should render a simple filter', async function () {
Filter.register('upcase', x => x.toUpperCase()) Filter.register('upcase', x => x.toUpperCase())
expect(await new Filter('upcase', [], false).render('foo', ctx)).to.equal('FOO') expect(await new Filter('upcase', [], false).render('foo', ctx)).to.equal('FOO')