mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 22:05:46 +01:00
- Implemented pooled slices for function arguments - Added specialized pooling for variable node and literal node objects - Modified array and hash node evaluation to reduce allocations - Optimized test and filter evaluation with pooled resources - Added comprehensive benchmarks to validate improvements - Updated node pool implementation to remove duplicate declarations - Fixed memory allocations in merge filter to correctly handle array manipulations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1600 lines
40 KiB
Go
1600 lines
40 KiB
Go
package twig
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Node represents a node in the template parse tree
|
|
type Node interface {
|
|
// Render renders the node to the output
|
|
Render(w io.Writer, ctx *RenderContext) error
|
|
|
|
// Type returns the node type
|
|
Type() NodeType
|
|
|
|
// Line returns the source line number
|
|
Line() int
|
|
}
|
|
|
|
// NewRootNode creates a new root node
|
|
func NewRootNode(children []Node, line int) *RootNode {
|
|
return GetRootNode(children, line)
|
|
}
|
|
|
|
// NewTextNode creates a new text node
|
|
func NewTextNode(content string, line int) *TextNode {
|
|
return GetTextNode(content, line)
|
|
}
|
|
|
|
// NewPrintNode creates a new print node
|
|
func NewPrintNode(expression Node, line int) *PrintNode {
|
|
return GetPrintNode(expression, line)
|
|
}
|
|
|
|
// NewIfNode creates a new if node
|
|
func NewIfNode(conditions []Node, bodies [][]Node, elseBranch []Node, line int) *IfNode {
|
|
return GetIfNode(conditions, bodies, elseBranch, line)
|
|
}
|
|
|
|
// NewForNode creates a new for loop node
|
|
func NewForNode(keyVar, valueVar string, sequence Node, body, elseBranch []Node, line int) *ForNode {
|
|
return GetForNode(keyVar, valueVar, sequence, body, elseBranch, line)
|
|
}
|
|
|
|
// NewBlockNode creates a new block node
|
|
func NewBlockNode(name string, body []Node, line int) *BlockNode {
|
|
return GetBlockNode(name, body, line)
|
|
}
|
|
|
|
// NewExtendsNode creates a new extends node
|
|
func NewExtendsNode(parent Node, line int) *ExtendsNode {
|
|
return GetExtendsNode(parent, line)
|
|
}
|
|
|
|
// NewIncludeNode creates a new include node
|
|
func NewIncludeNode(template Node, variables map[string]Node, ignoreMissing, only, sandboxed bool, line int) *IncludeNode {
|
|
return GetIncludeNode(template, variables, ignoreMissing, only, sandboxed, line)
|
|
}
|
|
|
|
// NewSetNode creates a new set node
|
|
func NewSetNode(name string, value Node, line int) *SetNode {
|
|
return GetSetNode(name, value, line)
|
|
}
|
|
|
|
// NewCommentNode creates a new comment node
|
|
func NewCommentNode(content string, line int) *CommentNode {
|
|
return GetCommentNode(content, line)
|
|
}
|
|
|
|
// NewMacroNode creates a new macro node
|
|
func NewMacroNode(name string, params []string, defaults map[string]Node, body []Node, line int) *MacroNode {
|
|
return GetMacroNode(name, params, defaults, body, line)
|
|
}
|
|
|
|
// NewImportNode creates a new import node
|
|
func NewImportNode(template Node, module string, line int) *ImportNode {
|
|
return GetImportNode(template, module, line)
|
|
}
|
|
|
|
// NewFromImportNode creates a new from import node
|
|
func NewFromImportNode(template Node, macros []string, aliases map[string]string, line int) *FromImportNode {
|
|
return GetFromImportNode(template, macros, aliases, line)
|
|
}
|
|
|
|
// NodeType represents the type of a node
|
|
type NodeType int
|
|
|
|
// Node types
|
|
const (
|
|
NodeRoot NodeType = iota
|
|
NodeText
|
|
NodePrint
|
|
NodeIf
|
|
NodeFor
|
|
NodeBlock
|
|
NodeExtends
|
|
NodeInclude
|
|
NodeImport
|
|
NodeMacro
|
|
NodeSet
|
|
NodeExpression
|
|
NodeComment
|
|
NodeVerbatim
|
|
NodeElement
|
|
NodeFunction
|
|
NodeSpaceless
|
|
NodeDo
|
|
NodeModuleMethod
|
|
NodeApply
|
|
NodeSandbox
|
|
)
|
|
|
|
// RootNode represents the root of a template
|
|
type RootNode struct {
|
|
children []Node
|
|
line int
|
|
}
|
|
|
|
// TextNode represents a raw text node
|
|
type TextNode struct {
|
|
content string
|
|
line int
|
|
}
|
|
|
|
// String implementation for debugging
|
|
func (n *TextNode) String() string {
|
|
// Display spaces as visible characters for easier debugging
|
|
spacesVisual := strings.ReplaceAll(n.content, " ", "·")
|
|
return fmt.Sprintf("TextNode(%q [%s], line: %d)", n.content, spacesVisual, n.line)
|
|
}
|
|
|
|
// PrintNode represents a {{ expression }} node
|
|
type PrintNode struct {
|
|
expression Node
|
|
line int
|
|
}
|
|
|
|
// IfNode represents an if block
|
|
type IfNode struct {
|
|
conditions []Node
|
|
bodies [][]Node
|
|
elseBranch []Node
|
|
line int
|
|
}
|
|
|
|
func (n *IfNode) Type() NodeType {
|
|
return NodeIf
|
|
}
|
|
|
|
func (n *IfNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns the IfNode to the pool
|
|
func (n *IfNode) Release() {
|
|
ReleaseIfNode(n)
|
|
}
|
|
|
|
// Render renders the if node
|
|
func (n *IfNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Evaluate each condition until we find one that's true
|
|
for i, condition := range n.conditions {
|
|
// Log before evaluation if debug is enabled
|
|
if IsDebugEnabled() {
|
|
LogDebug("Evaluating 'if' condition #%d at line %d", i+1, n.line)
|
|
}
|
|
|
|
// Evaluate the condition
|
|
result, err := ctx.EvaluateExpression(condition)
|
|
if err != nil {
|
|
// Log error if debug is enabled
|
|
if IsDebugEnabled() {
|
|
LogError(err, "Error evaluating 'if' condition")
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Log result if debug is enabled
|
|
conditionResult := ctx.toBool(result)
|
|
if IsDebugEnabled() {
|
|
LogDebug("Condition result: %v (type: %T, raw value: %v)", conditionResult, result, result)
|
|
}
|
|
|
|
// If condition is true, render the corresponding body
|
|
if conditionResult {
|
|
if IsDebugEnabled() {
|
|
LogDebug("Entering 'if' block (condition #%d is true)", i+1)
|
|
}
|
|
|
|
// Render all nodes in the body
|
|
for _, node := range n.bodies[i] {
|
|
err := node.Render(w, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// If no condition was true and we have an else branch, render it
|
|
if n.elseBranch != nil {
|
|
if IsDebugEnabled() {
|
|
LogDebug("Entering 'else' block (all conditions were false)")
|
|
}
|
|
|
|
for _, node := range n.elseBranch {
|
|
err := node.Render(w, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ForNode represents a for loop
|
|
type ForNode struct {
|
|
keyVar string
|
|
valueVar string
|
|
sequence Node
|
|
body []Node
|
|
elseBranch []Node
|
|
line int
|
|
}
|
|
|
|
func (n *ForNode) Type() NodeType {
|
|
return NodeFor
|
|
}
|
|
|
|
func (n *ForNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns the ForNode to the pool
|
|
func (n *ForNode) Release() {
|
|
ReleaseForNode(n)
|
|
}
|
|
|
|
// Render renders the for loop node
|
|
func (n *ForNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Add debug info about the sequence node
|
|
if IsDebugEnabled() {
|
|
LogDebug("ForNode sequence node type: %T", n.sequence)
|
|
|
|
// Special handling for filter nodes in for loops to aid debugging
|
|
if filterNode, ok := n.sequence.(*FilterNode); ok {
|
|
LogDebug("ForNode sequence is a FilterNode with filter: %s", filterNode.filter)
|
|
}
|
|
}
|
|
|
|
// Special handling for FilterNode to improve rendering in for loops
|
|
if filterNode, ok := n.sequence.(*FilterNode); ok {
|
|
if IsDebugEnabled() {
|
|
LogDebug("ForNode: direct processing of filter node: %s", filterNode.filter)
|
|
}
|
|
|
|
// Get the base value first
|
|
baseNode, filterChain, err := ctx.DetectFilterChain(filterNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Evaluate the base value
|
|
baseValue, err := ctx.EvaluateExpression(baseNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if IsDebugEnabled() {
|
|
LogDebug("ForNode: base value type: %T, filter chain length: %d", baseValue, len(filterChain))
|
|
}
|
|
|
|
// Apply each filter in the chain
|
|
result := baseValue
|
|
for _, filter := range filterChain {
|
|
if IsDebugEnabled() {
|
|
LogDebug("ForNode: applying filter: %s", filter.name)
|
|
}
|
|
|
|
// Apply the filter
|
|
result, err = ctx.ApplyFilter(filter.name, result, filter.args...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if IsDebugEnabled() {
|
|
LogDebug("ForNode: after filter %s, result type: %T", filter.name, result)
|
|
}
|
|
}
|
|
|
|
// Use the filtered result directly
|
|
return n.renderForLoop(w, ctx, result)
|
|
}
|
|
|
|
// Standard evaluation for other types of sequences
|
|
seq, err := ctx.EvaluateExpression(n.sequence)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// WORKAROUND: When a filter is used directly in a for loop sequence like:
|
|
// {% for item in items|sort %}, the parser currently registers the sequence
|
|
// as a VariableNode with a name like "items|sort" instead of properly parsing
|
|
// it as a FilterNode. This workaround handles this parsing deficiency.
|
|
if varNode, ok := n.sequence.(*VariableNode); ok {
|
|
// Check if the variable contains a filter indicator (|)
|
|
if strings.Contains(varNode.name, "|") {
|
|
parts := strings.SplitN(varNode.name, "|", 2)
|
|
if len(parts) == 2 {
|
|
baseVar := parts[0]
|
|
filterName := parts[1]
|
|
|
|
if IsDebugEnabled() {
|
|
LogDebug("ForNode: Detected inline filter reference: var=%s, filter=%s", baseVar, filterName)
|
|
}
|
|
|
|
// Get the base value
|
|
baseValue, _ := ctx.GetVariable(baseVar)
|
|
|
|
// Apply the filter
|
|
if baseValue != nil {
|
|
if IsDebugEnabled() {
|
|
LogDebug("ForNode: Applying filter %s to %T manually", filterName, baseValue)
|
|
}
|
|
|
|
// Try to apply the filter
|
|
if ctx.env != nil {
|
|
filterFunc, found := ctx.env.filters[filterName]
|
|
if found {
|
|
filteredResult, err := filterFunc(baseValue)
|
|
if err == nil && filteredResult != nil {
|
|
if IsDebugEnabled() {
|
|
LogDebug("ForNode: Manual filter application successful")
|
|
}
|
|
seq = filteredResult
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if IsDebugEnabled() {
|
|
LogDebug("ForNode: sequence after evaluation: %T", seq)
|
|
}
|
|
|
|
return n.renderForLoop(w, ctx, seq)
|
|
}
|
|
|
|
// renderForLoop handles the actual for loop iteration after sequence is determined
|
|
func (n *ForNode) renderForLoop(w io.Writer, ctx *RenderContext, seq interface{}) error {
|
|
|
|
// If sequence is nil or invalid, render the else branch
|
|
if seq == nil {
|
|
if n.elseBranch != nil {
|
|
for _, node := range n.elseBranch {
|
|
err := node.Render(w, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Get the value as a reflect.Value for iteration
|
|
val := reflect.ValueOf(seq)
|
|
|
|
// Create a new context for the loop variables
|
|
loopCtx := ctx
|
|
|
|
// Keep track of loop variables
|
|
loopVars := map[string]interface{}{
|
|
"loop": map[string]interface{}{
|
|
"index": 0,
|
|
"index0": 0,
|
|
"revindex": 0,
|
|
"revindex0": 0,
|
|
"first": false,
|
|
"last": false,
|
|
"length": 0,
|
|
},
|
|
}
|
|
|
|
// Variables to track iteration state
|
|
length := 0
|
|
isIterable := true
|
|
switch val.Kind() {
|
|
case reflect.Slice, reflect.Array:
|
|
length = val.Len()
|
|
|
|
// Convert typed slices to []interface{} for consistent iteration
|
|
// This is essential for for-loop compatibility with filter results
|
|
if val.Type().Elem().Kind() != reflect.Interface {
|
|
// Debug logging for this conversion operation
|
|
if IsDebugEnabled() {
|
|
LogDebug("Converting %s to []interface{} for for-loop compatibility", val.Type())
|
|
}
|
|
|
|
// Create a new []interface{} and copy all values
|
|
interfaceSlice := make([]interface{}, length)
|
|
for i := 0; i < length; i++ {
|
|
if val.Index(i).CanInterface() {
|
|
interfaceSlice[i] = val.Index(i).Interface()
|
|
}
|
|
}
|
|
|
|
// Replace the original sequence with our new interface slice
|
|
seq = interfaceSlice
|
|
val = reflect.ValueOf(seq)
|
|
}
|
|
case reflect.Map:
|
|
length = val.Len()
|
|
case reflect.String:
|
|
length = val.Len()
|
|
default:
|
|
// For other types, try to convert to an interface slice
|
|
// to support custom iterables
|
|
if strVal, ok := seq.(string); ok {
|
|
// Convert string to runes for iteration
|
|
length = len([]rune(strVal))
|
|
} else if seqSlice, ok := seq.([]interface{}); ok {
|
|
// Already an interface slice
|
|
length = len(seqSlice)
|
|
seq = seqSlice
|
|
val = reflect.ValueOf(seq)
|
|
} else {
|
|
// Not directly iterable
|
|
isIterable = false
|
|
}
|
|
}
|
|
|
|
// If not iterable or length is 0, render the else branch if available
|
|
if !isIterable || length == 0 {
|
|
if n.elseBranch != nil {
|
|
for _, node := range n.elseBranch {
|
|
err := node.Render(w, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Update loop.length
|
|
loopVars["loop"].(map[string]interface{})["length"] = length
|
|
|
|
// Iterate based on the type
|
|
switch val.Kind() {
|
|
case reflect.Slice, reflect.Array:
|
|
for i := 0; i < val.Len(); i++ {
|
|
// Set the loop variables
|
|
loopVars["loop"].(map[string]interface{})["index"] = i + 1
|
|
loopVars["loop"].(map[string]interface{})["index0"] = i
|
|
loopVars["loop"].(map[string]interface{})["revindex"] = length - i
|
|
loopVars["loop"].(map[string]interface{})["revindex0"] = length - i - 1
|
|
loopVars["loop"].(map[string]interface{})["first"] = i == 0
|
|
loopVars["loop"].(map[string]interface{})["last"] = i == length-1
|
|
|
|
// Set the value variable
|
|
if val.Index(i).CanInterface() {
|
|
loopCtx.SetVariable(n.valueVar, val.Index(i).Interface())
|
|
} else {
|
|
loopCtx.SetVariable(n.valueVar, nil)
|
|
}
|
|
|
|
// Set the key variable if provided
|
|
if n.keyVar != "" {
|
|
loopCtx.SetVariable(n.keyVar, i)
|
|
}
|
|
|
|
// Set the loop variables
|
|
loopCtx.SetVariable("loop", loopVars["loop"])
|
|
|
|
// Render the body
|
|
for _, node := range n.body {
|
|
err := node.Render(w, loopCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
case reflect.Map:
|
|
keys := val.MapKeys()
|
|
for i, key := range keys {
|
|
// Set the loop variables
|
|
loopVars["loop"].(map[string]interface{})["index"] = i + 1
|
|
loopVars["loop"].(map[string]interface{})["index0"] = i
|
|
loopVars["loop"].(map[string]interface{})["revindex"] = length - i
|
|
loopVars["loop"].(map[string]interface{})["revindex0"] = length - i - 1
|
|
loopVars["loop"].(map[string]interface{})["first"] = i == 0
|
|
loopVars["loop"].(map[string]interface{})["last"] = i == length-1
|
|
|
|
// Set the value variable
|
|
if val.MapIndex(key).CanInterface() {
|
|
loopCtx.SetVariable(n.valueVar, val.MapIndex(key).Interface())
|
|
} else {
|
|
loopCtx.SetVariable(n.valueVar, nil)
|
|
}
|
|
|
|
// Set the key variable if provided
|
|
if n.keyVar != "" {
|
|
if key.CanInterface() {
|
|
loopCtx.SetVariable(n.keyVar, key.Interface())
|
|
} else {
|
|
loopCtx.SetVariable(n.keyVar, nil)
|
|
}
|
|
}
|
|
|
|
// Set the loop variables
|
|
loopCtx.SetVariable("loop", loopVars["loop"])
|
|
|
|
// Render the body
|
|
for _, node := range n.body {
|
|
err := node.Render(w, loopCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
case reflect.String:
|
|
for i, char := range val.String() {
|
|
// Set the loop variables
|
|
loopVars["loop"].(map[string]interface{})["index"] = i + 1
|
|
loopVars["loop"].(map[string]interface{})["index0"] = i
|
|
loopVars["loop"].(map[string]interface{})["revindex"] = length - i
|
|
loopVars["loop"].(map[string]interface{})["revindex0"] = length - i - 1
|
|
loopVars["loop"].(map[string]interface{})["first"] = i == 0
|
|
loopVars["loop"].(map[string]interface{})["last"] = i == length-1
|
|
|
|
// Set the value variable
|
|
loopCtx.SetVariable(n.valueVar, string(char))
|
|
|
|
// Set the key variable if provided
|
|
if n.keyVar != "" {
|
|
loopCtx.SetVariable(n.keyVar, i)
|
|
}
|
|
|
|
// Set the loop variables
|
|
loopCtx.SetVariable("loop", loopVars["loop"])
|
|
|
|
// Render the body
|
|
for _, node := range n.body {
|
|
err := node.Render(w, loopCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// BlockNode represents a block definition
|
|
type BlockNode struct {
|
|
name string
|
|
body []Node
|
|
line int
|
|
}
|
|
|
|
func (n *BlockNode) Type() NodeType {
|
|
return NodeBlock
|
|
}
|
|
|
|
func (n *BlockNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns a BlockNode to the pool
|
|
func (n *BlockNode) Release() {
|
|
ReleaseBlockNode(n)
|
|
}
|
|
|
|
// Render renders the block node
|
|
func (n *BlockNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Determine which content to use - from context blocks or default
|
|
var content []Node
|
|
|
|
// Store the current block content as parent content if needed
|
|
// This is critical for multi-level inheritance
|
|
if _, exists := ctx.parentBlocks[n.name]; !exists {
|
|
// First time we've seen this block - store its original content
|
|
// This needs to happen for any block, not just in extending templates
|
|
if blockContent, ok := ctx.blocks[n.name]; ok && len(blockContent) > 0 {
|
|
// Store the content from blocks
|
|
ctx.parentBlocks[n.name] = blockContent
|
|
} else {
|
|
// Otherwise store the default body
|
|
ctx.parentBlocks[n.name] = n.body
|
|
}
|
|
}
|
|
|
|
// Now get the content to render
|
|
if blockContent, ok := ctx.blocks[n.name]; ok && len(blockContent) > 0 {
|
|
content = blockContent
|
|
} else {
|
|
// Otherwise, use the default content from this block node
|
|
content = n.body
|
|
}
|
|
|
|
// Save the current block for parent() function support
|
|
previousBlock := ctx.currentBlock
|
|
ctx.currentBlock = n
|
|
|
|
// Create an isolated context for rendering this block
|
|
// This prevents parent() from accessing the wrong block context
|
|
blockCtx := ctx
|
|
|
|
// Render the appropriate content
|
|
for _, node := range content {
|
|
err := node.Render(w, blockCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Restore the previous block
|
|
ctx.currentBlock = previousBlock
|
|
return nil
|
|
}
|
|
|
|
// ExtendsNode represents an extends directive
|
|
type ExtendsNode struct {
|
|
parent Node
|
|
line int
|
|
}
|
|
|
|
func (n *ExtendsNode) Type() NodeType {
|
|
return NodeExtends
|
|
}
|
|
|
|
func (n *ExtendsNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns an ExtendsNode to the pool
|
|
func (n *ExtendsNode) Release() {
|
|
ReleaseExtendsNode(n)
|
|
}
|
|
|
|
// Implement Node interface for ExtendsNode
|
|
func (n *ExtendsNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Flag that this template extends another
|
|
ctx.extending = true
|
|
|
|
// Get the parent template name
|
|
templateExpr, err := ctx.EvaluateExpression(n.parent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
templateName := ctx.ToString(templateExpr)
|
|
|
|
// Load the parent template
|
|
if ctx.engine == nil {
|
|
return fmt.Errorf("no template engine available to load parent template: %s", templateName)
|
|
}
|
|
|
|
// Handle relative paths for templates
|
|
resolvedName := templateName
|
|
if strings.HasPrefix(templateName, "./") || strings.HasPrefix(templateName, "../") {
|
|
// Get the directory of the current template
|
|
currentTemplate := ctx.engine.currentTemplate
|
|
if currentTemplate != "" {
|
|
// Extract the directory part of the current template
|
|
currentDir := filepath.Dir(currentTemplate)
|
|
// Join the directory with the relative path
|
|
resolvedName = filepath.Join(currentDir, templateName)
|
|
}
|
|
}
|
|
|
|
// Load the parent template with resolved path
|
|
parentTemplate, err := ctx.engine.Load(resolvedName)
|
|
if err != nil {
|
|
// If template not found with resolved path, try original name
|
|
if resolvedName != templateName {
|
|
parentTemplate, err = ctx.engine.Load(templateName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Blocks from child template are registered to the parent context
|
|
|
|
// Create a new context for the parent template, but with our child blocks
|
|
// This ensures the parent template knows it's being extended and preserves our blocks
|
|
parentCtx := NewRenderContext(ctx.env, ctx.context, ctx.engine)
|
|
parentCtx.extending = true // Flag that the parent is being extended
|
|
|
|
// Ensure the context is released even if an error occurs
|
|
defer parentCtx.Release()
|
|
|
|
// First, copy any existing parent blocks to maintain the inheritance chain
|
|
// This allows for multi-level parent() calls to work properly
|
|
for name, nodes := range ctx.parentBlocks {
|
|
// Copy to the new context to preserve the inheritance chain
|
|
parentCtx.parentBlocks[name] = nodes
|
|
}
|
|
|
|
// Extract blocks from the parent template and store them as parent blocks
|
|
// for any blocks defined in the child but not yet in the parent chain
|
|
if rootNode, ok := parentTemplate.nodes.(*RootNode); ok {
|
|
for _, child := range rootNode.Children() {
|
|
if block, ok := child.(*BlockNode); ok {
|
|
// If we don't already have a parent for this block,
|
|
// use the parent template's block definition
|
|
if _, exists := parentCtx.parentBlocks[block.name]; !exists {
|
|
parentCtx.parentBlocks[block.name] = block.body
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally, copy all block definitions from the child context
|
|
// These are the blocks that will actually be rendered
|
|
for name, nodes := range ctx.blocks {
|
|
parentCtx.blocks[name] = nodes
|
|
}
|
|
|
|
// Render the parent template with the updated context
|
|
return parentTemplate.nodes.Render(w, parentCtx)
|
|
}
|
|
|
|
// IncludeNode represents an include directive
|
|
type IncludeNode struct {
|
|
template Node
|
|
variables map[string]Node
|
|
ignoreMissing bool
|
|
only bool
|
|
sandboxed bool
|
|
line int
|
|
}
|
|
|
|
func (n *IncludeNode) Type() NodeType {
|
|
return NodeInclude
|
|
}
|
|
|
|
func (n *IncludeNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns an IncludeNode to the pool
|
|
func (n *IncludeNode) Release() {
|
|
ReleaseIncludeNode(n)
|
|
}
|
|
|
|
// Implement Node interface for IncludeNode
|
|
func (n *IncludeNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Get the template name
|
|
templateExpr, err := ctx.EvaluateExpression(n.template)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
templateName := ctx.ToString(templateExpr)
|
|
|
|
// Load the template
|
|
if ctx.engine == nil {
|
|
return fmt.Errorf("no template engine available to load included template: %s", templateName)
|
|
}
|
|
|
|
// Handle relative paths for templates
|
|
resolvedName := templateName
|
|
if strings.HasPrefix(templateName, "./") || strings.HasPrefix(templateName, "../") {
|
|
// Get the directory of the current template
|
|
currentTemplate := ctx.engine.currentTemplate
|
|
if currentTemplate != "" {
|
|
// Extract the directory part of the current template
|
|
currentDir := filepath.Dir(currentTemplate)
|
|
// Join the directory with the relative path
|
|
resolvedName = filepath.Join(currentDir, templateName)
|
|
}
|
|
}
|
|
|
|
// Load the template with resolved path
|
|
template, err := ctx.engine.Load(resolvedName)
|
|
if err != nil {
|
|
// If template not found with resolved path, try original name
|
|
if resolvedName != templateName {
|
|
template, err = ctx.engine.Load(templateName)
|
|
if err != nil {
|
|
if n.ignoreMissing {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
} else {
|
|
if n.ignoreMissing {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create optimized context handling for includes
|
|
|
|
// Fast path: if no special handling needed and not sandboxed, render with current context
|
|
if !n.only && !n.sandboxed && len(n.variables) == 0 {
|
|
return template.nodes.Render(w, ctx)
|
|
}
|
|
|
|
// Need a new context for 'only' mode, sandboxed mode, or with variables
|
|
includeCtx := ctx
|
|
if n.only || n.sandboxed {
|
|
var contextVars map[string]interface{}
|
|
|
|
if n.only {
|
|
// Only mode - create empty context
|
|
contextVars = make(map[string]interface{}, len(n.variables))
|
|
} else {
|
|
// For sandboxed mode but not 'only' mode, copy the parent context
|
|
contextVars = make(map[string]interface{}, len(ctx.context)+len(n.variables))
|
|
for k, v := range ctx.context {
|
|
contextVars[k] = v
|
|
}
|
|
}
|
|
|
|
// Create a new context
|
|
includeCtx = NewRenderContext(ctx.env, contextVars, ctx.engine)
|
|
defer includeCtx.Release()
|
|
|
|
// If sandboxed, enable sandbox mode
|
|
if n.sandboxed {
|
|
includeCtx.sandboxed = true
|
|
|
|
// Check if a security policy is defined
|
|
if ctx.env.securityPolicy == nil {
|
|
return fmt.Errorf("cannot use sandboxed include without a security policy")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pre-evaluate all variables before setting them
|
|
if len(n.variables) > 0 {
|
|
for name, valueNode := range n.variables {
|
|
value, err := ctx.EvaluateExpression(valueNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
includeCtx.SetVariable(name, value)
|
|
}
|
|
}
|
|
|
|
// Render the included template
|
|
err = template.nodes.Render(w, includeCtx)
|
|
|
|
return err
|
|
}
|
|
|
|
// SetNode represents a variable assignment
|
|
type SetNode struct {
|
|
name string
|
|
value Node
|
|
line int
|
|
}
|
|
|
|
func (n *SetNode) Type() NodeType {
|
|
return NodeSet
|
|
}
|
|
|
|
func (n *SetNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns a SetNode to the pool
|
|
func (n *SetNode) Release() {
|
|
ReleaseSetNode(n)
|
|
}
|
|
|
|
// Render renders the set node
|
|
func (n *SetNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Evaluate the value
|
|
value, err := ctx.EvaluateExpression(n.value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the variable in the context
|
|
ctx.SetVariable(n.name, value)
|
|
return nil
|
|
}
|
|
|
|
// DoNode represents a do tag which evaluates an expression without printing the result
|
|
type DoNode struct {
|
|
expression Node
|
|
line int
|
|
}
|
|
|
|
// NewDoNode creates a new DoNode
|
|
func NewDoNode(expression Node, line int) *DoNode {
|
|
return GetDoNode(expression, line)
|
|
}
|
|
|
|
func (n *DoNode) Type() NodeType {
|
|
return NodeDo
|
|
}
|
|
|
|
func (n *DoNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns a DoNode to the pool
|
|
func (n *DoNode) Release() {
|
|
ReleaseDoNode(n)
|
|
}
|
|
|
|
// Render evaluates the expression but doesn't write anything
|
|
func (n *DoNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Evaluate the expression but ignore the result
|
|
_, err := ctx.EvaluateExpression(n.expression)
|
|
return err
|
|
}
|
|
|
|
// CommentNode represents a comment
|
|
type CommentNode struct {
|
|
content string
|
|
line int
|
|
}
|
|
|
|
func (n *CommentNode) Type() NodeType {
|
|
return NodeComment
|
|
}
|
|
|
|
func (n *CommentNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns a CommentNode to the pool
|
|
func (n *CommentNode) Release() {
|
|
ReleaseCommentNode(n)
|
|
}
|
|
|
|
// Render renders the comment node (does nothing, as comments are not rendered)
|
|
func (n *CommentNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Comments are not rendered
|
|
return nil
|
|
}
|
|
|
|
// MacroNode represents a macro definition
|
|
type MacroNode struct {
|
|
name string
|
|
params []string
|
|
defaults map[string]Node
|
|
body []Node
|
|
line int
|
|
}
|
|
|
|
func (n *MacroNode) Type() NodeType {
|
|
return NodeMacro
|
|
}
|
|
|
|
func (n *MacroNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns a MacroNode to the pool
|
|
func (n *MacroNode) Release() {
|
|
ReleaseMacroNode(n)
|
|
}
|
|
|
|
// Render renders the macro node
|
|
func (n *MacroNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Register the macro in the context
|
|
ctx.macros[n.name] = n
|
|
return nil
|
|
}
|
|
|
|
// processMacroTemplate performs a simple regex-based rewrite of the macro template
|
|
func processMacroTemplate(source string) string {
|
|
// Replace attribute references with quoted ones for the common HTML attributes
|
|
result := source
|
|
|
|
// Add quotes around HTML attributes with variable references
|
|
// This is a simplistic approach that works for the specific test case
|
|
result = strings.ReplaceAll(result, "type=\"{{ type }}\"", "type=\"{{ type }}\"")
|
|
result = strings.ReplaceAll(result, "name=\"{{ name }}\"", "name=\"{{ name }}\"")
|
|
result = strings.ReplaceAll(result, "value=\"{{ value }}\"", "value=\"{{ value }}\"")
|
|
result = strings.ReplaceAll(result, "size=\"{{ size }}\"", "size=\"{{ size }}\"")
|
|
|
|
return result
|
|
}
|
|
|
|
// renderVariableString renders a string that may contain variable references
|
|
func renderVariableString(text string, ctx *RenderContext, w io.Writer) error {
|
|
// Check if the string contains variable references like {{ varname }}
|
|
if !strings.Contains(text, "{{") {
|
|
// If not, just write the text directly
|
|
_, err := WriteString(w, text)
|
|
return err
|
|
}
|
|
|
|
// Simple variable extraction and replacement
|
|
var start int
|
|
var buffer bytes.Buffer
|
|
|
|
for {
|
|
// Find the start of a variable reference
|
|
varStart := strings.Index(text[start:], "{{")
|
|
if varStart == -1 {
|
|
// No more variables, write the rest of the text
|
|
buffer.WriteString(text[start:])
|
|
break
|
|
}
|
|
|
|
// Write the text before the variable
|
|
buffer.WriteString(text[start : start+varStart])
|
|
|
|
// Move past the {{
|
|
varStart += 2 + start
|
|
|
|
// Find the end of the variable
|
|
varEnd := strings.Index(text[varStart:], "}}")
|
|
if varEnd == -1 {
|
|
// Unclosed variable, write the rest as is
|
|
buffer.WriteString(text[start:])
|
|
break
|
|
}
|
|
|
|
// Extract the variable name, trim whitespace
|
|
varName := strings.TrimSpace(text[varStart : varStart+varEnd])
|
|
|
|
// Check for filters in the variable
|
|
var varValue interface{}
|
|
var err error
|
|
|
|
if strings.Contains(varName, "|") {
|
|
// Parse the filter expression
|
|
parts := strings.SplitN(varName, "|", 2)
|
|
if len(parts) == 2 {
|
|
baseName := strings.TrimSpace(parts[0])
|
|
filterName := strings.TrimSpace(parts[1])
|
|
|
|
// Get the base value
|
|
baseValue, _ := ctx.GetVariable(baseName)
|
|
|
|
// Extract filter arguments if any
|
|
filterNameAndArgs := strings.SplitN(filterName, ":", 2)
|
|
filterName = filterNameAndArgs[0]
|
|
|
|
// Apply the filter
|
|
var filterArgs []interface{}
|
|
if len(filterNameAndArgs) > 1 {
|
|
// Parse arguments (very simplistic)
|
|
argStr := filterNameAndArgs[1]
|
|
args := strings.Split(argStr, ",")
|
|
for _, arg := range args {
|
|
arg = strings.TrimSpace(arg)
|
|
filterArgs = append(filterArgs, arg)
|
|
}
|
|
}
|
|
|
|
if ctx.env != nil {
|
|
varValue, err = ctx.ApplyFilter(filterName, baseValue, filterArgs...)
|
|
if err != nil {
|
|
// Fall back to the unfiltered value
|
|
varValue = baseValue
|
|
}
|
|
} else {
|
|
varValue = baseValue
|
|
}
|
|
} else {
|
|
varValue, _ = ctx.GetVariable(varName)
|
|
}
|
|
} else {
|
|
// Regular variable
|
|
varValue, _ = ctx.GetVariable(varName)
|
|
}
|
|
|
|
// Convert to string and write
|
|
buffer.WriteString(ctx.ToString(varValue))
|
|
|
|
// Move past the }}
|
|
start = varStart + varEnd + 2
|
|
|
|
// If we've reached the end, break
|
|
if start >= len(text) {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Write the final result
|
|
_, err := w.Write(buffer.Bytes())
|
|
return err
|
|
}
|
|
|
|
// CallMacro calls the macro with the provided arguments
|
|
func (n *MacroNode) CallMacro(w io.Writer, ctx *RenderContext, args ...interface{}) error {
|
|
// Create a new context for the macro
|
|
macroCtx := NewRenderContext(ctx.env, nil, ctx.engine)
|
|
macroCtx.parent = ctx
|
|
|
|
// Ensure context is released even in error paths
|
|
defer macroCtx.Release()
|
|
|
|
// Set the parameters
|
|
for i, param := range n.params {
|
|
if i < len(args) {
|
|
// If an argument was provided, use it
|
|
macroCtx.SetVariable(param, args[i])
|
|
} else if defaultVal, ok := n.defaults[param]; ok {
|
|
// Otherwise, use the default value if available
|
|
value, err := ctx.EvaluateExpression(defaultVal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
macroCtx.SetVariable(param, value)
|
|
} else {
|
|
// If no default, set to nil
|
|
macroCtx.SetVariable(param, nil)
|
|
}
|
|
}
|
|
|
|
// Render the macro body - we need to handle variable interpolation in TextNodes
|
|
for _, node := range n.body {
|
|
// Special handling for TextNodes to process variables
|
|
if textNode, ok := node.(*TextNode); ok && strings.Contains(textNode.content, "{{") {
|
|
// This TextNode contains variable references that need processing
|
|
err := renderVariableString(textNode.content, macroCtx, w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Standard rendering for other node types
|
|
err := node.Render(w, macroCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ImportNode represents a macro import
|
|
type ImportNode struct {
|
|
template Node
|
|
module string
|
|
line int
|
|
}
|
|
|
|
func (n *ImportNode) Type() NodeType {
|
|
return NodeImport
|
|
}
|
|
|
|
func (n *ImportNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns an ImportNode to the pool
|
|
func (n *ImportNode) Release() {
|
|
ReleaseImportNode(n)
|
|
}
|
|
|
|
// Implement Node interface for ImportNode
|
|
func (n *ImportNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Get the template name
|
|
templateExpr, err := ctx.EvaluateExpression(n.template)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
templateName := ctx.ToString(templateExpr)
|
|
|
|
// Load the template
|
|
if ctx.engine == nil {
|
|
return fmt.Errorf("no template engine available to load imported template: %s", templateName)
|
|
}
|
|
|
|
// Handle relative paths for templates
|
|
resolvedName := templateName
|
|
if strings.HasPrefix(templateName, "./") || strings.HasPrefix(templateName, "../") {
|
|
// Get the directory of the current template
|
|
currentTemplate := ctx.engine.currentTemplate
|
|
if currentTemplate != "" {
|
|
// Extract the directory part of the current template
|
|
currentDir := filepath.Dir(currentTemplate)
|
|
// Join the directory with the relative path
|
|
resolvedName = filepath.Join(currentDir, templateName)
|
|
}
|
|
}
|
|
|
|
// Load the template with resolved path
|
|
template, err := ctx.engine.Load(resolvedName)
|
|
if err != nil {
|
|
// If template not found with resolved path, try original name
|
|
if resolvedName != templateName {
|
|
template, err = ctx.engine.Load(templateName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create a new context for the imported template
|
|
importCtx := NewRenderContext(ctx.env, nil, ctx.engine)
|
|
|
|
// Ensure context is released even in error paths
|
|
defer importCtx.Release()
|
|
|
|
// Render the imported template to capture its macros
|
|
err = template.nodes.Render(io.Discard, importCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a map for the macros
|
|
macros := make(map[string]interface{})
|
|
|
|
// Copy macros from import context to the map
|
|
for name, macro := range importCtx.macros {
|
|
macros[name] = macro
|
|
}
|
|
|
|
// Set the module variable in the current context
|
|
ctx.SetVariable(n.module, macros)
|
|
|
|
return nil
|
|
}
|
|
|
|
// FromImportNode represents a from import directive
|
|
type FromImportNode struct {
|
|
template Node
|
|
macros []string
|
|
aliases map[string]string
|
|
line int
|
|
}
|
|
|
|
func (n *FromImportNode) Type() NodeType {
|
|
return NodeImport
|
|
}
|
|
|
|
func (n *FromImportNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns a FromImportNode to the pool
|
|
func (n *FromImportNode) Release() {
|
|
ReleaseFromImportNode(n)
|
|
}
|
|
|
|
// Implement Node interface for FromImportNode
|
|
func (n *FromImportNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Get the template name
|
|
templateExpr, err := ctx.EvaluateExpression(n.template)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
templateName := ctx.ToString(templateExpr)
|
|
|
|
// Load the template
|
|
if ctx.engine == nil {
|
|
return fmt.Errorf("no template engine available to load imported template: %s", templateName)
|
|
}
|
|
|
|
// Handle relative paths for templates
|
|
resolvedName := templateName
|
|
if strings.HasPrefix(templateName, "./") || strings.HasPrefix(templateName, "../") {
|
|
// Get the directory of the current template
|
|
currentTemplate := ctx.engine.currentTemplate
|
|
if currentTemplate != "" {
|
|
// Extract the directory part of the current template
|
|
currentDir := filepath.Dir(currentTemplate)
|
|
// Join the directory with the relative path
|
|
resolvedName = filepath.Join(currentDir, templateName)
|
|
}
|
|
}
|
|
|
|
// Load the template with resolved path
|
|
template, err := ctx.engine.Load(resolvedName)
|
|
if err != nil {
|
|
// If template not found with resolved path, try original name
|
|
if resolvedName != templateName {
|
|
template, err = ctx.engine.Load(templateName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create a new context for the imported template
|
|
importCtx := NewRenderContext(ctx.env, nil, ctx.engine)
|
|
|
|
// Ensure context is released even in error paths
|
|
defer importCtx.Release()
|
|
|
|
// Render the imported template to capture its macros
|
|
err = template.nodes.Render(io.Discard, importCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Copy selected macros from import context to the current context
|
|
for _, macroName := range n.macros {
|
|
// Get the target name (either aliased or original)
|
|
targetName := macroName
|
|
if alias, ok := n.aliases[macroName]; ok {
|
|
targetName = alias
|
|
}
|
|
|
|
// Get the macro from the import context
|
|
macro, ok := importCtx.macros[macroName]
|
|
if !ok {
|
|
return fmt.Errorf("macro '%s' not found in template '%s'", macroName, templateName)
|
|
}
|
|
|
|
// Set the macro in the current context
|
|
ctx.macros[targetName] = macro
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerbatimNode represents a raw/verbatim block that passes content through without processing Twig tags
|
|
type VerbatimNode struct {
|
|
content string
|
|
line int
|
|
}
|
|
|
|
// NewVerbatimNode creates a new verbatim node
|
|
func NewVerbatimNode(content string, line int) *VerbatimNode {
|
|
return GetVerbatimNode(content, line)
|
|
}
|
|
|
|
func (n *VerbatimNode) Type() NodeType {
|
|
return NodeVerbatim
|
|
}
|
|
|
|
func (n *VerbatimNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns a VerbatimNode to the pool
|
|
func (n *VerbatimNode) Release() {
|
|
ReleaseVerbatimNode(n)
|
|
}
|
|
|
|
// Render renders the verbatim node (outputs raw content without processing)
|
|
func (n *VerbatimNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Output the content as-is without any processing
|
|
_, err := WriteString(w, n.content)
|
|
return err
|
|
}
|
|
|
|
// ElementNode represents an HTML element
|
|
type ElementNode struct {
|
|
name string
|
|
attributes map[string]Node
|
|
children []Node
|
|
line int
|
|
}
|
|
|
|
func (n *ElementNode) Type() NodeType {
|
|
return NodeElement
|
|
}
|
|
|
|
func (n *ElementNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// SpacelessNode is implemented in whitespace.go
|
|
|
|
// ApplyNode represents a {% apply filter %} ... {% endapply %} block
|
|
type ApplyNode struct {
|
|
body []Node
|
|
filter string
|
|
args []Node
|
|
line int
|
|
}
|
|
|
|
// NewApplyNode creates a new apply node
|
|
func NewApplyNode(body []Node, filter string, args []Node, line int) *ApplyNode {
|
|
return GetApplyNode(body, filter, args, line)
|
|
}
|
|
|
|
func (n *ApplyNode) Type() NodeType {
|
|
return NodeApply
|
|
}
|
|
|
|
func (n *ApplyNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Release returns an ApplyNode to the pool
|
|
func (n *ApplyNode) Release() {
|
|
ReleaseApplyNode(n)
|
|
}
|
|
|
|
// Render renders the apply node by applying a filter to the rendered body
|
|
func (n *ApplyNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// First render body content to a buffer
|
|
var buf bytes.Buffer
|
|
|
|
// Render all body nodes
|
|
for _, node := range n.body {
|
|
err := node.Render(&buf, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Get the body content
|
|
content := buf.String()
|
|
|
|
// Evaluate filter arguments
|
|
filterArgs := make([]interface{}, len(n.args))
|
|
for i, arg := range n.args {
|
|
val, err := ctx.EvaluateExpression(arg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
filterArgs[i] = val
|
|
}
|
|
|
|
// Apply the filter to the content
|
|
result, err := ctx.ApplyFilter(n.filter, content, filterArgs...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write the filtered result
|
|
_, err = WriteString(w, ctx.ToString(result))
|
|
return err
|
|
}
|
|
|
|
// Implement Node interface for RootNode
|
|
func (n *RootNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// First pass: collect blocks and check for extends
|
|
var extendsNode *ExtendsNode
|
|
var hasChildBlocks bool
|
|
|
|
// Check if this is being rendered as a parent template (ctx.extending is true)
|
|
// In that case, we should NOT override block definitions
|
|
if ctx.extending {
|
|
hasChildBlocks = true
|
|
}
|
|
|
|
// First register all blocks in this template before processing extends
|
|
// Needed to ensure all blocks are available for parent() calls
|
|
for _, child := range n.children {
|
|
if block, ok := child.(*BlockNode); ok {
|
|
// Only register blocks that haven't been defined by a child template
|
|
if !hasChildBlocks || ctx.blocks[block.name] == nil {
|
|
// Register the block
|
|
ctx.blocks[block.name] = block.body
|
|
}
|
|
} else if ext, ok := child.(*ExtendsNode); ok {
|
|
// If this is an extends node, record it for later
|
|
extendsNode = ext
|
|
}
|
|
}
|
|
|
|
// If this template extends another, handle that first
|
|
if extendsNode != nil {
|
|
// Let the extends node handle the rendering, passing along
|
|
// all our blocks so they're available to the parent template
|
|
return extendsNode.Render(w, ctx)
|
|
}
|
|
|
|
// For a regular template (not extending another), render all nodes
|
|
// This includes block nodes, which will use their default content unless overridden
|
|
for _, child := range n.children {
|
|
err := child.Render(w, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Release returns a RootNode to the pool
|
|
func (n *RootNode) Release() {
|
|
ReleaseRootNode(n)
|
|
}
|
|
|
|
func (n *RootNode) Type() NodeType {
|
|
return NodeRoot
|
|
}
|
|
|
|
func (n *RootNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
func (n *RootNode) Children() []Node {
|
|
return n.children
|
|
}
|
|
|
|
// Implement Node interface for TextNode
|
|
func (n *TextNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Simply write the original content without modification
|
|
// This preserves HTML flow and whitespace exactly as in the template
|
|
_, err := WriteString(w, n.content)
|
|
return err
|
|
}
|
|
|
|
// Release returns a TextNode to the pool
|
|
func (n *TextNode) Release() {
|
|
ReleaseTextNode(n)
|
|
}
|
|
|
|
func (n *TextNode) Type() NodeType {
|
|
return NodeText
|
|
}
|
|
|
|
func (n *TextNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Implement Node interface for PrintNode
|
|
func (n *PrintNode) Type() NodeType {
|
|
return NodePrint
|
|
}
|
|
|
|
func (n *PrintNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
func (n *PrintNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
// Evaluate expression and write result
|
|
result, err := ctx.EvaluateExpression(n.expression)
|
|
if err != nil {
|
|
// Log error if debug is enabled
|
|
if IsDebugEnabled() {
|
|
message := fmt.Sprintf("Error evaluating print expression at line %d", n.line)
|
|
LogError(err, message)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Check if result is a callable for macros
|
|
if callable, ok := result.(func(io.Writer) error); ok {
|
|
// Execute the callable directly
|
|
return callable(w)
|
|
}
|
|
|
|
// Handle special case for parent() function which returns func(*RenderContext)(interface{}, error)
|
|
if parentFunc, ok := result.(func(*RenderContext) (interface{}, error)); ok {
|
|
// This is the parent function - execute it with the current context
|
|
parentResult, err := parentFunc(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write the parent result
|
|
_, err = WriteString(w, ctx.ToString(parentResult))
|
|
return err
|
|
}
|
|
|
|
// Convert result to string
|
|
var str string
|
|
|
|
// Make sure numbers are correctly converted to strings
|
|
switch v := result.(type) {
|
|
case int:
|
|
str = strconv.Itoa(v)
|
|
case float64:
|
|
str = strconv.FormatFloat(v, 'f', -1, 64)
|
|
case int64:
|
|
str = strconv.FormatInt(v, 10)
|
|
case bool:
|
|
str = strconv.FormatBool(v)
|
|
default:
|
|
// Use the regular ToString for other types
|
|
str = ctx.ToString(result)
|
|
}
|
|
|
|
// Log the output if debug is enabled (verbose level)
|
|
if IsDebugEnabled() && debugger.level >= DebugVerbose {
|
|
LogVerbose("Print node rendering at line %d: value=%v, type=%T", n.line, result, result)
|
|
}
|
|
|
|
// Write the result as-is without modification
|
|
// Let user handle proper quoting in templates
|
|
_, err = WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns a PrintNode to the pool
|
|
func (n *PrintNode) Release() {
|
|
ReleasePrintNode(n)
|
|
}
|