diff --git a/src/builtin/tags/for.ts b/src/builtin/tags/for.ts index 5d2ce8cd1b..fb7430355f 100644 --- a/src/builtin/tags/for.ts +++ b/src/builtin/tags/for.ts @@ -48,7 +48,7 @@ export default { if (isString(collection) && collection.length > 0) { collection = [collection] as string[] } else if (isObject(collection)) { - collection = Object.keys(collection).map((key) => [key, collection[key]]) as Array<[string, any]> + collection = Object.keys(collection).map((key) => [key, collection[key]]) } } if (!isArray(collection) || !collection.length) { diff --git a/src/context/context.ts b/src/context/context.ts index 8a72e5ceb9..eb481085e9 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -45,7 +45,7 @@ export default class Context { } return this.scopes.splice(i, 1)[0] } - findScope (key: string) { + private findScope (key: string) { for (let i = this.scopes.length - 1; i >= 0; i--) { const candidate = this.scopes[i] if (key in candidate) { @@ -73,7 +73,7 @@ export default class Context { * accessSeq("foo['b]r']") // ['foo', 'b]r'] * accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar' */ - async parseProp (str: string) { + private async parseProp (str: string) { str = String(str) const seq: string[] = [] let name = '' @@ -107,8 +107,7 @@ export default class Context { i++ break default:// foo.bar - name += str[i] - i++ + name += str[i++] } } push() diff --git a/src/parser/delimited-token.ts b/src/parser/delimited-token.ts index 625fa54bb1..bc3d011b8e 100644 --- a/src/parser/delimited-token.ts +++ b/src/parser/delimited-token.ts @@ -2,17 +2,26 @@ import Token from './token' import { last } from '../util/underscore' export default class DelimitedToken extends Token { - trimLeft: boolean - trimRight: boolean - constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) { + constructor ( + raw: string, + value: string, + input: string, + line: number, + pos: number, + trimLeft: boolean, + trimRight: boolean, + file?: string + ) { super(raw, input, line, pos, file) - this.trimLeft = value[0] === '-' - this.trimRight = last(value) === '-' + const tl = value[0] === '-' + const tr = last(value) === '-' this.value = value .slice( - this.trimLeft ? 1 : 0, - this.trimRight ? -1 : value.length + tl ? 1 : 0, + tr ? -1 : value.length ) .trim() + this.trimLeft = tl || trimLeft + this.trimRight = tr || trimRight } } diff --git a/src/parser/html-token.ts b/src/parser/html-token.ts index 5418388d57..53d70515ab 100644 --- a/src/parser/html-token.ts +++ b/src/parser/html-token.ts @@ -6,4 +6,7 @@ export default class HTMLToken extends Token { this.type = 'html' this.value = str } + static is (token: Token): token is HTMLToken { + return token.type === 'html' + } } diff --git a/src/parser/output-token.ts b/src/parser/output-token.ts index 31b9f7be81..7674729d59 100644 --- a/src/parser/output-token.ts +++ b/src/parser/output-token.ts @@ -1,8 +1,21 @@ import DelimitedToken from './delimited-token' +import Token from './token' +import { NormalizedFullOptions } from '../liquid-options' export default class OutputToken extends DelimitedToken { - constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) { - super(raw, value, input, line, pos, file) + constructor ( + raw: string, + value: string, + input: string, + line: number, + pos: number, + options: NormalizedFullOptions, + file?: string + ) { + super(raw, value, input, line, pos, options.trimOutputLeft, options.trimOutputRight, file) this.type = 'output' } + static is (token: Token): token is OutputToken { + return token.type === 'output' + } } diff --git a/src/parser/parse-stream.ts b/src/parser/parse-stream.ts index ea91df55fb..6141e092f6 100644 --- a/src/parser/parse-stream.ts +++ b/src/parser/parse-stream.ts @@ -18,20 +18,16 @@ export default class ParseStream { this.handlers[name] = cb return this } - trigger (event: string, arg?: T) { + private trigger (event: string, arg?: T) { const h = this.handlers[event] - if (typeof h === 'function') { - h(arg) - return true - } - return false + return h ? (h(arg), true) : false } start () { this.trigger('start') let token: Token | undefined while (!this.stopRequested && (token = this.tokens.shift())) { if (this.trigger('token', token)) continue - if (token.type === 'tag' && this.trigger(`tag:${(token).name}`, token)) { + if (TagToken.is(token) && this.trigger(`tag:${token.name}`, token)) { continue } const template = this.parseToken(token, this.tokens) diff --git a/src/parser/parser.ts b/src/parser/parser.ts index bf1f5fc295..4fead687a3 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -25,10 +25,10 @@ export default class Parser { } parseToken (token: Token, remainTokens: Array) { try { - if (token.type === 'tag') { - return new Tag(token as TagToken, remainTokens, this.liquid) + if (TagToken.is(token)) { + return new Tag(token, remainTokens, this.liquid) } - if (token.type === 'output') { + if (OutputToken.is(token)) { return new Output(token as OutputToken, this.liquid.options.strictFilters) } return new HTML(token) diff --git a/src/parser/tag-token.ts b/src/parser/tag-token.ts index a4ebc1d93d..7cec7673ee 100644 --- a/src/parser/tag-token.ts +++ b/src/parser/tag-token.ts @@ -1,12 +1,22 @@ import DelimitedToken from './delimited-token' +import Token from './token' import { TokenizationError } from '../util/error' import * as lexical from './lexical' +import { NormalizedFullOptions } from '../liquid-options' export default class TagToken extends DelimitedToken { name: string args: string - constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) { - super(raw, value, input, line, pos, file) + constructor ( + raw: string, + value: string, + input: string, + line: number, + pos: number, + options: NormalizedFullOptions, + file?: string + ) { + super(raw, value, input, line, pos, options.trimTagLeft, options.trimTagRight, file) this.type = 'tag' const match = this.value.match(lexical.tagLine) if (!match) { @@ -15,4 +25,7 @@ export default class TagToken extends DelimitedToken { this.name = match[1] this.args = match[2] } + static is (token: Token): token is TagToken { + return token.type === 'tag' + } } diff --git a/src/parser/token.ts b/src/parser/token.ts index a13b539c91..7a493ffc75 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -1,4 +1,6 @@ export default class Token { + trimLeft: boolean = false + trimRight: boolean = false type: string = 'notset' line: number col: number diff --git a/src/parser/tokenizer.ts b/src/parser/tokenizer.ts index 02c41414d8..40e7b7c3d5 100644 --- a/src/parser/tokenizer.ts +++ b/src/parser/tokenizer.ts @@ -9,16 +9,18 @@ import { NormalizedFullOptions, applyDefault } from '../liquid-options' enum ParseState { HTML, OUTPUT, TAG } export default class Tokenizer { - options: NormalizedFullOptions + private options: NormalizedFullOptions constructor (options?: NormalizedFullOptions) { this.options = applyDefault(options) } tokenize (input: string, file?: string) { const tokens: Token[] = [] - const tagL = this.options.tagDelimiterLeft - const tagR = this.options.tagDelimiterRight - const outputL = this.options.outputDelimiterLeft - const outputR = this.options.outputDelimiterRight + const { + tagDelimiterLeft, + tagDelimiterRight, + outputDelimiterLeft, + outputDelimiterRight + } = this.options let p = 0 let curLine = 1 let state = ParseState.HTML @@ -33,36 +35,39 @@ export default class Tokenizer { lineBegin = p + 1 } if (state === ParseState.HTML) { - if (input.substr(p, outputL.length) === outputL) { + if (input.substr(p, outputDelimiterLeft.length) === outputDelimiterLeft) { if (buffer) tokens.push(new HTMLToken(buffer, input, line, col, file)) - buffer = outputL + buffer = outputDelimiterLeft line = curLine col = p - lineBegin + 1 - p += outputL.length + p += outputDelimiterLeft.length state = ParseState.OUTPUT continue - } else if (input.substr(p, tagL.length) === tagL) { + } else if (input.substr(p, tagDelimiterLeft.length) === tagDelimiterLeft) { if (buffer) tokens.push(new HTMLToken(buffer, input, line, col, file)) - buffer = tagL + buffer = tagDelimiterLeft line = curLine col = p - lineBegin + 1 - p += tagL.length + p += tagDelimiterLeft.length state = ParseState.TAG continue } - } else if (state === ParseState.OUTPUT && input.substr(p, outputR.length) === outputR) { - buffer += outputR - tokens.push(new OutputToken(buffer, buffer.slice(outputL.length, -outputR.length), input, line, col, file)) - p += outputR.length + } else if ( + state === ParseState.OUTPUT && + input.substr(p, outputDelimiterRight.length) === outputDelimiterRight + ) { + buffer += outputDelimiterRight + tokens.push(new OutputToken(buffer, buffer.slice(outputDelimiterLeft.length, -outputDelimiterRight.length), input, line, col, this.options, file)) + p += outputDelimiterRight.length buffer = '' line = curLine col = p - lineBegin + 1 state = ParseState.HTML continue - } else if (input.substr(p, tagR.length) === tagR) { - buffer += tagR - tokens.push(new TagToken(buffer, buffer.slice(tagL.length, -tagR.length), input, line, col, file)) - p += tagR.length + } else if (input.substr(p, tagDelimiterRight.length) === tagDelimiterRight) { + buffer += tagDelimiterRight + tokens.push(new TagToken(buffer, buffer.slice(tagDelimiterLeft.length, -tagDelimiterRight.length), input, line, col, this.options, file)) + p += tagDelimiterRight.length buffer = '' line = curLine col = p - lineBegin + 1 diff --git a/src/parser/whitespace-ctrl.ts b/src/parser/whitespace-ctrl.ts index ce7d8600b9..c66fbe947f 100644 --- a/src/parser/whitespace-ctrl.ts +++ b/src/parser/whitespace-ctrl.ts @@ -1,47 +1,38 @@ -import DelimitedToken from '../parser/delimited-token' import Token from '../parser/token' import TagToken from '../parser/tag-token' +import HTMLToken from '../parser/html-token' import { NormalizedFullOptions } from '../liquid-options' export default function whiteSpaceCtrl (tokens: Token[], options: NormalizedFullOptions) { options = { greedy: true, ...options } let inRaw = false - tokens.forEach((token: Token, i: number) => { - if (shouldTrimLeft(token as DelimitedToken, inRaw, options)) { + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i] + if (!inRaw && token.trimLeft) { trimLeft(tokens[i - 1], options.greedy) } - if (token.type === 'tag' && (token as TagToken).name === 'raw') inRaw = true - if (token.type === 'tag' && (token as TagToken).name === 'endraw') inRaw = false + if (TagToken.is(token)) { + if (token.name === 'raw') inRaw = true + else if (token.name === 'endraw') inRaw = false + } - if (shouldTrimRight(token as DelimitedToken, inRaw, options)) { + if (!inRaw && token.trimRight) { trimRight(tokens[i + 1], options.greedy) } - }) -} - -function shouldTrimLeft (token: DelimitedToken, inRaw: boolean, options: NormalizedFullOptions) { - if (inRaw) return false - if (token.type === 'tag') return token.trimLeft || options.trimTagLeft - if (token.type === 'output') return token.trimLeft || options.trimOutputLeft -} - -function shouldTrimRight (token: DelimitedToken, inRaw: boolean, options: NormalizedFullOptions) { - if (inRaw) return false - if (token.type === 'tag') return token.trimRight || options.trimTagRight - if (token.type === 'output') return token.trimRight || options.trimOutputRight + } } function trimLeft (token: Token, greedy: boolean) { - if (!token || token.type !== 'html') return + if (!token || !HTMLToken.is(token)) return const rLeft = greedy ? /\s+$/g : /[\t\r ]*$/g token.value = token.value.replace(rLeft, '') } function trimRight (token: Token, greedy: boolean) { - if (!token || token.type !== 'html') return + if (!token || !HTMLToken.is(token)) return const rRight = greedy ? /^\s+/g : /^[\t\r ]*\n?/g token.value = token.value.replace(rRight, '') diff --git a/src/render/syntax.ts b/src/render/syntax.ts index e76594eb97..d694760f6f 100644 --- a/src/render/syntax.ts +++ b/src/render/syntax.ts @@ -1,7 +1,7 @@ import * as lexical from '../parser/lexical' import assert from '../util/assert' import Context from '../context/context' -import { range, last } from '../util/underscore' +import { range, last, isFunction } from '../util/underscore' import { isComparable } from '../drop/icomparable' import { NullDrop } from '../drop/null-drop' import { EmptyDrop } from '../drop/empty-drop' @@ -40,9 +40,7 @@ const binaryOperators: {[key: string]: (lhs: any, rhs: any) => boolean} = { return l <= r }, 'contains': (l: any, r: any) => { - if (!l) return false - if (typeof l.indexOf !== 'function') return false - return l.indexOf(r) > -1 + return l && isFunction(l.indexOf) ? l.indexOf(r) > -1 : false }, 'and': (l: any, r: any) => isTruthy(l) && isTruthy(r), 'or': (l: any, r: any) => isTruthy(l) || isTruthy(r) diff --git a/src/template/tag/tag.ts b/src/template/tag/tag.ts index e91744729b..91366153f0 100644 --- a/src/template/tag/tag.ts +++ b/src/template/tag/tag.ts @@ -1,4 +1,4 @@ -import { create, stringify } from '../../util/underscore' +import { stringify, isFunction } from '../../util/underscore' import assert from '../../util/assert' import Context from '../../context/context' import ITagImpl from './itag-impl' @@ -21,7 +21,8 @@ export default class Tag extends Template implements ITemplate { const impl = Tag.impls[token.name] assert(impl, `tag ${token.name} not found`) - this.impl = create(impl) + + this.impl = Object.create(impl) this.impl.liquid = liquid if (this.impl.parse) { this.impl.parse(token, tokens) @@ -30,11 +31,7 @@ export default class Tag extends Template implements ITemplate { async render (ctx: Context) { const hash = await Hash.create(this.token.args, ctx) const impl = this.impl - if (typeof impl.render !== 'function') { - return '' - } - const html = await impl.render(ctx, hash) - return stringify(html) + return isFunction(impl.render) ? stringify(await impl.render(ctx, hash)) : '' } static register (name: string, tag: ITagImplOptions) { Tag.impls[name] = tag diff --git a/src/template/value.ts b/src/template/value.ts index d4301a20b1..cfa064c9f8 100644 --- a/src/template/value.ts +++ b/src/template/value.ts @@ -4,8 +4,8 @@ import Context from '../context/context' export default class Value { private strictFilters: boolean - initial: string - filters: Array = [] + private initial: string + private filters: Array = [] /** * @param str value string, like: "i have a dream | truncate: 3 diff --git a/src/util/assert.ts b/src/util/assert.ts index 243ac788f4..d469f045ea 100644 --- a/src/util/assert.ts +++ b/src/util/assert.ts @@ -1,6 +1,6 @@ import { AssertionError } from './error' -export default function (predicate: any, message?: string) { +export default function (predicate: T | null | undefined, message?: string) { if (!predicate) { message = message || `expect ${predicate} to be true` throw new AssertionError(message) diff --git a/src/util/underscore.ts b/src/util/underscore.ts index 7de9c1c63c..8612e459a5 100644 --- a/src/util/underscore.ts +++ b/src/util/underscore.ts @@ -36,10 +36,6 @@ export function toLiquid (value: any): any { return value } -export function create (proto: T1): T2 { - return Object.create(proto) -} - export function isNil (value: any): boolean { return value === null || value === undefined } diff --git a/test/unit/context/context.ts b/test/unit/context/context.ts index af2a261b85..1f21773a8e 100644 --- a/test/unit/context/context.ts +++ b/test/unit/context/context.ts @@ -5,7 +5,7 @@ import { Scope } from '../../../src/context/scope' const expect = chai.expect describe('scope', function () { - let ctx: Context, scope: Scope + let ctx: any, scope: Scope beforeEach(function () { scope = { foo: 'zoo', diff --git a/test/unit/template/value.ts b/test/unit/template/value.ts index 381b291737..a5f824e0d6 100644 --- a/test/unit/template/value.ts +++ b/test/unit/template/value.ts @@ -14,70 +14,70 @@ describe('Value', function () { describe('#constructor()', function () { it('should parse "foo', function () { - const tpl = new Value('foo', false) + const tpl = new Value('foo', false) as any expect(tpl.initial).to.equal('foo') expect(tpl.filters).to.deep.equal([]) }) it('should parse "foo | add"', function () { - const tpl = new Value('foo | add', false) + const tpl = new Value('foo | add', false) as any expect(tpl.initial).to.equal('foo') expect(tpl.filters.length).to.equal(1) expect(tpl.filters[0].args).to.eql([]) }) it('should parse "foo,foo | add"', function () { - const tpl = new Value('foo,foo | add', false) - expect(tpl.initial).to.equal('foo') + const tpl = new Value('foo,foo | add', false) as any + expect(tpl.initial).to.equal('foo') as any expect(tpl.filters.length).to.equal(1) expect(tpl.filters[0].args).to.eql([]) }) 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"', false) as any expect(tpl.initial).to.equal('foo') expect(tpl.filters.length).to.equal(1) expect(tpl.filters[0].args).to.eql(['3', '"foo"']) }) 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', false) as any expect(tpl.initial).to.equal('foo') expect(tpl.filters.length).to.equal(1) expect(tpl.filters[0].name).to.eql('add') expect(tpl.filters[0].args).to.eql(['"foo"', '3']) }) it('should parse "foo | add: "|", 3', function () { - const tpl = new Value('foo | add: "|", 3', false) + const tpl = new Value('foo | add: "|", 3', false) as any expect(tpl.initial).to.equal('foo') expect(tpl.filters.length).to.equal(1) expect(tpl.filters[0].args).to.eql(['"|"', '3']) }) it('should parse "foo | add: "|", 3', function () { - const tpl = new Value('foo | add: "|", 3', false) + const tpl = new Value('foo | add: "|", 3', false) as any expect(tpl.initial).to.equal('foo') expect(tpl.filters.length).to.equal(1) expect(tpl.filters[0].args).to.eql(['"|"', '3']) }) 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', false) as any expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].args).to.eql([['key1', '"literal1"'], ['key2', 'value2']]) }) 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', false) as any expect(f.filters[0].name).to.equal('foo') 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 () { - const f = new Value('o | foo: test0, key1: "literal1", key2: value2', false) + const f = new Value('o | foo: test0, key1: "literal1", key2: value2', false) as any expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].args).to.deep.equal(['test0', ['key1', '"literal1"'], ['key2', 'value2']]) }) 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', false) as any expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].args).to.deep.equal([['a', 'a']]) }) 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"', false) as any expect(f.filters[0].name).to.equal('foo') expect(f.filters[0].args).to.deep.equal([['a', '"a"']]) })