diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b95bab7a53..0cf31d6874 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,5 @@ name: Release -on: - push: - branches: - - master +on: workflow_dispatch jobs: release: name: Release diff --git a/README.md b/README.md index 50194dbee7..cf3848945f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/harttle/liquidjs) -A simple, expressive, safe and [Shopify][shopify/liquid] compatible template engine in pure JavaScript. +A simple, expressive and safe [Shopify][shopify/liquid] / Github Pages compatible template engine in pure JavaScript. **The purpose of this repo** is to provide a standard Liquid implementation for the JavaScript community so that [Jekyll sites](https://jekyllrb.com), [Github Pages](https://pages.github.com/) and [Shopify templates](https://themes.shopify.com/) can be ported to Node.js without pain. * [Documentation][doc] diff --git a/docs/_config.yml b/docs/_config.yml index d95e3f6420..56f2b00cb0 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,6 +1,6 @@ title: LiquidJS subtitle: "A simple, expressive and safe template engine." -description: "LiquidJS is a simple, expressive and safe template engine." +description: "LiquidJS is a simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript." author: Harttle language: en timezone: UTC diff --git a/docs/source/index.pug b/docs/source/index.pug index 0f444f7ed3..7766699008 100644 --- a/docs/source/index.pug +++ b/docs/source/index.pug @@ -1,5 +1,5 @@ layout: index -description: LiquidJS is a simple, expressive and safe template engine. +description: LiquidJS is a simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript. subtitle: A simple, expressive and safe template engine. --- ul#intro-feature-list diff --git a/docs/source/tutorials/intro-to-liquid.md b/docs/source/tutorials/intro-to-liquid.md index 8e83cccb98..3d52aef634 100644 --- a/docs/source/tutorials/intro-to-liquid.md +++ b/docs/source/tutorials/intro-to-liquid.md @@ -2,7 +2,7 @@ title: Introduction to Liquid Template Language --- -LiquidJS is a simple, expressive, safe and shopify compatible template engine in pure JavaScript. The purpose of this repo is to provide a standard Liquid implementation for the JavaScript community. Liquid is originally implemented in Ruby and used by Github Pages, Jekyll and Shopify, see [Differences with Shopify/liquid][diff]. +LiquidJS is a simple, expressive and safe [Shopify][shopify/liquid] / Github Pages compatible template engine in pure JavaScript. The purpose of this repo is to provide a standard Liquid implementation for the JavaScript community. Liquid is originally implemented in Ruby and used by Github Pages, Jekyll and Shopify, see [Differences with Shopify/liquid][diff]. LiquidJS syntax is relatively simple. There're 2 types of markups in LiquidJS: diff --git a/docs/source/zh-cn/index.pug b/docs/source/zh-cn/index.pug index 2a22e4ff5d..5a5e74b502 100644 --- a/docs/source/zh-cn/index.pug +++ b/docs/source/zh-cn/index.pug @@ -1,5 +1,5 @@ layout: index -description: LiquidJS is a simple, expressive and safe template engine. +description: LiquidJS 是一个纯 JavaScript 实现的,简洁的、安全的模板引擎,兼容 Shopify / Github Pages。 subtitle: 简单安全的 Liquid 模板引擎 --- ul#intro-feature-list diff --git a/package.json b/package.json index 413f64ced2..02bc2995f2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "liquidjs", "version": "9.25.1", - "description": "A simple, expressive, safe and Shopify compatible template engine in pure JavaScript.", + "description": "A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.", "main": "dist/liquid.node.cjs.js", "module": "dist/liquid.node.esm.js", "es2015": "dist/liquid.browser.esm.js", diff --git a/src/builtin/tags/break.ts b/src/builtin/tags/break.ts index c224f3899e..bfa8450c9f 100644 --- a/src/builtin/tags/break.ts +++ b/src/builtin/tags/break.ts @@ -2,6 +2,6 @@ import { Emitter, Context } from '../../types' export default { render: function (ctx: Context, emitter: Emitter) { - emitter.break = true + emitter['break'] = true } } diff --git a/src/builtin/tags/continue.ts b/src/builtin/tags/continue.ts index 89450340ee..1f5b28cd7e 100644 --- a/src/builtin/tags/continue.ts +++ b/src/builtin/tags/continue.ts @@ -2,6 +2,6 @@ import { Emitter, Context } from '../../types' export default { render: function (ctx: Context, emitter: Emitter) { - emitter.continue = true + emitter['continue'] = true } } diff --git a/src/builtin/tags/for.ts b/src/builtin/tags/for.ts index e477b579a1..3f1000d336 100644 --- a/src/builtin/tags/for.ts +++ b/src/builtin/tags/for.ts @@ -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() diff --git a/src/builtin/tags/include.ts b/src/builtin/tags/include.ts index 6f7d5be3cc..a51808f2cb 100644 --- a/src/builtin/tags/include.ts +++ b/src/builtin/tags/include.ts @@ -1,14 +1,14 @@ -import { assert, evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types' +import { assert, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types' import BlockMode from '../../context/block-mode' +import { parseFilePath, renderFilePath } from './render' export default { + parseFilePath, + renderFilePath, parse: function (token: TagToken) { const args = token.args const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie) - this.file = this.liquid.options.dynamicPartials - ? tokenizer.readValue() - : tokenizer.readFileName() - assert(this.file, () => `illegal argument "${token.args}"`) + this['file'] = this.parseFilePath(tokenizer, this.liquid) const begin = tokenizer.p const withStr = tokenizer.readIdentifier() @@ -22,15 +22,10 @@ export default { this.hash = new Hash(tokenizer.remaining()) }, render: function * (ctx: Context, emitter: Emitter) { - const { liquid, hash, withVar, file } = this + const { liquid, hash, withVar } = this const { renderer } = liquid - // TODO try move all liquid.parse calls into parse() section - const filepath = ctx.opts.dynamicPartials - ? (TypeGuards.isQuotedToken(file) - ? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx) - : yield evalToken(file, ctx)) - : file.getText() - assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`) + const filepath = yield this.renderFilePath(this['file'], ctx, liquid) + assert(filepath, () => `illegal filename "${filepath}"`) const saved = ctx.saveRegister('blocks', 'blockMode') ctx.setRegister('blocks', {}) diff --git a/src/builtin/tags/layout.ts b/src/builtin/tags/layout.ts index 3b12ec6796..3a35eda745 100644 --- a/src/builtin/tags/layout.ts +++ b/src/builtin/tags/layout.ts @@ -1,31 +1,27 @@ -import { assert, evalQuotedToken, TypeGuards, evalToken, Tokenizer, Emitter, Hash, TagToken, TopLevelToken, Context, TagImplOptions } from '../../types' +import { assert, Tokenizer, Emitter, Hash, TagToken, TopLevelToken, Context, TagImplOptions } from '../../types' import BlockMode from '../../context/block-mode' +import { parseFilePath, renderFilePath } from './render' export default { + parseFilePath, + renderFilePath, parse: function (token: TagToken, remainTokens: TopLevelToken[]) { const tokenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie) - const file = this.liquid.options.dynamicPartials ? tokenizer.readValue() : tokenizer.readFileName() - assert(file, () => `illegal argument "${token.args}"`) - - this.file = file + this['file'] = this.parseFilePath(tokenizer, this.liquid) this.hash = new Hash(tokenizer.remaining()) this.tpls = this.liquid.parser.parse(remainTokens) }, render: function * (ctx: Context, emitter: Emitter) { const { liquid, hash, file } = this const { renderer } = liquid - if (file.getText() === 'none') { + if (file === null) { ctx.setRegister('blockMode', BlockMode.OUTPUT) const html = yield renderer.renderTemplates(this.tpls, ctx) emitter.write(html) return } - const filepath = ctx.opts.dynamicPartials - ? (TypeGuards.isQuotedToken(file) - ? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx) - : evalToken(this.file, ctx)) - : file.getText() - assert(filepath, () => `file "${file.getText()}"("${filepath}") not available`) + const filepath = yield this.renderFilePath(this['file'], ctx, liquid) + assert(filepath, () => `illegal filename "${filepath}"`) const templates = yield liquid.parseFileImpl(filepath, ctx.sync) // render remaining contents and store rendered results diff --git a/src/builtin/tags/render.ts b/src/builtin/tags/render.ts index 26bb142bf1..e281d66f70 100644 --- a/src/builtin/tags/render.ts +++ b/src/builtin/tags/render.ts @@ -1,16 +1,16 @@ import { assert } from '../../util/assert' import { ForloopDrop } from '../../drop/forloop-drop' import { toEnumerable } from '../../util/collection' -import { evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types' +import { Liquid } from '../../liquid' +import { Token, Template, evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types' export default { + parseFilePath, + renderFilePath, parse: function (token: TagToken) { const args = token.args const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie) - this.file = this.liquid.options.dynamicPartials - ? tokenizer.readValue() - : tokenizer.readFileName() - assert(this.file, () => `illegal argument "${token.args}"`) + this['file'] = this.parseFilePath(tokenizer, this.liquid) while (!tokenizer.end()) { tokenizer.skipBlank() @@ -40,14 +40,9 @@ export default { this.hash = new Hash(tokenizer.remaining()) }, render: function * (ctx: Context, emitter: Emitter) { - const { liquid, file, hash } = this - const { renderer } = liquid - const filepath = ctx.opts.dynamicPartials - ? (TypeGuards.isQuotedToken(file) - ? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx) - : evalToken(file, ctx)) - : file.getText() - assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`) + const { liquid, hash } = this + const filepath = yield this.renderFilePath(this['file'], ctx, liquid) + assert(filepath, () => `illegal filename "${filepath}"`) const childCtx = new Context({}, ctx.opts, ctx.sync) const scope = yield hash.render(ctx) @@ -65,12 +60,47 @@ export default { for (const item of collection) { scope[alias] = item const templates = yield liquid.parseFileImpl(filepath, childCtx.sync) - yield renderer.renderTemplates(templates, childCtx, emitter) + yield liquid.renderer.renderTemplates(templates, childCtx, emitter) scope.forloop.next() } } else { const templates = yield liquid.parseFileImpl(filepath, childCtx.sync) - yield renderer.renderTemplates(templates, childCtx, emitter) + yield liquid.renderer.renderTemplates(templates, childCtx, emitter) } } } as TagImplOptions + +type ParsedFileName = Template[] | Token | string | undefined + +/** + * @return null for "none", + * @return Template[] for quoted with tags and/or filters + * @return Token for expression (not quoted) + * @throws TypeError if cannot read next token + */ +export function parseFilePath (tokenizer: Tokenizer, liquid: Liquid): ParsedFileName | null { + if (liquid.options.dynamicPartials) { + const file = tokenizer.readValue() + if (file === undefined) throw new TypeError(`illegal argument "${tokenizer.input}"`) + if (file.getText() === 'none') return null + // for filenames like "files/{{file}}", eval as liquid template + if (TypeGuards.isQuotedToken(file)) { + const tpls = liquid.parse(evalQuotedToken(file)) + // for filenames like "files/file.liquid", extract the string directly + if (tpls.length === 1) { + const first = tpls[0] + if (TypeGuards.isHTMLToken(first)) return first.getText() + } + return tpls + } + return file + } + const filepath = tokenizer.readFileName().getText() + return filepath === 'none' ? null : filepath +} + +export function renderFilePath (file: ParsedFileName, ctx: Context, liquid: Liquid) { + if (typeof file === 'string') return file + if (Array.isArray(file)) return liquid.renderer.renderTemplates(file, ctx) + return evalToken(file, ctx) +} diff --git a/src/emitters/emitter.ts b/src/emitters/emitter.ts new file mode 100644 index 0000000000..381141d555 --- /dev/null +++ b/src/emitters/emitter.ts @@ -0,0 +1,4 @@ +export interface Emitter { + write (html: any): void; + end (): void; +} diff --git a/src/emitters/keeping-type-emitter.ts b/src/emitters/keeping-type-emitter.ts new file mode 100644 index 0000000000..ac488ffd1b --- /dev/null +++ b/src/emitters/keeping-type-emitter.ts @@ -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 + } +} diff --git a/src/emitters/simple-emitter.ts b/src/emitters/simple-emitter.ts new file mode 100644 index 0000000000..f96265b784 --- /dev/null +++ b/src/emitters/simple-emitter.ts @@ -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 + } +} diff --git a/src/emitters/streamed-emitter.ts b/src/emitters/streamed-emitter.ts new file mode 100644 index 0000000000..1fb9fb72ed --- /dev/null +++ b/src/emitters/streamed-emitter.ts @@ -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() + } +} diff --git a/src/liquid-options.ts b/src/liquid-options.ts index 5a9eee7055..f65905065a 100644 --- a/src/liquid-options.ts +++ b/src/liquid-options.ts @@ -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; diff --git a/src/liquid.ts b/src/liquid.ts index af5b1e43f7..975d3ddf3d 100644 --- a/src/liquid.ts +++ b/src/liquid.ts @@ -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' @@ -24,7 +23,7 @@ export class Liquid { public parser: Parser public filters: FilterMap public tags: TagMap - private parseFileImpl: (file: string, sync?: boolean) => Iterator + public parseFileImpl: (file: string, sync?: boolean) => Iterator public constructor (opts: LiquidOptions = {}) { this.options = applyDefault(normalize(opts)) @@ -45,8 +44,7 @@ export class Liquid { public _render (tpl: Template[], scope?: object, sync?: boolean): IterableIterator { 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 { return toPromise(this._render(tpl, scope, false)) @@ -54,6 +52,10 @@ export class Liquid { 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 { const tpl = this.parse(html) diff --git a/src/parser/tokenizer.ts b/src/parser/tokenizer.ts index 21cebfcddb..45c21fec2e 100644 --- a/src/parser/tokenizer.ts +++ b/src/parser/tokenizer.ts @@ -31,7 +31,7 @@ export class Tokenizer { private rawBeginAt = -1 constructor ( - private input: string, + public input: string, private trie: Trie, private file: string = '' ) { diff --git a/src/render/emitter.ts b/src/render/emitter.ts index eb4d4a33b4..40ddece04d 100644 --- a/src/render/emitter.ts +++ b/src/render/emitter.ts @@ -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(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 (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; } diff --git a/src/render/expression.ts b/src/render/expression.ts index 9c2b6a8949..f1efd51834 100644 --- a/src/render/expression.ts +++ b/src/render/expression.ts @@ -47,10 +47,9 @@ export function evalToken (token: Token | undefined, ctx: Context, lenient = fal } function evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean) { - const variable = token.getVariableAsText() const props: string[] = token.props.map(prop => evalToken(prop, ctx, false)) try { - return ctx.get([variable, ...props]) + return ctx.get([token.propertyName, ...props]) } catch (e) { if (lenient && e.name === 'InternalUndefinedVariableError') return null throw (new UndefinedVariableError(e, token)) diff --git a/src/render/render.ts b/src/render/render.ts index 3fa32cd992..5a452cfd64 100644 --- a/src/render/render.ts +++ b/src/render/render.ts @@ -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 { 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() } } diff --git a/src/template/html.ts b/src/template/html.ts index faf0d22662..50cfdb8b12 100644 --- a/src/template/html.ts +++ b/src/template/html.ts @@ -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 implements Template { private str: string diff --git a/src/template/output.ts b/src/template/output.ts index 1622f862b7..9b7eed85f9 100644 --- a/src/template/output.ts +++ b/src/template/output.ts @@ -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' diff --git a/src/template/tag/tag-impl-options.ts b/src/template/tag/tag-impl-options.ts index 432db13724..7a826b5e0d 100644 --- a/src/template/tag/tag-impl-options.ts +++ b/src/template/tag/tag-impl-options.ts @@ -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; diff --git a/src/template/template.ts b/src/template/template.ts index 1764c5877c..f210ba7044 100644 --- a/src/template/template.ts +++ b/src/template/template.ts @@ -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; diff --git a/src/tokens/property-access-token.ts b/src/tokens/property-access-token.ts index e0ed70b851..44262bbb59 100644 --- a/src/tokens/property-access-token.ts +++ b/src/tokens/property-access-token.ts @@ -5,19 +5,15 @@ import { TokenKind } from '../parser/token-kind' import { parseStringLiteral } from '../parser/parse-string-literal' export class PropertyAccessToken extends Token { + public propertyName: string constructor ( public variable: IdentifierToken | QuotedToken, public props: (IdentifierToken | QuotedToken | PropertyAccessToken)[], end: number ) { super(TokenKind.PropertyAccess, variable.input, variable.begin, end, variable.file) - } - - getVariableAsText () { - if (this.variable instanceof IdentifierToken) { - return this.variable.getText() - } else { - return parseStringLiteral(this.variable.getText()) - } + this.propertyName = this.variable instanceof IdentifierToken + ? this.variable.getText() + : parseStringLiteral(this.variable.getText()) } } diff --git a/src/types.ts b/src/types.ts index eec43dae9c..dbe2b1deb8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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' diff --git a/src/util/underscore.ts b/src/util/underscore.ts index f5b1adc8f9..e57ef74831 100644 --- a/src/util/underscore.ts +++ b/src/util/underscore.ts @@ -3,13 +3,8 @@ import { Drop } from '../drop/drop' const toStr = Object.prototype.toString const toLowerCase = String.prototype.toLowerCase -/* - * Checks if value is classified as a String primitive or object. - * @param {any} value The value to check. - * @return {Boolean} Returns true if value is a string, else false. - */ export function isString (value: any): value is string { - return toStr.call(value) === '[object String]' + return typeof value === 'string' } export function isFunction (value: any): value is Function { @@ -30,7 +25,9 @@ export function promisify (fn: any) { export function stringify (value: any): string { value = toValue(value) - return isNil(value) ? '' : String(value) + if (isString(value)) return value + if (isNil(value)) return '' + return String(value) } export function toValue (value: any): any { @@ -47,7 +44,7 @@ export function toLiquid (value: any): any { } export function isNil (value: any): boolean { - return value === null || value === undefined + return value == null } export function isArray (value: any): value is any[] { diff --git a/test/integration/builtin/tags/include.ts b/test/integration/builtin/tags/include.ts index fb9263b917..a40d947431 100644 --- a/test/integration/builtin/tags/include.ts +++ b/test/integration/builtin/tags/include.ts @@ -44,7 +44,7 @@ describe('tags/include', function () { }) return liquid.renderFile('/parent.html').catch(function (e) { expect(e.name).to.equal('RenderError') - expect(e.message).to.match(/illegal filename "not-exist"/) + expect(e.message).to.match(/illegal filename "undefined"/) }) }) diff --git a/test/integration/builtin/tags/layout.ts b/test/integration/builtin/tags/layout.ts index fe539a0735..4fbd21c4e0 100644 --- a/test/integration/builtin/tags/layout.ts +++ b/test/integration/builtin/tags/layout.ts @@ -38,7 +38,7 @@ describe('tags/layout', function () { }) return liquid.renderFile('/parent.html').catch(function (e) { expect(e.name).to.equal('RenderError') - expect(e.message).to.contain('file "foo"("undefined") not available') + expect(e.message).to.contain('illegal filename "undefined"') }) }) it('should handle layout none', async function () { diff --git a/test/integration/builtin/tags/render.ts b/test/integration/builtin/tags/render.ts index fa2a5a3c71..d7314786ad 100644 --- a/test/integration/builtin/tags/render.ts +++ b/test/integration/builtin/tags/render.ts @@ -44,7 +44,7 @@ describe('tags/render', function () { }) return liquid.renderFile('/parent.html').catch(function (e) { expect(e.name).to.equal('RenderError') - expect(e.message).to.match(/illegal filename "not-exist":"undefined"/) + expect(e.message).to.match(/illegal filename "undefined"/) }) }) diff --git a/test/integration/util/error.ts b/test/integration/util/error.ts index 2e91658649..3f4ef2b9c1 100644 --- a/test/integration/util/error.ts +++ b/test/integration/util/error.ts @@ -269,9 +269,8 @@ describe('error', function () { .to.throw(RenderError, /intended render error/) }) it('should contain original error info for {% include %}', function () { - const origin = ['1st', '2nd', '3rd', 'X{%throwingTag%} Y', '5th', '6th', '7th'] mock({ - '/throwing-tag.html': origin.join('\n') + '/throwing-tag.html': ['1st', '2nd', '3rd', 'X{%throwingTag%} Y', '5th', '6th', '7th'].join('\n') }) const html = '{%include "throwing-tag.html"%}' const message = [ diff --git a/test/unit/render/render.ts b/test/unit/render/render.ts index 4dfef1eafb..ee4316a595 100644 --- a/test/unit/render/render.ts +++ b/test/unit/render/render.ts @@ -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 @@ -16,8 +18,52 @@ 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, new Emitter(scope.opts.keepOutputType))) + const html = await toThenable(render.renderTemplates([new HTML(token)], scope, new SimpleEmitter())) return expect(html).to.equal('

') }) }) + + describe('.renderTemplatesToNodeStream()', function () { + it('should render to html stream', function (done) { + const scope = new Context() + const tpls = [ + new HTML({ getContent: () => '

' } as HTMLToken), + new HTML({ getContent: () => '

' } as HTMLToken) + ] + const stream = render.renderTemplatesToNodeStream(tpls, scope) + let result = '' + stream.on('data', (data) => { + result += data + }) + stream.on('end', () => { + expect(result).to.equal('

') + done() + }) + }) + it('should render to html stream asyncly', function (done) { + const scope = new Context() + const tpls = [ + new HTML({ getContent: () => '

' } 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: () => '

' } as HTMLToken) + ] + const stream = render.renderTemplatesToNodeStream(tpls, scope) + let result = '' + stream.on('data', (data) => { + result += data + }) + stream.on('end', () => { + expect(result).to.equal('

async tag

') + done() + }) + }) + }) }) diff --git a/test/unit/tokens/property-access-token.ts b/test/unit/tokens/property-access-token.ts index 16524e1bb6..536034aa3e 100644 --- a/test/unit/tokens/property-access-token.ts +++ b/test/unit/tokens/property-access-token.ts @@ -8,14 +8,14 @@ chai.use(sinonChai) const expect = chai.expect describe('PropertyAccessToken', function () { - describe('getVariableAsText', function () { + describe('#propertyName', function () { it('should return correct value for IdentifierToken', function () { const token = new PropertyAccessToken(new IdentifierToken('foo', 0, 3), [], 3) - expect(token.getVariableAsText()).to.equal('foo') + expect(token.propertyName).to.equal('foo') }) it('should return correct value for QuotedToken', function () { const token = new PropertyAccessToken(new QuotedToken('"foo bar"', 0, 9), [], 9) - expect(token.getVariableAsText()).to.equal('foo bar') + expect(token.propertyName).to.equal('foo bar') }) }) })