Skip to content

Commit

Permalink
feat: stream rendering, closed #361 fixes #360
Browse files Browse the repository at this point in the history
  • Loading branch information
Harttle authored and harttle committed Sep 30, 2021
1 parent abaf4af commit 9012133
Show file tree
Hide file tree
Showing 18 changed files with 147 additions and 53 deletions.
5 changes: 1 addition & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
name: Release
on:
push:
branches:
- master
on: workflow_dispatch
jobs:
release:
name: Release
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/break.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import { Emitter, Context } from '../../types'

export default {
render: function (ctx: Context, emitter: Emitter) {
emitter.break = true
emitter['break'] = true
}
}
2 changes: 1 addition & 1 deletion src/builtin/tags/continue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import { Emitter, Context } from '../../types'

export default {
render: function (ctx: Context, emitter: Emitter) {
emitter.continue = true
emitter['continue'] = true
}
}
6 changes: 3 additions & 3 deletions src/builtin/tags/for.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ export default {
for (const item of collection) {
scope[this.variable] = item
yield r.renderTemplates(this.templates, ctx, emitter)
if (emitter.break) {
emitter.break = false
if (emitter['break']) {
emitter['break'] = false
break
}
emitter.continue = false
emitter['continue'] = false
scope.forloop.next()
}
ctx.pop()
Expand Down
4 changes: 4 additions & 0 deletions src/emitters/emitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Emitter {
write (html: any): void;
end (): void;
}
22 changes: 22 additions & 0 deletions src/emitters/keeping-type-emitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { stringify, toValue } from '../util/underscore'

export class KeepingTypeEmitter {
public html: any = '';

public write (html: any) {
html = toValue(html)
// This will only preserve the type if the value is isolated.
// I.E:
// {{ my-port }} -> 42
// {{ my-host }}:{{ my-port }} -> 'host:42'
if (typeof html !== 'string' && this.html === '') {
this.html = html
} else {
this.html = stringify(this.html) + stringify(html)
}
}

public end () {
return this.html
}
}
14 changes: 14 additions & 0 deletions src/emitters/simple-emitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { stringify } from '../util/underscore'
import { Emitter } from './emitter'

export class SimpleEmitter implements Emitter {
public html: any = '';

public write (html: any) {
this.html += stringify(html)
}

public end () {
return this.html
}
}
12 changes: 12 additions & 0 deletions src/emitters/streamed-emitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { stringify } from '../util/underscore'

export class StreamedEmitter {
public html: any = '';
public stream = new (require('stream').PassThrough)()
public write (html: any) {
this.stream.write(stringify(html))
}
public end () {
this.stream.end()
}
}
2 changes: 1 addition & 1 deletion src/liquid-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ 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`. */
/** Whether or not to keep value type when writing the Output, not working for streamed rendering. Defaults to `false`. */
keepOutputType?: boolean;
/** An object of operators for conditional statements. Defaults to the regular Liquid operators. */
operators?: Operators;
Expand Down
10 changes: 6 additions & 4 deletions src/liquid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { FilterMap } from './template/filter/filter-map'
import { LiquidOptions, normalizeStringArray, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
import { FilterImplOptions } from './template/filter/filter-impl-options'
import { toPromise, toValue } from './util/async'
import { Emitter } from './render/emitter'

export * from './util/error'
export * from './types'
Expand All @@ -24,7 +23,7 @@ export class Liquid {
public parser: Parser
public filters: FilterMap
public tags: TagMap
private parseFileImpl: (file: string, sync?: boolean) => Iterator<Template[]>
public parseFileImpl: (file: string, sync?: boolean) => Iterator<Template[]>

public constructor (opts: LiquidOptions = {}) {
this.options = applyDefault(normalize(opts))
Expand All @@ -45,15 +44,18 @@ export class Liquid {

public _render (tpl: Template[], scope?: object, sync?: boolean): IterableIterator<any> {
const ctx = new Context(scope, this.options, sync)
const emitter = new Emitter(this.options.keepOutputType)
return this.renderer.renderTemplates(tpl, ctx, emitter)
return this.renderer.renderTemplates(tpl, ctx)
}
public async render (tpl: Template[], scope?: object): Promise<any> {
return toPromise(this._render(tpl, scope, false))
}
public renderSync (tpl: Template[], scope?: object): any {
return toValue(this._render(tpl, scope, true))
}
public renderToNodeStream (tpl: Template[], scope?: object): NodeJS.ReadableStream {
const ctx = new Context(scope, this.options)
return this.renderer.renderTemplatesToNodeStream(tpl, ctx)
}

public _parseAndRender (html: string, scope?: object, sync?: boolean): IterableIterator<any> {
const tpl = this.parse(html)
Expand Down
42 changes: 14 additions & 28 deletions src/render/emitter.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,15 @@
import { stringify, toValue } from '../util/underscore'

export class Emitter {
public html: any = '';
public break = false;
public continue = false;
private keepOutputType? = false;

constructor (keepOutputType: boolean|undefined) {
this.keepOutputType = keepOutputType
}

public write (html: any) {
if (this.keepOutputType === true) {
html = toValue(html)
} else {
html = stringify(html)
}
// This will only preserve the type if the value is isolated.
// I.E:
// {{ my-port }} -> 42
// {{ my-host }}:{{ my-port }} -> 'host:42'
if (this.keepOutputType === true && typeof html !== 'string' && this.html === '') {
this.html = html
} else {
this.html = stringify(this.html) + stringify(html)
}
}
export interface Emitter {
/**
* Write a html value into emitter
* @param html string, Drop or other primitive value
*/
write (html: any): void;
/**
* Notify the emitter render has ended
*/
end (): void;
/**
* Collect rendered string value immediately
*/
collect (): string;
}
19 changes: 15 additions & 4 deletions src/render/render.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
import { RenderError } from '../util/error'
import { Context } from '../context/context'
import { Template } from '../template/template'
import { Emitter } from './emitter'
import { Emitter } from '../emitters/emitter'
import { SimpleEmitter } from '../emitters/simple-emitter'
import { StreamedEmitter } from '../emitters/streamed-emitter'
import { toThenable } from '../util/async'
import { KeepingTypeEmitter } from '../emitters/keeping-type-emitter'

export class Render {
public renderTemplatesToNodeStream (templates: Template[], ctx: Context): NodeJS.ReadableStream {
const emitter = new StreamedEmitter()
toThenable(this.renderTemplates(templates, ctx, emitter))
return emitter.stream
}
public * renderTemplates (templates: Template[], ctx: Context, emitter?: Emitter): IterableIterator<any> {
if (!emitter) {
emitter = new Emitter(ctx.opts.keepOutputType)
emitter = ctx.opts.keepOutputType ? new KeepingTypeEmitter() : new SimpleEmitter()
}
for (const tpl of templates) {
try {
// if tpl.render supports emitter, it'll return empty `html`
const html = yield tpl.render(ctx, emitter)
// if not, it'll return an `html`, write to the emitter for it
html && emitter.write(html)
if (emitter.break || emitter.continue) break
if (emitter['break'] || emitter['continue']) break
} catch (e) {
const err = RenderError.is(e) ? e : new RenderError(e, tpl)
throw err
}
}
return emitter.html
return emitter.end()
}
}
2 changes: 1 addition & 1 deletion src/template/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { TemplateImpl } from '../template/template-impl'
import { Template } from '../template/template'
import { HTMLToken } from '../tokens/html-token'
import { Context } from '../context/context'
import { Emitter } from '../render/emitter'
import { Emitter } from '../emitters/emitter'

export class HTML extends TemplateImpl<HTMLToken> implements Template {
private str: string
Expand Down
2 changes: 1 addition & 1 deletion src/template/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Value } from './value'
import { TemplateImpl } from '../template/template-impl'
import { Template } from '../template/template'
import { Context } from '../context/context'
import { Emitter } from '../render/emitter'
import { Emitter } from '../emitters/emitter'
import { OutputToken } from '../tokens/output-token'
import { Liquid } from '../liquid'

Expand Down
2 changes: 1 addition & 1 deletion src/template/tag/tag-impl-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TagToken } from '../../tokens/tag-token'
import { TopLevelToken } from '../../tokens/toplevel-token'
import { TagImpl } from './tag-impl'
import { Hash } from '../../template/tag/hash'
import { Emitter } from '../../render/emitter'
import { Emitter } from '../../emitters/emitter'

export interface TagImplOptions {
parse?: (this: TagImpl, token: TagToken, remainingTokens: TopLevelToken[]) => void;
Expand Down
2 changes: 1 addition & 1 deletion src/template/template.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Context } from '../context/context'
import { Token } from '../tokens/token'
import { Emitter } from '../render/emitter'
import { Emitter } from '../emitters/emitter'

export interface Template {
token: Token;
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export { TypeGuards }
export { ParseError, TokenizationError, AssertionError } from './util/error'
export { assert } from './util/assert'
export { Drop } from './drop/drop'
export { Emitter } from './render/emitter'
export { Emitter } from './emitters/emitter'
export { Expression } from './render/expression'
export { isFalsy, isTruthy } from './render/boolean'
export { TagToken } from './tokens/tag-token'
Expand Down
50 changes: 48 additions & 2 deletions test/unit/render/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ 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 { SimpleEmitter } from '../../../src/emitters/simple-emitter'
import { toThenable } from '../../../src/util/async'
import { Tag } from '../../../src/template/tag/tag'
import { TagToken } from '../../../src/types'

describe('render', function () {
let render: Render
Expand All @@ -16,8 +18,52 @@ describe('render', function () {
it('should render html', async function () {
const scope = new Context()
const token = { getContent: () => '<p>' } as HTMLToken
const html = await toThenable(render.renderTemplates([new HTML(token)], scope, new Emitter(scope.opts.keepOutputType)))
const html = await toThenable(render.renderTemplates([new HTML(token)], scope, new SimpleEmitter()))
return expect(html).to.equal('<p>')
})
})

describe('.renderTemplatesToNodeStream()', function () {
it('should render to html stream', function (done) {
const scope = new Context()
const tpls = [
new HTML({ getContent: () => '<p>' } as HTMLToken),
new HTML({ getContent: () => '</p>' } as HTMLToken)
]
const stream = render.renderTemplatesToNodeStream(tpls, scope)
let result = ''
stream.on('data', (data) => {
result += data
})
stream.on('end', () => {
expect(result).to.equal('<p></p>')
done()
})
})
it('should render to html stream asyncly', function (done) {
const scope = new Context()
const tpls = [
new HTML({ getContent: () => '<p>' } as HTMLToken),
new Tag({ content: 'foo', args: '', name: 'foo' } as TagToken, [], {
tags: {
get: () => ({
render: () => new Promise(
resolve => setTimeout(() => resolve('async tag'), 10)
)
})
}
} as any),
new HTML({ getContent: () => '</p>' } as HTMLToken)
]
const stream = render.renderTemplatesToNodeStream(tpls, scope)
let result = ''
stream.on('data', (data) => {
result += data
})
stream.on('end', () => {
expect(result).to.equal('<p>async tag</p>')
done()
})
})
})
})

0 comments on commit 9012133

Please sign in to comment.