mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
feat: pass context to filters
This commit is contained in:
@@ -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}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
@@ -1 +1,5 @@
|
|||||||
export type FilterImpl = (value: any, ...args: any[]) => any
|
import Context from "../../context/context";
|
||||||
|
|
||||||
|
export interface FilterImpl {
|
||||||
|
context: Context
|
||||||
|
}
|
||||||
@@ -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 () {
|
||||||
|
|||||||
@@ -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')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user