mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 13:55:46 +01:00
Implement macro rendering functionality
- Added methods to execute macros and handle imported macros - Implemented ImportNode and FromImportNode for template imports - Added CallFunction method to support function and macro calls - Enhanced PrintNode to handle callable macro results - Updated parseMacro, parseImport, and parseFrom methods - Added NullWriter for importing macros without output - Fixed parser handling of endmacro tags 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
512a48bb41
commit
1148e71d11
3 changed files with 789 additions and 7 deletions
296
node.go
296
node.go
|
|
@ -38,6 +38,7 @@ const (
|
|||
NodeComment
|
||||
NodeVerbatim
|
||||
NodeElement
|
||||
NodeFunction
|
||||
)
|
||||
|
||||
// RootNode represents the root of a template
|
||||
|
|
@ -111,6 +112,40 @@ type CommentNode struct {
|
|||
line int
|
||||
}
|
||||
|
||||
// We use the FunctionNode from expr.go
|
||||
|
||||
// MacroNode represents a macro definition
|
||||
type MacroNode struct {
|
||||
name string
|
||||
params []string
|
||||
defaults map[string]Node
|
||||
body []Node
|
||||
line int
|
||||
}
|
||||
|
||||
// ImportNode represents an import statement
|
||||
type ImportNode struct {
|
||||
template Node
|
||||
module string
|
||||
line int
|
||||
}
|
||||
|
||||
// FromImportNode represents a from import statement
|
||||
type FromImportNode struct {
|
||||
template Node
|
||||
macros []string
|
||||
aliases map[string]string
|
||||
line int
|
||||
}
|
||||
|
||||
// NullWriter is a writer that discards all data
|
||||
type NullWriter struct{}
|
||||
|
||||
// Write implements io.Writer for NullWriter
|
||||
func (w *NullWriter) Write(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Implement Node interface for RootNode
|
||||
func (n *RootNode) Render(w io.Writer, ctx *RenderContext) error {
|
||||
// Check if this is an extending template
|
||||
|
|
@ -187,6 +222,12 @@ func (n *PrintNode) Render(w io.Writer, ctx *RenderContext) error {
|
|||
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)
|
||||
}
|
||||
|
||||
// Convert result to string and write
|
||||
str := ctx.ToString(result)
|
||||
_, err = w.Write([]byte(str))
|
||||
|
|
@ -704,4 +745,259 @@ func NewSetNode(name string, value Node, line int) *SetNode {
|
|||
value: value,
|
||||
line: line,
|
||||
}
|
||||
}
|
||||
|
||||
// Make FunctionNode implement Node interface
|
||||
func (n *FunctionNode) Render(w io.Writer, ctx *RenderContext) error {
|
||||
// Evaluate arguments
|
||||
args := make([]interface{}, len(n.args))
|
||||
for i, arg := range n.args {
|
||||
val, err := ctx.EvaluateExpression(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args[i] = val
|
||||
}
|
||||
|
||||
// Call the function
|
||||
result, err := ctx.CallFunction(n.name, args)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// Write the result
|
||||
_, err = w.Write([]byte(ctx.ToString(result)))
|
||||
return err
|
||||
}
|
||||
|
||||
func (n *FunctionNode) Type() NodeType {
|
||||
return NodeFunction
|
||||
}
|
||||
|
||||
func (n *FunctionNode) Line() int {
|
||||
return n.line
|
||||
}
|
||||
|
||||
// Implement Node interface for MacroNode
|
||||
func (n *MacroNode) Render(w io.Writer, ctx *RenderContext) error {
|
||||
// Register the macro in the context
|
||||
if ctx.macros == nil {
|
||||
ctx.macros = make(map[string]Node)
|
||||
}
|
||||
ctx.macros[n.name] = n
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *MacroNode) Type() NodeType {
|
||||
return NodeMacro
|
||||
}
|
||||
|
||||
func (n *MacroNode) Line() int {
|
||||
return n.line
|
||||
}
|
||||
|
||||
// Call executes the macro with the given arguments
|
||||
func (n *MacroNode) Call(w io.Writer, ctx *RenderContext, args []interface{}) error {
|
||||
// Create a new context for the macro
|
||||
macroContext := &RenderContext{
|
||||
env: ctx.env,
|
||||
context: make(map[string]interface{}),
|
||||
blocks: ctx.blocks,
|
||||
macros: ctx.macros,
|
||||
parent: ctx.parent,
|
||||
engine: ctx.engine,
|
||||
}
|
||||
|
||||
// Set parameter values from arguments
|
||||
for i, param := range n.params {
|
||||
if i < len(args) {
|
||||
// Use provided argument
|
||||
macroContext.context[param] = args[i]
|
||||
} else if defaultValue, ok := n.defaults[param]; ok {
|
||||
// Use default value
|
||||
value, err := ctx.EvaluateExpression(defaultValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
macroContext.context[param] = value
|
||||
} else {
|
||||
// No argument or default, set to nil
|
||||
macroContext.context[param] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Render the macro body
|
||||
for _, node := range n.body {
|
||||
if err := node.Render(w, macroContext); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Implement Node interface for ImportNode
|
||||
func (n *ImportNode) Render(w io.Writer, ctx *RenderContext) error {
|
||||
// Evaluate the template name
|
||||
templateNameVal, err := ctx.EvaluateExpression(n.template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
templateName, ok := templateNameVal.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("template name must be a string at line %d", n.line)
|
||||
}
|
||||
|
||||
// Load the template
|
||||
template, err := ctx.engine.Load(templateName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new context for executing the template
|
||||
importContext := &RenderContext{
|
||||
env: ctx.env,
|
||||
context: make(map[string]interface{}),
|
||||
blocks: make(map[string][]Node),
|
||||
macros: make(map[string]Node),
|
||||
engine: ctx.engine,
|
||||
}
|
||||
|
||||
// Execute the template without output to collect macros
|
||||
var nullWriter NullWriter
|
||||
if err := template.nodes.Render(&nullWriter, importContext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a module object with the macros
|
||||
module := make(map[string]interface{})
|
||||
for name, macro := range importContext.macros {
|
||||
module[name] = macro
|
||||
}
|
||||
|
||||
// Add the module to the current context
|
||||
ctx.SetVariable(n.module, module)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *ImportNode) Type() NodeType {
|
||||
return NodeImport
|
||||
}
|
||||
|
||||
func (n *ImportNode) Line() int {
|
||||
return n.line
|
||||
}
|
||||
|
||||
// Implement Node interface for FromImportNode
|
||||
func (n *FromImportNode) Render(w io.Writer, ctx *RenderContext) error {
|
||||
// Evaluate the template name
|
||||
templateNameVal, err := ctx.EvaluateExpression(n.template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
templateName, ok := templateNameVal.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("template name must be a string at line %d", n.line)
|
||||
}
|
||||
|
||||
// Load the template
|
||||
template, err := ctx.engine.Load(templateName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new context for executing the template
|
||||
importContext := &RenderContext{
|
||||
env: ctx.env,
|
||||
context: make(map[string]interface{}),
|
||||
blocks: make(map[string][]Node),
|
||||
macros: make(map[string]Node),
|
||||
engine: ctx.engine,
|
||||
}
|
||||
|
||||
// Execute the template without output to collect macros
|
||||
var nullWriter NullWriter
|
||||
if err := template.nodes.Render(&nullWriter, importContext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Import the specified macros into the current context
|
||||
if ctx.macros == nil {
|
||||
ctx.macros = make(map[string]Node)
|
||||
}
|
||||
|
||||
// Add the directly imported macros
|
||||
for _, macroName := range n.macros {
|
||||
if macro, ok := importContext.macros[macroName]; ok {
|
||||
ctx.macros[macroName] = macro
|
||||
}
|
||||
}
|
||||
|
||||
// Add the aliased macros
|
||||
for macroName, alias := range n.aliases {
|
||||
if macro, ok := importContext.macros[macroName]; ok {
|
||||
ctx.macros[alias] = macro
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *FromImportNode) Type() NodeType {
|
||||
return NodeImport
|
||||
}
|
||||
|
||||
func (n *FromImportNode) Line() int {
|
||||
return n.line
|
||||
}
|
||||
|
||||
// NewFunctionNode creates a new function call node
|
||||
func NewFunctionNode(name string, args []Node, line int) *FunctionNode {
|
||||
return &FunctionNode{
|
||||
ExpressionNode: ExpressionNode{
|
||||
exprType: ExprFunction,
|
||||
line: line,
|
||||
},
|
||||
name: name,
|
||||
args: args,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMacroNode creates a new macro node
|
||||
func NewMacroNode(name string, params []string, defaults map[string]Node, body []Node, line int) *MacroNode {
|
||||
return &MacroNode{
|
||||
name: name,
|
||||
params: params,
|
||||
defaults: defaults,
|
||||
body: body,
|
||||
line: line,
|
||||
}
|
||||
}
|
||||
|
||||
// NewImportNode creates a new import node
|
||||
func NewImportNode(templateExpr Node, alias string, line int) *ImportNode {
|
||||
return &ImportNode{
|
||||
template: templateExpr,
|
||||
module: alias,
|
||||
line: line,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFromImportNode creates a new from import node
|
||||
func NewFromImportNode(templateExpr Node, macros []string, aliases map[string]string, line int) *FromImportNode {
|
||||
return &FromImportNode{
|
||||
template: templateExpr,
|
||||
macros: macros,
|
||||
aliases: aliases,
|
||||
line: line,
|
||||
}
|
||||
}
|
||||
221
parser.go
221
parser.go
|
|
@ -86,6 +86,7 @@ func (p *Parser) initBlockHandlers() {
|
|||
// Special closing tags - they will be handled in their corresponding open tag parsers
|
||||
"endif": p.parseEndTag,
|
||||
"endfor": p.parseEndTag,
|
||||
"endmacro": p.parseEndTag,
|
||||
"endblock": p.parseEndTag,
|
||||
"else": p.parseEndTag,
|
||||
"elseif": p.parseEndTag,
|
||||
|
|
@ -338,7 +339,7 @@ func (p *Parser) parseOuterTemplate() ([]Node, error) {
|
|||
|
||||
// Check if this is a control ending tag (endif, endfor, endblock, etc.)
|
||||
if blockName == "endif" || blockName == "endfor" || blockName == "endblock" ||
|
||||
blockName == "else" || blockName == "elseif" {
|
||||
blockName == "endmacro" || blockName == "else" || blockName == "elseif" {
|
||||
// We should return to the parent parser that's handling the parent block
|
||||
// First move back two steps to the start of the block tag
|
||||
p.tokenIndex -= 2
|
||||
|
|
@ -917,18 +918,224 @@ func (p *Parser) parseDo(parser *Parser) (Node, error) {
|
|||
}
|
||||
|
||||
func (p *Parser) parseMacro(parser *Parser) (Node, error) {
|
||||
// Placeholder for macro parsing
|
||||
return nil, fmt.Errorf("macros not implemented yet")
|
||||
// Get the line number of the macro token
|
||||
macroLine := parser.tokens[parser.tokenIndex-2].Line
|
||||
|
||||
// Get the macro name
|
||||
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
|
||||
return nil, fmt.Errorf("expected macro name after macro keyword at line %d", macroLine)
|
||||
}
|
||||
|
||||
macroName := parser.tokens[parser.tokenIndex].Value
|
||||
parser.tokenIndex++
|
||||
|
||||
// Expect opening parenthesis for parameters
|
||||
if parser.tokenIndex >= len(parser.tokens) ||
|
||||
parser.tokens[parser.tokenIndex].Type != TOKEN_PUNCTUATION ||
|
||||
parser.tokens[parser.tokenIndex].Value != "(" {
|
||||
return nil, fmt.Errorf("expected '(' after macro name at line %d", macroLine)
|
||||
}
|
||||
parser.tokenIndex++
|
||||
|
||||
// Parse parameters
|
||||
var params []string
|
||||
defaults := make(map[string]Node)
|
||||
|
||||
// If we don't have a closing parenthesis immediately, we have parameters
|
||||
if parser.tokenIndex < len(parser.tokens) &&
|
||||
(parser.tokens[parser.tokenIndex].Type != TOKEN_PUNCTUATION ||
|
||||
parser.tokens[parser.tokenIndex].Value != ")") {
|
||||
|
||||
for {
|
||||
// Get parameter name
|
||||
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
|
||||
return nil, fmt.Errorf("expected parameter name at line %d", macroLine)
|
||||
}
|
||||
|
||||
paramName := parser.tokens[parser.tokenIndex].Value
|
||||
params = append(params, paramName)
|
||||
parser.tokenIndex++
|
||||
|
||||
// Check for default value
|
||||
if parser.tokenIndex < len(parser.tokens) &&
|
||||
parser.tokens[parser.tokenIndex].Type == TOKEN_OPERATOR &&
|
||||
parser.tokens[parser.tokenIndex].Value == "=" {
|
||||
parser.tokenIndex++ // Skip =
|
||||
|
||||
// Parse default value expression
|
||||
defaultExpr, err := parser.parseExpression()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaults[paramName] = defaultExpr
|
||||
}
|
||||
|
||||
// Check if we have more parameters
|
||||
if parser.tokenIndex < len(parser.tokens) &&
|
||||
parser.tokens[parser.tokenIndex].Type == TOKEN_PUNCTUATION &&
|
||||
parser.tokens[parser.tokenIndex].Value == "," {
|
||||
parser.tokenIndex++ // Skip comma
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Expect closing parenthesis
|
||||
if parser.tokenIndex >= len(parser.tokens) ||
|
||||
parser.tokens[parser.tokenIndex].Type != TOKEN_PUNCTUATION ||
|
||||
parser.tokens[parser.tokenIndex].Value != ")" {
|
||||
return nil, fmt.Errorf("expected ')' after macro parameters at line %d", macroLine)
|
||||
}
|
||||
parser.tokenIndex++
|
||||
|
||||
// Expect block end
|
||||
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
|
||||
return nil, fmt.Errorf("expected block end token after macro declaration at line %d", macroLine)
|
||||
}
|
||||
parser.tokenIndex++
|
||||
|
||||
// Parse the macro body
|
||||
bodyNodes, err := parser.parseOuterTemplate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Expect endmacro tag
|
||||
if parser.tokenIndex+1 >= len(parser.tokens) ||
|
||||
parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_START ||
|
||||
parser.tokens[parser.tokenIndex+1].Type != TOKEN_NAME ||
|
||||
parser.tokens[parser.tokenIndex+1].Value != "endmacro" {
|
||||
return nil, fmt.Errorf("missing endmacro tag for macro '%s' at line %d",
|
||||
macroName, macroLine)
|
||||
}
|
||||
|
||||
// Skip {% endmacro %}
|
||||
parser.tokenIndex += 2 // Skip {% endmacro
|
||||
|
||||
// Expect block end
|
||||
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
|
||||
return nil, fmt.Errorf("expected block end token after endmacro at line %d", parser.tokens[parser.tokenIndex].Line)
|
||||
}
|
||||
parser.tokenIndex++
|
||||
|
||||
// Create the macro node
|
||||
return NewMacroNode(macroName, params, defaults, bodyNodes, macroLine), nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseImport(parser *Parser) (Node, error) {
|
||||
// Placeholder for import parsing
|
||||
return nil, fmt.Errorf("import not implemented yet")
|
||||
// Get the line number of the import token
|
||||
importLine := parser.tokens[parser.tokenIndex-2].Line
|
||||
|
||||
// Get the template to import
|
||||
templateExpr, err := parser.parseExpression()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Expect 'as' keyword
|
||||
if parser.tokenIndex >= len(parser.tokens) ||
|
||||
parser.tokens[parser.tokenIndex].Type != TOKEN_NAME ||
|
||||
parser.tokens[parser.tokenIndex].Value != "as" {
|
||||
return nil, fmt.Errorf("expected 'as' after template path at line %d", importLine)
|
||||
}
|
||||
parser.tokenIndex++
|
||||
|
||||
// Get the alias name
|
||||
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
|
||||
return nil, fmt.Errorf("expected identifier after 'as' at line %d", importLine)
|
||||
}
|
||||
|
||||
alias := parser.tokens[parser.tokenIndex].Value
|
||||
parser.tokenIndex++
|
||||
|
||||
// Expect block end
|
||||
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
|
||||
return nil, fmt.Errorf("expected block end token after import statement at line %d", importLine)
|
||||
}
|
||||
parser.tokenIndex++
|
||||
|
||||
// Create import node
|
||||
return NewImportNode(templateExpr, alias, importLine), nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseFrom(parser *Parser) (Node, error) {
|
||||
// Placeholder for from parsing
|
||||
return nil, fmt.Errorf("from not implemented yet")
|
||||
// Get the line number of the from token
|
||||
fromLine := parser.tokens[parser.tokenIndex-2].Line
|
||||
|
||||
// Get the template to import from
|
||||
templateExpr, err := parser.parseExpression()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Expect 'import' keyword
|
||||
if parser.tokenIndex >= len(parser.tokens) ||
|
||||
parser.tokens[parser.tokenIndex].Type != TOKEN_NAME ||
|
||||
parser.tokens[parser.tokenIndex].Value != "import" {
|
||||
return nil, fmt.Errorf("expected 'import' after template path at line %d", fromLine)
|
||||
}
|
||||
parser.tokenIndex++
|
||||
|
||||
// Parse the imported items
|
||||
var macros []string
|
||||
aliases := make(map[string]string)
|
||||
|
||||
// We need at least one macro to import
|
||||
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
|
||||
return nil, fmt.Errorf("expected at least one identifier after 'import' at line %d", fromLine)
|
||||
}
|
||||
|
||||
for parser.tokenIndex < len(parser.tokens) && parser.tokens[parser.tokenIndex].Type == TOKEN_NAME {
|
||||
// Get macro name
|
||||
macroName := parser.tokens[parser.tokenIndex].Value
|
||||
parser.tokenIndex++
|
||||
|
||||
// Check for 'as' keyword for aliasing
|
||||
if parser.tokenIndex < len(parser.tokens) &&
|
||||
parser.tokens[parser.tokenIndex].Type == TOKEN_NAME &&
|
||||
parser.tokens[parser.tokenIndex].Value == "as" {
|
||||
parser.tokenIndex++ // Skip 'as'
|
||||
|
||||
// Get alias name
|
||||
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
|
||||
return nil, fmt.Errorf("expected identifier after 'as' at line %d", fromLine)
|
||||
}
|
||||
|
||||
aliasName := parser.tokens[parser.tokenIndex].Value
|
||||
aliases[macroName] = aliasName
|
||||
parser.tokenIndex++
|
||||
} else {
|
||||
// No alias, just add to macros list
|
||||
macros = append(macros, macroName)
|
||||
}
|
||||
|
||||
// Check for comma to separate items
|
||||
if parser.tokenIndex < len(parser.tokens) &&
|
||||
parser.tokens[parser.tokenIndex].Type == TOKEN_PUNCTUATION &&
|
||||
parser.tokens[parser.tokenIndex].Value == "," {
|
||||
parser.tokenIndex++ // Skip comma
|
||||
|
||||
// Expect another identifier after comma
|
||||
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
|
||||
return nil, fmt.Errorf("expected identifier after ',' at line %d", fromLine)
|
||||
}
|
||||
} else {
|
||||
// End of imports
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Expect block end
|
||||
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
|
||||
return nil, fmt.Errorf("expected block end token after from import statement at line %d", fromLine)
|
||||
}
|
||||
parser.tokenIndex++
|
||||
|
||||
// Create from import node
|
||||
return NewFromImportNode(templateExpr, macros, aliases, fromLine), nil
|
||||
}
|
||||
|
||||
// parseEndTag handles closing tags like endif, endfor, endblock, etc.
|
||||
|
|
|
|||
279
render.go
279
render.go
|
|
@ -3,6 +3,7 @@ package twig
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
|
@ -58,6 +59,235 @@ func (ctx *RenderContext) SetVariable(name string, value interface{}) {
|
|||
ctx.context[name] = value
|
||||
}
|
||||
|
||||
// GetMacro gets a macro from the context
|
||||
func (ctx *RenderContext) GetMacro(name string) (Node, bool) {
|
||||
// Check local macros first
|
||||
if macro, ok := ctx.macros[name]; ok {
|
||||
return macro, true
|
||||
}
|
||||
|
||||
// Check parent context
|
||||
if ctx.parent != nil {
|
||||
return ctx.parent.GetMacro(name)
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// CallMacro calls a macro with the given arguments
|
||||
func (ctx *RenderContext) CallMacro(w io.Writer, name string, args []interface{}) error {
|
||||
// Find the macro
|
||||
macro, ok := ctx.GetMacro(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("macro '%s' not found", name)
|
||||
}
|
||||
|
||||
// Check if it's a MacroNode
|
||||
macroNode, ok := macro.(*MacroNode)
|
||||
if !ok {
|
||||
return fmt.Errorf("'%s' is not a macro", name)
|
||||
}
|
||||
|
||||
// Call the macro
|
||||
return macroNode.Call(w, ctx, args)
|
||||
}
|
||||
|
||||
// CallFunction calls a function with the given arguments
|
||||
func (ctx *RenderContext) CallFunction(name string, args []interface{}) (interface{}, error) {
|
||||
// Check if it's a built-in function
|
||||
switch name {
|
||||
case "range":
|
||||
return ctx.callRangeFunction(args)
|
||||
case "length", "count":
|
||||
return ctx.callLengthFunction(args)
|
||||
case "max":
|
||||
return ctx.callMaxFunction(args)
|
||||
case "min":
|
||||
return ctx.callMinFunction(args)
|
||||
}
|
||||
|
||||
// Check if it's a function in the environment
|
||||
if ctx.env != nil {
|
||||
if fn, ok := ctx.env.functions[name]; ok {
|
||||
return fn(ctx, args)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a macro
|
||||
if macro, ok := ctx.GetMacro(name); ok {
|
||||
// Return a callable function
|
||||
return func(w io.Writer) error {
|
||||
macroNode, ok := macro.(*MacroNode)
|
||||
if !ok {
|
||||
return fmt.Errorf("'%s' is not a macro", name)
|
||||
}
|
||||
return macroNode.Call(w, ctx, args)
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("function '%s' not found", name)
|
||||
}
|
||||
|
||||
// callRangeFunction implements the range function
|
||||
func (ctx *RenderContext) callRangeFunction(args []interface{}) (interface{}, error) {
|
||||
if len(args) < 2 {
|
||||
return nil, fmt.Errorf("range function requires at least 2 arguments")
|
||||
}
|
||||
|
||||
// Get the start and end values
|
||||
start, ok1 := ctx.toNumber(args[0])
|
||||
end, ok2 := ctx.toNumber(args[1])
|
||||
|
||||
if !ok1 || !ok2 {
|
||||
return nil, fmt.Errorf("range arguments must be numbers")
|
||||
}
|
||||
|
||||
// Get the step value (default is 1)
|
||||
step := 1.0
|
||||
if len(args) > 2 {
|
||||
if s, ok := ctx.toNumber(args[2]); ok {
|
||||
step = s
|
||||
}
|
||||
}
|
||||
|
||||
// Create the range
|
||||
var result []int
|
||||
for i := start; i <= end; i += step {
|
||||
result = append(result, int(i))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// callLengthFunction implements the length/count function
|
||||
func (ctx *RenderContext) callLengthFunction(args []interface{}) (interface{}, error) {
|
||||
if len(args) != 1 {
|
||||
return nil, fmt.Errorf("length/count function requires exactly 1 argument")
|
||||
}
|
||||
|
||||
val := args[0]
|
||||
v := reflect.ValueOf(val)
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
return len(v.String()), nil
|
||||
case reflect.Slice, reflect.Array:
|
||||
return v.Len(), nil
|
||||
case reflect.Map:
|
||||
return v.Len(), nil
|
||||
default:
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
// callMaxFunction implements the max function
|
||||
func (ctx *RenderContext) callMaxFunction(args []interface{}) (interface{}, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, fmt.Errorf("max function requires at least 1 argument")
|
||||
}
|
||||
|
||||
// If the argument is a slice or array, find the max value in it
|
||||
if len(args) == 1 {
|
||||
v := reflect.ValueOf(args[0])
|
||||
if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
|
||||
if v.Len() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
max := v.Index(0).Interface()
|
||||
maxNum, ok := ctx.toNumber(max)
|
||||
if !ok {
|
||||
return max, nil
|
||||
}
|
||||
|
||||
for i := 1; i < v.Len(); i++ {
|
||||
val := v.Index(i).Interface()
|
||||
if valNum, ok := ctx.toNumber(val); ok {
|
||||
if valNum > maxNum {
|
||||
max = val
|
||||
maxNum = valNum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return max, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Find the max value in the arguments
|
||||
max := args[0]
|
||||
maxNum, ok := ctx.toNumber(max)
|
||||
if !ok {
|
||||
return max, nil
|
||||
}
|
||||
|
||||
for i := 1; i < len(args); i++ {
|
||||
val := args[i]
|
||||
if valNum, ok := ctx.toNumber(val); ok {
|
||||
if valNum > maxNum {
|
||||
max = val
|
||||
maxNum = valNum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return max, nil
|
||||
}
|
||||
|
||||
// callMinFunction implements the min function
|
||||
func (ctx *RenderContext) callMinFunction(args []interface{}) (interface{}, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, fmt.Errorf("min function requires at least 1 argument")
|
||||
}
|
||||
|
||||
// If the argument is a slice or array, find the min value in it
|
||||
if len(args) == 1 {
|
||||
v := reflect.ValueOf(args[0])
|
||||
if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
|
||||
if v.Len() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
min := v.Index(0).Interface()
|
||||
minNum, ok := ctx.toNumber(min)
|
||||
if !ok {
|
||||
return min, nil
|
||||
}
|
||||
|
||||
for i := 1; i < v.Len(); i++ {
|
||||
val := v.Index(i).Interface()
|
||||
if valNum, ok := ctx.toNumber(val); ok {
|
||||
if valNum < minNum {
|
||||
min = val
|
||||
minNum = valNum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return min, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Find the min value in the arguments
|
||||
min := args[0]
|
||||
minNum, ok := ctx.toNumber(min)
|
||||
if !ok {
|
||||
return min, nil
|
||||
}
|
||||
|
||||
for i := 1; i < len(args); i++ {
|
||||
val := args[i]
|
||||
if valNum, ok := ctx.toNumber(val); ok {
|
||||
if valNum < minNum {
|
||||
min = val
|
||||
minNum = valNum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return min, nil
|
||||
}
|
||||
|
||||
// EvaluateExpression evaluates an expression node
|
||||
func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
|
||||
switch n := node.(type) {
|
||||
|
|
@ -65,6 +295,12 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
|
|||
return n.value, nil
|
||||
|
||||
case *VariableNode:
|
||||
// Check if it's a macro first
|
||||
if macro, ok := ctx.GetMacro(n.name); ok {
|
||||
return macro, nil
|
||||
}
|
||||
|
||||
// Otherwise, look up variable
|
||||
return ctx.GetVariable(n.name)
|
||||
|
||||
case *GetAttrNode:
|
||||
|
|
@ -83,6 +319,13 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
|
|||
return nil, fmt.Errorf("attribute name must be a string")
|
||||
}
|
||||
|
||||
// Check if obj is a map containing macros (from import)
|
||||
if moduleMap, ok := obj.(map[string]interface{}); ok {
|
||||
if macro, ok := moduleMap[attrStr]; ok {
|
||||
return macro, nil
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.getAttribute(obj, attrStr)
|
||||
|
||||
case *BinaryNode:
|
||||
|
|
@ -98,6 +341,42 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
|
|||
|
||||
return ctx.evaluateBinaryOp(n.operator, left, right)
|
||||
|
||||
case *FunctionNode:
|
||||
// Check if it's a macro call
|
||||
if macro, ok := ctx.GetMacro(n.name); ok {
|
||||
// Evaluate arguments
|
||||
args := make([]interface{}, len(n.args))
|
||||
for i, arg := range n.args {
|
||||
val, err := ctx.EvaluateExpression(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args[i] = val
|
||||
}
|
||||
|
||||
// Return a callable that can be rendered later
|
||||
return func(w io.Writer) error {
|
||||
macroNode, ok := macro.(*MacroNode)
|
||||
if !ok {
|
||||
return fmt.Errorf("'%s' is not a macro", n.name)
|
||||
}
|
||||
return macroNode.Call(w, ctx, args)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Otherwise, it's a regular function call
|
||||
// Evaluate arguments
|
||||
args := make([]interface{}, len(n.args))
|
||||
for i, arg := range n.args {
|
||||
val, err := ctx.EvaluateExpression(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args[i] = val
|
||||
}
|
||||
|
||||
return ctx.CallFunction(n.name, args)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported expression type: %T", node)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue