mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
feat: Support for the "render" tag #163
This commit is contained in:
@@ -4,6 +4,7 @@ import capture from './capture'
|
|||||||
import Case from './case'
|
import Case from './case'
|
||||||
import comment from './comment'
|
import comment from './comment'
|
||||||
import include from './include'
|
import include from './include'
|
||||||
|
import render from './render'
|
||||||
import decrement from './decrement'
|
import decrement from './decrement'
|
||||||
import cycle from './cycle'
|
import cycle from './cycle'
|
||||||
import If from './if'
|
import If from './if'
|
||||||
@@ -18,7 +19,7 @@ import Continue from './continue'
|
|||||||
import { ITagImplOptions } from '../../template/tag/itag-impl-options'
|
import { ITagImplOptions } from '../../template/tag/itag-impl-options'
|
||||||
|
|
||||||
const tags: { [key: string]: ITagImplOptions } = {
|
const tags: { [key: string]: ITagImplOptions } = {
|
||||||
assign, 'for': For, capture, 'case': Case, comment, include, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue
|
assign, 'for': For, capture, 'case': Case, comment, include, render, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue
|
||||||
}
|
}
|
||||||
|
|
||||||
export default tags
|
export default tags
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Hash, Emitter, TagToken, Token, ITagImplOptions, Context } from '../../types'
|
import { TagToken, Token, ITagImplOptions } from '../../types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
||||||
@@ -15,7 +15,7 @@ export default {
|
|||||||
})
|
})
|
||||||
stream.start()
|
stream.start()
|
||||||
},
|
},
|
||||||
render: function (ctx: Context, hash: Hash, emitter: Emitter) {
|
render: function () {
|
||||||
emitter.write(this.tokens.map((token: Token) => token.raw).join(''))
|
return this.tokens.map((token: Token) => token.raw).join('')
|
||||||
}
|
}
|
||||||
} as ITagImplOptions
|
} as ITagImplOptions
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { assert } from '../../util/assert'
|
||||||
|
import { Expression, Hash, Emitter, TagToken, Context, ITagImplOptions } from '../../types'
|
||||||
|
import { value, quotedLine } from '../../parser/lexical'
|
||||||
|
import BlockMode from '../../context/block-mode'
|
||||||
|
|
||||||
|
const staticFileRE = /[^\s,]+/
|
||||||
|
const withRE = new RegExp(`with\\s+(${value.source})`)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
parse: function (token: TagToken) {
|
||||||
|
let match = staticFileRE.exec(token.args)
|
||||||
|
if (match) this.staticValue = match[0]
|
||||||
|
|
||||||
|
match = value.exec(token.args)
|
||||||
|
if (match) this.value = match[0]
|
||||||
|
|
||||||
|
match = withRE.exec(token.args)
|
||||||
|
if (match) this.with = match[1]
|
||||||
|
},
|
||||||
|
render: function * (ctx: Context, hash: Hash, emitter: Emitter) {
|
||||||
|
let filepath
|
||||||
|
if (ctx.opts.dynamicPartials) {
|
||||||
|
if (quotedLine.exec(this.value)) {
|
||||||
|
const template = this.value.slice(1, -1)
|
||||||
|
filepath = yield this.liquid._parseAndRender(template, ctx.getAll(), ctx.opts, ctx.sync)
|
||||||
|
} else {
|
||||||
|
filepath = yield new Expression(this.value).value(ctx)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filepath = this.staticValue
|
||||||
|
}
|
||||||
|
assert(filepath, `cannot render with empty filename`)
|
||||||
|
|
||||||
|
const originBlocks = ctx.getRegister('blocks')
|
||||||
|
const originBlockMode = ctx.getRegister('blockMode')
|
||||||
|
|
||||||
|
const childCtx = new Context({}, ctx.opts, ctx.sync)
|
||||||
|
childCtx.setRegister('blocks', {})
|
||||||
|
childCtx.setRegister('blockMode', BlockMode.OUTPUT)
|
||||||
|
if (this.with) {
|
||||||
|
hash[filepath] = yield new Expression(this.with).evaluate(ctx)
|
||||||
|
}
|
||||||
|
childCtx.push(hash)
|
||||||
|
const templates = yield this.liquid._parseFile(filepath, childCtx.opts, childCtx.sync)
|
||||||
|
yield this.liquid.renderer.renderTemplates(templates, childCtx, emitter)
|
||||||
|
|
||||||
|
childCtx.setRegister('blocks', originBlocks)
|
||||||
|
childCtx.setRegister('blockMode', originBlockMode)
|
||||||
|
}
|
||||||
|
} as ITagImplOptions
|
||||||
@@ -11,10 +11,10 @@ export class Context {
|
|||||||
public environments: Scope
|
public environments: Scope
|
||||||
public sync: boolean
|
public sync: boolean
|
||||||
public opts: NormalizedFullOptions
|
public opts: NormalizedFullOptions
|
||||||
public constructor (ctx: object = {}, opts?: NormalizedFullOptions, sync = false) {
|
public constructor (env: object = {}, opts?: NormalizedFullOptions, sync = false) {
|
||||||
this.sync = sync
|
this.sync = sync
|
||||||
this.opts = applyDefault(opts)
|
this.opts = applyDefault(opts)
|
||||||
this.environments = ctx
|
this.environments = env
|
||||||
}
|
}
|
||||||
public getRegister (key: string, defaultValue = {}) {
|
public getRegister (key: string, defaultValue = {}) {
|
||||||
return (this.registers[key] = this.registers[key] || defaultValue)
|
return (this.registers[key] = this.registers[key] || defaultValue)
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import { FilterImplOptions } from './template/filter/filter-impl-options'
|
|||||||
import IFS from './fs/ifs'
|
import IFS from './fs/ifs'
|
||||||
import { toThenable, toValue } from './util/async'
|
import { toThenable, toValue } from './util/async'
|
||||||
|
|
||||||
type nullableTemplates = ITemplate[] | null
|
|
||||||
|
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
|
||||||
export class Liquid {
|
export class Liquid {
|
||||||
|
|||||||
+2
-2
@@ -34,7 +34,7 @@ function isCustomIterable (val: any): val is IterableIterator<any> {
|
|||||||
return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return)
|
return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toThenable (val: IterableIterator<any> | Thenable): Thenable {
|
export function toThenable (val: IterableIterator<any> | Thenable | any): Thenable {
|
||||||
if (isThenable(val)) return val
|
if (isThenable(val)) return val
|
||||||
if (isCustomIterable(val)) return reduce()
|
if (isCustomIterable(val)) return reduce()
|
||||||
return mkResolve(val)
|
return mkResolve(val)
|
||||||
@@ -61,7 +61,7 @@ export function toThenable (val: IterableIterator<any> | Thenable): Thenable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toValue (val: IterableIterator<any> | Thenable) {
|
export function toValue (val: IterableIterator<any> | Thenable | any) {
|
||||||
let ret: any
|
let ret: any
|
||||||
toThenable(val)
|
toThenable(val)
|
||||||
.then((x: any) => {
|
.then((x: any) => {
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import { Liquid, Drop } from '../../../../src/liquid'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { mock, restore } from '../../../stub/mockfs'
|
||||||
|
|
||||||
|
describe('tags/render', function () {
|
||||||
|
let liquid: Liquid
|
||||||
|
before(function () {
|
||||||
|
liquid = new Liquid({
|
||||||
|
root: '/',
|
||||||
|
extname: '.html'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
afterEach(restore)
|
||||||
|
it('should support render', async function () {
|
||||||
|
mock({
|
||||||
|
'/current.html': 'bar{% render "bar/foo.html" %}bar',
|
||||||
|
'/bar/foo.html': 'foo'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('/current.html')
|
||||||
|
return expect(html).to.equal('barfoobar')
|
||||||
|
})
|
||||||
|
it('should support template string', async function () {
|
||||||
|
mock({
|
||||||
|
'/current.html': 'bar{% render "bar/{{name}}" %}bar',
|
||||||
|
'/bar/foo.html': 'foo'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('/current.html', { name: 'foo.html' })
|
||||||
|
return expect(html).to.equal('barfoobar')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw when not specified', function () {
|
||||||
|
mock({
|
||||||
|
'/parent.html': '{%render%}'
|
||||||
|
})
|
||||||
|
return liquid.renderFile('/parent.html').catch(function (e) {
|
||||||
|
console.log(e)
|
||||||
|
expect(e.name).to.equal('RenderError')
|
||||||
|
expect(e.message).to.match(/cannot render with empty filename/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw when not exist', function () {
|
||||||
|
mock({
|
||||||
|
'/parent.html': '{%render not-exist%}'
|
||||||
|
})
|
||||||
|
return liquid.renderFile('/parent.html').catch(function (e) {
|
||||||
|
expect(e.name).to.equal('RenderError')
|
||||||
|
expect(e.message).to.match(/cannot render with empty filename/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support render with relative path', async function () {
|
||||||
|
mock({
|
||||||
|
'/bar/foo.html': 'foo',
|
||||||
|
'/foo/relative.html': 'bar{% render "../bar/foo.html" %}bar'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('foo/relative.html')
|
||||||
|
return expect(html).to.equal('barfoobar')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support render: hash list', async function () {
|
||||||
|
mock({
|
||||||
|
'/hash.html': '{% assign name="harttle" %}{% render "user.html", role: "admin", alias: name %}',
|
||||||
|
'/user.html': '{{role}} : {{alias}}'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('hash.html')
|
||||||
|
return expect(html).to.equal('admin : harttle')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not bleed into child template', async function () {
|
||||||
|
mock({
|
||||||
|
'/hash.html': '{% assign name="harttle" %}InParent: {{name}} {% render "user.html" %}',
|
||||||
|
'/user.html': 'InChild: {{name}}'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('hash.html')
|
||||||
|
return expect(html).to.equal('InParent: harttle InChild: ')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support render: with', async function () {
|
||||||
|
mock({
|
||||||
|
'/with.html': '{% render "color" with "red", shape: "rect" %}',
|
||||||
|
'/color.html': 'color:{{color}}, shape:{{shape}}'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('with.html')
|
||||||
|
return expect(html).to.equal('color:red, shape:rect')
|
||||||
|
})
|
||||||
|
it('should support render: with as Drop', async function () {
|
||||||
|
class ColorDrop extends Drop {
|
||||||
|
public valueOf (): string {
|
||||||
|
return 'red!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock({
|
||||||
|
'/with.html': '{% render "color" with color %}',
|
||||||
|
'/color.html': 'color:{{color}}'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('with.html', { color: new ColorDrop() })
|
||||||
|
expect(html).to.equal('color:red!')
|
||||||
|
})
|
||||||
|
it('should support render: with passed as Drop', async function () {
|
||||||
|
class ColorDrop extends Drop {
|
||||||
|
public valueOf (): string {
|
||||||
|
return 'red!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
liquid.registerFilter('name', x => x.constructor.name)
|
||||||
|
mock({
|
||||||
|
'/with.html': '{% render "color" with color %}',
|
||||||
|
'/color.html': '{{color | name}}'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('with.html', { color: new ColorDrop() })
|
||||||
|
expect(html).to.equal('ColorDrop')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support nested renders', async function () {
|
||||||
|
mock({
|
||||||
|
'/personInfo.html': 'This is a person {% render "card.html", person: person%}',
|
||||||
|
'/card.html': '<p>{{person.firstName}} {{person.lastName}}<br/>{% render "address", address: person.address %}</p>',
|
||||||
|
'/address.html': 'City: {{address.city}}'
|
||||||
|
})
|
||||||
|
const ctx = {
|
||||||
|
person: {
|
||||||
|
firstName: 'Joe',
|
||||||
|
lastName: 'Shmoe',
|
||||||
|
address: {
|
||||||
|
city: 'Dallas'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const html = await liquid.renderFile('personInfo.html', ctx)
|
||||||
|
return expect(html).to.equal('This is a person <p>Joe Shmoe<br/>City: Dallas</p>')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('static partial', function () {
|
||||||
|
it('should support filename with extention', async function () {
|
||||||
|
mock({
|
||||||
|
'/parent.html': 'X{% render child.html color:"red" %}Y',
|
||||||
|
'/child.html': 'child with {{color}}'
|
||||||
|
})
|
||||||
|
const staticLiquid = new Liquid({ dynamicPartials: false, root: '/' })
|
||||||
|
const html = await staticLiquid.renderFile('parent.html')
|
||||||
|
return expect(html).to.equal('Xchild with redY')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support parent paths', async function () {
|
||||||
|
mock({
|
||||||
|
'/parent.html': 'X{% render bar/./../foo/child.html %}Y',
|
||||||
|
'/foo/child.html': 'child'
|
||||||
|
})
|
||||||
|
const staticLiquid = new Liquid({ dynamicPartials: false, root: '/' })
|
||||||
|
const html = await staticLiquid.renderFile('parent.html')
|
||||||
|
return expect(html).to.equal('XchildY')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support subpaths', async function () {
|
||||||
|
mock({
|
||||||
|
'/parent.html': 'X{% render foo/child.html %}Y',
|
||||||
|
'/foo/child.html': 'child'
|
||||||
|
})
|
||||||
|
const staticLiquid = new Liquid({ dynamicPartials: false, root: '/' })
|
||||||
|
const html = await staticLiquid.renderFile('parent.html')
|
||||||
|
return expect(html).to.equal('XchildY')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support comma separated arguments', async function () {
|
||||||
|
mock({
|
||||||
|
'/parent.html': 'X{% render child.html, color:"red" %}Y',
|
||||||
|
'/child.html': 'child with {{color}}'
|
||||||
|
})
|
||||||
|
const staticLiquid = new Liquid({ dynamicPartials: false, root: '/' })
|
||||||
|
const html = await staticLiquid.renderFile('parent.html')
|
||||||
|
return expect(html).to.equal('Xchild with redY')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('sync support', function () {
|
||||||
|
it('should support quoted string', function () {
|
||||||
|
mock({
|
||||||
|
'/current.html': 'bar{% render "bar/foo.html" %}bar',
|
||||||
|
'/bar/foo.html': 'foo'
|
||||||
|
})
|
||||||
|
const html = liquid.renderFileSync('/current.html')
|
||||||
|
return expect(html).to.equal('barfoobar')
|
||||||
|
})
|
||||||
|
it('should support template string', function () {
|
||||||
|
mock({
|
||||||
|
'/current.html': 'bar{% render name" %}bar',
|
||||||
|
'/bar/foo.html': 'foo'
|
||||||
|
})
|
||||||
|
const html = liquid.renderFileSync('/current.html', { name: '/bar/foo.html' })
|
||||||
|
return expect(html).to.equal('barfoobar')
|
||||||
|
})
|
||||||
|
it('should support render: with', function () {
|
||||||
|
mock({
|
||||||
|
'/with.html': '{% render "color" with "red", shape: "rect" %}',
|
||||||
|
'/color.html': 'color:{{color}}, shape:{{shape}}'
|
||||||
|
})
|
||||||
|
const html = liquid.renderFileSync('with.html')
|
||||||
|
return expect(html).to.equal('color:red, shape:rect')
|
||||||
|
})
|
||||||
|
it('should support filename with extention', function () {
|
||||||
|
mock({
|
||||||
|
'/parent.html': 'X{% render child.html color:"red" %}Y',
|
||||||
|
'/child.html': 'child with {{color}}'
|
||||||
|
})
|
||||||
|
const staticLiquid = new Liquid({ dynamicPartials: false, root: '/' })
|
||||||
|
const html = staticLiquid.renderFileSync('parent.html')
|
||||||
|
return expect(html).to.equal('Xchild with redY')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user