feat: more flexible squared property read expression, fixes #643 (#646)

* 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:
Jun Yang
2023-08-23 00:45:49 +08:00
committed by GitHub
parent dc6a301387
commit 660d9be55f
22 changed files with 261 additions and 167 deletions
+27 -1
View File
@@ -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
View File
@@ -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) {
+34
View File
@@ -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')
})
})
+49
View File
@@ -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
}