Skip to content

Commit

Permalink
perf: use polymophism instead duck test
Browse files Browse the repository at this point in the history
  • Loading branch information
harttle committed Mar 25, 2019
1 parent 64dd057 commit 82d7673
Show file tree
Hide file tree
Showing 18 changed files with 120 additions and 98 deletions.
2 changes: 1 addition & 1 deletion src/builtin/tags/for.ts
Expand Up @@ -48,7 +48,7 @@ export default <ITagImplOptions>{
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) {
Expand Down
7 changes: 3 additions & 4 deletions src/context/context.ts
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = ''
Expand Down Expand Up @@ -107,8 +107,7 @@ export default class Context {
i++
break
default:// foo.bar
name += str[i]
i++
name += str[i++]
}
}
push()
Expand Down
23 changes: 16 additions & 7 deletions src/parser/delimited-token.ts
Expand Up @@ -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
}
}
3 changes: 3 additions & 0 deletions src/parser/html-token.ts
Expand Up @@ -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'
}
}
17 changes: 15 additions & 2 deletions 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'
}
}
10 changes: 3 additions & 7 deletions src/parser/parse-stream.ts
Expand Up @@ -18,20 +18,16 @@ export default class ParseStream {
this.handlers[name] = cb
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]
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:${(<TagToken>token).name}`, token)) {
if (TagToken.is(token) && this.trigger(`tag:${token.name}`, token)) {
continue
}
const template = this.parseToken(token, this.tokens)
Expand Down
6 changes: 3 additions & 3 deletions src/parser/parser.ts
Expand Up @@ -25,10 +25,10 @@ export default class Parser {
}
parseToken (token: Token, remainTokens: Array<Token>) {
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)
Expand Down
17 changes: 15 additions & 2 deletions 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) {
Expand All @@ -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'
}
}
2 changes: 2 additions & 0 deletions 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
Expand Down
43 changes: 24 additions & 19 deletions src/parser/tokenizer.ts
Expand Up @@ -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
Expand All @@ -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
Expand Down
33 changes: 12 additions & 21 deletions 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, '')
Expand Down
6 changes: 2 additions & 4 deletions 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'
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 4 additions & 7 deletions 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'
Expand All @@ -21,7 +21,8 @@ export default class Tag extends Template<TagToken> implements ITemplate {

const impl = Tag.impls[token.name]
assert(impl, `tag ${token.name} not found`)
this.impl = create<ITagImplOptions, ITagImpl>(impl)

this.impl = Object.create(impl)
this.impl.liquid = liquid
if (this.impl.parse) {
this.impl.parse(token, tokens)
Expand All @@ -30,11 +31,7 @@ export default class Tag extends Template<TagToken> 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
Expand Down
4 changes: 2 additions & 2 deletions src/template/value.ts
Expand Up @@ -4,8 +4,8 @@ import Context from '../context/context'

export default class Value {
private strictFilters: boolean
initial: string
filters: Array<Filter> = []
private initial: string
private filters: Array<Filter> = []

/**
* @param str value string, like: "i have a dream | truncate: 3
Expand Down
2 changes: 1 addition & 1 deletion src/util/assert.ts
@@ -1,6 +1,6 @@
import { AssertionError } from './error'

export default function (predicate: any, message?: string) {
export default function<T> (predicate: T | null | undefined, message?: string) {
if (!predicate) {
message = message || `expect ${predicate} to be true`
throw new AssertionError(message)
Expand Down
4 changes: 0 additions & 4 deletions src/util/underscore.ts
Expand Up @@ -36,10 +36,6 @@ export function toLiquid (value: any): any {
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 {
return value === null || value === undefined
}
Expand Down

0 comments on commit 82d7673

Please sign in to comment.