From bef290923c559e81ef8a8eaff0accf90a1f74d5b Mon Sep 17 00:00:00 2001 From: harttle Date: Sun, 12 May 2019 20:03:32 +0800 Subject: [PATCH] fix: pass drops directly to filters/tags --- package.json | 2 +- rollup.config.ts | 21 ++++++++++++++---- src/builtin/filters/object.ts | 5 ++++- src/builtin/tags/for.ts | 4 ++-- src/builtin/tags/include.ts | 16 +++++--------- src/drop/blank-drop.ts | 5 ++--- src/drop/null-drop.ts | 4 ++-- src/render/syntax.ts | 11 ++++------ src/template/filter/filter.ts | 14 ++++++++---- src/template/output.ts | 6 +++--- src/template/tag/hash.ts | 4 ++-- src/template/value.ts | 4 ++-- src/util/underscore.ts | 6 ++++++ test/integration/builtin/tags/include.ts | 27 ++++++++++++++++++++++++ test/unit/template/filter/filter.ts | 6 ++++++ 15 files changed, 93 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index cc3370779c..500f8d8151 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "unit": "mocha \"test/unit/**/*.ts\"", "integration": "mocha \"test/integration/**/*.ts\"", "e2e": "npm run build && mocha \"test/e2e/**/*.ts\"", - "test": "npm run build && mocha \"test/**/*.ts\"", + "test": "BUNDLES=cjs npm run build && mocha \"test/**/*.ts\"", "benchmark": "ts-node benchmark", "coverage-html": "nyc --reporter=html mocha \"test/{unit,integration}/**/*.ts\"", "coverage-coveralls": "nyc mocha \"test/{unit,integration}/**/*.ts\" && nyc report --reporter=text-lcov | coveralls", diff --git a/rollup.config.ts b/rollup.config.ts index 2005ef08da..1dceba4d29 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -15,7 +15,7 @@ const treeshake = { } const input = './src/liquid.ts' -export default [{ +const cjs = { output: [{ file: 'dist/liquid.common.js', name: 'Liquid', @@ -36,7 +36,9 @@ export default [{ })], treeshake, input -}, { +} + +const umd = { output: [{ file: 'dist/liquid.js', name: 'Liquid', @@ -63,7 +65,9 @@ export default [{ ], treeshake, input -}, { +} + +const min = { output: [{ file: 'dist/liquid.min.js', name: 'Liquid', @@ -90,4 +94,13 @@ export default [{ ], treeshake, input -}] +} + +const bundles = [] +const env = process.env.BUNDLES || '' +if (env.includes('cjs')) bundles.push(cjs) +if (env.includes('umd')) bundles.push(umd) +if (env.includes('min')) bundles.push(min) +if (bundles.length === 0) bundles.push(cjs, umd, min) + +export default bundles diff --git a/src/builtin/filters/object.ts b/src/builtin/filters/object.ts index 4aa6c8ec15..34a462781d 100644 --- a/src/builtin/filters/object.ts +++ b/src/builtin/filters/object.ts @@ -1,5 +1,8 @@ import { isFalsy } from '../../render/syntax' +import { toValue } from '../../util/underscore' export default { - 'default': (v: string | T1, arg: T2): string | T1 | T2 => isFalsy(v) || v === '' ? arg : v + 'default': function (v: string | T1, arg: T2): string | T1 | T2 { + return isFalsy(toValue(v)) || v === '' ? arg : v + } } diff --git a/src/builtin/tags/for.ts b/src/builtin/tags/for.ts index fb7430355f..dcd427c4d2 100644 --- a/src/builtin/tags/for.ts +++ b/src/builtin/tags/for.ts @@ -1,5 +1,5 @@ import { isString, isObject, isArray } from '../../util/underscore' -import { evalExp } from '../../render/syntax' +import { parseExp } from '../../render/syntax' import assert from '../../util/assert' import { identifier, value, hash } from '../../parser/lexical' import TagToken from '../../parser/tag-token' @@ -42,7 +42,7 @@ export default { stream.start() }, render: async function (ctx: Context, hash: Hash) { - let collection = await evalExp(this.collection, ctx) + let collection = await parseExp(this.collection, ctx) if (!isArray(collection)) { if (isString(collection) && collection.length > 0) { diff --git a/src/builtin/tags/include.ts b/src/builtin/tags/include.ts index 0413b833bd..f0c34f2d17 100644 --- a/src/builtin/tags/include.ts +++ b/src/builtin/tags/include.ts @@ -1,6 +1,6 @@ import assert from '../../util/assert' import { value, quotedLine } from '../../parser/lexical' -import { evalValue } from '../../render/syntax' +import { evalValue, parseValue } from '../../render/syntax' import BlockMode from '../../context/block-mode' import TagToken from '../../parser/tag-token' import Context from '../../context/context' @@ -13,19 +13,13 @@ 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] - } + if (match) this.staticValue = match[0] match = value.exec(token.args) - if (match) { - this.value = match[0] - } + if (match) this.value = match[0] match = withRE.exec(token.args) - if (match) { - this.with = match[1] - } + if (match) this.with = match[1] }, render: async function (ctx: Context, hash: Hash) { let filepath @@ -47,7 +41,7 @@ export default { ctx.setRegister('blocks', {}) ctx.setRegister('blockMode', BlockMode.OUTPUT) if (this.with) { - hash[filepath] = await evalValue(this.with, ctx) + hash[filepath] = await parseValue(this.with, ctx) } const templates = await this.liquid.getTemplate(filepath, ctx.opts) ctx.push(hash) diff --git a/src/drop/blank-drop.ts b/src/drop/blank-drop.ts index 24234b026e..23713e0d13 100644 --- a/src/drop/blank-drop.ts +++ b/src/drop/blank-drop.ts @@ -1,11 +1,10 @@ -import { isNil, isString } from '../util/underscore' -import { Drop } from '../drop/drop' +import { isNil, isString, toValue } from '../util/underscore' import { EmptyDrop } from '../drop/empty-drop' export class BlankDrop extends EmptyDrop { equals (value: any) { if (value === false) return true - if (isNil(value instanceof Drop ? value.valueOf() : value)) return true + if (isNil(toValue(value))) return true if (isString(value)) return /^\s*$/.test(value) return super.equals(value) } diff --git a/src/drop/null-drop.ts b/src/drop/null-drop.ts index 5475b99eb7..239b923019 100644 --- a/src/drop/null-drop.ts +++ b/src/drop/null-drop.ts @@ -1,11 +1,11 @@ import { Drop } from './drop' import { IComparable } from './icomparable' -import { isNil } from '../util/underscore' +import { isNil, toValue } from '../util/underscore' import { BlankDrop } from '../drop/blank-drop' export class NullDrop extends Drop implements IComparable { equals (value: any) { - return isNil(value instanceof Drop ? value.valueOf() : value) || value instanceof BlankDrop + return isNil(toValue(value)) || value instanceof BlankDrop } gt () { return false diff --git a/src/render/syntax.ts b/src/render/syntax.ts index d694760f6f..361aa9535a 100644 --- a/src/render/syntax.ts +++ b/src/render/syntax.ts @@ -1,12 +1,11 @@ import * as lexical from '../parser/lexical' import assert from '../util/assert' import Context from '../context/context' -import { range, last, isFunction } from '../util/underscore' +import { range, last, isFunction, toValue } from '../util/underscore' import { isComparable } from '../drop/icomparable' import { NullDrop } from '../drop/null-drop' import { EmptyDrop } from '../drop/empty-drop' import { BlankDrop } from '../drop/blank-drop' -import { Drop } from '../drop/drop' const binaryOperators: {[key: string]: (lhs: any, rhs: any) => boolean} = { '==': (l: any, r: any) => { @@ -71,11 +70,10 @@ export async function parseExp (exp: string, ctx: Context): Promise { } export async function evalExp (str: string, ctx: Context): Promise { - const value = await parseExp(str, ctx) - return value instanceof Drop ? value.valueOf() : value + return toValue(await parseExp(str, ctx)) } -async function parseValue (str: string | undefined, ctx: Context): Promise { +export async function parseValue (str: string | undefined, ctx: Context): Promise { if (!str) return null str = str.trim() @@ -90,8 +88,7 @@ async function parseValue (str: string | undefined, ctx: Context): Promise } export async function evalValue (str: string | undefined, ctx: Context) { - const value = await parseValue(str, ctx) - return value instanceof Drop ? value.valueOf() : value + return toValue(await parseValue(str, ctx)) } export function isTruthy (val: any): boolean { diff --git a/src/template/filter/filter.ts b/src/template/filter/filter.ts index 70a60a6089..faae475f7b 100644 --- a/src/template/filter/filter.ts +++ b/src/template/filter/filter.ts @@ -1,9 +1,11 @@ -import { evalValue } from '../../render/syntax' +import { parseValue } from '../../render/syntax' import Context from '../../context/context' import { isArray } from '../../util/underscore' import { FilterImplOptions } from './filter-impl-options' -export type FilterArgs = Array +type KeyValuePair = [string?, string?] +type FilterArg = string|KeyValuePair +export type FilterArgs = Array export class Filter { name: string @@ -22,8 +24,8 @@ export class Filter { async render (value: any, context: Context) { const argv: any[] = [] for (const arg of this.args) { - if (isArray(arg)) argv.push([arg[0], await evalValue(arg[1], context)]) - else argv.push(await evalValue(arg, context)) + if (isKeyValuePair(arg)) argv.push([arg[0], await parseValue(arg[1], context)]) + else argv.push(await parseValue(arg, context)) } return this.impl.apply({ context }, [value, ...argv]) } @@ -34,3 +36,7 @@ export class Filter { Filter.impls = {} } } + +function isKeyValuePair (arr: FilterArg): arr is KeyValuePair { + return isArray(arr) +} diff --git a/src/template/output.ts b/src/template/output.ts index e4730a7b8b..60b31b7d57 100644 --- a/src/template/output.ts +++ b/src/template/output.ts @@ -1,5 +1,5 @@ import Value from './value' -import { stringify } from '../util/underscore' +import { stringify, toValue } from '../util/underscore' import Template from '../template/template' import ITemplate from '../template/itemplate' import Context from '../context/context' @@ -12,7 +12,7 @@ export default class Output extends Template implements ITemplate { this.value = new Value(token.value, strictFilters) } async render (ctx: Context): Promise { - const html = await this.value.value(ctx) - return stringify(html) + const val = await this.value.value(ctx) + return stringify(toValue(val)) } } diff --git a/src/template/tag/hash.ts b/src/template/tag/hash.ts index 65c100e517..f95c63fca8 100644 --- a/src/template/tag/hash.ts +++ b/src/template/tag/hash.ts @@ -1,5 +1,5 @@ import { hashCapture } from '../../parser/lexical' -import { evalValue } from '../../render/syntax' +import { parseValue } from '../../render/syntax' import Context from '../../context/context' /** @@ -17,7 +17,7 @@ export default class Hash { while ((match = hashCapture.exec(markup))) { const k = match[1] const v = match[2] - instance[k] = await evalValue(v, ctx) + instance[k] = await parseValue(v, ctx) } return instance } diff --git a/src/template/value.ts b/src/template/value.ts index cfa064c9f8..3e25c7e636 100644 --- a/src/template/value.ts +++ b/src/template/value.ts @@ -1,4 +1,4 @@ -import { evalExp } from '../render/syntax' +import { parseExp } from '../render/syntax' import { FilterArgs, Filter } from './filter/filter' import Context from '../context/context' @@ -48,7 +48,7 @@ export default class Value { this.filters.push(new Filter(name, args, this.strictFilters)) } async value (ctx: Context) { - let val = await evalExp(this.initial, ctx) + let val = await parseExp(this.initial, ctx) for (const filter of this.filters) { val = await filter.render(val, ctx) } diff --git a/src/util/underscore.ts b/src/util/underscore.ts index 8612e459a5..40eccfda40 100644 --- a/src/util/underscore.ts +++ b/src/util/underscore.ts @@ -1,3 +1,5 @@ +import { Drop } from '../drop/drop' + const toStr = Object.prototype.toString /* @@ -31,6 +33,10 @@ export function stringify (value: any): string { return String(value) } +export function toValue (value: any): any { + return value instanceof Drop ? value.valueOf() : value +} + export function toLiquid (value: any): any { if (isFunction(value.toLiquid)) return toLiquid(value.toLiquid()) return value diff --git a/test/integration/builtin/tags/include.ts b/test/integration/builtin/tags/include.ts index dc8787b73e..83531b2b28 100644 --- a/test/integration/builtin/tags/include.ts +++ b/test/integration/builtin/tags/include.ts @@ -83,6 +83,33 @@ describe('tags/include', function () { const html = await liquid.renderFile('with.html') return expect(html).to.equal('color:red, shape:rect') }) + it('should support include: with as Liquid Drop', async function () { + class ColorDrop extends Liquid.Types.Drop { + valueOf (): string { + return 'red!' + } + } + mock({ + '/with.html': '{% include "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 include: with passed as Liquid Drop', async function () { + class ColorDrop extends Liquid.Types.Drop { + valueOf (): string { + return 'red!' + } + } + liquid.registerFilter('name', x => x.constructor.name) + mock({ + '/with.html': '{% include "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 includes', async function () { mock({ diff --git a/test/unit/template/filter/filter.ts b/test/unit/template/filter/filter.ts index 94948f2e29..7b2cd292ed 100644 --- a/test/unit/template/filter/filter.ts +++ b/test/unit/template/filter/filter.ts @@ -49,6 +49,12 @@ describe('filter', function () { expect(await new Filter('add', ['2', '"c"'], false).render(3, ctx)).to.equal('5c') }) + it('should pass Objects/Drops as it is', async function () { + Filter.register('name', a => a.constructor.name) + class Foo {} + expect(await new Filter('name', [], false).render(new Foo(), ctx)).to.equal('Foo') + }) + it('should not throw when filter name illegal', function () { expect(function () { new Filter('/', [], false)