litenode
Version:
Lightweight and modular web framework
326 lines (273 loc) • 7.33 kB
JavaScript
import { TokenType } from "../syntax/TokenTypes.js"
import { BaseParser } from "./BaseParser.js"
export class ExpressionParser extends BaseParser {
constructor(state) {
super(state) // Shared state
}
parseExpression() {
return this.parseFilter() // Start with filter
}
// Check for filter first
parseFilter() {
// Get the complete expression first
let expr = this.ternary()
// Keep checking for more filters while we see pipe tokens
while (this.match(TokenType.PIPE_FILTER)) {
// Get the filter name - change IDENTIFIER to explicit filter name consume
const filterName = this.consume(TokenType.IDENTIFIER, "Expect filter name after '|'").lexeme
// Check for filter arguments
let args = []
if (this.match(TokenType.LPAREN)) {
if (!this.check(TokenType.RPAREN)) {
do {
if (this.check(TokenType.STRING)) {
this.advance()
args.push({
type: "literal",
value: this.previous().literal,
})
} else {
args.push(this.parseExpression())
}
} while (this.match(TokenType.COMMA))
}
this.consume(TokenType.RPAREN, "Expect ')' after filter arguments")
}
// Create filter node
expr = {
type: "filter",
expression: expr,
filter: filterName,
arguments: args,
}
}
return expr
}
ternary() {
let expr = this.logical()
if (this.match(TokenType.QUESTION)) {
const trueExpr = this.parseExpression()
this.consume(TokenType.COLON, "Expect ':' after true branch in ternary operator.")
// Recursively parse the false expression as potentially another ternary
const falseExpr = this.ternary()
expr = {
type: "ternary",
condition: expr,
trueExpr,
falseExpr,
}
}
return expr
}
logical() {
let expr = this.comparison()
while (this.match(TokenType.AND, TokenType.OR)) {
const operator = this.previous().type
const right = this.comparison()
expr = {
type: "logical",
operator,
left: expr,
right,
}
}
return expr
}
comparison() {
let expr = this.addition()
while (
this.match(
TokenType.GREATER,
TokenType.GREATER_EQUAL,
TokenType.LESS,
TokenType.LESS_EQUAL,
TokenType.EQUAL,
TokenType.NOT_EQUAL,
TokenType.STRICT_EQUAL,
TokenType.STRICT_NOT_EQUAL
)
) {
const operator = this.previous().type
const right = this.addition()
expr = {
type: "comparison",
operator,
left: expr,
right,
}
}
return expr
}
addition() {
let expr = this.multiplication()
while (this.match(TokenType.PLUS, TokenType.MINUS)) {
const operator = this.previous().type
const right = this.multiplication()
expr = {
type: "binary",
operator,
left: expr,
right,
}
}
return expr
}
multiplication() {
let expr = this.unary()
while (this.match(TokenType.MULTIPLY, TokenType.DIVIDE, TokenType.MODULO, TokenType.POWER)) {
const operator = this.previous().type
const right = this.unary()
expr = {
type: "binary",
operator,
left: expr,
right,
}
}
return expr
}
unary() {
if (this.match(TokenType.MINUS, TokenType.NOT)) {
const operator = this.previous().type
const right = this.unary() // Recursive unary
return {
type: "unary",
operator,
right,
}
}
return this.primary()
}
primary() {
if (this.match(TokenType.STRING)) {
const previousToken = this.previous()
const value = previousToken.literal
const lexeme = previousToken.lexeme
const wasQuoted = lexeme.startsWith('"') || lexeme.startsWith("'")
// Handle quoted strings in specific contexts
if (wasQuoted) {
// Case 1: Quoted strings in set value context or object literal context
if (this.state.parsingSetValue || this.state.isInObjectLiteral) {
return { type: "literal", value }
}
// Case 2: Quoted strings with dots, treated as variable names (outside include context)
// Quoted strings with dots are only treated as variables if they're not being used in a filter
if (!this.state.parsingInclude && value.includes(".") && !this.check(TokenType.PIPE_FILTER)) {
return { type: "variable", name: value }
}
}
// Default case: Treat as a regular string literal
return { type: "literal", value }
}
if (this.match(TokenType.NUMBER)) {
return {
type: "literal",
value: this.previous().literal,
}
}
if (this.match(TokenType.TRUE)) {
return { type: "literal", value: true }
}
if (this.match(TokenType.FALSE)) {
return { type: "literal", value: false }
}
if (this.match(TokenType.LBRACKET)) {
return this.array()
}
if (this.match(TokenType.LBRACE)) {
return this.object()
}
if (this.match(TokenType.LPAREN)) {
const expr = this.parseExpression()
this.consume(TokenType.RPAREN, "Expect ')' after expression.")
return {
type: "grouping",
expression: expr,
}
}
// Handle RAW_HTML tokens (terminal values)
if (this.match(TokenType.RAW_HTML)) {
return {
type: "raw_html",
name: this.previous().lexeme,
}
}
// Handle regular IDENTIFIER tokens
if (this.match(TokenType.IDENTIFIER)) {
let expr = {
type: "variable",
name: this.previous().lexeme,
}
while (this.match(TokenType.DOT, TokenType.LBRACKET)) {
const access = this.previous().type
if (access === TokenType.DOT) {
const name = this.consume(TokenType.IDENTIFIER, "Expect property name after '.'").lexeme
expr = {
type: "property",
object: expr,
property: name,
}
} else {
// Handle both numeric indices and string literals in brackets
let index
if (this.match(TokenType.STRING)) {
// String literal access
index = {
type: "literal",
value: this.previous().literal,
}
} else {
// Regular numeric or expression access
index = this.parseExpression()
}
this.consume(TokenType.RBRACKET, "Expect ']' after property access")
expr = {
type: "computed_property", // Node type for computed property access
object: expr,
property: index,
}
}
}
return expr
}
throw new Error(`Unexpected token: ${this.peek().lexeme} at position ${this.peek().position}`)
}
// Collections: array - object
array() {
const elements = []
if (!this.check(TokenType.RBRACKET)) {
do {
elements.push(this.parseExpression())
} while (this.match(TokenType.COMMA))
}
this.consume(TokenType.RBRACKET, "Expect ']' after array elements.")
return {
type: "array",
elements,
}
}
object() {
const properties = new Map()
this.state.isInObjectLiteral = true // Set flag when entering object
if (!this.check(TokenType.RBRACE)) {
do {
// Parse key
let key
if (this.match(TokenType.STRING)) {
key = this.previous().literal
} else {
key = this.consume(TokenType.IDENTIFIER, "Expect property name").lexeme
}
this.consume(TokenType.COLON, "Expect ':' after property name")
const value = this.parseExpression()
properties.set(key, value)
} while (this.match(TokenType.COMMA))
}
this.state.isInObjectLiteral = false // Reset flag when exiting object
this.consume(TokenType.RBRACE, "Expect '}' after object literal")
return {
type: "object",
properties,
}
}
}