mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 13:55:46 +01:00
164 lines
4.2 KiB
Go
164 lines
4.2 KiB
Go
package twig
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// ApplyFilter applies a filter to a value
|
|
func (ctx *RenderContext) ApplyFilter(name string, value interface{}, args ...interface{}) (interface{}, error) {
|
|
// Look for the filter in the environment
|
|
if ctx.env != nil {
|
|
if filter, ok := ctx.env.filters[name]; ok {
|
|
result, err := filter(value, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We've moved the script-specific string handling to PrintNode.Render
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
// Handle built-in filters for macro compatibility
|
|
switch name {
|
|
case "e", "escape":
|
|
// Optimized HTML escape using a single pass with strings.Builder
|
|
str := ctx.ToString(value)
|
|
if str == "" {
|
|
return "", nil
|
|
}
|
|
|
|
// Preallocate with a reasonable estimate (slightly larger than original)
|
|
// This avoids most reallocations
|
|
var b strings.Builder
|
|
b.Grow(len(str) + len(str)/8)
|
|
|
|
// Single-pass iteration is much more efficient than nested Replace calls
|
|
for _, c := range str {
|
|
switch c {
|
|
case '&':
|
|
b.WriteString("&")
|
|
case '<':
|
|
b.WriteString("<")
|
|
case '>':
|
|
b.WriteString(">")
|
|
case '"':
|
|
b.WriteString(""")
|
|
case '\'':
|
|
b.WriteString("'")
|
|
default:
|
|
b.WriteRune(c)
|
|
}
|
|
}
|
|
|
|
return b.String(), nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("filter '%s' not found", name)
|
|
}
|
|
|
|
// FilterChainItem represents a single filter in a chain
|
|
type FilterChainItem struct {
|
|
name string
|
|
args []interface{}
|
|
}
|
|
|
|
// DetectFilterChain analyzes a filter node and extracts all filters in the chain
|
|
// Returns the base node and a slice of all filters to be applied
|
|
func (ctx *RenderContext) DetectFilterChain(node Node) (Node, []FilterChainItem, error) {
|
|
// First, count the depth of the filter chain to properly allocate
|
|
depth := 0
|
|
currentNode := node
|
|
for {
|
|
filterNode, isFilter := currentNode.(*FilterNode)
|
|
if !isFilter {
|
|
break
|
|
}
|
|
depth++
|
|
currentNode = filterNode.node
|
|
}
|
|
|
|
// Now that we know the depth, allocate the proper size slice
|
|
chain := make([]FilterChainItem, depth)
|
|
|
|
// Traverse the chain again, but this time fill the slice in reverse order
|
|
// This avoids the O(n²) complexity of the previous implementation
|
|
currentNode = node
|
|
for i := depth - 1; i >= 0; i-- {
|
|
filterNode := currentNode.(*FilterNode) // Safe because we validated in first pass
|
|
|
|
// Evaluate filter arguments
|
|
args := make([]interface{}, len(filterNode.args))
|
|
for j, arg := range filterNode.args {
|
|
val, err := ctx.EvaluateExpression(arg)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
args[j] = val
|
|
}
|
|
|
|
// Add to the chain in the correct position
|
|
chain[i] = FilterChainItem{
|
|
name: filterNode.filter,
|
|
args: args,
|
|
}
|
|
|
|
// Continue with the next node
|
|
currentNode = filterNode.node
|
|
}
|
|
|
|
// Return the base node and the chain
|
|
return currentNode, chain, nil
|
|
}
|
|
|
|
// ApplyFilterChain applies a chain of filters to a value
|
|
func (ctx *RenderContext) ApplyFilterChain(baseValue interface{}, chain []FilterChainItem) (interface{}, error) {
|
|
// Start with the base value
|
|
result := baseValue
|
|
var err error
|
|
|
|
// Apply each filter in the chain
|
|
for _, filter := range chain {
|
|
result, err = ctx.ApplyFilter(filter.name, result, filter.args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Override for the original FilterNode evaluation in render.go
|
|
func (ctx *RenderContext) evaluateFilterNode(n *FilterNode) (interface{}, error) {
|
|
// Detect the complete filter chain
|
|
baseNode, filterChain, err := ctx.DetectFilterChain(n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Evaluate the base value
|
|
value, err := ctx.EvaluateExpression(baseNode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Log for debugging
|
|
if IsDebugEnabled() {
|
|
LogDebug("Evaluating filter chain: %s on value type %T", n.filter, value)
|
|
}
|
|
|
|
// Apply the entire filter chain in a single operation
|
|
result, err := ctx.ApplyFilterChain(value, filterChain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Ensure filter chain results are directly usable in for loops
|
|
// This is especially important for filters like 'sort' that transform arrays
|
|
// We convert to a []interface{} which is what ForNode.Render expects
|
|
if IsDebugEnabled() {
|
|
LogDebug("Filter result type: %T", result)
|
|
}
|
|
return result, nil
|
|
}
|