mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 20:30:39 -07:00
* fix: more flexible squared property read expression, fixes #643 * fix: unecessary error wrapping in browser bundles * style: update code style and types * perf: use token.value when evalToken
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
export * from './tokenizer'
|
export * from './tokenizer'
|
||||||
export * from './parser'
|
export * from './parser'
|
||||||
export * from './parse-stream'
|
export * from './parse-stream'
|
||||||
export * from './parse-string-literal'
|
|
||||||
export * from './token-kind'
|
export * from './token-kind'
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { matchOperator } from './match-operator'
|
|
||||||
import { defaultOperators } from '..'
|
|
||||||
import { createTrie } from '../util/operator-trie'
|
|
||||||
|
|
||||||
describe('parser/matchOperator()', function () {
|
|
||||||
const trie = createTrie(defaultOperators)
|
|
||||||
it('should match contains', () => {
|
|
||||||
expect(matchOperator('contains', 0, trie)).toBe(8)
|
|
||||||
})
|
|
||||||
it('should match comparision', () => {
|
|
||||||
expect(matchOperator('>', 0, trie)).toBe(1)
|
|
||||||
expect(matchOperator('>=', 0, trie)).toBe(2)
|
|
||||||
expect(matchOperator('<', 0, trie)).toBe(1)
|
|
||||||
expect(matchOperator('<=', 0, trie)).toBe(2)
|
|
||||||
})
|
|
||||||
it('should match binary logic', () => {
|
|
||||||
expect(matchOperator('and', 0, trie)).toBe(3)
|
|
||||||
expect(matchOperator('or', 0, trie)).toBe(2)
|
|
||||||
})
|
|
||||||
it('should not match if word not terminate', () => {
|
|
||||||
expect(matchOperator('true1', 0, trie)).toBe(-1)
|
|
||||||
expect(matchOperator('containsa', 0, trie)).toBe(-1)
|
|
||||||
})
|
|
||||||
it('should match if word boundary found', () => {
|
|
||||||
expect(matchOperator('>=1', 0, trie)).toBe(2)
|
|
||||||
expect(matchOperator('contains b', 0, trie)).toBe(8)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Trie, TrieNode, IDENTIFIER, TYPES } from '../util'
|
|
||||||
|
|
||||||
export function matchOperator (str: string, begin: number, trie: Trie, end = str.length) {
|
|
||||||
let node: TrieNode = trie
|
|
||||||
let i = begin
|
|
||||||
let info
|
|
||||||
while (node[str[i]] && i < end) {
|
|
||||||
node = node[str[i++]]
|
|
||||||
if (node['end']) info = node
|
|
||||||
}
|
|
||||||
if (!info) return -1
|
|
||||||
if (info['needBoundary'] && (TYPES[str.charCodeAt(i)] & IDENTIFIER)) return -1
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
@@ -48,7 +48,7 @@ export class Parser {
|
|||||||
}
|
}
|
||||||
return new HTML(token)
|
return new HTML(token)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof LiquidError) throw e
|
if (LiquidError.is(e)) throw e
|
||||||
throw new ParseError(e as Error, token)
|
throw new ParseError(e as Error, token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { LiquidTagToken, HTMLToken, QuotedToken, OutputToken, TagToken, OperatorToken, RangeToken, PropertyAccessToken, NumberToken, IdentifierToken } from '../tokens'
|
import { LiquidTagToken, HTMLToken, QuotedToken, OutputToken, TagToken, OperatorToken, RangeToken, PropertyAccessToken, NumberToken, IdentifierToken } from '../tokens'
|
||||||
import { Tokenizer } from './tokenizer'
|
import { Tokenizer } from './tokenizer'
|
||||||
|
import { defaultOperators } from '../render/operator'
|
||||||
|
import { createTrie } from '../util/operator-trie'
|
||||||
|
|
||||||
describe('Tokenizer', function () {
|
describe('Tokenizer', function () {
|
||||||
it('should read quoted', () => {
|
it('should read quoted', () => {
|
||||||
@@ -15,12 +17,31 @@ describe('Tokenizer', function () {
|
|||||||
// eslint-disable-next-line deprecation/deprecation
|
// eslint-disable-next-line deprecation/deprecation
|
||||||
expect(new Tokenizer('foo bar').readWord()).toHaveProperty('content', 'foo')
|
expect(new Tokenizer('foo bar').readWord()).toHaveProperty('content', 'foo')
|
||||||
})
|
})
|
||||||
it('should read number value', () => {
|
it('should read integer number', () => {
|
||||||
const token: NumberToken = new Tokenizer('2.33.2').readValueOrThrow() as any
|
const token: NumberToken = new Tokenizer('123').readValueOrThrow() as any
|
||||||
expect(token).toBeInstanceOf(NumberToken)
|
expect(token).toBeInstanceOf(NumberToken)
|
||||||
expect(token.whole.getText()).toBe('2')
|
expect(token.getText()).toBe('123')
|
||||||
expect(token.decimal!.getText()).toBe('33')
|
expect(token.content).toBe(123)
|
||||||
expect(token.getText()).toBe('2.33')
|
})
|
||||||
|
it('should read negative number', () => {
|
||||||
|
const token: NumberToken = new Tokenizer('-123').readValueOrThrow() as any
|
||||||
|
expect(token).toBeInstanceOf(NumberToken)
|
||||||
|
expect(token.getText()).toBe('-123')
|
||||||
|
expect(token.content).toBe(-123)
|
||||||
|
})
|
||||||
|
it('should read float number', () => {
|
||||||
|
const token: NumberToken = new Tokenizer('1.23').readValueOrThrow() as any
|
||||||
|
expect(token).toBeInstanceOf(NumberToken)
|
||||||
|
expect(token.getText()).toBe('1.23')
|
||||||
|
expect(token.content).toBe(1.23)
|
||||||
|
})
|
||||||
|
it('should treat 1.2.3 as property read', () => {
|
||||||
|
const token: PropertyAccessToken = new Tokenizer('1.2.3').readValueOrThrow() as any
|
||||||
|
expect(token).toBeInstanceOf(PropertyAccessToken)
|
||||||
|
expect(token.props).toHaveLength(3)
|
||||||
|
expect(token.props[0].getText()).toBe('1')
|
||||||
|
expect(token.props[1].getText()).toBe('2')
|
||||||
|
expect(token.props[2].getText()).toBe('3')
|
||||||
})
|
})
|
||||||
it('should read quoted value', () => {
|
it('should read quoted value', () => {
|
||||||
const value = new Tokenizer('"foo"a').readValue()
|
const value = new Tokenizer('"foo"a').readValue()
|
||||||
@@ -33,11 +54,7 @@ describe('Tokenizer', function () {
|
|||||||
it('should read quoted property access value', () => {
|
it('should read quoted property access value', () => {
|
||||||
const value = new Tokenizer('["a prop"]').readValue()
|
const value = new Tokenizer('["a prop"]').readValue()
|
||||||
expect(value).toBeInstanceOf(PropertyAccessToken)
|
expect(value).toBeInstanceOf(PropertyAccessToken)
|
||||||
expect((value as PropertyAccessToken).variable.getText()).toBe('"a prop"')
|
expect((value as QuotedToken).getText()).toBe('["a prop"]')
|
||||||
})
|
|
||||||
it('should throw for broken quoted property access', () => {
|
|
||||||
const tokenizer = new Tokenizer('[5]')
|
|
||||||
expect(() => tokenizer.readValueOrThrow()).toThrow()
|
|
||||||
})
|
})
|
||||||
it('should throw for incomplete quoted property access', () => {
|
it('should throw for incomplete quoted property access', () => {
|
||||||
const tokenizer = new Tokenizer('["a prop"')
|
const tokenizer = new Tokenizer('["a prop"')
|
||||||
@@ -277,10 +294,10 @@ describe('Tokenizer', function () {
|
|||||||
|
|
||||||
const pa: PropertyAccessToken = token!.args[0] as any
|
const pa: PropertyAccessToken = token!.args[0] as any
|
||||||
expect(token!.args[0]).toBeInstanceOf(PropertyAccessToken)
|
expect(token!.args[0]).toBeInstanceOf(PropertyAccessToken)
|
||||||
expect((pa.variable as any).content).toBe('arr')
|
expect(pa.props).toHaveLength(2)
|
||||||
expect(pa.props).toHaveLength(1)
|
expect((pa.props[0] as any).content).toBe('arr')
|
||||||
expect(pa.props[0]).toBeInstanceOf(NumberToken)
|
expect(pa.props[1]).toBeInstanceOf(NumberToken)
|
||||||
expect(pa.props[0].getText()).toBe('0')
|
expect(pa.props[1].getText()).toBe('0')
|
||||||
})
|
})
|
||||||
it('should read a filter with obj.foo argument', function () {
|
it('should read a filter with obj.foo argument', function () {
|
||||||
const tokenizer = new Tokenizer('| plus: obj.foo')
|
const tokenizer = new Tokenizer('| plus: obj.foo')
|
||||||
@@ -290,10 +307,10 @@ describe('Tokenizer', function () {
|
|||||||
|
|
||||||
const pa: PropertyAccessToken = token!.args[0] as any
|
const pa: PropertyAccessToken = token!.args[0] as any
|
||||||
expect(token!.args[0]).toBeInstanceOf(PropertyAccessToken)
|
expect(token!.args[0]).toBeInstanceOf(PropertyAccessToken)
|
||||||
expect((pa.variable as any).content).toBe('obj')
|
expect(pa.props).toHaveLength(2)
|
||||||
expect(pa.props).toHaveLength(1)
|
expect((pa.props[0] as any).content).toBe('obj')
|
||||||
expect(pa.props[0]).toBeInstanceOf(IdentifierToken)
|
expect(pa.props[1]).toBeInstanceOf(IdentifierToken)
|
||||||
expect(pa.props[0].getText()).toBe('foo')
|
expect(pa.props[1].getText()).toBe('foo')
|
||||||
})
|
})
|
||||||
it('should read a filter with obj["foo"] argument', function () {
|
it('should read a filter with obj["foo"] argument', function () {
|
||||||
const tokenizer = new Tokenizer('| plus: obj["good luck"]')
|
const tokenizer = new Tokenizer('| plus: obj["good luck"]')
|
||||||
@@ -304,8 +321,8 @@ describe('Tokenizer', function () {
|
|||||||
const pa: PropertyAccessToken = token!.args[0] as any
|
const pa: PropertyAccessToken = token!.args[0] as any
|
||||||
expect(token!.args[0]).toBeInstanceOf(PropertyAccessToken)
|
expect(token!.args[0]).toBeInstanceOf(PropertyAccessToken)
|
||||||
expect(pa.getText()).toBe('obj["good luck"]')
|
expect(pa.getText()).toBe('obj["good luck"]')
|
||||||
expect((pa.variable as any).content).toBe('obj')
|
expect((pa.props[0] as any).content).toBe('obj')
|
||||||
expect(pa.props[0].getText()).toBe('"good luck"')
|
expect(pa.props[1].getText()).toBe('"good luck"')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('#readFilters()', () => {
|
describe('#readFilters()', () => {
|
||||||
@@ -341,7 +358,7 @@ describe('Tokenizer', function () {
|
|||||||
expect(tokens[2].args).toHaveLength(1)
|
expect(tokens[2].args).toHaveLength(1)
|
||||||
expect(tokens[2].args[0]).toBeInstanceOf(PropertyAccessToken)
|
expect(tokens[2].args[0]).toBeInstanceOf(PropertyAccessToken)
|
||||||
expect((tokens[2].args[0] as any).getText()).toBe('foo[a.b["c d"]]')
|
expect((tokens[2].args[0] as any).getText()).toBe('foo[a.b["c d"]]')
|
||||||
expect((tokens[2].args[0] as any).props[0].getText()).toBe('a.b["c d"]')
|
expect((tokens[2].args[0] as any).props[1].getText()).toBe('a.b["c d"]')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('#readExpression()', () => {
|
describe('#readExpression()', () => {
|
||||||
@@ -358,10 +375,10 @@ describe('Tokenizer', function () {
|
|||||||
expect(exp).toHaveLength(1)
|
expect(exp).toHaveLength(1)
|
||||||
const pa = exp[0] as PropertyAccessToken
|
const pa = exp[0] as PropertyAccessToken
|
||||||
expect(pa).toBeInstanceOf(PropertyAccessToken)
|
expect(pa).toBeInstanceOf(PropertyAccessToken)
|
||||||
expect((pa.variable as any).content).toEqual('a')
|
expect(pa.props).toHaveLength(3)
|
||||||
expect(pa.props).toHaveLength(2)
|
expect((pa.props[0] as any).content).toEqual('a')
|
||||||
|
|
||||||
const [p1, p2] = pa.props
|
const [, p1, p2] = pa.props
|
||||||
expect(p1).toBeInstanceOf(IdentifierToken)
|
expect(p1).toBeInstanceOf(IdentifierToken)
|
||||||
expect(p1.getText()).toBe('')
|
expect(p1.getText()).toBe('')
|
||||||
expect(p2).toBeInstanceOf(PropertyAccessToken)
|
expect(p2).toBeInstanceOf(PropertyAccessToken)
|
||||||
@@ -373,8 +390,8 @@ describe('Tokenizer', function () {
|
|||||||
expect(exp).toHaveLength(1)
|
expect(exp).toHaveLength(1)
|
||||||
const pa = exp[0] as PropertyAccessToken
|
const pa = exp[0] as PropertyAccessToken
|
||||||
expect(pa).toBeInstanceOf(PropertyAccessToken)
|
expect(pa).toBeInstanceOf(PropertyAccessToken)
|
||||||
expect((pa.variable as any).content).toEqual('a')
|
expect(pa.props).toHaveLength(1)
|
||||||
expect(pa.props).toHaveLength(0)
|
expect((pa.props[0] as any).content).toEqual('a')
|
||||||
})
|
})
|
||||||
it('should read expression `a ==`', () => {
|
it('should read expression `a ==`', () => {
|
||||||
const exp = [...new Tokenizer('a ==').readExpressionTokens()]
|
const exp = [...new Tokenizer('a ==').readExpressionTokens()]
|
||||||
@@ -481,6 +498,30 @@ describe('Tokenizer', function () {
|
|||||||
expect(rhs.getText()).toEqual('"\\""')
|
expect(rhs.getText()).toEqual('"\\""')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
describe('#matchTrie()', function () {
|
||||||
|
const opTrie = createTrie(defaultOperators)
|
||||||
|
it('should match contains', () => {
|
||||||
|
expect(new Tokenizer('contains').matchTrie(opTrie)).toBe(8)
|
||||||
|
})
|
||||||
|
it('should match comparision', () => {
|
||||||
|
expect(new Tokenizer('>').matchTrie(opTrie)).toBe(1)
|
||||||
|
expect(new Tokenizer('>=').matchTrie(opTrie)).toBe(2)
|
||||||
|
expect(new Tokenizer('<').matchTrie(opTrie)).toBe(1)
|
||||||
|
expect(new Tokenizer('<=').matchTrie(opTrie)).toBe(2)
|
||||||
|
})
|
||||||
|
it('should match binary logic', () => {
|
||||||
|
expect(new Tokenizer('and').matchTrie(opTrie)).toBe(3)
|
||||||
|
expect(new Tokenizer('or').matchTrie(opTrie)).toBe(2)
|
||||||
|
})
|
||||||
|
it('should not match if word not terminate', () => {
|
||||||
|
expect(new Tokenizer('true1').matchTrie(opTrie)).toBe(-1)
|
||||||
|
expect(new Tokenizer('containsa').matchTrie(opTrie)).toBe(-1)
|
||||||
|
})
|
||||||
|
it('should match if word boundary found', () => {
|
||||||
|
expect(new Tokenizer('>=1').matchTrie(opTrie)).toBe(2)
|
||||||
|
expect(new Tokenizer('contains b').matchTrie(opTrie)).toBe(8)
|
||||||
|
})
|
||||||
|
})
|
||||||
describe('#readLiquidTagTokens', () => {
|
describe('#readLiquidTagTokens', () => {
|
||||||
it('should read newline terminated tokens', () => {
|
it('should read newline terminated tokens', () => {
|
||||||
const tokenizer = new Tokenizer('echo \'hello\'')
|
const tokenizer = new Tokenizer('echo \'hello\'')
|
||||||
|
|||||||
+71
-32
@@ -1,16 +1,17 @@
|
|||||||
import { FilteredValueToken, TagToken, HTMLToken, HashToken, QuotedToken, LiquidTagToken, OutputToken, ValueToken, Token, RangeToken, FilterToken, TopLevelToken, PropertyAccessToken, OperatorToken, LiteralToken, IdentifierToken, NumberToken } from '../tokens'
|
import { FilteredValueToken, TagToken, HTMLToken, HashToken, QuotedToken, LiquidTagToken, OutputToken, ValueToken, Token, RangeToken, FilterToken, TopLevelToken, PropertyAccessToken, OperatorToken, LiteralToken, IdentifierToken, NumberToken } from '../tokens'
|
||||||
import { Trie, createTrie, ellipsis, literalValues, TokenizationError, TYPES, QUOTE, BLANK, IDENTIFIER } from '../util'
|
import { OperatorHandler } from '../render/operator'
|
||||||
|
import { TrieNode, LiteralValue, Trie, createTrie, ellipsis, literalValues, TokenizationError, TYPES, QUOTE, BLANK, IDENTIFIER, NUMBER, SIGN } from '../util'
|
||||||
import { Operators, Expression } from '../render'
|
import { Operators, Expression } from '../render'
|
||||||
import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
|
import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
|
||||||
import { FilterArg } from './filter-arg'
|
import { FilterArg } from './filter-arg'
|
||||||
import { matchOperator } from './match-operator'
|
|
||||||
import { whiteSpaceCtrl } from './whitespace-ctrl'
|
import { whiteSpaceCtrl } from './whitespace-ctrl'
|
||||||
|
|
||||||
export class Tokenizer {
|
export class Tokenizer {
|
||||||
p: number
|
p: number
|
||||||
N: number
|
N: number
|
||||||
private rawBeginAt = -1
|
private rawBeginAt = -1
|
||||||
private opTrie: Trie
|
private opTrie: Trie<OperatorHandler>
|
||||||
|
private literalTrie: Trie<LiteralValue>
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public input: string,
|
public input: string,
|
||||||
@@ -21,6 +22,7 @@ export class Tokenizer {
|
|||||||
this.p = range ? range[0] : 0
|
this.p = range ? range[0] : 0
|
||||||
this.N = range ? range[1] : input.length
|
this.N = range ? range[1] : input.length
|
||||||
this.opTrie = createTrie(operators)
|
this.opTrie = createTrie(operators)
|
||||||
|
this.literalTrie = createTrie(literalValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
readExpression () {
|
readExpression () {
|
||||||
@@ -44,10 +46,22 @@ export class Tokenizer {
|
|||||||
}
|
}
|
||||||
readOperator (): OperatorToken | undefined {
|
readOperator (): OperatorToken | undefined {
|
||||||
this.skipBlank()
|
this.skipBlank()
|
||||||
const end = matchOperator(this.input, this.p, this.opTrie)
|
const end = this.matchTrie(this.opTrie)
|
||||||
if (end === -1) return
|
if (end === -1) return
|
||||||
return new OperatorToken(this.input, this.p, (this.p = end), this.file)
|
return new OperatorToken(this.input, this.p, (this.p = end), this.file)
|
||||||
}
|
}
|
||||||
|
matchTrie<T> (trie: Trie<T>) {
|
||||||
|
let node: TrieNode<T> = trie
|
||||||
|
let i = this.p
|
||||||
|
let info
|
||||||
|
while (node[this.input[i]] && i < this.N) {
|
||||||
|
node = node[this.input[i++]]
|
||||||
|
if (node['end']) info = node
|
||||||
|
}
|
||||||
|
if (!info) return -1
|
||||||
|
if (info['needBoundary'] && (this.peekType(i - this.p) & IDENTIFIER)) return -1
|
||||||
|
return i
|
||||||
|
}
|
||||||
readFilteredValue (): FilteredValueToken {
|
readFilteredValue (): FilteredValueToken {
|
||||||
const begin = this.p
|
const begin = this.p
|
||||||
const initial = this.readExpression()
|
const initial = this.readExpression()
|
||||||
@@ -272,8 +286,8 @@ export class Tokenizer {
|
|||||||
return this.input.slice(this.p, this.N)
|
return this.input.slice(this.p, this.N)
|
||||||
}
|
}
|
||||||
|
|
||||||
advance (i = 1) {
|
advance (step = 1) {
|
||||||
this.p += i
|
this.p += step
|
||||||
}
|
}
|
||||||
|
|
||||||
end () {
|
end () {
|
||||||
@@ -289,43 +303,68 @@ export class Tokenizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readValue (): ValueToken | undefined {
|
readValue (): ValueToken | undefined {
|
||||||
const value = this.readQuoted() || this.readRange()
|
this.skipBlank()
|
||||||
if (value) return value
|
const begin = this.p
|
||||||
|
const variable = this.readLiteral() || this.readQuoted() || this.readRange() || this.readNumber()
|
||||||
if (this.peek() === '[') {
|
const props: (ValueToken | IdentifierToken)[] = []
|
||||||
this.p++
|
|
||||||
const prop = this.readQuoted()
|
|
||||||
if (!prop) return
|
|
||||||
if (this.peek() !== ']') return
|
|
||||||
this.p++
|
|
||||||
return new PropertyAccessToken(prop, [], this.p)
|
|
||||||
}
|
|
||||||
|
|
||||||
const variable = this.readIdentifier()
|
|
||||||
if (!variable.size()) return
|
|
||||||
|
|
||||||
let isNumber = variable.isNumber(true)
|
|
||||||
const props: (QuotedToken | IdentifierToken)[] = []
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (this.peek() === '[') {
|
if (this.peek() === '[') {
|
||||||
isNumber = false
|
|
||||||
this.p++
|
this.p++
|
||||||
const prop = this.readValue() || new IdentifierToken(this.input, this.p, this.p, this.file)
|
const prop = this.readValue() || new IdentifierToken(this.input, this.p, this.p, this.file)
|
||||||
this.readTo(']')
|
this.assert(this.readTo(']') !== -1, '[ not closed')
|
||||||
props.push(prop)
|
props.push(prop)
|
||||||
} else if (this.peek() === '.' && this.peek(1) !== '.') { // skip range syntax
|
continue
|
||||||
|
}
|
||||||
|
if (!variable && !props.length) {
|
||||||
|
const prop = this.readIdentifier()
|
||||||
|
if (prop.size()) {
|
||||||
|
props.push(prop)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.peek() === '.' && this.peek(1) !== '.') { // skip range syntax
|
||||||
this.p++
|
this.p++
|
||||||
const prop = this.readIdentifier()
|
const prop = this.readIdentifier()
|
||||||
if (!prop.size()) break
|
if (!prop.size()) break
|
||||||
if (!prop.isNumber()) isNumber = false
|
|
||||||
props.push(prop)
|
props.push(prop)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (!props.length) return variable
|
||||||
|
return new PropertyAccessToken(variable, props, this.input, begin, this.p)
|
||||||
|
}
|
||||||
|
|
||||||
|
readNumber (): NumberToken | undefined {
|
||||||
|
this.skipBlank()
|
||||||
|
let decimalFound = false
|
||||||
|
let digitFound = false
|
||||||
|
let n = 0
|
||||||
|
if (this.peekType() & SIGN) n++
|
||||||
|
while (this.p + n <= this.N) {
|
||||||
|
if (this.peekType(n) & NUMBER) {
|
||||||
|
digitFound = true
|
||||||
|
n++
|
||||||
|
} else if (this.peek(n) === '.' && this.peek(n + 1) !== '.') {
|
||||||
|
if (decimalFound || !digitFound) return
|
||||||
|
decimalFound = true
|
||||||
|
n++
|
||||||
} else break
|
} else break
|
||||||
}
|
}
|
||||||
if (!props.length && literalValues.hasOwnProperty(variable.content)) {
|
if (digitFound && !(this.peekType(n) & IDENTIFIER)) {
|
||||||
return new LiteralToken(this.input, variable.begin, variable.end, this.file)
|
const num = new NumberToken(this.input, this.p, this.p + n, this.file)
|
||||||
|
this.advance(n)
|
||||||
|
return num
|
||||||
}
|
}
|
||||||
if (isNumber) return new NumberToken(variable, props[0] as IdentifierToken)
|
}
|
||||||
return new PropertyAccessToken(variable, props, this.p)
|
|
||||||
|
readLiteral (): LiteralToken | undefined {
|
||||||
|
this.skipBlank()
|
||||||
|
const end = this.matchTrie(this.literalTrie)
|
||||||
|
if (end === -1) return
|
||||||
|
const literal = new LiteralToken(this.input, this.p, end, this.file)
|
||||||
|
this.p = end
|
||||||
|
return literal
|
||||||
}
|
}
|
||||||
|
|
||||||
readRange (): RangeToken | undefined {
|
readRange (): RangeToken | undefined {
|
||||||
@@ -388,7 +427,7 @@ export class Tokenizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
peekType (n = 0) {
|
peekType (n = 0) {
|
||||||
return TYPES[this.input.charCodeAt(this.p + n)]
|
return this.p + n >= this.N ? 0 : TYPES[this.input.charCodeAt(this.p + n)]
|
||||||
}
|
}
|
||||||
|
|
||||||
peek (n = 0): string {
|
peek (n = 0): string {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Tokenizer } from '../parser'
|
import { Tokenizer } from '../parser'
|
||||||
import { Drop } from '../drop'
|
import { Drop } from '../drop'
|
||||||
|
import { QuotedToken } from '../tokens'
|
||||||
import { Context } from '../context'
|
import { Context } from '../context'
|
||||||
import { toPromise, toValueSync } from '../util'
|
import { toPromise, toValueSync } from '../util'
|
||||||
|
import { evalQuotedToken } from './expression'
|
||||||
|
|
||||||
describe('Expression', function () {
|
describe('Expression', function () {
|
||||||
const ctx = new Context({})
|
const ctx = new Context({})
|
||||||
@@ -32,7 +34,9 @@ describe('Expression', function () {
|
|||||||
expect(await toPromise(create('"foo"').evaluate(ctx, false))).toBe('foo')
|
expect(await toPromise(create('"foo"').evaluate(ctx, false))).toBe('foo')
|
||||||
expect(await toPromise(create('false').evaluate(ctx, false))).toBe(false)
|
expect(await toPromise(create('false').evaluate(ctx, false))).toBe(false)
|
||||||
})
|
})
|
||||||
|
it('should support evalQuotedToken()', async function () {
|
||||||
|
expect(evalQuotedToken(new QuotedToken('"foo"', 0, 5))).toBe('foo')
|
||||||
|
})
|
||||||
it('should eval property access', async function () {
|
it('should eval property access', async function () {
|
||||||
const ctx = new Context({
|
const ctx = new Context({
|
||||||
foo: { bar: 'BAR' },
|
foo: { bar: 'BAR' },
|
||||||
@@ -162,6 +166,10 @@ describe('Expression', function () {
|
|||||||
const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } })
|
const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } })
|
||||||
expect(await toPromise(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).toBe('FOO')
|
expect(await toPromise(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).toBe('FOO')
|
||||||
})
|
})
|
||||||
|
it('should allow bracket quoted property access', async function () {
|
||||||
|
const ctx = new Context({ 'foo bar': { coo: 'FOO BAR' } })
|
||||||
|
expect(await toPromise(create('["foo bar"].coo').evaluate(ctx, false))).toBe('FOO BAR')
|
||||||
|
})
|
||||||
it('should support not', async function () {
|
it('should support not', async function () {
|
||||||
expect(await toPromise(create('not 1 < 2').evaluate(ctx))).toBe(false)
|
expect(await toPromise(create('not 1 < 2').evaluate(ctx))).toBe(false)
|
||||||
})
|
})
|
||||||
@@ -169,6 +177,24 @@ describe('Expression', function () {
|
|||||||
expect(await toPromise(create('not 1 < 2 or not 1 > 2').evaluate(ctx))).toBe(true)
|
expect(await toPromise(create('not 1 < 2 or not 1 > 2').evaluate(ctx))).toBe(true)
|
||||||
expect(await toPromise(create('not 1 < 2 and not 1 > 2').evaluate(ctx))).toBe(false)
|
expect(await toPromise(create('not 1 < 2 and not 1 > 2').evaluate(ctx))).toBe(false)
|
||||||
})
|
})
|
||||||
|
it('should allow variable as squared sub property key', async function () {
|
||||||
|
const ctx = new Context({ 'foo': { bar: 'BAR' }, 'key': 'bar' })
|
||||||
|
expect(await toPromise(create('foo[key]').evaluate(ctx))).toBe('BAR')
|
||||||
|
})
|
||||||
|
it('should allow propertyAccessToken as squared sub property key', async function () {
|
||||||
|
const ctx = new Context({ 'foo': { bar: 'BAR', key: 'bar' } })
|
||||||
|
expect(await toPromise(create('foo[foo.key]').evaluate(ctx))).toBe('BAR')
|
||||||
|
})
|
||||||
|
it('should allow nested squared property read', async function () {
|
||||||
|
const ctx = new Context({ 'foo': { bar: 'BAR', key: 'bar' } })
|
||||||
|
expect(await toPromise(create('foo[foo["key"]]').evaluate(ctx))).toBe('BAR')
|
||||||
|
})
|
||||||
|
it('should allow string as property read variable', async function () {
|
||||||
|
expect(await toPromise(create('"foo"[2]').evaluate(ctx))).toBe('o')
|
||||||
|
})
|
||||||
|
it('should allow range as property read variable', async function () {
|
||||||
|
expect(await toPromise(create('(3..5).size').evaluate(ctx))).toBe(3)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('sync', function () {
|
describe('sync', function () {
|
||||||
|
|||||||
+11
-18
@@ -1,6 +1,5 @@
|
|||||||
import { RangeToken, OperatorToken, Token, LiteralToken, NumberToken, PropertyAccessToken, QuotedToken, OperatorType, operatorTypes } from '../tokens'
|
import { QuotedToken, RangeToken, OperatorToken, Token, PropertyAccessToken, OperatorType, operatorTypes } from '../tokens'
|
||||||
import { isQuotedToken, isWordToken, isNumberToken, isLiteralToken, isRangeToken, isPropertyAccessToken, UndefinedVariableError, range, isOperatorToken, literalValues, assert } from '../util'
|
import { isRangeToken, isPropertyAccessToken, UndefinedVariableError, range, isOperatorToken, assert } from '../util'
|
||||||
import { parseStringLiteral } from '../parser'
|
|
||||||
import type { Context } from '../context'
|
import type { Context } from '../context'
|
||||||
import type { UnaryOperatorHandler } from '../render'
|
import type { UnaryOperatorHandler } from '../render'
|
||||||
|
|
||||||
@@ -36,38 +35,32 @@ export class Expression {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function * evalToken (token: Token | undefined, ctx: Context, lenient = false): IterableIterator<unknown> {
|
export function * evalToken (token: Token | undefined, ctx: Context, lenient = false): IterableIterator<unknown> {
|
||||||
|
if (!token) return
|
||||||
|
if ('content' in token) return token.content
|
||||||
if (isPropertyAccessToken(token)) return yield evalPropertyAccessToken(token, ctx, lenient)
|
if (isPropertyAccessToken(token)) return yield evalPropertyAccessToken(token, ctx, lenient)
|
||||||
if (isRangeToken(token)) return yield evalRangeToken(token, ctx)
|
if (isRangeToken(token)) return yield evalRangeToken(token, ctx)
|
||||||
if (isLiteralToken(token)) return evalLiteralToken(token)
|
|
||||||
if (isNumberToken(token)) return evalNumberToken(token)
|
|
||||||
if (isWordToken(token)) return token.getText()
|
|
||||||
if (isQuotedToken(token)) return evalQuotedToken(token)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function * evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean): IterableIterator<unknown> {
|
function * evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean): IterableIterator<unknown> {
|
||||||
const props: string[] = []
|
const props: string[] = []
|
||||||
|
const variable = yield evalToken(token.variable, ctx, lenient)
|
||||||
for (const prop of token.props) {
|
for (const prop of token.props) {
|
||||||
props.push((yield evalToken(prop, ctx, false)) as unknown as string)
|
props.push((yield evalToken(prop, ctx, false)) as unknown as string)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return yield ctx._get([token.propertyName, ...props])
|
if (token.variable) {
|
||||||
|
return yield ctx._getFromScope(variable, props)
|
||||||
|
} else {
|
||||||
|
return yield ctx._get(props)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (lenient && (e as Error).name === 'InternalUndefinedVariableError') return null
|
if (lenient && (e as Error).name === 'InternalUndefinedVariableError') return null
|
||||||
throw (new UndefinedVariableError(e as Error, token))
|
throw (new UndefinedVariableError(e as Error, token))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function evalNumberToken (token: NumberToken) {
|
|
||||||
const str = token.whole.content + '.' + (token.decimal ? token.decimal.content : '')
|
|
||||||
return Number(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function evalQuotedToken (token: QuotedToken) {
|
export function evalQuotedToken (token: QuotedToken) {
|
||||||
return parseStringLiteral(token.getText())
|
return token.content
|
||||||
}
|
|
||||||
|
|
||||||
function evalLiteralToken (token: LiteralToken) {
|
|
||||||
return literalValues[token.literal]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function * evalRangeToken (token: RangeToken, ctx: Context) {
|
function * evalRangeToken (token: RangeToken, ctx: Context) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { parseStringLiteral } from './parse-string-literal'
|
import { parseStringLiteral } from './string'
|
||||||
|
|
||||||
describe('parseStringLiteral()', function () {
|
describe('parseStringLiteral()', function () {
|
||||||
it('should parse octal escape', () => {
|
it('should parse octal escape', () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Context } from '../context'
|
import { Context } from '../context'
|
||||||
import { toPromise } from '../util'
|
import { toPromise } from '../util'
|
||||||
import { IdentifierToken, NumberToken, QuotedToken } from '../tokens'
|
import { NumberToken, QuotedToken } from '../tokens'
|
||||||
import { Filter } from './filter'
|
import { Filter } from './filter'
|
||||||
|
|
||||||
describe('filter', function () {
|
describe('filter', function () {
|
||||||
@@ -13,7 +13,7 @@ describe('filter', function () {
|
|||||||
|
|
||||||
it('should call filter impl with correct arguments', async function () {
|
it('should call filter impl with correct arguments', async function () {
|
||||||
const spy = jest.fn()
|
const spy = jest.fn()
|
||||||
const thirty = new NumberToken(new IdentifierToken('30', 0, 2), undefined)
|
const thirty = new NumberToken('30', 0, 2, undefined)
|
||||||
const filter = new Filter('foo', spy, [thirty], liquid)
|
const filter = new Filter('foo', spy, [thirty], liquid)
|
||||||
await toPromise(filter.render('foo', ctx))
|
await toPromise(filter.render('foo', ctx))
|
||||||
expect(spy).toHaveBeenCalledWith('foo', 30)
|
expect(spy).toHaveBeenCalledWith('foo', 30)
|
||||||
@@ -23,7 +23,7 @@ describe('filter', function () {
|
|||||||
const val = yield this.context._get([valStr])
|
const val = yield this.context._get([valStr])
|
||||||
return `${this.liquid.testVersion}: ${val + diff}`
|
return `${this.liquid.testVersion}: ${val + diff}`
|
||||||
})
|
})
|
||||||
const ten = new NumberToken(new IdentifierToken('10', 0, 2), undefined)
|
const ten = new NumberToken('10', 0, 2, undefined)
|
||||||
const filter = new Filter('add', spy, [ten], liquid)
|
const filter = new Filter('add', spy, [ten], liquid)
|
||||||
const val = await toPromise(filter.render('thirty', ctx))
|
const val = await toPromise(filter.render('thirty', ctx))
|
||||||
expect(val).toEqual('1.0: 40')
|
expect(val).toEqual('1.0: 40')
|
||||||
@@ -33,12 +33,12 @@ describe('filter', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render filters with argument', async function () {
|
it('should render filters with argument', async function () {
|
||||||
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
|
const two = new NumberToken('2', 0, 1, undefined)
|
||||||
expect(await toPromise(new Filter('add', (a: number, b: number) => a + b, [two], liquid).render(3, ctx))).toBe(5)
|
expect(await toPromise(new Filter('add', (a: number, b: number) => a + b, [two], liquid).render(3, ctx))).toBe(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render filters with multiple arguments', async function () {
|
it('should render filters with multiple arguments', async function () {
|
||||||
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
|
const two = new NumberToken('2', 0, 1, undefined)
|
||||||
const c = new QuotedToken('"c"', 0, 3)
|
const c = new QuotedToken('"c"', 0, 3)
|
||||||
expect(await toPromise(new Filter('add', (a: number, b: number, c: number) => a + b + c, [two, c], liquid).render(3, ctx))).toBe('5c')
|
expect(await toPromise(new Filter('add', (a: number, b: number, c: number) => a + b + c, [two, c], liquid).render(3, ctx))).toBe('5c')
|
||||||
})
|
})
|
||||||
@@ -49,7 +49,7 @@ describe('filter', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support key value pairs', async function () {
|
it('should support key value pairs', async function () {
|
||||||
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
|
const two = new NumberToken('2', 0, 1, undefined)
|
||||||
expect(await toPromise(new Filter('add', (a: number, b: number[]) => b[0] + ':' + (a + b[1]), [['num', two]], liquid).render(3, ctx))).toBe('num:5')
|
expect(await toPromise(new Filter('add', (a: number, b: number[]) => b[0] + ':' + (a + b[1]), [['num', two]], liquid).render(3, ctx))).toBe('num:5')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Token } from './token'
|
import { Token } from './token'
|
||||||
import { TokenKind } from '../parser'
|
import { TokenKind } from '../parser'
|
||||||
|
import { literalValues, LiteralValue } from '../util'
|
||||||
|
|
||||||
export class LiteralToken extends Token {
|
export class LiteralToken extends Token {
|
||||||
|
public content: LiteralValue
|
||||||
public literal: string
|
public literal: string
|
||||||
public constructor (
|
public constructor (
|
||||||
public input: string,
|
public input: string,
|
||||||
@@ -11,5 +13,6 @@ export class LiteralToken extends Token {
|
|||||||
) {
|
) {
|
||||||
super(TokenKind.Literal, input, begin, end, file)
|
super(TokenKind.Literal, input, begin, end, file)
|
||||||
this.literal = this.getText()
|
this.literal = this.getText()
|
||||||
|
this.content = literalValues[this.literal]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { Token } from './token'
|
import { Token } from './token'
|
||||||
import { IdentifierToken } from './identifier-token'
|
|
||||||
import { TokenKind } from '../parser'
|
import { TokenKind } from '../parser'
|
||||||
|
|
||||||
export class NumberToken extends Token {
|
export class NumberToken extends Token {
|
||||||
|
public content: number
|
||||||
constructor (
|
constructor (
|
||||||
public whole: IdentifierToken,
|
public input: string,
|
||||||
public decimal?: IdentifierToken
|
public begin: number,
|
||||||
|
public end: number,
|
||||||
|
public file?: string
|
||||||
) {
|
) {
|
||||||
super(TokenKind.Number, whole.input, whole.begin, decimal ? decimal.end : whole.end, whole.file)
|
super(TokenKind.Number, input, begin, end, file)
|
||||||
|
this.content = Number(this.getText())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { QuotedToken, PropertyAccessToken, IdentifierToken } from '.'
|
|
||||||
|
|
||||||
describe('PropertyAccessToken', function () {
|
|
||||||
describe('#propertyName', function () {
|
|
||||||
it('should return correct value for IdentifierToken', function () {
|
|
||||||
const token = new PropertyAccessToken(new IdentifierToken('foo', 0, 3), [], 3)
|
|
||||||
expect(token.propertyName).toBe('foo')
|
|
||||||
})
|
|
||||||
it('should return correct value for QuotedToken', function () {
|
|
||||||
const token = new PropertyAccessToken(new QuotedToken('"foo bar"', 0, 9), [], 9)
|
|
||||||
expect(token.propertyName).toBe('foo bar')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,18 +1,21 @@
|
|||||||
import { Token } from './token'
|
import { Token } from './token'
|
||||||
|
import { LiteralToken } from './literal-token'
|
||||||
|
import { ValueToken } from './value-token'
|
||||||
import { IdentifierToken } from './identifier-token'
|
import { IdentifierToken } from './identifier-token'
|
||||||
|
import { NumberToken } from './number-token'
|
||||||
|
import { RangeToken } from './range-token'
|
||||||
import { QuotedToken } from './quoted-token'
|
import { QuotedToken } from './quoted-token'
|
||||||
import { TokenKind, parseStringLiteral } from '../parser'
|
import { TokenKind } from '../parser'
|
||||||
|
|
||||||
export class PropertyAccessToken extends Token {
|
export class PropertyAccessToken extends Token {
|
||||||
public propertyName: string
|
|
||||||
constructor (
|
constructor (
|
||||||
public variable: IdentifierToken | QuotedToken,
|
public variable: QuotedToken | RangeToken | LiteralToken | NumberToken | undefined,
|
||||||
public props: (IdentifierToken | QuotedToken | PropertyAccessToken)[],
|
public props: (ValueToken | IdentifierToken)[],
|
||||||
end: number
|
input: string,
|
||||||
|
begin: number,
|
||||||
|
end: number,
|
||||||
|
file?: string
|
||||||
) {
|
) {
|
||||||
super(TokenKind.PropertyAccess, variable.input, variable.begin, end, variable.file)
|
super(TokenKind.PropertyAccess, input, begin, end, file)
|
||||||
this.propertyName = this.variable instanceof IdentifierToken
|
|
||||||
? this.variable.getText()
|
|
||||||
: parseStringLiteral(this.variable.getText())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Token } from './token'
|
import { Token } from './token'
|
||||||
import { TokenKind } from '../parser'
|
import { TokenKind } from '../parser'
|
||||||
|
import { parseStringLiteral } from '../render/string'
|
||||||
|
|
||||||
export class QuotedToken extends Token {
|
export class QuotedToken extends Token {
|
||||||
|
public readonly content: string
|
||||||
constructor (
|
constructor (
|
||||||
public input: string,
|
public input: string,
|
||||||
public begin: number,
|
public begin: number,
|
||||||
@@ -9,5 +11,6 @@ export class QuotedToken extends Token {
|
|||||||
public file?: string
|
public file?: string
|
||||||
) {
|
) {
|
||||||
super(TokenKind.Quoted, input, begin, end, file)
|
super(TokenKind.Quoted, input, begin, end, file)
|
||||||
|
this.content = parseStringLiteral(this.getText())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { RangeToken } from './range-token'
|
import { RangeToken } from './range-token'
|
||||||
import { LiteralToken } from './literal-token'
|
import { LiteralToken } from './literal-token'
|
||||||
|
import { NumberToken } from './number-token'
|
||||||
import { QuotedToken } from './quoted-token'
|
import { QuotedToken } from './quoted-token'
|
||||||
import { PropertyAccessToken } from './property-access-token'
|
import { PropertyAccessToken } from './property-access-token'
|
||||||
|
|
||||||
export type ValueToken = RangeToken | LiteralToken | QuotedToken | PropertyAccessToken
|
export type ValueToken = RangeToken | LiteralToken | QuotedToken | PropertyAccessToken | NumberToken
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import * as _ from './underscore'
|
|||||||
import { Token } from '../tokens/token'
|
import { Token } from '../tokens/token'
|
||||||
import { Template } from '../template/template'
|
import { Template } from '../template/template'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* targeting ES5, extends Error won't create a proper prototype chain, need a trait to kee track of classes
|
||||||
|
*/
|
||||||
|
const TRAIT = '__liquidClass__'
|
||||||
|
|
||||||
export abstract class LiquidError extends Error {
|
export abstract class LiquidError extends Error {
|
||||||
private token!: Token
|
private token!: Token
|
||||||
public context = ''
|
public context = ''
|
||||||
@@ -14,6 +19,7 @@ export abstract class LiquidError extends Error {
|
|||||||
super(typeof err === 'string' ? err : err.message)
|
super(typeof err === 'string' ? err : err.message)
|
||||||
if (typeof err !== 'string') Object.defineProperty(this, 'originalError', { value: err, enumerable: false })
|
if (typeof err !== 'string') Object.defineProperty(this, 'originalError', { value: err, enumerable: false })
|
||||||
Object.defineProperty(this, 'token', { value: token, enumerable: false })
|
Object.defineProperty(this, 'token', { value: token, enumerable: false })
|
||||||
|
Object.defineProperty(this, TRAIT, { value: 'LiquidError', enumerable: false })
|
||||||
}
|
}
|
||||||
protected update () {
|
protected update () {
|
||||||
Object.defineProperty(this, 'context', { value: mkContext(this.token), enumerable: false })
|
Object.defineProperty(this, 'context', { value: mkContext(this.token), enumerable: false })
|
||||||
@@ -22,6 +28,9 @@ export abstract class LiquidError extends Error {
|
|||||||
'\n' + this.stack
|
'\n' + this.stack
|
||||||
if (this.originalError) this.stack += '\nFrom ' + this.originalError.stack
|
if (this.originalError) this.stack += '\nFrom ' + this.originalError.stack
|
||||||
}
|
}
|
||||||
|
static is (obj: unknown): obj is LiquidError {
|
||||||
|
return obj?.[TRAIT] === 'LiquidError'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TokenizationError extends LiquidError {
|
export class TokenizationError extends LiquidError {
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ export const literalValues = {
|
|||||||
'empty': new EmptyDrop(),
|
'empty': new EmptyDrop(),
|
||||||
'blank': new BlankDrop()
|
'blank': new BlankDrop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LiteralKey = keyof typeof literalValues
|
||||||
|
export type LiteralValue = typeof literalValues[LiteralKey]
|
||||||
|
|||||||
+14
-11
@@ -1,22 +1,25 @@
|
|||||||
import { Operators, OperatorHandler } from '../render/operator'
|
|
||||||
import { IDENTIFIER, TYPES } from '../util/character'
|
import { IDENTIFIER, TYPES } from '../util/character'
|
||||||
|
|
||||||
interface TrieLeafNode {
|
interface TrieInput<T> {
|
||||||
handler: OperatorHandler;
|
[key: string]: T
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TrieLeafNode<T> {
|
||||||
|
data: T;
|
||||||
end: true;
|
end: true;
|
||||||
needBoundary?: true;
|
needBoundary?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Trie {
|
export interface Trie<T> {
|
||||||
[key: string]: Trie | TrieLeafNode;
|
[key: string]: Trie<T> | TrieLeafNode<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TrieNode = Trie | TrieLeafNode
|
export type TrieNode<T> = Trie<T> | TrieLeafNode<T>
|
||||||
|
|
||||||
export function createTrie (operators: Operators): Trie {
|
export function createTrie<T = any> (input: TrieInput<T>): Trie<T> {
|
||||||
const trie: Trie = {}
|
const trie: Trie<T> = {}
|
||||||
for (const [name, handler] of Object.entries(operators)) {
|
for (const [name, data] of Object.entries(input)) {
|
||||||
let node: Trie | TrieLeafNode = trie
|
let node: Trie<T> | TrieLeafNode<T> = trie
|
||||||
|
|
||||||
for (let i = 0; i < name.length; i++) {
|
for (let i = 0; i < name.length; i++) {
|
||||||
const c = name[i]
|
const c = name[i]
|
||||||
@@ -29,7 +32,7 @@ export function createTrie (operators: Operators): Trie {
|
|||||||
node = node[c]
|
node = node[c]
|
||||||
}
|
}
|
||||||
|
|
||||||
node.handler = handler
|
node.data = data
|
||||||
node.end = true
|
node.end = true
|
||||||
}
|
}
|
||||||
return trie
|
return trie
|
||||||
|
|||||||
@@ -7,4 +7,18 @@ describe('browser', function () {
|
|||||||
message: 'output "{{huh" not closed, line:1, col:1'
|
message: 'output "{{huh" not closed, line:1, col:1'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
it('should throw tokenization error for invalid filter syntax', async () => {
|
||||||
|
const engine = new LiquidUMD()
|
||||||
|
const message = 'expected filter name, line:1, col:10'
|
||||||
|
const stack = [
|
||||||
|
'>> 1| {{ foo | ^ }}',
|
||||||
|
' ^',
|
||||||
|
`TokenizationError: ${message}`
|
||||||
|
].join('\n')
|
||||||
|
await expect(engine.parseAndRender('{{ foo | ^ }}')).rejects.toMatchObject({
|
||||||
|
message,
|
||||||
|
stack: expect.stringContaining(stack),
|
||||||
|
name: 'TokenizationError'
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -454,4 +454,14 @@ describe('Issues', function () {
|
|||||||
const result = engine.parseAndRenderSync(template, { product })
|
const result = engine.parseAndRenderSync(template, { product })
|
||||||
expect(result).toEqual('This is a love potion!')
|
expect(result).toEqual('This is a love potion!')
|
||||||
})
|
})
|
||||||
|
it('#643 Error When Accessing Subproperty of Bracketed Reference', () => {
|
||||||
|
const engine = new Liquid()
|
||||||
|
const tpl = '{{ ["Key String with Spaces"].subpropertyKey }}'
|
||||||
|
const ctx = {
|
||||||
|
'Key String with Spaces': {
|
||||||
|
subpropertyKey: 'FOO'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(engine.parseAndRenderSync(tpl, ctx)).toEqual('FOO')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user