diff --git a/src/builtin/tags/block.ts b/src/builtin/tags/block.ts index f8ad384bf..302381fdd 100644 --- a/src/builtin/tags/block.ts +++ b/src/builtin/tags/block.ts @@ -20,7 +20,7 @@ export default { const r = this.liquid.renderer const html = childDefined !== undefined ? childDefined - : yield r.renderTemplates(this.tpls, ctx) + : yield r.renderTemplates(this.tpls, ctx, new Emitter(ctx.opts.keepOutputType)) if (ctx.getRegister('blockMode', BlockMode.OUTPUT) === BlockMode.STORE) { blocks[this.block] = html diff --git a/src/builtin/tags/capture.ts b/src/builtin/tags/capture.ts index 3b0aa1fd3..3febc3a29 100644 --- a/src/builtin/tags/capture.ts +++ b/src/builtin/tags/capture.ts @@ -1,5 +1,6 @@ import { Tokenizer, assert, Template, Context, TagImplOptions, TagToken, TopLevelToken } from '../../types' import { evalQuotedToken } from '../../render/expression' +import { Emitter } from '../../render/emitter' export default { parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { @@ -19,7 +20,7 @@ export default { }, render: function * (ctx: Context) { const r = this.liquid.renderer - const html = yield r.renderTemplates(this.templates, ctx) + const html = yield r.renderTemplates(this.templates, ctx, new Emitter(ctx.opts.keepOutputType)) ctx.bottom()[this.variable] = html } } as TagImplOptions diff --git a/src/builtin/tags/include.ts b/src/builtin/tags/include.ts index 504f4eb83..1b67e7b98 100644 --- a/src/builtin/tags/include.ts +++ b/src/builtin/tags/include.ts @@ -26,7 +26,7 @@ export default { const { renderer } = liquid const filepath = ctx.opts.dynamicPartials ? (TypeGuards.isQuotedToken(file) - ? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx) + ? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx, new Emitter(ctx.opts.keepOutputType)) : yield evalToken(file, ctx)) : file.getText() assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`) diff --git a/src/builtin/tags/layout.ts b/src/builtin/tags/layout.ts index a27cf5a13..374633c4e 100644 --- a/src/builtin/tags/layout.ts +++ b/src/builtin/tags/layout.ts @@ -16,7 +16,7 @@ export default { const { renderer } = liquid const filepath = ctx.opts.dynamicPartials ? (TypeGuards.isQuotedToken(file) - ? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx) + ? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx, new Emitter(ctx.opts.keepOutputType)) : evalToken(this.file, ctx)) : file.getText() assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`) @@ -24,12 +24,12 @@ export default { // render the remaining tokens immediately ctx.setRegister('blockMode', BlockMode.STORE) const blocks = ctx.getRegister('blocks') - const html = yield renderer.renderTemplates(this.tpls, ctx) + const html = yield renderer.renderTemplates(this.tpls, ctx, new Emitter(ctx.opts.keepOutputType)) if (blocks[''] === undefined) blocks[''] = html const templates = yield liquid._parseFile(filepath, ctx.opts, ctx.sync) ctx.push(yield hash.render(ctx)) ctx.setRegister('blockMode', BlockMode.OUTPUT) - const partial = yield renderer.renderTemplates(templates, ctx) + const partial = yield renderer.renderTemplates(templates, ctx, new Emitter(ctx.opts.keepOutputType)) ctx.pop() emitter.write(partial) } diff --git a/src/builtin/tags/render.ts b/src/builtin/tags/render.ts index 48d0c7f50..70e3a7b44 100644 --- a/src/builtin/tags/render.ts +++ b/src/builtin/tags/render.ts @@ -44,7 +44,7 @@ export default { const { renderer } = liquid const filepath = ctx.opts.dynamicPartials ? (TypeGuards.isQuotedToken(file) - ? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx) + ? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx, new Emitter(ctx.opts.keepOutputType)) : evalToken(file, ctx)) : file.getText() assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`) diff --git a/src/liquid-options.ts b/src/liquid-options.ts index 0c066ef13..b8213479a 100644 --- a/src/liquid-options.ts +++ b/src/liquid-options.ts @@ -43,6 +43,8 @@ export interface LiquidOptions { fs?: FS; /** the global environment passed down to all partial templates, i.e. templates included by `include`, `layout` and `render` tags. */ globals?: object; + /** Whether or not to keep value type when writing the Output. Defaults to `false`. */ + keepOutputType?: boolean; } interface NormalizedOptions extends LiquidOptions { @@ -69,6 +71,7 @@ export interface NormalizedFullOptions extends NormalizedOptions { outputDelimiterRight: string; greedy: boolean; globals: object; + keepOutputType: boolean; } export const defaultOptions: NormalizedFullOptions = { @@ -89,7 +92,8 @@ export const defaultOptions: NormalizedFullOptions = { strictFilters: false, strictVariables: false, lenientIf: false, - globals: {} + globals: {}, + keepOutputType: false } export function normalize (options?: LiquidOptions): NormalizedOptions { diff --git a/src/liquid.ts b/src/liquid.ts index 3ee61bbf0..84804d24c 100644 --- a/src/liquid.ts +++ b/src/liquid.ts @@ -15,6 +15,7 @@ import { LiquidOptions, normalizeStringArray, NormalizedFullOptions, applyDefaul import { FilterImplOptions } from './template/filter/filter-impl-options' import { FS } from './fs/fs' import { toPromise, toValue } from './util/async' +import { Emitter } from './render/emitter' export * from './types' @@ -43,26 +44,27 @@ export class Liquid { return this.parser.parse(tokens) } - public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator { + public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator { const options = { ...this.options, ...normalize(opts) } const ctx = new Context(scope, options, sync) - return this.renderer.renderTemplates(tpl, ctx) + const emitter = new Emitter(options.keepOutputType) + return this.renderer.renderTemplates(tpl, ctx, emitter) } - public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise { + public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise { return toPromise(this._render(tpl, scope, opts, false)) } - public renderSync (tpl: Template[], scope?: object, opts?: LiquidOptions): string { + public renderSync (tpl: Template[], scope?: object, opts?: LiquidOptions): any { return toValue(this._render(tpl, scope, opts, true)) } - public _parseAndRender (html: string, scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator { + public _parseAndRender (html: string, scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator { const tpl = this.parse(html) return this._render(tpl, scope, opts, sync) } - public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise { + public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise { return toPromise(this._parseAndRender(html, scope, opts, false)) } - public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): string { + public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): any { return toValue(this._parseAndRender(html, scope, opts, true)) } diff --git a/src/render/emitter.ts b/src/render/emitter.ts index 6231a6382..40b6f2c19 100644 --- a/src/render/emitter.ts +++ b/src/render/emitter.ts @@ -1,9 +1,18 @@ export class Emitter { - public html = ''; + public html: any = ''; public break = false; public continue = false; + private keepOutputType? = false; - public write (html: string) { - this.html += html + constructor (keepOutputType: boolean|undefined) { + this.keepOutputType = keepOutputType + } + + public write (html: any) { + if (this.keepOutputType && typeof html !== 'string') { + this.html = html + } else { + this.html += html as string + } } } diff --git a/src/render/render.ts b/src/render/render.ts index 76e8096b5..800765e24 100644 --- a/src/render/render.ts +++ b/src/render/render.ts @@ -4,7 +4,7 @@ import { Template } from '../template/template' import { Emitter } from './emitter' export class Render { - public * renderTemplates (templates: Template[], ctx: Context, emitter = new Emitter()): IterableIterator { + public * renderTemplates (templates: Template[], ctx: Context, emitter: Emitter): IterableIterator { for (const tpl of templates) { try { const html = yield tpl.render(ctx, emitter) diff --git a/src/template/output.ts b/src/template/output.ts index 4c7da7c5c..db102db8e 100644 --- a/src/template/output.ts +++ b/src/template/output.ts @@ -16,6 +16,10 @@ export class Output extends TemplateImpl implements Template { } public * render (ctx: Context, emitter: Emitter) { const val = yield this.value.value(ctx) - emitter.write(stringify(toValue(val))) + if (ctx.opts.keepOutputType) { + emitter.write(toValue(val)) + } else { + emitter.write(stringify(toValue(val))) + } } } diff --git a/test/unit/render/render.ts b/test/unit/render/render.ts index 0f45f3615..4dfef1eaf 100644 --- a/test/unit/render/render.ts +++ b/test/unit/render/render.ts @@ -3,6 +3,7 @@ import { Context } from '../../../src/context/context' import { HTMLToken } from '../../../src/tokens/html-token' import { Render } from '../../../src/render/render' import { HTML } from '../../../src/template/html' +import { Emitter } from '../../../src/render/emitter' import { toThenable } from '../../../src/util/async' describe('render', function () { @@ -15,7 +16,7 @@ describe('render', function () { it('should render html', async function () { const scope = new Context() const token = { getContent: () => '

' } as HTMLToken - const html = await toThenable(render.renderTemplates([new HTML(token)], scope)) + const html = await toThenable(render.renderTemplates([new HTML(token)], scope, new Emitter(scope.opts.keepOutputType))) return expect(html).to.equal('

') }) })