feat: add option for keeping variable type in output

This commit is contained in:
Alex de Wergifosse
2020-12-16 10:20:10 +08:00
committed by Jun Yang
parent e84fbf32ad
commit cd92e77726
11 changed files with 42 additions and 21 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ export default {
const r = this.liquid.renderer const r = this.liquid.renderer
const html = childDefined !== undefined const html = childDefined !== undefined
? childDefined ? 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) { if (ctx.getRegister('blockMode', BlockMode.OUTPUT) === BlockMode.STORE) {
blocks[this.block] = html blocks[this.block] = html
+2 -1
View File
@@ -1,5 +1,6 @@
import { Tokenizer, assert, Template, Context, TagImplOptions, TagToken, TopLevelToken } from '../../types' import { Tokenizer, assert, Template, Context, TagImplOptions, TagToken, TopLevelToken } from '../../types'
import { evalQuotedToken } from '../../render/expression' import { evalQuotedToken } from '../../render/expression'
import { Emitter } from '../../render/emitter'
export default { export default {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
@@ -19,7 +20,7 @@ export default {
}, },
render: function * (ctx: Context) { render: function * (ctx: Context) {
const r = this.liquid.renderer 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 ctx.bottom()[this.variable] = html
} }
} as TagImplOptions } as TagImplOptions
+1 -1
View File
@@ -26,7 +26,7 @@ export default {
const { renderer } = liquid const { renderer } = liquid
const filepath = ctx.opts.dynamicPartials const filepath = ctx.opts.dynamicPartials
? (TypeGuards.isQuotedToken(file) ? (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)) : yield evalToken(file, ctx))
: file.getText() : file.getText()
assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`) assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`)
+3 -3
View File
@@ -16,7 +16,7 @@ export default {
const { renderer } = liquid const { renderer } = liquid
const filepath = ctx.opts.dynamicPartials const filepath = ctx.opts.dynamicPartials
? (TypeGuards.isQuotedToken(file) ? (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)) : evalToken(this.file, ctx))
: file.getText() : file.getText()
assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`) assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`)
@@ -24,12 +24,12 @@ export default {
// render the remaining tokens immediately // render the remaining tokens immediately
ctx.setRegister('blockMode', BlockMode.STORE) ctx.setRegister('blockMode', BlockMode.STORE)
const blocks = ctx.getRegister('blocks') 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 if (blocks[''] === undefined) blocks[''] = html
const templates = yield liquid._parseFile(filepath, ctx.opts, ctx.sync) const templates = yield liquid._parseFile(filepath, ctx.opts, ctx.sync)
ctx.push(yield hash.render(ctx)) ctx.push(yield hash.render(ctx))
ctx.setRegister('blockMode', BlockMode.OUTPUT) 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() ctx.pop()
emitter.write(partial) emitter.write(partial)
} }
+1 -1
View File
@@ -44,7 +44,7 @@ export default {
const { renderer } = liquid const { renderer } = liquid
const filepath = ctx.opts.dynamicPartials const filepath = ctx.opts.dynamicPartials
? (TypeGuards.isQuotedToken(file) ? (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)) : evalToken(file, ctx))
: file.getText() : file.getText()
assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`) assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`)
+5 -1
View File
@@ -43,6 +43,8 @@ export interface LiquidOptions {
fs?: FS; fs?: FS;
/** the global environment passed down to all partial templates, i.e. templates included by `include`, `layout` and `render` tags. */ /** the global environment passed down to all partial templates, i.e. templates included by `include`, `layout` and `render` tags. */
globals?: object; globals?: object;
/** Whether or not to keep value type when writing the Output. Defaults to `false`. */
keepOutputType?: boolean;
} }
interface NormalizedOptions extends LiquidOptions { interface NormalizedOptions extends LiquidOptions {
@@ -69,6 +71,7 @@ export interface NormalizedFullOptions extends NormalizedOptions {
outputDelimiterRight: string; outputDelimiterRight: string;
greedy: boolean; greedy: boolean;
globals: object; globals: object;
keepOutputType: boolean;
} }
export const defaultOptions: NormalizedFullOptions = { export const defaultOptions: NormalizedFullOptions = {
@@ -89,7 +92,8 @@ export const defaultOptions: NormalizedFullOptions = {
strictFilters: false, strictFilters: false,
strictVariables: false, strictVariables: false,
lenientIf: false, lenientIf: false,
globals: {} globals: {},
keepOutputType: false
} }
export function normalize (options?: LiquidOptions): NormalizedOptions { export function normalize (options?: LiquidOptions): NormalizedOptions {
+9 -7
View File
@@ -15,6 +15,7 @@ import { LiquidOptions, normalizeStringArray, NormalizedFullOptions, applyDefaul
import { FilterImplOptions } from './template/filter/filter-impl-options' import { FilterImplOptions } from './template/filter/filter-impl-options'
import { FS } from './fs/fs' import { FS } from './fs/fs'
import { toPromise, toValue } from './util/async' import { toPromise, toValue } from './util/async'
import { Emitter } from './render/emitter'
export * from './types' export * from './types'
@@ -43,26 +44,27 @@ export class Liquid {
return this.parser.parse(tokens) return this.parser.parse(tokens)
} }
public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<string> { public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> {
const options = { ...this.options, ...normalize(opts) } const options = { ...this.options, ...normalize(opts) }
const ctx = new Context(scope, options, sync) 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<string> { public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise<any> {
return toPromise(this._render(tpl, scope, opts, false)) 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)) return toValue(this._render(tpl, scope, opts, true))
} }
public _parseAndRender (html: string, scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<string> { public _parseAndRender (html: string, scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> {
const tpl = this.parse(html) const tpl = this.parse(html)
return this._render(tpl, scope, opts, sync) return this._render(tpl, scope, opts, sync)
} }
public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<string> { public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<any> {
return toPromise(this._parseAndRender(html, scope, opts, false)) 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)) return toValue(this._parseAndRender(html, scope, opts, true))
} }
+12 -3
View File
@@ -1,9 +1,18 @@
export class Emitter { export class Emitter {
public html = ''; public html: any = '';
public break = false; public break = false;
public continue = false; public continue = false;
private keepOutputType? = false;
public write (html: string) { constructor (keepOutputType: boolean|undefined) {
this.html += html this.keepOutputType = keepOutputType
}
public write (html: any) {
if (this.keepOutputType && typeof html !== 'string') {
this.html = html
} else {
this.html += html as string
}
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import { Template } from '../template/template'
import { Emitter } from './emitter' import { Emitter } from './emitter'
export class Render { export class Render {
public * renderTemplates (templates: Template[], ctx: Context, emitter = new Emitter()): IterableIterator<string> { public * renderTemplates (templates: Template[], ctx: Context, emitter: Emitter): IterableIterator<any> {
for (const tpl of templates) { for (const tpl of templates) {
try { try {
const html = yield tpl.render(ctx, emitter) const html = yield tpl.render(ctx, emitter)
+5 -1
View File
@@ -16,6 +16,10 @@ export class Output extends TemplateImpl<OutputToken> implements Template {
} }
public * render (ctx: Context, emitter: Emitter) { public * render (ctx: Context, emitter: Emitter) {
const val = yield this.value.value(ctx) 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)))
}
} }
} }
+2 -1
View File
@@ -3,6 +3,7 @@ import { Context } from '../../../src/context/context'
import { HTMLToken } from '../../../src/tokens/html-token' import { HTMLToken } from '../../../src/tokens/html-token'
import { Render } from '../../../src/render/render' import { Render } from '../../../src/render/render'
import { HTML } from '../../../src/template/html' import { HTML } from '../../../src/template/html'
import { Emitter } from '../../../src/render/emitter'
import { toThenable } from '../../../src/util/async' import { toThenable } from '../../../src/util/async'
describe('render', function () { describe('render', function () {
@@ -15,7 +16,7 @@ describe('render', function () {
it('should render html', async function () { it('should render html', async function () {
const scope = new Context() const scope = new Context()
const token = { getContent: () => '<p>' } as HTMLToken const token = { getContent: () => '<p>' } 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('<p>') return expect(html).to.equal('<p>')
}) })
}) })