go-twig/expr.go
2025-03-12 20:49:11 +03:00

777 lines
17 KiB
Go

package twig
import (
"fmt"
"io"
"strconv"
"strings"
)
// ExpressionType represents the type of an expression
type ExpressionType int
// Expression types
const (
ExprLiteral ExpressionType = iota
ExprVariable
ExprUnary
ExprBinary
ExprFunction
ExprFilter
ExprTest
ExprGetAttr
ExprGetItem
ExprMethodCall
ExprArray
ExprHash
ExprConditional
ExprModuleMethod
)
// ExpressionNode represents a Twig expression
type ExpressionNode struct {
exprType ExpressionType
line int
}
// LiteralNode represents a literal value (string, number, boolean, null)
type LiteralNode struct {
ExpressionNode
value interface{}
}
// VariableNode represents a variable reference
type VariableNode struct {
ExpressionNode
name string
}
// UnaryNode represents a unary operation (not, -, +)
type UnaryNode struct {
ExpressionNode
operator string
node Node
}
// BinaryNode represents a binary operation (+, -, *, /, etc)
type BinaryNode struct {
ExpressionNode
operator string
left Node
right Node
}
// FunctionNode represents a function call
type FunctionNode struct {
ExpressionNode
name string
args []Node
moduleExpr Node // Optional module for module.function() calls
}
// FilterNode represents a filter application
type FilterNode struct {
ExpressionNode
node Node
filter string
args []Node
}
// TestNode represents a test (is defined, is null, etc)
type TestNode struct {
ExpressionNode
node Node
test string
args []Node
}
// GetAttrNode represents attribute access (obj.attr)
type GetAttrNode struct {
ExpressionNode
node Node
attribute Node
}
// GetItemNode represents item access (array[key])
type GetItemNode struct {
ExpressionNode
node Node
item Node
}
// MethodCallNode represents method call (obj.method())
type MethodCallNode struct {
ExpressionNode
node Node
method string
args []Node
}
// ArrayNode represents an array literal
type ArrayNode struct {
ExpressionNode
items []Node
}
// HashNode represents a hash/map literal
type HashNode struct {
ExpressionNode
items map[Node]Node
}
// ConditionalNode represents ternary operator (condition ? true : false)
type ConditionalNode struct {
ExpressionNode
condition Node
trueExpr Node
falseExpr Node
}
// Type implementation for ExpressionNode
func (n *ExpressionNode) Type() NodeType {
return NodeExpression
}
func (n *ExpressionNode) Line() int {
return n.line
}
// Render implementation for LiteralNode
func (n *LiteralNode) Render(w io.Writer, ctx *RenderContext) error {
var str string
switch v := n.value.(type) {
case string:
str = v
case int:
str = strconv.Itoa(v)
case float64:
str = strconv.FormatFloat(v, 'f', -1, 64)
case bool:
str = strconv.FormatBool(v)
case nil:
str = ""
default:
str = ctx.ToString(v)
}
_, err := WriteString(w, str)
return err
}
// Release returns a LiteralNode to the pool
func (n *LiteralNode) Release() {
ReleaseLiteralNode(n)
}
// NewLiteralNode creates a new literal node
func NewLiteralNode(value interface{}, line int) *LiteralNode {
node := GetLiteralNode(value, line)
node.ExpressionNode.exprType = ExprLiteral
return node
}
// NewVariableNode creates a new variable node
func NewVariableNode(name string, line int) *VariableNode {
node := GetVariableNode(name, line)
node.ExpressionNode.exprType = ExprVariable
return node
}
// NewBinaryNode creates a new binary operation node
func NewBinaryNode(operator string, left, right Node, line int) *BinaryNode {
return GetBinaryNode(operator, left, right, line)
}
// NewGetAttrNode creates a new attribute access node
func NewGetAttrNode(node, attribute Node, line int) *GetAttrNode {
return GetGetAttrNode(node, attribute, line)
}
// NewGetItemNode creates a new item access node
func NewGetItemNode(node, item Node, line int) *GetItemNode {
return GetGetItemNode(node, item, line)
}
// Render implementation for VariableNode
func (n *VariableNode) Render(w io.Writer, ctx *RenderContext) error {
value, err := ctx.GetVariable(n.name)
if err != nil {
return err
}
// If debug is enabled, log variable access and value
if IsDebugEnabled() {
if value == nil {
// Log undefined variable at error level if debug is enabled
message := fmt.Sprintf("Variable lookup at line %d", n.line)
LogError(fmt.Errorf("%w: %s", ErrUndefinedVar, n.name), message)
// If in strict debug mode with error level, return an error for undefined variables
if debugger.level >= DebugError && ctx.engine != nil && ctx.engine.debug {
templateName := "unknown"
if ctx.engine.currentTemplate != "" {
templateName = ctx.engine.currentTemplate
}
return NewError(fmt.Errorf("%w: %s", ErrUndefinedVar, n.name), templateName, n.line, 0, "")
}
} else if debugger.level >= DebugVerbose {
// Log defined variables at verbose level
LogVerbose("Variable access at line %d: %s = %v (type: %T)", n.line, n.name, value, value)
}
}
str := ctx.ToString(value)
_, err = WriteString(w, str)
return err
}
// Release returns a VariableNode to the pool
func (n *VariableNode) Release() {
ReleaseVariableNode(n)
}
// Render implementation for GetAttrNode
func (n *GetAttrNode) Render(w io.Writer, ctx *RenderContext) error {
obj, err := ctx.EvaluateExpression(n.node)
if err != nil {
return err
}
attrName, err := ctx.EvaluateExpression(n.attribute)
if err != nil {
return err
}
attrStr, ok := attrName.(string)
if !ok {
return fmt.Errorf("attribute name must be a string")
}
value, err := ctx.getAttribute(obj, attrStr)
if err != nil {
return err
}
str := ctx.ToString(value)
_, err = WriteString(w, str)
return err
}
// Release returns a GetAttrNode to the pool
func (n *GetAttrNode) Release() {
ReleaseGetAttrNode(n)
}
// Render implementation for GetItemNode
func (n *GetItemNode) Render(w io.Writer, ctx *RenderContext) error {
container, err := ctx.EvaluateExpression(n.node)
if err != nil {
return err
}
index, err := ctx.EvaluateExpression(n.item)
if err != nil {
return err
}
value, err := ctx.getItem(container, index)
if err != nil {
return err
}
str := ctx.ToString(value)
_, err = WriteString(w, str)
return err
}
// Release returns a GetItemNode to the pool
func (n *GetItemNode) Release() {
ReleaseGetItemNode(n)
}
// Render implementation for BinaryNode
func (n *BinaryNode) Render(w io.Writer, ctx *RenderContext) error {
result, err := ctx.EvaluateExpression(n)
if err != nil {
return err
}
str := ctx.ToString(result)
_, err = WriteString(w, str)
return err
}
// Release returns a BinaryNode to the pool
func (n *BinaryNode) Release() {
ReleaseBinaryNode(n)
}
// Render implementation for FilterNode
func (n *FilterNode) Render(w io.Writer, ctx *RenderContext) error {
result, err := ctx.EvaluateExpression(n)
if err != nil {
return err
}
str := ctx.ToString(result)
_, err = WriteString(w, str)
return err
}
// Release returns a FilterNode to the pool
func (n *FilterNode) Release() {
ReleaseFilterNode(n)
}
// Render implementation for TestNode
func (n *TestNode) Render(w io.Writer, ctx *RenderContext) error {
result, err := ctx.EvaluateExpression(n)
if err != nil {
return err
}
str := ctx.ToString(result)
_, err = WriteString(w, str)
return err
}
// Release returns a TestNode to the pool
func (n *TestNode) Release() {
ReleaseTestNode(n)
}
// Render implementation for UnaryNode
func (n *UnaryNode) Render(w io.Writer, ctx *RenderContext) error {
result, err := ctx.EvaluateExpression(n)
if err != nil {
return err
}
str := ctx.ToString(result)
_, err = WriteString(w, str)
return err
}
// Release returns a UnaryNode to the pool
func (n *UnaryNode) Release() {
ReleaseUnaryNode(n)
}
// Render implementation for ConditionalNode
func (n *ConditionalNode) Render(w io.Writer, ctx *RenderContext) error {
result, err := ctx.EvaluateExpression(n)
if err != nil {
return err
}
str := ctx.ToString(result)
_, err = WriteString(w, str)
return err
}
// Release returns a ConditionalNode to the pool
func (n *ConditionalNode) Release() {
ReleaseConditionalNode(n)
}
// Render implementation for ArrayNode
func (n *ArrayNode) Render(w io.Writer, ctx *RenderContext) error {
result, err := ctx.EvaluateExpression(n)
if err != nil {
return err
}
str := ctx.ToString(result)
_, err = WriteString(w, str)
return err
}
// Release returns an ArrayNode to the pool
func (n *ArrayNode) Release() {
ReleaseArrayNode(n)
}
// Render implementation for HashNode
func (n *HashNode) Render(w io.Writer, ctx *RenderContext) error {
result, err := ctx.EvaluateExpression(n)
if err != nil {
return err
}
str := ctx.ToString(result)
_, err = WriteString(w, str)
return err
}
// Release returns a HashNode to the pool
func (n *HashNode) Release() {
ReleaseHashNode(n)
}
// Render implementation for FunctionNode
func (n *FunctionNode) Render(w io.Writer, ctx *RenderContext) error {
result, err := ctx.EvaluateExpression(n)
if err != nil {
return err
}
str := ctx.ToString(result)
_, err = WriteString(w, str)
return err
}
// Release returns a FunctionNode to the pool
func (n *FunctionNode) Release() {
ReleaseFunctionNode(n)
}
// NewFilterNode creates a new filter node
func NewFilterNode(node Node, filter string, args []Node, line int) *FilterNode {
return GetFilterNode(node, filter, args, line)
}
// NewTestNode creates a new test node
func NewTestNode(node Node, test string, args []Node, line int) *TestNode {
return GetTestNode(node, test, args, line)
}
// NewUnaryNode creates a new unary operation node
func NewUnaryNode(operator string, node Node, line int) *UnaryNode {
return GetUnaryNode(operator, node, line)
}
// NewConditionalNode creates a new conditional (ternary) node
func NewConditionalNode(condition, trueExpr, falseExpr Node, line int) *ConditionalNode {
return GetConditionalNode(condition, trueExpr, falseExpr, line)
}
// NewArrayNode creates a new array node
func NewArrayNode(items []Node, line int) *ArrayNode {
return GetArrayNode(items, line)
}
// NewHashNode creates a new hash node
func NewHashNode(items map[Node]Node, line int) *HashNode {
return GetHashNode(items, line)
}
// NewFunctionNode creates a new function call node
func NewFunctionNode(name string, args []Node, line int) *FunctionNode {
return GetFunctionNode(name, args, line)
}
// ParseExpressionOptimized parses simple expressions with minimal allocations
// Returns the parsed expression node and a boolean indicating success
func ParseExpressionOptimized(expr string) (Node, bool) {
// Trim whitespace
expr = strings.TrimSpace(expr)
// Quick empty check
if len(expr) == 0 {
return nil, false
}
// Try parsing as literal
if val, ok := ParseLiteralOptimized(expr); ok {
return NewLiteralNode(val, 0), true
}
// Check for simple variable references
if IsValidVariableName(expr) {
return NewVariableNode(expr, 0), true
}
// More complex expression - will need the full parser
return nil, false
}
// ParseLiteralOptimized parses literals (strings, numbers, booleans) with minimal allocations
func ParseLiteralOptimized(expr string) (interface{}, bool) {
// Quick check for common literal types
if len(expr) == 0 {
return nil, false
}
// Check for string literals
if len(expr) >= 2 && ((expr[0] == '"' && expr[len(expr)-1] == '"') ||
(expr[0] == '\'' && expr[len(expr)-1] == '\'')) {
// String literal
return expr[1 : len(expr)-1], true
}
// Check for number literals
if isDigit(expr[0]) || (expr[0] == '-' && len(expr) > 1 && isDigit(expr[1])) {
// Try parsing as integer first (most common case)
if i, err := strconv.Atoi(expr); err == nil {
return i, true
}
// Try parsing as float
if f, err := strconv.ParseFloat(expr, 64); err == nil {
return f, true
}
}
// Check for boolean literals
if expr == "true" {
return true, true
}
if expr == "false" {
return false, true
}
if expr == "null" || expr == "nil" {
return nil, true
}
// Not a simple literal
return nil, false
}
// IsValidVariableName checks if a string is a valid variable name
func IsValidVariableName(name string) bool {
if len(name) == 0 {
return false
}
// First character must be a letter or underscore
if !isAlpha(name[0]) {
return false
}
// Rest can be letters, digits, or underscores
for i := 1; i < len(name); i++ {
if !isNameChar(name[i]) {
return false
}
}
// Check for reserved keywords
switch name {
case "true", "false", "null", "nil", "not", "and", "or", "in", "is":
return false
}
return true
}
// ProcessStringEscapes efficiently processes string escapes with minimal allocations
// Returns the processed string and a flag indicating if any processing was done
func ProcessStringEscapes(text string) (string, bool) {
// Check if there are any escapes to process
hasEscape := false
for i := 0; i < len(text); i++ {
if text[i] == '\\' && i+1 < len(text) {
hasEscape = true
break
}
}
// If no escapes, return the original string
if !hasEscape {
return text, false
}
// Process escapes
var sb strings.Builder
sb.Grow(len(text))
i := 0
for i < len(text) {
if text[i] == '\\' && i+1 < len(text) {
// Escape sequence found
i++
// Process the escape sequence
switch text[i] {
case 'n':
sb.WriteByte('\n')
case 'r':
sb.WriteByte('\r')
case 't':
sb.WriteByte('\t')
case 'b':
sb.WriteByte('\b')
case 'f':
sb.WriteByte('\f')
case '\'':
sb.WriteByte('\'')
case '"':
sb.WriteByte('"')
case '\\':
sb.WriteByte('\\')
default:
// Unknown escape, just keep it as-is
sb.WriteByte('\\')
sb.WriteByte(text[i])
}
} else {
// Regular character
sb.WriteByte(text[i])
}
i++
}
// Return the processed string
return sb.String(), true
}
// ParseNumberOptimized parses a number without using strconv for standard cases
// This provides better performance for typical integers and simple floats
func ParseNumberOptimized(text string) (interface{}, bool) {
// Empty string is not a number
if len(text) == 0 {
return nil, false
}
// Check for negative sign
isNegative := false
pos := 0
if text[pos] == '-' {
isNegative = true
pos++
// Just a minus sign is not a number
if pos >= len(text) {
return nil, false
}
}
// Parse the integer part
var intValue int64
hasDigits := false
for pos < len(text) && isDigit(text[pos]) {
digit := int64(text[pos] - '0')
intValue = intValue*10 + digit
hasDigits = true
pos++
}
if !hasDigits {
return nil, false
}
// Check for decimal point
hasDecimal := false
var floatValue float64
if pos < len(text) && text[pos] == '.' {
hasDecimal = true
pos++
// Need at least one digit after decimal point
if pos >= len(text) || !isDigit(text[pos]) {
return nil, false
}
// Parse the fractional part
floatValue = float64(intValue)
fraction := 0.0
multiplier := 0.1
for pos < len(text) && isDigit(text[pos]) {
digit := float64(text[pos] - '0')
fraction += digit * multiplier
multiplier *= 0.1
pos++
}
floatValue += fraction
}
// Check for exponent
if pos < len(text) && (text[pos] == 'e' || text[pos] == 'E') {
hasDecimal = true
pos++
// Parse exponent sign
expNegative := false
if pos < len(text) && (text[pos] == '+' || text[pos] == '-') {
expNegative = text[pos] == '-'
pos++
}
// Need at least one digit in exponent
if pos >= len(text) || !isDigit(text[pos]) {
return nil, false
}
// Parse exponent value
exp := 0
for pos < len(text) && isDigit(text[pos]) {
digit := int(text[pos] - '0')
exp = exp*10 + digit
pos++
}
// Apply exponent
// Convert to float regardless of hasDecimal
hasDecimal = true
if floatValue == 0 { // Not set yet
floatValue = float64(intValue)
}
// Apply exponent using a more efficient approach
if expNegative {
// For negative exponents, divide by 10^exp
multiplier := 1.0
for i := 0; i < exp; i++ {
multiplier *= 0.1
}
floatValue *= multiplier
} else {
// For positive exponents, multiply by 10^exp
multiplier := 1.0
for i := 0; i < exp; i++ {
multiplier *= 10
}
floatValue *= multiplier
}
}
// Check that we consumed the whole string
if pos < len(text) {
return nil, false
}
// Return the appropriate value
if hasDecimal {
if isNegative {
return -floatValue, true
}
return floatValue, true
} else {
if isNegative {
return -intValue, true
}
return intValue, true
}
}
// Helper functions for character classification
// isAlpha checks if a character is a letter or underscore
func isAlpha(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
}
// isNameChar checks if a character is valid in a name
func isNameChar(c byte) bool {
return isAlpha(c) || isDigit(c)
}
// isDigit is defined elsewhere
// Helper for optimized hash calculation
// This is useful for consistent hashing of variable names or strings
func calcStringHash(s string) uint32 {
var h uint32
for i := 0; i < len(s); i++ {
h = 31*h + uint32(s[i])
}
return h
}