From 1148e71d117499f412aa50ec8bdce484c1f2fc41 Mon Sep 17 00:00:00 2001 From: semihalev Date: Mon, 10 Mar 2025 05:11:11 +0300 Subject: [PATCH] Implement macro rendering functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- node.go | 296 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ parser.go | 221 ++++++++++++++++++++++++++++++++++++++-- render.go | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 789 insertions(+), 7 deletions(-) diff --git a/node.go b/node.go index d5748e5..76c5ee0 100644 --- a/node.go +++ b/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, + } } \ No newline at end of file diff --git a/parser.go b/parser.go index a8a7644..a5fc2dc 100644 --- a/parser.go +++ b/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. diff --git a/render.go b/render.go index ebdcee8..8cb0c4f 100644 --- a/render.go +++ b/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) }