mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 12:20:40 -07:00
* feat: static variable analysis * Accept any iterable from `children`, `arguments`, etc. * Test analysis of standard tags * Use `TagToken.tokenizer` instead of creating a new one * Test analysis of netsted tags * Group variables by their root value * Test analysis of nested globals and locals * Analyze included and rendered templates WIP * Use existing tokenizer when constructing `Hash` * Improve test coverage * Analyze variables from `layout` and `block` tags * Test analysis of Jekyll style includes * Handle variables that start with a nested variable * Async analysis * Test non-standard tag end to end * Implement convenience analysis methods on the `Liquid` class * More analysis convenience methods * Accept string or template array * Draft static analysis docs * Deduplicate variables names * Fix isolated scope global variable map * Coerce variables to strings instead of extending String * Private map instead of extending Map * Fix e2e test * Tentatively implement analysis of aliased variables * Fix nested variable segments array * Update docs sidebar
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
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'
|
|
import { Drop } from '../drop'
|
|
|
|
export class Expression {
|
|
readonly postfix: Token[]
|
|
|
|
public constructor (tokens: IterableIterator<Token>) {
|
|
this.postfix = [...toPostfix(tokens)]
|
|
}
|
|
public * evaluate (ctx: Context, lenient?: boolean): Generator<unknown, unknown, unknown> {
|
|
assert(ctx, 'unable to evaluate: context not defined')
|
|
const operands: any[] = []
|
|
for (const token of this.postfix) {
|
|
if (isOperatorToken(token)) {
|
|
const r = operands.pop()
|
|
let result
|
|
if (operatorTypes[token.operator] === OperatorType.Unary) {
|
|
result = yield (ctx.opts.operators[token.operator] as UnaryOperatorHandler)(r, ctx)
|
|
} else {
|
|
const l = operands.pop()
|
|
result = yield ctx.opts.operators[token.operator](l, r, ctx)
|
|
}
|
|
operands.push(result)
|
|
} else {
|
|
operands.push(yield evalToken(token, ctx, lenient))
|
|
}
|
|
}
|
|
return operands[0]
|
|
}
|
|
public valid () {
|
|
return !!this.postfix.length
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
function * evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean): IterableIterator<unknown> {
|
|
const props: (string | number | Drop)[] = []
|
|
for (const prop of token.props) {
|
|
props.push((yield evalToken(prop, ctx, false)) as unknown as string | number | Drop)
|
|
}
|
|
try {
|
|
if (token.variable) {
|
|
const variable = yield evalToken(token.variable, ctx, lenient)
|
|
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))
|
|
}
|
|
}
|
|
|
|
export function evalQuotedToken (token: QuotedToken) {
|
|
return token.content
|
|
}
|
|
|
|
function * evalRangeToken (token: RangeToken, ctx: Context) {
|
|
const low: number = yield evalToken(token.lhs, ctx)
|
|
const high: number = yield evalToken(token.rhs, ctx)
|
|
ctx.memoryLimit.use(high - low + 1)
|
|
return range(+low, +high + 1)
|
|
}
|
|
|
|
function * toPostfix (tokens: IterableIterator<Token>): IterableIterator<Token> {
|
|
const ops: OperatorToken[] = []
|
|
for (const token of tokens) {
|
|
if (isOperatorToken(token)) {
|
|
while (ops.length && ops[ops.length - 1].getPrecedence() > token.getPrecedence()) {
|
|
yield ops.pop()!
|
|
}
|
|
ops.push(token)
|
|
} else yield token
|
|
}
|
|
while (ops.length) {
|
|
yield ops.pop()!
|
|
}
|
|
}
|