Skip to content

Commit

Permalink
feat: create trie programmatically in options
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonEtco authored and harttle committed Feb 4, 2021
1 parent 8734e2e commit befc33c
Show file tree
Hide file tree
Showing 28 changed files with 196 additions and 146 deletions.
2 changes: 1 addition & 1 deletion src/builtin/tags/assign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Tokenizer, assert, TagImplOptions, TagToken, Context } from '../../type

export default {
parse: function (token: TagToken) {
const tokenizer = new Tokenizer(token.args)
const tokenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie)
this.key = tokenizer.readIdentifier().content
tokenizer.skipBlank()
assert(tokenizer.peek() === '=', () => `illegal token ${token.getText()}`)
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { evalQuotedToken } from '../../render/expression'

export default {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
const tokenizer = new Tokenizer(tagToken.args)
const tokenizer = new Tokenizer(tagToken.args, this.liquid.options.operatorsTrie)
this.variable = readVariableName(tokenizer)
assert(this.variable, () => `${tagToken.args} not valid identifier`)

Expand Down
5 changes: 3 additions & 2 deletions src/builtin/tags/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ export default {

render: function * (ctx: Context, emitter: Emitter) {
const r = this.liquid.renderer
const cond = yield new Expression(this.cond, this.liquid.options.operators).value(ctx)
const { operators, operatorsTrie } = this.liquid.options
const cond = yield new Expression(this.cond, operators, operatorsTrie).value(ctx)
for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i]
const val = yield new Expression(branch.val, this.liquid.options.operators).value(ctx)
const val = yield new Expression(branch.val, operators, operatorsTrie).value(ctx)
if (val === cond) {
yield r.renderTemplates(branch.templates, ctx, emitter)
return
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/cycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Tokenizer } from '../../parser/tokenizer'

export default {
parse: function (tagToken: TagToken) {
const tokenizer = new Tokenizer(tagToken.args)
const tokenizer = new Tokenizer(tagToken.args, this.liquid.options.operatorsTrie)
const group = tokenizer.readValue()
tokenizer.skipBlank()

Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/decrement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isNumber, stringify } from '../../util/underscore'

export default {
parse: function (token: TagToken) {
const tokenizer = new Tokenizer(token.args)
const tokenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie)
this.variable = tokenizer.readIdentifier().content
},
render: function (context: Context, emitter: Emitter) {
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/for.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Hash } from '../../template/tag/hash'
export default {
type: 'block',
parse: function (token: TagToken, remainTokens: TopLevelToken[]) {
const toknenizer = new Tokenizer(token.args)
const toknenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie)

const variable = toknenizer.readIdentifier()
const inStr = toknenizer.readIdentifier()
Expand Down
3 changes: 2 additions & 1 deletion src/builtin/tags/if.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ export default {

render: function * (ctx: Context, emitter: Emitter) {
const r = this.liquid.renderer
const { operators, operatorsTrie } = this.liquid.options

for (const branch of this.branches) {
const cond = yield new Expression(branch.cond, this.liquid.options.operators, ctx.opts.lenientIf).value(ctx)
const cond = yield new Expression(branch.cond, operators, operatorsTrie, ctx.opts.lenientIf).value(ctx)
if (isTruthy(cond, ctx)) {
yield r.renderTemplates(branch.templates, ctx, emitter)
return
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/include.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import BlockMode from '../../context/block-mode'
export default {
parse: function (token: TagToken) {
const args = token.args
const tokenizer = new Tokenizer(args)
const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie)
this.file = this.liquid.options.dynamicPartials
? tokenizer.readValue()
: tokenizer.readFileName()
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/increment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Tokenizer, Emitter, TagToken, Context, TagImplOptions } from '../../typ

export default {
parse: function (token: TagToken) {
const tokenizer = new Tokenizer(token.args)
const tokenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie)
this.variable = tokenizer.readIdentifier().content
},
render: function (context: Context, emitter: Emitter) {
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import BlockMode from '../../context/block-mode'

export default {
parse: function (token: TagToken, remainTokens: TopLevelToken[]) {
const tokenizer = new Tokenizer(token.args)
const tokenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie)
const file = this.liquid.options.dynamicPartials ? tokenizer.readValue() : tokenizer.readFileName()
assert(file, () => `illegal argument "${token.args}"`)

Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagTo
export default {
parse: function (token: TagToken) {
const args = token.args
const tokenizer = new Tokenizer(args)
const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie)
this.file = this.liquid.options.dynamicPartials
? tokenizer.readValue()
: tokenizer.readFileName()
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/tablerow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Tokenizer } from '../../parser/tokenizer'

export default {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
const tokenizer = new Tokenizer(tagToken.args)
const tokenizer = new Tokenizer(tagToken.args, this.liquid.options.operatorsTrie)

this.variable = tokenizer.readIdentifier()
tokenizer.skipBlank()
Expand Down
5 changes: 3 additions & 2 deletions src/builtin/tags/unless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ export default {

render: function * (ctx: Context, emitter: Emitter) {
const r = this.liquid.renderer
const cond = yield new Expression(this.cond, this.liquid.options.operators, ctx.opts.lenientIf).value(ctx)
const { operators, operatorsTrie } = this.liquid.options
const cond = yield new Expression(this.cond, operators, operatorsTrie, ctx.opts.lenientIf).value(ctx)

if (isFalsy(cond, ctx)) {
yield r.renderTemplates(this.templates, ctx, emitter)
return
}

for (const branch of this.branches) {
const cond = yield new Expression(branch.cond, this.liquid.options.operators, ctx.opts.lenientIf).value(ctx)
const cond = yield new Expression(branch.cond, operators, operatorsTrie, ctx.opts.lenientIf).value(ctx)
if (isTruthy(cond, ctx)) {
yield r.renderTemplates(branch.templates, ctx, emitter)
return
Expand Down
9 changes: 8 additions & 1 deletion src/liquid-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Cache } from './cache/cache'
import { LRU } from './cache/lru'
import { FS } from './fs/fs'
import { defaultOperators, Operators } from './render/operator'
import { createTrie, Trie } from './util/operator-trie'

export interface LiquidOptions {
/** A directory or an array of directories from where to resolve layout and include templates, and the filename passed to `.renderFile()`. If it's an array, the files are looked up in the order they occur in the array. Defaults to `["."]` */
Expand Down Expand Up @@ -55,6 +56,7 @@ export interface LiquidOptions {
interface NormalizedOptions extends LiquidOptions {
root?: string[];
cache?: Cache<Template[]>;
operatorsTrie?: Trie;
}

export interface NormalizedFullOptions extends NormalizedOptions {
Expand All @@ -79,6 +81,7 @@ export interface NormalizedFullOptions extends NormalizedOptions {
globals: object;
keepOutputType: boolean;
operators: Operators;
operatorsTrie: Trie;
}

export const defaultOptions: NormalizedFullOptions = {
Expand All @@ -102,7 +105,8 @@ export const defaultOptions: NormalizedFullOptions = {
lenientIf: false,
globals: {},
keepOutputType: false,
operators: defaultOperators
operators: defaultOperators,
operatorsTrie: createTrie(defaultOperators)
}

export function normalize (options?: LiquidOptions): NormalizedOptions {
Expand All @@ -117,6 +121,9 @@ export function normalize (options?: LiquidOptions): NormalizedOptions {
else cache = options.cache ? new LRU<Template[]>(1024) : undefined
options.cache = cache
}
if (options.hasOwnProperty('operators')) {
(options as NormalizedOptions).operatorsTrie = createTrie(options.operators!)
}
return options as NormalizedOptions
}

Expand Down
2 changes: 1 addition & 1 deletion src/liquid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class Liquid {
forOwn(builtinFilters, (handler: FilterImplOptions, name: string) => this.registerFilter(snakeCase(name), handler))
}
public parse (html: string, filepath?: string): Template[] {
const tokenizer = new Tokenizer(html, filepath)
const tokenizer = new Tokenizer(html, this.options.operatorsTrie, filepath)
const tokens = tokenizer.readTopLevelTokens(this.options)
return this.parser.parse(tokens)
}
Expand Down
13 changes: 2 additions & 11 deletions src/parser/match-operator.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import { IDENTIFIER } from '../util/character'
import { Trie } from '../util/operator-trie'

const trie = {
a: { n: { d: { end: true, needBoundary: true } } },
o: { r: { end: true, needBoundary: true } },
c: { o: { n: { t: { a: { i: { n: { s: { end: true, needBoundary: true } } } } } } } },
'=': { '=': { end: true } },
'!': { '=': { end: true } },
'>': { end: true, '=': { end: true } },
'<': { end: true, '=': { end: true } }
}

export function matchOperator (str: string, begin: number, end = str.length) {
export function matchOperator (str: string, begin: number, trie: Trie, end = str.length) {
let node = trie
let i = begin
let info
Expand Down
4 changes: 3 additions & 1 deletion src/parser/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { TokenizationError } from '../util/error'
import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
import { TYPES, QUOTE, BLANK, IDENTIFIER } from '../util/character'
import { matchOperator } from './match-operator'
import { Trie } from '../util/operator-trie'

export class Tokenizer {
p = 0
Expand All @@ -30,6 +31,7 @@ export class Tokenizer {

constructor (
private input: string,
private trie: Trie,
private file: string = ''
) {
this.N = input.length
Expand All @@ -54,7 +56,7 @@ export class Tokenizer {
}
readOperator (): OperatorToken | undefined {
this.skipBlank()
const end = matchOperator(this.input, this.p, this.p + 8)
const end = matchOperator(this.input, this.p, this.trie, this.p + 8)
if (end === -1) return
return new OperatorToken(this.input, this.p, (this.p = end), this.file)
}
Expand Down
5 changes: 3 additions & 2 deletions src/render/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ import { range, toValue } from '../util/underscore'
import { Tokenizer } from '../parser/tokenizer'
import { Operators } from '../render/operator'
import { UndefinedVariableError, InternalUndefinedVariableError } from '../util/error'
import { Trie } from '../util/operator-trie'

export class Expression {
private operands: any[] = []
private postfix: Token[]
private lenient: boolean
private operators: Operators

public constructor (str: string, operators: Operators, lenient = false) {
const tokenizer = new Tokenizer(str)
public constructor (str: string, operators: Operators, operatorsTrie: Trie, lenient = false) {
const tokenizer = new Tokenizer(str, operatorsTrie)
this.postfix = [...toPostfix(tokenizer.readExpression())]
this.lenient = lenient
this.operators = operators
Expand Down
2 changes: 1 addition & 1 deletion src/template/tag/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Tokenizer } from '../../parser/tokenizer'
export class Hash {
hash: { [key: string]: any } = {}
constructor (markup: string) {
const tokenizer = new Tokenizer(markup)
const tokenizer = new Tokenizer(markup, {})
for (const hash of tokenizer.readHashes()) {
this.hash[hash.name.content] = hash.value
}
Expand Down
2 changes: 1 addition & 1 deletion src/template/value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class Value {
* @param str the value to be valuated, eg.: "foobar" | truncate: 3
*/
public constructor (str: string, private readonly filterMap: FilterMap, liquid: Liquid) {
const tokenizer = new Tokenizer(str)
const tokenizer = new Tokenizer(str, liquid.options.operatorsTrie)
this.initial = tokenizer.readValue()
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.filterMap.get(name), args, liquid))
}
Expand Down
2 changes: 1 addition & 1 deletion src/tokens/tag-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class TagToken extends DelimitedToken {
const value = input.slice(begin + tagDelimiterLeft.length, end - tagDelimiterRight.length)
super(TokenKind.Tag, value, input, begin, end, trimTagLeft, trimTagRight, file)

const tokenizer = new Tokenizer(this.content)
const tokenizer = new Tokenizer(this.content, options.operatorsTrie)
this.name = tokenizer.readIdentifier().getText()
if (!this.name) throw new TokenizationError(`illegal tag syntax`, this)

Expand Down
27 changes: 27 additions & 0 deletions src/util/operator-trie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Operators } from '../render/operator'

export interface Trie {
[key: string]: any;
}

export function createTrie (operators: Operators): Trie {
const trie: Trie = {}
for (const [name, handler] of Object.entries(operators)) {
let node = trie

for (let i = 0; i < name.length; i++) {
const c = name[i]
node[c] = node[c] || {}

if (i === name.length - 1 && c !== '=') {
node[c].needBoundary = true
}

node = node[c]
}

node.handler = handler
node.end = true
}
return trie
}
6 changes: 4 additions & 2 deletions test/integration/liquid/operators-option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ describe('LiquidOptions#operators', function () {
})

it('should evaluate a custom operator', async function () {
const result = await engine.parseAndRender('{% if "foo" isFooBar "bar" %}True{% endif %}')
expect(result).to.equal('True')
const first = await engine.parseAndRender('{% if "foo" isFooBar "bar" %}True{% else %}False{% endif %}')
expect(first).to.equal('True')
const second = await engine.parseAndRender('{% if "foo" isFooBar "foo" %}True{% else %}False{% endif %}')
expect(second).to.equal('False')
})
})
25 changes: 14 additions & 11 deletions test/unit/parser/match-operator.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import { expect } from 'chai'
import { matchOperator } from '../../../src/parser/match-operator'
import { defaultOperators } from '../../../src/types'
import { createTrie } from '../../../src/util/operator-trie'

describe('parser/matchOperator()', function () {
const trie = createTrie(defaultOperators)
it('should match contains', () => {
expect(matchOperator('contains', 0)).to.equal(8)
expect(matchOperator('contains', 0, trie)).to.equal(8)
})
it('should match comparision', () => {
expect(matchOperator('>', 0)).to.equal(1)
expect(matchOperator('>=', 0)).to.equal(2)
expect(matchOperator('<', 0)).to.equal(1)
expect(matchOperator('<=', 0)).to.equal(2)
expect(matchOperator('>', 0, trie)).to.equal(1)
expect(matchOperator('>=', 0, trie)).to.equal(2)
expect(matchOperator('<', 0, trie)).to.equal(1)
expect(matchOperator('<=', 0, trie)).to.equal(2)
})
it('should match binary logic', () => {
expect(matchOperator('and', 0)).to.equal(3)
expect(matchOperator('or', 0)).to.equal(2)
expect(matchOperator('and', 0, trie)).to.equal(3)
expect(matchOperator('or', 0, trie)).to.equal(2)
})
it('should not match if word not terminate', () => {
expect(matchOperator('true1', 0)).to.equal(-1)
expect(matchOperator('containsa', 0)).to.equal(-1)
expect(matchOperator('true1', 0, trie)).to.equal(-1)
expect(matchOperator('containsa', 0, trie)).to.equal(-1)
})
it('should match if word boundary found', () => {
expect(matchOperator('>=1', 0)).to.equal(2)
expect(matchOperator('contains b', 0)).to.equal(8)
expect(matchOperator('>=1', 0, trie)).to.equal(2)
expect(matchOperator('contains b', 0, trie)).to.equal(8)
})
})
Loading

0 comments on commit befc33c

Please sign in to comment.