perf: use polymophism instead duck test

This commit is contained in:
harttle
2019-03-25 20:11:23 +08:00
parent 64dd057552
commit 82d7673554
18 changed files with 120 additions and 98 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ export default <ITagImplOptions>{
if (isString(collection) && collection.length > 0) { if (isString(collection) && collection.length > 0) {
collection = [collection] as string[] collection = [collection] as string[]
} else if (isObject(collection)) { } 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) { if (!isArray(collection) || !collection.length) {
+3 -4
View File
@@ -45,7 +45,7 @@ export default class Context {
} }
return this.scopes.splice(i, 1)[0] return this.scopes.splice(i, 1)[0]
} }
findScope (key: string) { private findScope (key: string) {
for (let i = this.scopes.length - 1; i >= 0; i--) { for (let i = this.scopes.length - 1; i >= 0; i--) {
const candidate = this.scopes[i] const candidate = this.scopes[i]
if (key in candidate) { if (key in candidate) {
@@ -73,7 +73,7 @@ export default class Context {
* accessSeq("foo['b]r']") // ['foo', 'b]r'] * accessSeq("foo['b]r']") // ['foo', 'b]r']
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar' * accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
*/ */
async parseProp (str: string) { private async parseProp (str: string) {
str = String(str) str = String(str)
const seq: string[] = [] const seq: string[] = []
let name = '' let name = ''
@@ -107,8 +107,7 @@ export default class Context {
i++ i++
break break
default:// foo.bar default:// foo.bar
name += str[i] name += str[i++]
i++
} }
} }
push() push()
+16 -7
View File
@@ -2,17 +2,26 @@ import Token from './token'
import { last } from '../util/underscore' import { last } from '../util/underscore'
export default class DelimitedToken extends Token { export default class DelimitedToken extends Token {
trimLeft: boolean constructor (
trimRight: boolean raw: string,
constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) { value: string,
input: string,
line: number,
pos: number,
trimLeft: boolean,
trimRight: boolean,
file?: string
) {
super(raw, input, line, pos, file) super(raw, input, line, pos, file)
this.trimLeft = value[0] === '-' const tl = value[0] === '-'
this.trimRight = last(value) === '-' const tr = last(value) === '-'
this.value = value this.value = value
.slice( .slice(
this.trimLeft ? 1 : 0, tl ? 1 : 0,
this.trimRight ? -1 : value.length tr ? -1 : value.length
) )
.trim() .trim()
this.trimLeft = tl || trimLeft
this.trimRight = tr || trimRight
} }
} }
+3
View File
@@ -6,4 +6,7 @@ export default class HTMLToken extends Token {
this.type = 'html' this.type = 'html'
this.value = str this.value = str
} }
static is (token: Token): token is HTMLToken {
return token.type === 'html'
}
} }
+15 -2
View File
@@ -1,8 +1,21 @@
import DelimitedToken from './delimited-token' import DelimitedToken from './delimited-token'
import Token from './token'
import { NormalizedFullOptions } from '../liquid-options'
export default class OutputToken extends DelimitedToken { export default class OutputToken extends DelimitedToken {
constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) { constructor (
super(raw, value, input, line, pos, file) 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' this.type = 'output'
} }
static is (token: Token): token is OutputToken {
return token.type === 'output'
}
} }
+3 -7
View File
@@ -18,20 +18,16 @@ export default class ParseStream {
this.handlers[name] = cb this.handlers[name] = cb
return this return this
} }
trigger <T extends Token | ITemplate> (event: string, arg?: T) { private trigger <T extends Token | ITemplate> (event: string, arg?: T) {
const h = this.handlers[event] const h = this.handlers[event]
if (typeof h === 'function') { return h ? (h(arg), true) : false
h(arg)
return true
}
return false
} }
start () { start () {
this.trigger('start') this.trigger('start')
let token: Token | undefined let token: Token | undefined
while (!this.stopRequested && (token = this.tokens.shift())) { while (!this.stopRequested && (token = this.tokens.shift())) {
if (this.trigger('token', token)) continue if (this.trigger('token', token)) continue
if (token.type === 'tag' && this.trigger(`tag:${(<TagToken>token).name}`, token)) { if (TagToken.is(token) && this.trigger(`tag:${token.name}`, token)) {
continue continue
} }
const template = this.parseToken(token, this.tokens) const template = this.parseToken(token, this.tokens)
+3 -3
View File
@@ -25,10 +25,10 @@ export default class Parser {
} }
parseToken (token: Token, remainTokens: Array<Token>) { parseToken (token: Token, remainTokens: Array<Token>) {
try { try {
if (token.type === 'tag') { if (TagToken.is(token)) {
return new Tag(token as TagToken, remainTokens, this.liquid) 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 Output(token as OutputToken, this.liquid.options.strictFilters)
} }
return new HTML(token) return new HTML(token)
+15 -2
View File
@@ -1,12 +1,22 @@
import DelimitedToken from './delimited-token' import DelimitedToken from './delimited-token'
import Token from './token'
import { TokenizationError } from '../util/error' import { TokenizationError } from '../util/error'
import * as lexical from './lexical' import * as lexical from './lexical'
import { NormalizedFullOptions } from '../liquid-options'
export default class TagToken extends DelimitedToken { export default class TagToken extends DelimitedToken {
name: string name: string
args: string args: string
constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) { constructor (
super(raw, value, input, line, pos, file) 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' this.type = 'tag'
const match = this.value.match(lexical.tagLine) const match = this.value.match(lexical.tagLine)
if (!match) { if (!match) {
@@ -15,4 +25,7 @@ export default class TagToken extends DelimitedToken {
this.name = match[1] this.name = match[1]
this.args = match[2] this.args = match[2]
} }
static is (token: Token): token is TagToken {
return token.type === 'tag'
}
} }
+2
View File
@@ -1,4 +1,6 @@
export default class Token { export default class Token {
trimLeft: boolean = false
trimRight: boolean = false
type: string = 'notset' type: string = 'notset'
line: number line: number
col: number col: number
+24 -19
View File
@@ -9,16 +9,18 @@ import { NormalizedFullOptions, applyDefault } from '../liquid-options'
enum ParseState { HTML, OUTPUT, TAG } enum ParseState { HTML, OUTPUT, TAG }
export default class Tokenizer { export default class Tokenizer {
options: NormalizedFullOptions private options: NormalizedFullOptions
constructor (options?: NormalizedFullOptions) { constructor (options?: NormalizedFullOptions) {
this.options = applyDefault(options) this.options = applyDefault(options)
} }
tokenize (input: string, file?: string) { tokenize (input: string, file?: string) {
const tokens: Token[] = [] const tokens: Token[] = []
const tagL = this.options.tagDelimiterLeft const {
const tagR = this.options.tagDelimiterRight tagDelimiterLeft,
const outputL = this.options.outputDelimiterLeft tagDelimiterRight,
const outputR = this.options.outputDelimiterRight outputDelimiterLeft,
outputDelimiterRight
} = this.options
let p = 0 let p = 0
let curLine = 1 let curLine = 1
let state = ParseState.HTML let state = ParseState.HTML
@@ -33,36 +35,39 @@ export default class Tokenizer {
lineBegin = p + 1 lineBegin = p + 1
} }
if (state === ParseState.HTML) { 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)) if (buffer) tokens.push(new HTMLToken(buffer, input, line, col, file))
buffer = outputL buffer = outputDelimiterLeft
line = curLine line = curLine
col = p - lineBegin + 1 col = p - lineBegin + 1
p += outputL.length p += outputDelimiterLeft.length
state = ParseState.OUTPUT state = ParseState.OUTPUT
continue 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)) if (buffer) tokens.push(new HTMLToken(buffer, input, line, col, file))
buffer = tagL buffer = tagDelimiterLeft
line = curLine line = curLine
col = p - lineBegin + 1 col = p - lineBegin + 1
p += tagL.length p += tagDelimiterLeft.length
state = ParseState.TAG state = ParseState.TAG
continue continue
} }
} else if (state === ParseState.OUTPUT && input.substr(p, outputR.length) === outputR) { } else if (
buffer += outputR state === ParseState.OUTPUT &&
tokens.push(new OutputToken(buffer, buffer.slice(outputL.length, -outputR.length), input, line, col, file)) input.substr(p, outputDelimiterRight.length) === outputDelimiterRight
p += outputR.length ) {
buffer += outputDelimiterRight
tokens.push(new OutputToken(buffer, buffer.slice(outputDelimiterLeft.length, -outputDelimiterRight.length), input, line, col, this.options, file))
p += outputDelimiterRight.length
buffer = '' buffer = ''
line = curLine line = curLine
col = p - lineBegin + 1 col = p - lineBegin + 1
state = ParseState.HTML state = ParseState.HTML
continue continue
} else if (input.substr(p, tagR.length) === tagR) { } else if (input.substr(p, tagDelimiterRight.length) === tagDelimiterRight) {
buffer += tagR buffer += tagDelimiterRight
tokens.push(new TagToken(buffer, buffer.slice(tagL.length, -tagR.length), input, line, col, file)) tokens.push(new TagToken(buffer, buffer.slice(tagDelimiterLeft.length, -tagDelimiterRight.length), input, line, col, this.options, file))
p += tagR.length p += tagDelimiterRight.length
buffer = '' buffer = ''
line = curLine line = curLine
col = p - lineBegin + 1 col = p - lineBegin + 1
+12 -21
View File
@@ -1,47 +1,38 @@
import DelimitedToken from '../parser/delimited-token'
import Token from '../parser/token' import Token from '../parser/token'
import TagToken from '../parser/tag-token' import TagToken from '../parser/tag-token'
import HTMLToken from '../parser/html-token'
import { NormalizedFullOptions } from '../liquid-options' import { NormalizedFullOptions } from '../liquid-options'
export default function whiteSpaceCtrl (tokens: Token[], options: NormalizedFullOptions) { export default function whiteSpaceCtrl (tokens: Token[], options: NormalizedFullOptions) {
options = { greedy: true, ...options } options = { greedy: true, ...options }
let inRaw = false let inRaw = false
tokens.forEach((token: Token, i: number) => { for (let i = 0; i < tokens.length; i++) {
if (shouldTrimLeft(token as DelimitedToken, inRaw, options)) { const token = tokens[i]
if (!inRaw && token.trimLeft) {
trimLeft(tokens[i - 1], options.greedy) trimLeft(tokens[i - 1], options.greedy)
} }
if (token.type === 'tag' && (token as TagToken).name === 'raw') inRaw = true if (TagToken.is(token)) {
if (token.type === 'tag' && (token as TagToken).name === 'endraw') inRaw = false 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) 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) { 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 const rLeft = greedy ? /\s+$/g : /[\t\r ]*$/g
token.value = token.value.replace(rLeft, '') token.value = token.value.replace(rLeft, '')
} }
function trimRight (token: Token, greedy: boolean) { 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 const rRight = greedy ? /^\s+/g : /^[\t\r ]*\n?/g
token.value = token.value.replace(rRight, '') token.value = token.value.replace(rRight, '')
+2 -4
View File
@@ -1,7 +1,7 @@
import * as lexical from '../parser/lexical' import * as lexical from '../parser/lexical'
import assert from '../util/assert' import assert from '../util/assert'
import Context from '../context/context' import Context from '../context/context'
import { range, last } from '../util/underscore' import { range, last, isFunction } from '../util/underscore'
import { isComparable } from '../drop/icomparable' import { isComparable } from '../drop/icomparable'
import { NullDrop } from '../drop/null-drop' import { NullDrop } from '../drop/null-drop'
import { EmptyDrop } from '../drop/empty-drop' import { EmptyDrop } from '../drop/empty-drop'
@@ -40,9 +40,7 @@ const binaryOperators: {[key: string]: (lhs: any, rhs: any) => boolean} = {
return l <= r return l <= r
}, },
'contains': (l: any, r: any) => { 'contains': (l: any, r: any) => {
if (!l) return false return l && isFunction(l.indexOf) ? l.indexOf(r) > -1 : false
if (typeof l.indexOf !== 'function') return false
return l.indexOf(r) > -1
}, },
'and': (l: any, r: any) => isTruthy(l) && isTruthy(r), 'and': (l: any, r: any) => isTruthy(l) && isTruthy(r),
'or': (l: any, r: any) => isTruthy(l) || isTruthy(r) 'or': (l: any, r: any) => isTruthy(l) || isTruthy(r)
+4 -7
View File
@@ -1,4 +1,4 @@
import { create, stringify } from '../../util/underscore' import { stringify, isFunction } from '../../util/underscore'
import assert from '../../util/assert' import assert from '../../util/assert'
import Context from '../../context/context' import Context from '../../context/context'
import ITagImpl from './itag-impl' import ITagImpl from './itag-impl'
@@ -21,7 +21,8 @@ export default class Tag extends Template<TagToken> implements ITemplate {
const impl = Tag.impls[token.name] const impl = Tag.impls[token.name]
assert(impl, `tag ${token.name} not found`) assert(impl, `tag ${token.name} not found`)
this.impl = create<ITagImplOptions, ITagImpl>(impl)
this.impl = Object.create(impl)
this.impl.liquid = liquid this.impl.liquid = liquid
if (this.impl.parse) { if (this.impl.parse) {
this.impl.parse(token, tokens) this.impl.parse(token, tokens)
@@ -30,11 +31,7 @@ export default class Tag extends Template<TagToken> implements ITemplate {
async render (ctx: Context) { async render (ctx: Context) {
const hash = await Hash.create(this.token.args, ctx) const hash = await Hash.create(this.token.args, ctx)
const impl = this.impl const impl = this.impl
if (typeof impl.render !== 'function') { return isFunction(impl.render) ? stringify(await impl.render(ctx, hash)) : ''
return ''
}
const html = await impl.render(ctx, hash)
return stringify(html)
} }
static register (name: string, tag: ITagImplOptions) { static register (name: string, tag: ITagImplOptions) {
Tag.impls[name] = tag Tag.impls[name] = tag
+2 -2
View File
@@ -4,8 +4,8 @@ import Context from '../context/context'
export default class Value { export default class Value {
private strictFilters: boolean private strictFilters: boolean
initial: string private initial: string
filters: Array<Filter> = [] private filters: Array<Filter> = []
/** /**
* @param str value string, like: "i have a dream | truncate: 3 * @param str value string, like: "i have a dream | truncate: 3
+1 -1
View File
@@ -1,6 +1,6 @@
import { AssertionError } from './error' import { AssertionError } from './error'
export default function (predicate: any, message?: string) { export default function<T> (predicate: T | null | undefined, message?: string) {
if (!predicate) { if (!predicate) {
message = message || `expect ${predicate} to be true` message = message || `expect ${predicate} to be true`
throw new AssertionError(message) throw new AssertionError(message)
-4
View File
@@ -36,10 +36,6 @@ export function toLiquid (value: any): any {
return value return value
} }
export function create<T1 extends object, T2 extends T1 = T1> (proto: T1): T2 {
return Object.create(proto)
}
export function isNil (value: any): boolean { export function isNil (value: any): boolean {
return value === null || value === undefined return value === null || value === undefined
} }
+1 -1
View File
@@ -5,7 +5,7 @@ import { Scope } from '../../../src/context/scope'
const expect = chai.expect const expect = chai.expect
describe('scope', function () { describe('scope', function () {
let ctx: Context, scope: Scope let ctx: any, scope: Scope
beforeEach(function () { beforeEach(function () {
scope = { scope = {
foo: 'zoo', foo: 'zoo',
+13 -13
View File
@@ -14,70 +14,70 @@ describe('Value', function () {
describe('#constructor()', function () { describe('#constructor()', function () {
it('should parse "foo', 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.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', false) as any
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', false) as any
expect(tpl.initial).to.equal('foo') expect(tpl.initial).to.equal('foo') as any
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"', false) as any
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', false) as any
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', false) as any
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', false) as any
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', false) as any
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', false) as any
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', false) as any
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', false) as any
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"', false) as any
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"']])
}) })