mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-16 12:50:38 -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,7 +1,9 @@
|
||||
import { Tokenizer } from '../parser'
|
||||
import { Drop } from '../drop'
|
||||
import { QuotedToken } from '../tokens'
|
||||
import { Context } from '../context'
|
||||
import { toPromise, toValueSync } from '../util'
|
||||
import { evalQuotedToken } from './expression'
|
||||
|
||||
describe('Expression', function () {
|
||||
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('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 () {
|
||||
const ctx = new Context({
|
||||
foo: { bar: 'BAR' },
|
||||
@@ -162,6 +166,10 @@ describe('Expression', function () {
|
||||
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')
|
||||
})
|
||||
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 () {
|
||||
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 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 () {
|
||||
|
||||
+11
-18
@@ -1,6 +1,5 @@
|
||||
import { RangeToken, OperatorToken, Token, LiteralToken, NumberToken, PropertyAccessToken, QuotedToken, OperatorType, operatorTypes } from '../tokens'
|
||||
import { isQuotedToken, isWordToken, isNumberToken, isLiteralToken, isRangeToken, isPropertyAccessToken, UndefinedVariableError, range, isOperatorToken, literalValues, assert } from '../util'
|
||||
import { parseStringLiteral } from '../parser'
|
||||
import { QuotedToken, RangeToken, OperatorToken, Token, PropertyAccessToken, OperatorType, operatorTypes } from '../tokens'
|
||||
import { isRangeToken, isPropertyAccessToken, UndefinedVariableError, range, isOperatorToken, assert } from '../util'
|
||||
import type { Context } from '../context'
|
||||
import type { UnaryOperatorHandler } from '../render'
|
||||
|
||||
@@ -36,38 +35,32 @@ export class Expression {
|
||||
}
|
||||
|
||||
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 (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> {
|
||||
const props: string[] = []
|
||||
const variable = yield evalToken(token.variable, ctx, lenient)
|
||||
for (const prop of token.props) {
|
||||
props.push((yield evalToken(prop, ctx, false)) as unknown as string)
|
||||
}
|
||||
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) {
|
||||
if (lenient && (e as Error).name === 'InternalUndefinedVariableError') return null
|
||||
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) {
|
||||
return parseStringLiteral(token.getText())
|
||||
}
|
||||
|
||||
function evalLiteralToken (token: LiteralToken) {
|
||||
return literalValues[token.literal]
|
||||
return token.content
|
||||
}
|
||||
|
||||
function * evalRangeToken (token: RangeToken, ctx: Context) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { parseStringLiteral } from './string'
|
||||
|
||||
describe('parseStringLiteral()', function () {
|
||||
it('should parse octal escape', () => {
|
||||
expect(parseStringLiteral(String.raw`"\1010"`)).toBe('A0')
|
||||
expect(parseStringLiteral(String.raw`"\12"`)).toBe('\n')
|
||||
expect(parseStringLiteral(String.raw`"\01"`)).toBe('\u0001')
|
||||
expect(parseStringLiteral(String.raw`"\0"`)).toBe('\0')
|
||||
})
|
||||
it('should skip invalid octal escape', () => {
|
||||
expect(parseStringLiteral(String.raw`"\9"`)).toBe('9')
|
||||
})
|
||||
it('should parse \\n, \\t, \\r', () => {
|
||||
expect(parseStringLiteral(String.raw`"fo\no"`)).toBe('fo\no')
|
||||
expect(parseStringLiteral(String.raw`'fo\to'`)).toBe('fo\to')
|
||||
expect(parseStringLiteral(String.raw`'fo\ro'`)).toBe('fo\ro')
|
||||
})
|
||||
it('should parse unicode(hex) escape', () => {
|
||||
expect(parseStringLiteral('"\\u003C"')).toBe('<')
|
||||
expect(parseStringLiteral('"\\u003cZ"')).toBe('<Z')
|
||||
expect(parseStringLiteral('"\\u41"')).toBe('A')
|
||||
})
|
||||
it('should skip invalid unicode(hex) escape', () => {
|
||||
expect(parseStringLiteral('"\\u41Z"')).toBe('AZ')
|
||||
expect(parseStringLiteral('"\\uZ"')).toBe('\0Z')
|
||||
})
|
||||
it('should parse quote escape', () => {
|
||||
expect(parseStringLiteral(String.raw`"fo\'o"`)).toBe("fo'o")
|
||||
expect(parseStringLiteral(String.raw`'fo\"o'`)).toBe('fo"o')
|
||||
})
|
||||
it('should parse slash escape', () => {
|
||||
expect(parseStringLiteral(String.raw`'fo\\o'`)).toBe('fo\\o')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
const rHex = /[\da-fA-F]/
|
||||
const rOct = /[0-7]/
|
||||
const escapeChar = {
|
||||
b: '\b',
|
||||
f: '\f',
|
||||
n: '\n',
|
||||
r: '\r',
|
||||
t: '\t',
|
||||
v: '\x0B'
|
||||
}
|
||||
|
||||
function hexVal (c: string) {
|
||||
const code = c.charCodeAt(0)
|
||||
if (code >= 97) return code - 87
|
||||
if (code >= 65) return code - 55
|
||||
return code - 48
|
||||
}
|
||||
|
||||
export function parseStringLiteral (str: string): string {
|
||||
let ret = ''
|
||||
for (let i = 1; i < str.length - 1; i++) {
|
||||
if (str[i] !== '\\') {
|
||||
ret += str[i]
|
||||
continue
|
||||
}
|
||||
if (escapeChar[str[i + 1]] !== undefined) {
|
||||
ret += escapeChar[str[++i]]
|
||||
} else if (str[i + 1] === 'u') {
|
||||
let val = 0
|
||||
let j = i + 2
|
||||
while (j <= i + 5 && rHex.test(str[j])) {
|
||||
val = val * 16 + hexVal(str[j++])
|
||||
}
|
||||
i = j - 1
|
||||
ret += String.fromCharCode(val)
|
||||
} else if (!rOct.test(str[i + 1])) {
|
||||
ret += str[++i]
|
||||
} else {
|
||||
let j = i + 1
|
||||
let val = 0
|
||||
while (j <= i + 3 && rOct.test(str[j])) {
|
||||
val = val * 8 + hexVal(str[j++])
|
||||
}
|
||||
i = j - 1
|
||||
ret += String.fromCharCode(val)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
Reference in New Issue
Block a user