feat: move filters/tags to instances, fixes #188

This commit is contained in:
harttle
2020-03-03 00:02:29 +08:00
parent f6762078e0
commit df8a919f71
47 changed files with 240 additions and 221 deletions
+1
View File
@@ -19,6 +19,7 @@
"no-unused-vars": "off", "no-unused-vars": "off",
"indent": "off", "indent": "off",
"no-dupe-class-members": "off", "no-dupe-class-members": "off",
"no-useless-constructor": "off",
"@typescript-eslint/indent": ["error", 2], "@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
+16 -16
View File
@@ -1,6 +1,6 @@
{ {
"name": "liquidjs", "name": "liquidjs",
"version": "9.6.2", "version": "9.8.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -905,9 +905,9 @@
"dev": true "dev": true
}, },
"@types/body-parser": { "@types/body-parser": {
"version": "1.17.0", "version": "1.19.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/connect": "*", "@types/connect": "*",
@@ -930,9 +930,9 @@
} }
}, },
"@types/connect": { "@types/connect": {
"version": "3.4.32", "version": "3.4.33",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
"integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "*" "@types/node": "*"
@@ -963,9 +963,9 @@
"dev": true "dev": true
}, },
"@types/express": { "@types/express": {
"version": "4.17.0", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.0.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.2.tgz",
"integrity": "sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==", "integrity": "sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/body-parser": "*", "@types/body-parser": "*",
@@ -974,9 +974,9 @@
} }
}, },
"@types/express-serve-static-core": { "@types/express-serve-static-core": {
"version": "4.16.7", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.7.tgz", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.2.tgz",
"integrity": "sha512-847KvL8Q1y3TtFLRTXcVakErLJQgdpFSaq+k043xefz9raEf0C7HalpSY7OW5PyjCnY8P7bPW5t/Co9qqp+USg==", "integrity": "sha512-El9yMpctM6tORDAiBwZVLMcxoTMcqqRO9dVyYcn7ycLWbvR8klrDn8CAOwRfZujZtWD7yS/mshTdz43jMOejbg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "*", "@types/node": "*",
@@ -1060,9 +1060,9 @@
"dev": true "dev": true
}, },
"@types/serve-static": { "@types/serve-static": {
"version": "1.13.2", "version": "1.13.3",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz",
"integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/express-serve-static-core": "*", "@types/express-serve-static-core": "*",
+2 -1
View File
@@ -66,7 +66,7 @@
"@types/benchmark": "^1.0.31", "@types/benchmark": "^1.0.31",
"@types/chai": "^4.1.7", "@types/chai": "^4.1.7",
"@types/chai-as-promised": "^7.1.0", "@types/chai-as-promised": "^7.1.0",
"@types/express": "^4.16.1", "@types/express": "^4.17.2",
"@types/jsdom": "^12.2.2", "@types/jsdom": "^12.2.2",
"@types/mocha": "^5.2.6", "@types/mocha": "^5.2.6",
"@types/sinon": "^7.0.6", "@types/sinon": "^7.0.6",
@@ -118,6 +118,7 @@
{ {
"assets": [ "assets": [
"package.json", "package.json",
"package-lock.json",
"docs", "docs",
"CHANGELOG.md" "CHANGELOG.md"
], ],
+2 -2
View File
@@ -1,6 +1,6 @@
import { assert } from '../../util/assert' import { assert } from '../../util/assert'
import { identifier } from '../../parser/lexical' import { identifier } from '../../parser/lexical'
import { ITagImplOptions, TagToken, Context } from '../../types' import { TagImplOptions, TagToken, Context } from '../../types'
const re = new RegExp(`(${identifier.source})\\s*=([^]*)`) const re = new RegExp(`(${identifier.source})\\s*=([^]*)`)
@@ -14,4 +14,4 @@ export default {
render: function * (ctx: Context) { render: function * (ctx: Context) {
ctx.front()[this.key] = yield this.liquid._evalValue(this.value, ctx) ctx.front()[this.key] = yield this.liquid._evalValue(this.value, ctx)
} }
} as ITagImplOptions } as TagImplOptions
+4 -4
View File
@@ -1,14 +1,14 @@
import BlockMode from '../../context/block-mode' import BlockMode from '../../context/block-mode'
import { ParseStream, TagToken, Token, ITemplate, Context, ITagImplOptions, Emitter, Hash } from '../../types' import { ParseStream, TagToken, Token, Template, Context, TagImplOptions, Emitter, Hash } from '../../types'
export default { export default {
parse: function (token: TagToken, remainTokens: Token[]) { parse: function (token: TagToken, remainTokens: Token[]) {
const match = /\w+/.exec(token.args) const match = /\w+/.exec(token.args)
this.block = match ? match[0] : '' this.block = match ? match[0] : ''
this.tpls = [] as ITemplate[] this.tpls = [] as Template[]
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens) const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('tag:endblock', () => stream.stop()) .on('tag:endblock', () => stream.stop())
.on('template', (tpl: ITemplate) => this.tpls.push(tpl)) .on('template', (tpl: Template) => this.tpls.push(tpl))
.on('end', () => { .on('end', () => {
throw new Error(`tag ${token.raw} not closed`) throw new Error(`tag ${token.raw} not closed`)
}) })
@@ -28,4 +28,4 @@ export default {
} }
emitter.write(html) emitter.write(html)
} }
} as ITagImplOptions } as TagImplOptions
+3 -3
View File
@@ -1,6 +1,6 @@
import { assert } from '../../util/assert' import { assert } from '../../util/assert'
import { identifier } from '../../parser/lexical' import { identifier } from '../../parser/lexical'
import { ITemplate, Context, ITagImplOptions, TagToken, Token } from '../../types' import { Template, Context, TagImplOptions, TagToken, Token } from '../../types'
const re = new RegExp(`(${identifier.source})`) const re = new RegExp(`(${identifier.source})`)
@@ -14,7 +14,7 @@ export default {
const stream = this.liquid.parser.parseStream(remainTokens) const stream = this.liquid.parser.parseStream(remainTokens)
stream.on('tag:endcapture', () => stream.stop()) stream.on('tag:endcapture', () => stream.stop())
.on('template', (tpl: ITemplate) => this.templates.push(tpl)) .on('template', (tpl: Template) => this.templates.push(tpl))
.on('end', () => { .on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`) throw new Error(`tag ${tagToken.raw} not closed`)
}) })
@@ -25,4 +25,4 @@ export default {
const html = yield r.renderTemplates(this.templates, ctx) const html = yield r.renderTemplates(this.templates, ctx)
ctx.front()[this.variable] = html ctx.front()[this.variable] = html
} }
} as ITagImplOptions } as TagImplOptions
+4 -4
View File
@@ -1,4 +1,4 @@
import { Expression, Hash, Emitter, TagToken, Token, Context, ITemplate, ITagImplOptions, ParseStream } from '../../types' import { Expression, Hash, Emitter, TagToken, Token, Context, Template, TagImplOptions, ParseStream } from '../../types'
export default { export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) { parse: function (tagToken: TagToken, remainTokens: Token[]) {
@@ -6,7 +6,7 @@ export default {
this.cases = [] this.cases = []
this.elseTemplates = [] this.elseTemplates = []
let p: ITemplate[] = [] let p: Template[] = []
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens) const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('tag:when', (token: TagToken) => { .on('tag:when', (token: TagToken) => {
this.cases.push({ this.cases.push({
@@ -16,7 +16,7 @@ export default {
}) })
.on('tag:else', () => (p = this.elseTemplates)) .on('tag:else', () => (p = this.elseTemplates))
.on('tag:endcase', () => stream.stop()) .on('tag:endcase', () => stream.stop())
.on('template', (tpl: ITemplate) => p.push(tpl)) .on('template', (tpl: Template) => p.push(tpl))
.on('end', () => { .on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`) throw new Error(`tag ${tagToken.raw} not closed`)
}) })
@@ -37,4 +37,4 @@ export default {
} }
yield r.renderTemplates(this.elseTemplates, ctx, emitter) yield r.renderTemplates(this.elseTemplates, ctx, emitter)
} }
} as ITagImplOptions } as TagImplOptions
+2 -2
View File
@@ -1,6 +1,6 @@
import { TagToken } from '../../parser/tag-token' import { TagToken } from '../../parser/tag-token'
import { Token } from '../../parser/token' import { Token } from '../../parser/token'
import { ITagImplOptions } from '../../template/tag/itag-impl-options' import { TagImplOptions } from '../../template/tag/tag-impl-options'
export default { export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) { parse: function (tagToken: TagToken, remainTokens: Token[]) {
@@ -14,4 +14,4 @@ export default {
}) })
stream.start() stream.start()
} }
} as ITagImplOptions } as TagImplOptions
+2 -2
View File
@@ -1,6 +1,6 @@
import { assert } from '../../util/assert' import { assert } from '../../util/assert'
import { value as rValue } from '../../parser/lexical' import { value as rValue } from '../../parser/lexical'
import { Emitter, Expression, TagToken, Context, ITagImplOptions, Hash } from '../../types' import { Emitter, Expression, TagToken, Context, TagImplOptions, Hash } from '../../types'
const groupRE = new RegExp(`^(?:(${rValue.source})\\s*:\\s*)?(.*)$`) const groupRE = new RegExp(`^(?:(${rValue.source})\\s*:\\s*)?(.*)$`)
const candidatesRE = new RegExp(rValue.source, 'g') const candidatesRE = new RegExp(rValue.source, 'g')
@@ -37,4 +37,4 @@ export default {
const html = yield new Expression(candidate).value(ctx) const html = yield new Expression(candidate).value(ctx)
emitter.write(html) emitter.write(html)
} }
} as ITagImplOptions } as TagImplOptions
+2 -2
View File
@@ -1,6 +1,6 @@
import { assert } from '../../util/assert' import { assert } from '../../util/assert'
import { identifier } from '../../parser/lexical' import { identifier } from '../../parser/lexical'
import { Emitter, TagToken, Context, ITagImplOptions, Hash } from '../../types' import { Emitter, TagToken, Context, TagImplOptions, Hash } from '../../types'
import { isNumber, stringify } from '../../util/underscore' import { isNumber, stringify } from '../../util/underscore'
export default { export default {
@@ -16,4 +16,4 @@ export default {
} }
emitter.write(stringify(--scope[this.variable])) emitter.write(stringify(--scope[this.variable]))
} }
} as ITagImplOptions } as TagImplOptions
+3 -3
View File
@@ -1,4 +1,4 @@
import { Emitter, TagToken, Token, Context, ITemplate, ITagImplOptions, ParseStream } from '../../types' import { Emitter, TagToken, Token, Context, Template, TagImplOptions, ParseStream } from '../../types'
import { isString, isObject, isArray } from '../../util/underscore' import { isString, isObject, isArray } from '../../util/underscore'
import { Expression } from '../../render/expression' import { Expression } from '../../render/expression'
import { assert } from '../../util/assert' import { assert } from '../../util/assert'
@@ -29,7 +29,7 @@ export default {
.on('start', () => (p = this.templates)) .on('start', () => (p = this.templates))
.on('tag:else', () => (p = this.elseTemplates)) .on('tag:else', () => (p = this.elseTemplates))
.on('tag:endfor', () => stream.stop()) .on('tag:endfor', () => stream.stop())
.on('template', (tpl: ITemplate) => p.push(tpl)) .on('template', (tpl: Template) => p.push(tpl))
.on('end', () => { .on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`) throw new Error(`tag ${tagToken.raw} not closed`)
}) })
@@ -72,4 +72,4 @@ export default {
} }
ctx.pop() ctx.pop()
} }
} as ITagImplOptions } as TagImplOptions
+3 -3
View File
@@ -1,4 +1,4 @@
import { Hash, Emitter, isTruthy, Expression, TagToken, Token, Context, ITemplate, ITagImplOptions, ParseStream } from '../../types' import { Hash, Emitter, isTruthy, Expression, TagToken, Token, Context, Template, TagImplOptions, ParseStream } from '../../types'
export default { export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) { parse: function (tagToken: TagToken, remainTokens: Token[]) {
@@ -19,7 +19,7 @@ export default {
}) })
.on('tag:else', () => (p = this.elseTemplates)) .on('tag:else', () => (p = this.elseTemplates))
.on('tag:endif', () => stream.stop()) .on('tag:endif', () => stream.stop())
.on('template', (tpl: ITemplate) => p.push(tpl)) .on('template', (tpl: Template) => p.push(tpl))
.on('end', () => { .on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`) throw new Error(`tag ${tagToken.raw} not closed`)
}) })
@@ -39,4 +39,4 @@ export default {
} }
yield r.renderTemplates(this.elseTemplates, ctx, emitter) yield r.renderTemplates(this.elseTemplates, ctx, emitter)
} }
} as ITagImplOptions } as TagImplOptions
+2 -2
View File
@@ -1,5 +1,5 @@
import { assert } from '../../util/assert' import { assert } from '../../util/assert'
import { Expression, Hash, Emitter, TagToken, Context, ITagImplOptions } from '../../types' import { Expression, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
import { value, quotedLine } from '../../parser/lexical' import { value, quotedLine } from '../../parser/lexical'
import BlockMode from '../../context/block-mode' import BlockMode from '../../context/block-mode'
@@ -46,4 +46,4 @@ export default {
ctx.setRegister('blocks', originBlocks) ctx.setRegister('blocks', originBlocks)
ctx.setRegister('blockMode', originBlockMode) ctx.setRegister('blockMode', originBlockMode)
} }
} as ITagImplOptions } as TagImplOptions
+2 -2
View File
@@ -1,7 +1,7 @@
import { assert } from '../../util/assert' import { assert } from '../../util/assert'
import { identifier } from '../../parser/lexical' import { identifier } from '../../parser/lexical'
import { isNumber, stringify } from '../../util/underscore' import { isNumber, stringify } from '../../util/underscore'
import { Emitter, TagToken, Context, ITagImplOptions, Hash } from '../../types' import { Emitter, TagToken, Context, TagImplOptions, Hash } from '../../types'
export default { export default {
parse: function (token: TagToken) { parse: function (token: TagToken) {
@@ -18,4 +18,4 @@ export default {
scope[this.variable]++ scope[this.variable]++
emitter.write(stringify(val)) emitter.write(stringify(val))
} }
} as ITagImplOptions } as TagImplOptions
+2 -2
View File
@@ -16,9 +16,9 @@ import tablerow from './tablerow'
import unless from './unless' import unless from './unless'
import Break from './break' import Break from './break'
import Continue from './continue' import Continue from './continue'
import { ITagImplOptions } from '../../template/tag/itag-impl-options' import { TagImplOptions } from '../../template/tag/tag-impl-options'
const tags: { [key: string]: ITagImplOptions } = { const tags: { [key: string]: TagImplOptions } = {
assign, 'for': For, capture, 'case': Case, comment, include, render, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue assign, 'for': For, capture, 'case': Case, comment, include, render, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue
} }
+2 -2
View File
@@ -1,6 +1,6 @@
import { assert } from '../../util/assert' import { assert } from '../../util/assert'
import { value as rValue } from '../../parser/lexical' import { value as rValue } from '../../parser/lexical'
import { Emitter, Hash, Expression, TagToken, Token, Context, ITagImplOptions } from '../../types' import { Emitter, Hash, Expression, TagToken, Token, Context, TagImplOptions } from '../../types'
import BlockMode from '../../context/block-mode' import BlockMode from '../../context/block-mode'
const staticFileRE = /\S+/ const staticFileRE = /\S+/
@@ -40,4 +40,4 @@ export default {
ctx.pop() ctx.pop()
emitter.write(partial) emitter.write(partial)
} }
} as ITagImplOptions } as TagImplOptions
+2 -2
View File
@@ -1,4 +1,4 @@
import { TagToken, Token, ITagImplOptions } from '../../types' import { TagToken, Token, TagImplOptions } from '../../types'
export default { export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) { parse: function (tagToken: TagToken, remainTokens: Token[]) {
@@ -18,4 +18,4 @@ export default {
render: function () { render: function () {
return this.tokens.map((token: Token) => token.raw).join('') return this.tokens.map((token: Token) => token.raw).join('')
} }
} as ITagImplOptions } as TagImplOptions
+2 -2
View File
@@ -1,5 +1,5 @@
import { assert } from '../../util/assert' import { assert } from '../../util/assert'
import { Expression, Hash, Emitter, TagToken, Context, ITagImplOptions } from '../../types' import { Expression, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
import { value, quotedLine } from '../../parser/lexical' import { value, quotedLine } from '../../parser/lexical'
import BlockMode from '../../context/block-mode' import BlockMode from '../../context/block-mode'
@@ -47,4 +47,4 @@ export default {
childCtx.setRegister('blocks', originBlocks) childCtx.setRegister('blocks', originBlocks)
childCtx.setRegister('blockMode', originBlockMode) childCtx.setRegister('blockMode', originBlockMode)
} }
} as ITagImplOptions } as TagImplOptions
+3 -3
View File
@@ -1,5 +1,5 @@
import { assert } from '../../util/assert' import { assert } from '../../util/assert'
import { Expression, Emitter, Hash, TagToken, Token, Context, ITemplate, ITagImplOptions, ParseStream } from '../../types' import { Expression, Emitter, Hash, TagToken, Token, Context, Template, TagImplOptions, ParseStream } from '../../types'
import { identifier, value, hash } from '../../parser/lexical' import { identifier, value, hash } from '../../parser/lexical'
import { TablerowloopDrop } from '../../drop/tablerowloop-drop' import { TablerowloopDrop } from '../../drop/tablerowloop-drop'
@@ -20,7 +20,7 @@ export default {
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens) const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates)) .on('start', () => (p = this.templates))
.on('tag:endtablerow', () => stream.stop()) .on('tag:endtablerow', () => stream.stop())
.on('template', (tpl: ITemplate) => p.push(tpl)) .on('template', (tpl: Template) => p.push(tpl))
.on('end', () => { .on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`) throw new Error(`tag ${tagToken.raw} not closed`)
}) })
@@ -54,4 +54,4 @@ export default {
if (collection.length) emitter.write('</tr>') if (collection.length) emitter.write('</tr>')
ctx.pop() ctx.pop()
} }
} as ITagImplOptions } as TagImplOptions
+2 -2
View File
@@ -1,4 +1,4 @@
import { Emitter, Expression, isFalsy, ParseStream, Context, ITagImplOptions, Token, Hash, TagToken } from '../../types' import { Emitter, Expression, isFalsy, ParseStream, Context, TagImplOptions, Token, Hash, TagToken } from '../../types'
export default { export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) { parse: function (tagToken: TagToken, remainTokens: Token[]) {
@@ -27,4 +27,4 @@ export default {
? r.renderTemplates(this.templates, ctx, emitter) ? r.renderTemplates(this.templates, ctx, emitter)
: r.renderTemplates(this.elseTemplates, ctx, emitter)) : r.renderTemplates(this.elseTemplates, ctx, emitter))
} }
} as ITagImplOptions } as TagImplOptions
+22 -18
View File
@@ -1,16 +1,16 @@
import { Context } from './context/context' import { Context } from './context/context'
import fs from './fs/node' import fs from './fs/node'
import * as _ from './util/underscore' import * as _ from './util/underscore'
import { ITemplate } from './template/itemplate' import { Template } from './template/template'
import { Tokenizer } from './parser/tokenizer' import { Tokenizer } from './parser/tokenizer'
import { Render } from './render/render' import { Render } from './render/render'
import { Tag } from './template/tag/tag'
import { Filter } from './template/filter/filter'
import Parser from './parser/parser' import Parser from './parser/parser'
import { ITagImplOptions } from './template/tag/itag-impl-options' import { TagImplOptions } from './template/tag/tag-impl-options'
import { Value } from './template/value' import { Value } from './template/value'
import builtinTags from './builtin/tags' import builtinTags from './builtin/tags'
import builtinFilters from './builtin/filters' import builtinFilters from './builtin/filters'
import { TagMap } from './template/tag/tag-map'
import { FilterMap } from './template/filter/filter-map'
import { LiquidOptions, normalizeStringArray, NormalizedFullOptions, applyDefault, normalize } from './liquid-options' import { LiquidOptions, normalizeStringArray, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
import { FilterImplOptions } from './template/filter/filter-impl-options' import { FilterImplOptions } from './template/filter/filter-impl-options'
import IFS from './fs/ifs' import IFS from './fs/ifs'
@@ -22,6 +22,8 @@ export class Liquid {
public options: NormalizedFullOptions public options: NormalizedFullOptions
public renderer: Render public renderer: Render
public parser: Parser public parser: Parser
public filters: FilterMap
public tags: TagMap
private cache: object = {} private cache: object = {}
private tokenizer: Tokenizer private tokenizer: Tokenizer
private fs: IFS private fs: IFS
@@ -32,24 +34,26 @@ export class Liquid {
this.renderer = new Render() this.renderer = new Render()
this.tokenizer = new Tokenizer(this.options) this.tokenizer = new Tokenizer(this.options)
this.fs = opts.fs || fs this.fs = opts.fs || fs
this.filters = new FilterMap(this.options.strictFilters)
this.tags = new TagMap()
_.forOwn(builtinTags, (conf, name) => this.registerTag(name, conf)) _.forOwn(builtinTags, (conf, name) => this.registerTag(name, conf))
_.forOwn(builtinFilters, (handler, name) => this.registerFilter(name, handler)) _.forOwn(builtinFilters, (handler, name) => this.registerFilter(name, handler))
} }
public parse (html: string, filepath?: string): ITemplate[] { public parse (html: string, filepath?: string): Template[] {
const tokens = this.tokenizer.tokenize(html, filepath) const tokens = this.tokenizer.tokenize(html, filepath)
return this.parser.parse(tokens) return this.parser.parse(tokens)
} }
public _render (tpl: ITemplate[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<string> { public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<string> {
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) return this.renderer.renderTemplates(tpl, ctx)
} }
public async render (tpl: ITemplate[], scope?: object, opts?: LiquidOptions): Promise<string> { public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise<string> {
return toThenable(this._render(tpl, scope, opts, false)) return toThenable(this._render(tpl, scope, opts, false))
} }
public renderSync (tpl: ITemplate[], scope?: object, opts?: LiquidOptions): string { public renderSync (tpl: Template[], scope?: object, opts?: LiquidOptions): string {
return toValue(this._render(tpl, scope, opts, true)) return toValue(this._render(tpl, scope, opts, true))
} }
@@ -80,10 +84,10 @@ export class Liquid {
} }
throw this.lookupError(file, options.root) throw this.lookupError(file, options.root)
} }
public async parseFile (file: string, opts?: LiquidOptions): Promise<ITemplate[]> { public async parseFile (file: string, opts?: LiquidOptions): Promise<Template[]> {
return toThenable(this._parseFile(file, opts, false)) return toThenable(this._parseFile(file, opts, false))
} }
public parseFileSync (file: string, opts?: LiquidOptions): ITemplate[] { public parseFileSync (file: string, opts?: LiquidOptions): Template[] {
return toValue(this._parseFile(file, opts, true)) return toValue(this._parseFile(file, opts, true))
} }
public async renderFile (file: string, ctx?: object, opts?: LiquidOptions) { public async renderFile (file: string, ctx?: object, opts?: LiquidOptions) {
@@ -97,7 +101,7 @@ export class Liquid {
} }
public _evalValue (str: string, ctx: Context): IterableIterator<any> { public _evalValue (str: string, ctx: Context): IterableIterator<any> {
const value = new Value(str, this.options.strictFilters) const value = new Value(str, this.filters)
return value.value(ctx) return value.value(ctx)
} }
public async evalValue (str: string, ctx: Context): Promise<any> { public async evalValue (str: string, ctx: Context): Promise<any> {
@@ -108,19 +112,19 @@ export class Liquid {
} }
public registerFilter (name: string, filter: FilterImplOptions) { public registerFilter (name: string, filter: FilterImplOptions) {
return Filter.register(name, filter) this.filters.set(name, filter)
} }
public registerTag (name: string, tag: ITagImplOptions) { public registerTag (name: string, tag: TagImplOptions) {
return Tag.register(name, tag) this.tags.set(name, tag)
} }
public plugin (plugin: (this: Liquid, L: typeof Liquid) => void) { public plugin (plugin: (this: Liquid, L: typeof Liquid) => void) {
return plugin.call(this, Liquid) return plugin.call(this, Liquid)
} }
public express () { public express () {
const self = this // eslint-disable-line const self = this // eslint-disable-line
return function (this: any, filePath: string, ctx: object, cb: (err: Error | null, html?: string) => void) { return function (this: any, filePath: string, ctx: object, callback: (err: Error | null, rendered: string) => void) {
const opts = { root: [...normalizeStringArray(this.root), ...self.options.root] } const opts = { root: [...normalizeStringArray(this.root), ...self.options.root] }
self.renderFile(filePath, ctx, opts).then(html => cb(null, html), cb) self.renderFile(filePath, ctx, opts).then(html => callback(null, html) as any, callback as any)
} }
} }
@@ -134,13 +138,13 @@ export class Liquid {
/** /**
* @deprecated use parseFile instead * @deprecated use parseFile instead
*/ */
public async getTemplate (file: string, opts?: LiquidOptions): Promise<ITemplate[]> { public async getTemplate (file: string, opts?: LiquidOptions): Promise<Template[]> {
return this.parseFile(file, opts) return this.parseFile(file, opts)
} }
/** /**
* @deprecated use parseFileSync instead * @deprecated use parseFileSync instead
*/ */
public getTemplateSync (file: string, opts?: LiquidOptions): ITemplate[] { public getTemplateSync (file: string, opts?: LiquidOptions): Template[] {
return this.parseFileSync(file, opts) return this.parseFileSync(file, opts)
} }
} }
+4 -4
View File
@@ -1,8 +1,8 @@
import { Token } from '../parser/token' import { Token } from '../parser/token'
import { ITemplate } from '../template/itemplate' import { Template } from '../template/template'
import { TagToken } from './tag-token' import { TagToken } from './tag-token'
type ParseToken = ((token: Token, remainTokens: Token[]) => ITemplate) type ParseToken = ((token: Token, remainTokens: Token[]) => Template)
export class ParseStream { export class ParseStream {
private tokens: Token[] private tokens: Token[]
@@ -14,11 +14,11 @@ export class ParseStream {
this.tokens = tokens this.tokens = tokens
this.parseToken = parseToken this.parseToken = parseToken
} }
public on<T extends ITemplate | Token | undefined> (name: string, cb: (arg: T) => void): ParseStream { public on<T extends Template | Token | undefined> (name: string, cb: (arg: T) => void): ParseStream {
this.handlers[name] = cb this.handlers[name] = cb
return this return this
} }
private trigger <T extends Token | ITemplate> (event: string, arg?: T) { private trigger <T extends Token | Template> (event: string, arg?: T) {
const h = this.handlers[event] const h = this.handlers[event]
return h ? (h(arg), true) : false return h ? (h(arg), true) : false
} }
+3 -3
View File
@@ -7,7 +7,7 @@ import { OutputToken } from './output-token'
import { Tag } from '../template/tag/tag' import { Tag } from '../template/tag/tag'
import { Output } from '../template/output' import { Output } from '../template/output'
import { HTML } from '../template/html' import { HTML } from '../template/html'
import { ITemplate } from '../template/itemplate' import { Template } from '../template/template'
export default class Parser { export default class Parser {
private liquid: Liquid private liquid: Liquid
@@ -17,7 +17,7 @@ export default class Parser {
} }
public parse (tokens: Token[]) { public parse (tokens: Token[]) {
let token let token
const templates: ITemplate[] = [] const templates: Template[] = []
while ((token = tokens.shift())) { while ((token = tokens.shift())) {
templates.push(this.parseToken(token, tokens)) templates.push(this.parseToken(token, tokens))
} }
@@ -29,7 +29,7 @@ export default class Parser {
return new Tag(token, remainTokens, this.liquid) return new Tag(token, remainTokens, this.liquid)
} }
if (OutputToken.is(token)) { if (OutputToken.is(token)) {
return new Output(token as OutputToken, this.liquid.options.strictFilters) return new Output(token as OutputToken, this.liquid.filters)
} }
return new HTML(token) return new HTML(token)
} catch (e) { } catch (e) {
+2 -2
View File
@@ -1,10 +1,10 @@
import { RenderError } from '../util/error' import { RenderError } from '../util/error'
import { Context } from '../context/context' import { Context } from '../context/context'
import { ITemplate } from '../template/itemplate' import { Template } from '../template/template'
import { Emitter } from './emitter' import { Emitter } from './emitter'
export class Render { export class Render {
public * renderTemplates (templates: ITemplate[], ctx: Context, emitter = new Emitter()): IterableIterator<string> { public * renderTemplates (templates: Template[], ctx: Context, emitter = new Emitter()): IterableIterator<string> {
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)
+23
View File
@@ -0,0 +1,23 @@
import { FilterImplOptions } from './filter-impl-options'
import { Filter, FilterArgs } from './filter'
import { assert } from '../../util/assert'
export class FilterMap {
private impls: {[key: string]: FilterImplOptions} = {}
constructor (private readonly strictFilters: boolean) {}
get (name: string) {
const impl = this.impls[name]
assert(impl || !this.strictFilters, `undefined filter: ${name}`)
return impl
}
set (name: string, impl: FilterImplOptions) {
this.impls[name] = impl
}
create (name: string, args: FilterArgs) {
return new Filter(name, this.get(name), args)
}
}
+1 -11
View File
@@ -11,12 +11,8 @@ export class Filter {
public name: string public name: string
public args: FilterArgs public args: FilterArgs
private impl: FilterImplOptions private impl: FilterImplOptions
private static impls: {[key: string]: FilterImplOptions} = {}
public constructor (name: string, args: FilterArgs, strictFilters: boolean) {
const impl = Filter.impls[name]
if (!impl && strictFilters) throw new TypeError(`undefined filter: ${name}`)
public constructor (name: string, impl: FilterImplOptions, args: FilterArgs) {
this.name = name this.name = name
this.impl = impl || identify this.impl = impl || identify
this.args = args this.args = args
@@ -29,12 +25,6 @@ export class Filter {
} }
return this.impl.apply({ context }, [value, ...argv]) return this.impl.apply({ context }, [value, ...argv])
} }
public static register (name: string, filter: FilterImplOptions) {
Filter.impls[name] = filter
}
public static clear () {
Filter.impls = {}
}
} }
function isKeyValuePair (arr: FilterArg): arr is KeyValuePair { function isKeyValuePair (arr: FilterArg): arr is KeyValuePair {
+2 -2
View File
@@ -1,10 +1,10 @@
import { TemplateImpl } from '../template/template-impl'
import { Template } from '../template/template' import { Template } from '../template/template'
import { ITemplate } from '../template/itemplate'
import { HTMLToken } from '../parser/html-token' import { HTMLToken } from '../parser/html-token'
import { Context } from '../context/context' import { Context } from '../context/context'
import { Emitter } from '../render/emitter' import { Emitter } from '../render/emitter'
export class HTML extends Template<HTMLToken> implements ITemplate { export class HTML extends TemplateImpl<HTMLToken> implements Template {
private str: string private str: string
public constructor (token: HTMLToken) { public constructor (token: HTMLToken) {
super(token) super(token)
-8
View File
@@ -1,8 +0,0 @@
import { Context } from '../context/context'
import { Token } from '../parser/token'
import { Emitter } from '../render/emitter'
export interface ITemplate {
token: Token;
render(ctx: Context, emitter: Emitter): any;
}
+5 -4
View File
@@ -1,16 +1,17 @@
import { Value } from './value' import { Value } from './value'
import { FilterMap } from './filter/filter-map'
import { stringify, toValue } from '../util/underscore' import { stringify, toValue } from '../util/underscore'
import { TemplateImpl } from '../template/template-impl'
import { Template } from '../template/template' import { Template } from '../template/template'
import { ITemplate } from '../template/itemplate'
import { Context } from '../context/context' import { Context } from '../context/context'
import { Emitter } from '../render/emitter' import { Emitter } from '../render/emitter'
import { OutputToken } from '../parser/output-token' import { OutputToken } from '../parser/output-token'
export class Output extends Template<OutputToken> implements ITemplate { export class Output extends TemplateImpl<OutputToken> implements Template {
private value: Value private value: Value
public constructor (token: OutputToken, strictFilters: boolean) { public constructor (token: OutputToken, filters: FilterMap) {
super(token) super(token)
this.value = new Value(token.value, strictFilters) this.value = new Value(token.value, filters)
} }
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)
-7
View File
@@ -1,7 +0,0 @@
import { Liquid } from '../../liquid'
import { ITagImplOptions } from './itag-impl-options'
export interface ITagImpl extends ITagImplOptions {
liquid: Liquid;
[key: string]: any;
}
@@ -1,11 +1,11 @@
import { Context } from '../../context/context' import { Context } from '../../context/context'
import { TagToken } from '../../parser/tag-token' import { TagToken } from '../../parser/tag-token'
import { Token } from '../../parser/token' import { Token } from '../../parser/token'
import { ITagImpl } from './itag-impl' import { TagImpl } from './tag-impl'
import { Hash } from '../../template/tag/hash' import { Hash } from '../../template/tag/hash'
import { Emitter } from '../../render/emitter' import { Emitter } from '../../render/emitter'
export interface ITagImplOptions { export interface TagImplOptions {
parse?: (this: ITagImpl, token: TagToken, remainingTokens: Token[]) => void; parse?: (this: TagImpl, token: TagToken, remainingTokens: Token[]) => void;
render: (this: ITagImpl, ctx: Context, hash: Hash, emitter: Emitter) => any; render: (this: TagImpl, ctx: Context, hash: Hash, emitter: Emitter) => any;
} }
+7
View File
@@ -0,0 +1,7 @@
import { Liquid } from '../../liquid'
import { TagImplOptions } from './tag-impl-options'
export interface TagImpl extends TagImplOptions {
liquid: Liquid;
[key: string]: any;
}
+16
View File
@@ -0,0 +1,16 @@
import { TagImplOptions } from './tag-impl-options'
import { assert } from '../../util/assert'
export class TagMap {
private impls: {[key: string]: TagImplOptions} = {}
get (name: string) {
const impl = this.impls[name]
assert(impl, `tag "${name}" not found`)
return impl
}
set (name: string, impl: TagImplOptions) {
this.impls[name] = impl
}
}
+7 -15
View File
@@ -1,21 +1,19 @@
import { isFunction } from '../../util/underscore' import { isFunction } from '../../util/underscore'
import { assert } from '../../util/assert'
import { Liquid } from '../../liquid' import { Liquid } from '../../liquid'
import { Template } from '../../template/template' import { TemplateImpl } from '../../template/template-impl'
import { Emitter, Hash, Context, ITagImplOptions, TagToken, ITemplate, Token } from '../../types' import { Emitter, Hash, Context, TagImplOptions, TagToken, Template, Token } from '../../types'
import { ITagImpl } from './itag-impl' import { TagImpl } from './tag-impl'
export class Tag extends Template<TagToken> implements ITemplate { export class Tag extends TemplateImpl<TagToken> implements Template {
public name: string public name: string
private impl: ITagImpl private impl: TagImpl
private static impls: { [key: string]: ITagImplOptions } = {} private static impls: { [key: string]: TagImplOptions } = {}
public constructor (token: TagToken, tokens: Token[], liquid: Liquid) { public constructor (token: TagToken, tokens: Token[], liquid: Liquid) {
super(token) super(token)
this.name = token.name this.name = token.name
const impl = Tag.impls[token.name] const impl = liquid.tags.get(token.name)
assert(impl, `tag ${token.name} not found`)
this.impl = Object.create(impl) this.impl = Object.create(impl)
this.impl.liquid = liquid this.impl.liquid = liquid
@@ -28,10 +26,4 @@ export class Tag extends Template<TagToken> implements ITemplate {
const impl = this.impl const impl = this.impl
if (isFunction(impl.render)) return yield impl.render(ctx, hash, emitter) if (isFunction(impl.render)) return yield impl.render(ctx, hash, emitter)
} }
public static register (name: string, tag: ITagImplOptions) {
Tag.impls[name] = tag
}
public static clear () {
Tag.impls = {}
}
} }
+6
View File
@@ -0,0 +1,6 @@
export abstract class TemplateImpl<T> {
public token: T;
public constructor (token: T) {
this.token = token
}
}
+7 -5
View File
@@ -1,6 +1,8 @@
export abstract class Template<T> { import { Context } from '../context/context'
public token: T; import { Token } from '../parser/token'
public constructor (token: T) { import { Emitter } from '../render/emitter'
this.token = token
} export interface Template {
token: Token;
render(ctx: Context, emitter: Emitter): any;
} }
+3 -4
View File
@@ -1,18 +1,17 @@
import { Expression } from '../render/expression' import { Expression } from '../render/expression'
import { FilterMap } from '../template/filter/filter-map'
import { FilterArgs, Filter } from './filter/filter' import { FilterArgs, Filter } from './filter/filter'
import { Context } from '../context/context' import { Context } from '../context/context'
export class Value { export class Value {
public readonly filters: Filter[] = [] public readonly filters: Filter[] = []
public readonly initial: string public readonly initial: string
private strictFilters: boolean
/** /**
* @param str value string, like: "i have a dream | truncate: 3 * @param str value string, like: "i have a dream | truncate: 3
*/ */
public constructor (str: string, strictFilters: boolean) { public constructor (str: string, private readonly filterMap: FilterMap) {
const tokens = Value.tokenize(str) const tokens = Value.tokenize(str)
this.strictFilters = strictFilters
this.initial = tokens[0] this.initial = tokens[0]
this.parseFilters(tokens, 1) this.parseFilters(tokens, 1)
} }
@@ -45,7 +44,7 @@ export class Value {
argValue = tokens[i] argValue = tokens[i]
} }
} }
this.filters.push(new Filter(name, args, this.strictFilters)) this.filters.push(new Filter(name, this.filterMap.get(name), args))
} }
public * value (ctx: Context) { public * value (ctx: Context) {
let val = yield new Expression(this.initial).evaluate(ctx) let val = yield new Expression(this.initial).evaluate(ctx)
+2 -2
View File
@@ -5,8 +5,8 @@ export { Expression } from './render/expression'
export { isFalsy, isTruthy } from './render/boolean' export { isFalsy, isTruthy } from './render/boolean'
export { TagToken } from './parser/tag-token' export { TagToken } from './parser/tag-token'
export { Context } from './context/context' export { Context } from './context/context'
export { ITemplate } from './template/itemplate' export { Template } from './template/template'
export { ITagImplOptions } from './template/tag/itag-impl-options' export { TagImplOptions } from './template/tag/tag-impl-options'
export { ParseStream } from './parser/parse-stream' export { ParseStream } from './parser/parse-stream'
export { Token } from './parser/token' export { Token } from './parser/token'
export { Hash } from './template/tag/hash' export { Hash } from './template/tag/hash'
+2 -2
View File
@@ -1,6 +1,6 @@
import * as _ from './underscore' import * as _ from './underscore'
import { Token } from '../parser/token' import { Token } from '../parser/token'
import { ITemplate } from '../template/itemplate' import { Template } from '../template/template'
abstract class LiquidError extends Error { abstract class LiquidError extends Error {
private token: Token private token: Token
@@ -37,7 +37,7 @@ export class ParseError extends LiquidError {
} }
export class RenderError extends LiquidError { export class RenderError extends LiquidError {
public constructor (err: Error, tpl: ITemplate) { public constructor (err: Error, tpl: Template) {
super(err, tpl.token) super(err, tpl.token)
this.name = 'RenderError' this.name = 'RenderError'
this.message = err.message this.message = err.message
+2 -2
View File
@@ -9,13 +9,13 @@ describe('liquid#registerFilter()', function () {
return JSON.stringify(args) return JSON.stringify(args)
}) })
it('should support object', async () => { it('should support object', async () => {
const src = `{{ "a" | obj_test: k1: "v1", k2: foo }}`, const src = `{{ "a" | obj_test: k1: "v1", k2: foo }}`
const dst = '["a",["k1","v1"],["k2","bar"]]' const dst = '["a",["k1","v1"],["k2","bar"]]'
const html = await liquid.parseAndRender(src, { foo: 'bar' }) const html = await liquid.parseAndRender(src, { foo: 'bar' })
return expect(html).to.equal(dst) return expect(html).to.equal(dst)
}) })
it('should support mixed object', async () => { it('should support mixed object', async () => {
const src = `{{ "a" | obj_test: "something", k1: "v1", k2: foo }}`, const src = `{{ "a" | obj_test: "something", k1: "v1", k2: foo }}`
const dst = '["a","something",["k1","v1"],["k2","bar"]]' const dst = '["a","something",["k1","v1"],["k2","bar"]]'
const html = await liquid.parseAndRender(src, { foo: 'bar' }) const html = await liquid.parseAndRender(src, { foo: 'bar' })
return expect(html).to.equal(dst) return expect(html).to.equal(dst)
+8 -8
View File
@@ -214,12 +214,12 @@ describe('error', function () {
const src = '{%if true%}\naaa{%endif%}\n{% -a %}\n3' const src = '{%if true%}\naaa{%endif%}\n{% -a %}\n3'
const err = await expect(engine.parseAndRender(src)).be.rejected const err = await expect(engine.parseAndRender(src)).be.rejected
expect(err.name).to.equal('ParseError') expect(err.name).to.equal('ParseError')
expect(err.message).to.contain('tag -a not found') expect(err.message).to.contain('tag "-a" not found')
}) })
it('should throw ParseError when tag not exist', async function () { it('should throw ParseError when tag not exist', async function () {
const err = await expect(engine.parseAndRender('{% a %}')).be.rejected const err = await expect(engine.parseAndRender('{% a %}')).be.rejected
expect(err.name).to.equal('ParseError') expect(err.name).to.equal('ParseError')
expect(err.message).to.contain('tag a not found') expect(err.message).to.contain('tag "a" not found')
}) })
it('should contain template context in err.stack', async function () { it('should contain template context in err.stack', async function () {
@@ -231,10 +231,10 @@ describe('error', function () {
' 5| 5th', ' 5| 5th',
' 6| 6th', ' 6| 6th',
' 7| 7th', ' 7| 7th',
'ParseError: tag a not found' 'ParseError: tag "a" not found'
] ]
const err = await expect(engine.parseAndRender(html.join('\n'))).be.rejected const err = await expect(engine.parseAndRender(html.join('\n'))).be.rejected
expect(err.message).to.equal('tag a not found, line:4, col:2') expect(err.message).to.equal('tag "a" not found, line:4, col:2')
expect(err.stack).to.contain(message.join('\n')) expect(err.stack).to.contain(message.join('\n'))
expect(err.name).to.equal('ParseError') expect(err.name).to.equal('ParseError')
}) })
@@ -246,10 +246,10 @@ describe('error', function () {
'>> 2| X{% a %} {% enda %} Y', '>> 2| X{% a %} {% enda %} Y',
' 3| 3rd', ' 3| 3rd',
' 4| 4th', ' 4| 4th',
'ParseError: tag a not found' 'ParseError: tag "a" not found'
] ]
const err = await expect(engine.parseAndRender(html.join('\n'))).be.rejected const err = await expect(engine.parseAndRender(html.join('\n'))).be.rejected
expect(err.message).to.equal('tag a not found, line:2, col:2') expect(err.message).to.equal('tag "a" not found, line:2, col:2')
expect(err.stack).to.contain(message.join('\n')) expect(err.stack).to.contain(message.join('\n'))
}) })
@@ -261,12 +261,12 @@ describe('error', function () {
it('should contain stack in err.stack', async function () { it('should contain stack in err.stack', async function () {
const err = await expect(engine.parseAndRender('{% -a %}')).be.rejected const err = await expect(engine.parseAndRender('{% -a %}')).be.rejected
expect(err.stack).to.contain('ParseError: tag -a not found') expect(err.stack).to.contain('ParseError: tag "-a" not found')
expect(err.stack).to.match(/at .*:\d+:\d+\)/) expect(err.stack).to.match(/at .*:\d+:\d+\)/)
}) })
}) })
describe('sync support', function () { describe('sync support', function () {
let engine let engine: Liquid
beforeEach(function () { beforeEach(function () {
engine = new Liquid({ engine = new Liquid({
root: '/' root: '/'
-4
View File
@@ -1,8 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { Context } from '../../../src/context/context' import { Context } from '../../../src/context/context'
import { Token } from '../../../src/parser/token' import { Token } from '../../../src/parser/token'
import { Tag } from '../../../src/template/tag/tag'
import { Filter } from '../../../src/template/filter/filter'
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 { toThenable } from '../../../src/util/async' import { toThenable } from '../../../src/util/async'
@@ -10,8 +8,6 @@ import { toThenable } from '../../../src/util/async'
describe('render', function () { describe('render', function () {
let render: Render let render: Render
before(function () { before(function () {
Filter.clear()
Tag.clear()
render = new Render() render = new Render()
}) })
+20 -19
View File
@@ -1,69 +1,70 @@
import * as chai from 'chai' import * as chai from 'chai'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai' import * as sinonChai from 'sinon-chai'
import { Filter } from '../../../../src/template/filter/filter'
import { Context } from '../../../../src/context/context' import { Context } from '../../../../src/context/context'
import { toThenable } from '../../../../src/util/async' import { toThenable } from '../../../../src/util/async'
import { FilterMap } from '../../../../src/template/filter/filter-map'
chai.use(sinonChai) chai.use(sinonChai)
const expect = chai.expect const expect = chai.expect
describe('filter', function () { describe('filter', function () {
let ctx: Context let ctx: Context
let filters: FilterMap
beforeEach(function () { beforeEach(function () {
Filter.clear() filters = new FilterMap(false)
ctx = new Context() ctx = new Context()
}) })
it('should create default filter if not registered', async function () { it('should create default filter if not registered', async function () {
const result = new Filter('foo', [], false) as any const result = filters.create('foo', []) as any
expect(result.name).to.equal('foo') expect(result.name).to.equal('foo')
}) })
it('should render input if filter not registered', async function () { it('should render input if filter not registered', async function () {
expect(await toThenable(new Filter('undefined', [], false).render('foo', ctx))).to.equal('foo') expect(await toThenable(filters.create('undefined', []).render('foo', ctx))).to.equal('foo')
}) })
it('should call filter impl with correct arguments', async function () { it('should call filter impl with correct arguments', async function () {
const spy = sinon.spy() const spy = sinon.spy()
Filter.register('foo', spy) filters.set('foo', spy)
await toThenable(new Filter('foo', ['33'], false).render('foo', ctx)) await toThenable(filters.create('foo', ['33']).render('foo', ctx))
expect(spy).to.have.been.calledWith('foo', 33) expect(spy).to.have.been.calledWith('foo', 33)
}) })
it('should call filter impl with correct this arg', async function () { it('should call filter impl with correct this arg', async function () {
const spy = sinon.spy() const spy = sinon.spy()
Filter.register('foo', spy) filters.set('foo', spy)
await toThenable(new Filter('foo', ['33'], false).render('foo', ctx)) await toThenable(filters.create('foo', ['33']).render('foo', ctx))
expect(spy).to.have.been.calledOn(sinon.match.has('context', ctx)) expect(spy).to.have.been.calledOn(sinon.match.has('context', ctx))
}) })
it('should render a simple filter', async function () { it('should render a simple filter', async function () {
Filter.register('upcase', x => x.toUpperCase()) filters.set('upcase', x => x.toUpperCase())
expect(await toThenable(new Filter('upcase', [], false).render('foo', ctx))).to.equal('FOO') expect(await toThenable(filters.create('upcase', []).render('foo', ctx))).to.equal('FOO')
}) })
it('should render filters with argument', async function () { it('should render filters with argument', async function () {
Filter.register('add', (a, b) => a + b) filters.set('add', (a, b) => a + b)
expect(await toThenable(new Filter('add', ['2'], false).render(3, ctx))).to.equal(5) expect(await toThenable(filters.create('add', ['2']).render(3, ctx))).to.equal(5)
}) })
it('should render filters with multiple arguments', async function () { it('should render filters with multiple arguments', async function () {
Filter.register('add', (a, b, c) => a + b + c) filters.set('add', (a, b, c) => a + b + c)
expect(await toThenable(new Filter('add', ['2', '"c"'], false).render(3, ctx))).to.equal('5c') expect(await toThenable(filters.create('add', ['2', '"c"']).render(3, ctx))).to.equal('5c')
}) })
it('should pass Objects/Drops as it is', async function () { it('should pass Objects/Drops as it is', async function () {
Filter.register('name', a => a.constructor.name) filters.set('name', a => a.constructor.name)
class Foo {} class Foo {}
expect(await toThenable(new Filter('name', [], false).render(new Foo(), ctx))).to.equal('Foo') expect(await toThenable(filters.create('name', []).render(new Foo(), ctx))).to.equal('Foo')
}) })
it('should not throw when filter name illegal', function () { it('should not throw when filter name illegal', function () {
expect(function () { expect(function () {
new Filter('/', [], false) filters.create('/', [])
}).to.not.throw() }).to.not.throw()
}) })
it('should support key value pairs', async function () { it('should support key value pairs', async function () {
Filter.register('add', (a, b) => b[0] + ':' + (a + b[1])) filters.set('add', (a, b) => b[0] + ':' + (a + b[1]))
expect(await toThenable((new Filter('add', [['num', '2']], false).render(3, ctx)))).to.equal('num:5') expect(await toThenable((filters.create('add', [['num', '2']]).render(3, ctx)))).to.equal('num:5')
}) })
}) })
+7 -6
View File
@@ -3,14 +3,15 @@ import { toThenable } from '../../../src/util/async'
import { Context } from '../../../src/context/context' import { Context } from '../../../src/context/context'
import { Output } from '../../../src/template/output' import { Output } from '../../../src/template/output'
import { OutputToken } from '../../../src/parser/output-token' import { OutputToken } from '../../../src/parser/output-token'
import { Filter } from '../../../src/template/filter/filter' import { FilterMap } from '../../../src/template/filter/filter-map'
const expect = chai.expect const expect = chai.expect
describe('Output', function () { describe('Output', function () {
const emitter: any = { write: (html: string) => (emitter.html += html), html: '' } const emitter: any = { write: (html: string) => (emitter.html += html), html: '' }
let filters: FilterMap
beforeEach(function () { beforeEach(function () {
Filter.clear() filters = new FilterMap(false)
emitter.html = '' emitter.html = ''
}) })
@@ -18,25 +19,25 @@ describe('Output', function () {
const scope = new Context({ const scope = new Context({
foo: { obj: { arr: ['a', 2] } } foo: { obj: { arr: ['a', 2] } }
}) })
const output = new Output({ value: 'foo' } as OutputToken, false) const output = new Output({ value: 'foo' } as OutputToken, filters)
await toThenable(output.render(scope, emitter)) await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('[object Object]') return expect(emitter.html).to.equal('[object Object]')
}) })
it('should skip function property', async function () { it('should skip function property', async function () {
const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } }) const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } })
const output = new Output({ value: 'obj' } as OutputToken, false) const output = new Output({ value: 'obj' } as OutputToken, filters)
await toThenable(output.render(scope, emitter)) await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('[object Object]') return expect(emitter.html).to.equal('[object Object]')
}) })
it('should respect to .toString()', async () => { it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } }) const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ value: 'obj' } as OutputToken, false) const output = new Output({ value: 'obj' } as OutputToken, filters)
await toThenable(output.render(scope, emitter)) await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('FOO') return expect(emitter.html).to.equal('FOO')
}) })
it('should respect to .toString()', async () => { it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } }) const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ value: 'obj' } as OutputToken, false) const output = new Output({ value: 'obj' } as OutputToken, filters)
await toThenable(output.render(scope, emitter)) await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('FOO') return expect(emitter.html).to.equal('FOO')
}) })
+7 -13
View File
@@ -9,10 +9,10 @@ import { toThenable } from '../../../src/util/async'
chai.use(sinonChai) chai.use(sinonChai)
const expect = chai.expect const expect = chai.expect
const liquid = new Liquid()
describe('Tag', function () { describe('Tag', function () {
let ctx: Context let ctx: Context
let liquid: Liquid
const emitter: any = { write: (html: string) => (emitter.html += html), html: '' } const emitter: any = { write: (html: string) => (emitter.html += html), html: '' }
before(function () { before(function () {
ctx = new Context({ ctx = new Context({
@@ -22,7 +22,7 @@ describe('Tag', function () {
coo: 'uoo' coo: 'uoo'
} }
}) })
Tag.clear() liquid = new Liquid()
}) })
beforeEach(function () { beforeEach(function () {
emitter.html = '' emitter.html = ''
@@ -33,24 +33,20 @@ describe('Tag', function () {
new Tag({ // eslint-disable-line new Tag({ // eslint-disable-line
type: 'tag', type: 'tag',
value: 'foo', value: 'foo',
name: 'foo' name: 'not-exist'
} as TagToken, [], liquid) } as TagToken, [], liquid)
}).to.throw(/tag foo not found/) }).to.throw(/tag "not-exist" not found/)
}) })
it('should register simple tag', function () { it('should register simple tag', function () {
expect(function () { expect(function () {
Tag.register('foo', { liquid.registerTag('foo', { render: () => 'bar' })
render: () => 'bar'
})
}).not.throw() }).not.throw()
}) })
it('should call tag.render', async function () { it('should call tag.render', async function () {
const spy = sinon.spy() const spy = sinon.spy()
Tag.register('foo', { liquid.registerTag('foo', { render: spy })
render: spy
})
const token = { const token = {
type: 'tag', type: 'tag',
value: 'foo', value: 'foo',
@@ -64,9 +60,7 @@ describe('Tag', function () {
let spy: sinon.SinonSpy, token: TagToken let spy: sinon.SinonSpy, token: TagToken
beforeEach(function () { beforeEach(function () {
spy = sinon.spy() spy = sinon.spy()
Tag.register('foo', { liquid.registerTag('foo', { render: spy })
render: spy
})
token = { token = {
type: 'tag', type: 'tag',
value: 'foo aa:foo bb: arr[0] cc: 2.3\ndd:bar.coo', value: 'foo aa:foo bb: arr[0] cc: 2.3\ndd:bar.coo',
+18 -18
View File
@@ -1,9 +1,9 @@
import * as chai from 'chai' import * as chai from 'chai'
import { toThenable } from '../../../src/util/async' import { toThenable } from '../../../src/util/async'
import { FilterMap } from '../../../src/template/filter/filter-map'
import * as sinonChai from 'sinon-chai' import * as sinonChai from 'sinon-chai'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import { Context } from '../../../src/context/context' import { Context } from '../../../src/context/context'
import { Filter } from '../../../src/template/filter/filter'
import { Value } from '../../../src/template/value' import { Value } from '../../../src/template/value'
chai.use(sinonChai) chai.use(sinonChai)
@@ -11,74 +11,73 @@ chai.use(sinonChai)
const expect = chai.expect const expect = chai.expect
describe('Value', function () { describe('Value', function () {
beforeEach(() => Filter.clear())
describe('#constructor()', function () { describe('#constructor()', function () {
const filterMap = new FilterMap(false)
it('should parse "foo', function () { it('should parse "foo', function () {
const tpl = new Value('foo', false) const tpl = new Value('foo', filterMap)
expect(tpl.initial).to.equal('foo') expect(tpl.initial).to.equal('foo')
expect(tpl.filters).to.deep.equal([]) expect(tpl.filters).to.deep.equal([])
}) })
it('should parse "foo | add"', function () { it('should parse "foo | add"', function () {
const tpl = new Value('foo | add', false) const tpl = new Value('foo | add', filterMap)
expect(tpl.initial).to.equal('foo') expect(tpl.initial).to.equal('foo')
expect(tpl.filters.length).to.equal(1) expect(tpl.filters.length).to.equal(1)
expect(tpl.filters[0].args).to.eql([]) expect(tpl.filters[0].args).to.eql([])
}) })
it('should parse "foo,foo | add"', function () { it('should parse "foo,foo | add"', function () {
const tpl = new Value('foo,foo | add', false) const tpl = new Value('foo,foo | add', filterMap)
expect(tpl.initial).to.equal('foo') expect(tpl.initial).to.equal('foo')
expect(tpl.filters.length).to.equal(1) expect(tpl.filters.length).to.equal(1)
expect(tpl.filters[0].args).to.eql([]) expect(tpl.filters[0].args).to.eql([])
}) })
it('should parse "foo | add: 3, false"', function () { it('should parse "foo | add: 3, false"', function () {
const tpl = new Value('foo | add: 3, "foo"', false) const tpl = new Value('foo | add: 3, "foo"', filterMap)
expect(tpl.initial).to.equal('foo') expect(tpl.initial).to.equal('foo')
expect(tpl.filters.length).to.equal(1) expect(tpl.filters.length).to.equal(1)
expect(tpl.filters[0].args).to.eql(['3', '"foo"']) expect(tpl.filters[0].args).to.eql(['3', '"foo"'])
}) })
it('should parse "foo | add: "foo" bar, 3"', function () { it('should parse "foo | add: "foo" bar, 3"', function () {
const tpl = new Value('foo | add: "foo" bar, 3', false) const tpl = new Value('foo | add: "foo" bar, 3', filterMap)
expect(tpl.initial).to.equal('foo') expect(tpl.initial).to.equal('foo')
expect(tpl.filters.length).to.equal(1) expect(tpl.filters.length).to.equal(1)
expect(tpl.filters[0].name).to.eql('add') expect(tpl.filters[0].name).to.eql('add')
expect(tpl.filters[0].args).to.eql(['"foo"', '3']) expect(tpl.filters[0].args).to.eql(['"foo"', '3'])
}) })
it('should parse "foo | add: "|", 3', function () { it('should parse "foo | add: "|", 3', function () {
const tpl = new Value('foo | add: "|", 3', false) const tpl = new Value('foo | add: "|", 3', filterMap)
expect(tpl.initial).to.equal('foo') expect(tpl.initial).to.equal('foo')
expect(tpl.filters.length).to.equal(1) expect(tpl.filters.length).to.equal(1)
expect(tpl.filters[0].args).to.eql(['"|"', '3']) expect(tpl.filters[0].args).to.eql(['"|"', '3'])
}) })
it('should parse "foo | add: "|", 3', function () { it('should parse "foo | add: "|", 3', function () {
const tpl = new Value('foo | add: "|", 3', false) const tpl = new Value('foo | add: "|", 3', filterMap)
expect(tpl.initial).to.equal('foo') expect(tpl.initial).to.equal('foo')
expect(tpl.filters.length).to.equal(1) expect(tpl.filters.length).to.equal(1)
expect(tpl.filters[0].args).to.eql(['"|"', '3']) expect(tpl.filters[0].args).to.eql(['"|"', '3'])
}) })
it('should support arguments as named key/values', function () { it('should support arguments as named key/values', function () {
const f = new Value('o | foo: key1: "literal1", key2: value2', false) const f = new Value('o | foo: key1: "literal1", key2: value2', filterMap)
expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].name).to.equal('foo')
expect(f.filters[0].args).to.eql([['key1', '"literal1"'], ['key2', 'value2']]) expect(f.filters[0].args).to.eql([['key1', '"literal1"'], ['key2', 'value2']])
}) })
it('should support arguments as named key/values with inline literals', function () { it('should support arguments as named key/values with inline literals', function () {
const f = new Value('o | foo: "test0", key1: "literal1", key2: value2', false) const f = new Value('o | foo: "test0", key1: "literal1", key2: value2', filterMap)
expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].name).to.equal('foo')
expect(f.filters[0].args).to.deep.equal(['"test0"', ['key1', '"literal1"'], ['key2', 'value2']]) expect(f.filters[0].args).to.deep.equal(['"test0"', ['key1', '"literal1"'], ['key2', 'value2']])
}) })
it('should support arguments as named key/values with inline values', function () { it('should support arguments as named key/values with inline values', function () {
const f = new Value('o | foo: test0, key1: "literal1", key2: value2', false) const f = new Value('o | foo: test0, key1: "literal1", key2: value2', filterMap)
expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].name).to.equal('foo')
expect(f.filters[0].args).to.deep.equal(['test0', ['key1', '"literal1"'], ['key2', 'value2']]) expect(f.filters[0].args).to.deep.equal(['test0', ['key1', '"literal1"'], ['key2', 'value2']])
}) })
it('should support argument values named same as keys', function () { it('should support argument values named same as keys', function () {
const f = new Value('o | foo: a: a', false) const f = new Value('o | foo: a: a', filterMap)
expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].name).to.equal('foo')
expect(f.filters[0].args).to.deep.equal([['a', 'a']]) expect(f.filters[0].args).to.deep.equal([['a', 'a']])
}) })
it('should support argument literals named same as keys', function () { it('should support argument literals named same as keys', function () {
const f = new Value('o | foo: a: "a"', false) const f = new Value('o | foo: a: "a"', filterMap)
expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].name).to.equal('foo')
expect(f.filters[0].args).to.deep.equal([['a', '"a"']]) expect(f.filters[0].args).to.deep.equal([['a', '"a"']])
}) })
@@ -115,9 +114,10 @@ describe('Value', function () {
it('should call chained filters correctly', async function () { it('should call chained filters correctly', async function () {
const date = sinon.stub().returns('y') const date = sinon.stub().returns('y')
const time = sinon.spy() const time = sinon.spy()
Filter.register('date', date) const filterMap = new FilterMap(false)
Filter.register('time', time) filterMap.set('date', date)
const tpl = new Value('foo.bar | date: "b" | time:2', false) filterMap.set('time', time)
const tpl = new Value('foo.bar | date: "b" | time:2', filterMap)
const scope = new Context({ const scope = new Context({
foo: { bar: 'bar' } foo: { bar: 'bar' }
}) })
+1 -1
View File
@@ -13,5 +13,5 @@
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"all": true, "all": true,
"exclude": [ "node_modules", "dist" ] "exclude": [ "node_modules", "dist", "demo" ]
} }