ollama/template/rewrite.go

150 lines
3.9 KiB
Go

package template
import (
"text/template"
"text/template/parse"
)
// rewritePropertiesCheck walks the template AST and rewrites .Function.Parameters.Properties
// to .Function.Parameters.HasProperties in if/with conditions to fix truthiness checking.
// This maintains backward compatibility with templates that check if Properties exist.
func rewritePropertiesCheck(tmpl *template.Template) {
walk(tmpl.Tree.Root)
}
func walk(n parse.Node) {
if n == nil {
return
}
switch node := n.(type) {
case *parse.ListNode:
for _, child := range node.Nodes {
walk(child)
}
case *parse.ActionNode:
// Rewrite len calls in action nodes
rewritePipeProperties(node.Pipe)
case *parse.IfNode:
rewritePipeProperties(node.Pipe)
walk(&node.BranchNode)
case *parse.WithNode:
rewritePipeProperties(node.Pipe)
walk(&node.BranchNode)
case *parse.RangeNode:
// Don't rewrite the pipe for range nodes - they need .Properties for iteration
walk(&node.BranchNode)
case *parse.BranchNode:
if node.List != nil {
walk(node.List)
}
if node.ElseList != nil {
walk(node.ElseList)
}
}
}
func rewritePipeProperties(pipe *parse.PipeNode) {
if pipe == nil {
return
}
for _, cmd := range pipe.Cmds {
rewriteCommand(cmd)
}
}
// rewriteCommand recursively rewrites a command and all its nested command arguments
func rewriteCommand(cmd *parse.CommandNode) {
// Check if this is a "len .Function.Parameters.Properties" call
if isLenPropertiesCall(cmd) {
// Replace entire command with .Function.Parameters.Len field access
replaceLenWithLenMethod(cmd)
return
}
// Recursively process all arguments
for i, arg := range cmd.Args {
switch argNode := arg.(type) {
case *parse.FieldNode:
// Check for direct .Properties field access
if isPropertiesField(argNode.Ident) {
cmd.Args[i] = replaceWithHasProperties(argNode)
}
case *parse.CommandNode:
// Recursively process nested commands (e.g., inside "and", "gt", etc.)
rewriteCommand(argNode)
case *parse.PipeNode:
// Template function arguments can be wrapped in PipeNodes
rewritePipeProperties(argNode)
}
}
}
// isLenPropertiesCall checks if a command is "len .Function.Parameters.Properties"
func isLenPropertiesCall(cmd *parse.CommandNode) bool {
if len(cmd.Args) != 2 {
return false
}
// First arg should be the "len" identifier
if ident, ok := cmd.Args[0].(*parse.IdentifierNode); !ok || ident.Ident != "len" {
return false
}
// Second arg should be .Function.Parameters.Properties field
if field, ok := cmd.Args[1].(*parse.FieldNode); ok {
return isPropertiesField(field.Ident)
}
return false
}
// replaceLenWithLenMethod replaces "len .Function.Parameters.Properties" with ".Function.Parameters.Len"
func replaceLenWithLenMethod(cmd *parse.CommandNode) {
if len(cmd.Args) < 2 {
return
}
field, ok := cmd.Args[1].(*parse.FieldNode)
if !ok {
return
}
// Create new field node with .Len instead of .Properties
newIdent := make([]string, len(field.Ident))
copy(newIdent, field.Ident)
newIdent[len(newIdent)-1] = "Len"
newField := &parse.FieldNode{
NodeType: parse.NodeField,
Ident: newIdent,
Pos: field.Pos,
}
// Replace the command with just the field access (remove "len" function call)
cmd.Args = []parse.Node{newField}
}
func isPropertiesField(ident []string) bool {
// Match: .Function.Parameters.Properties
// We only rewrite if it ends with Parameters.Properties to avoid false positives
if len(ident) < 3 {
return false
}
return ident[len(ident)-1] == "Properties" && ident[len(ident)-2] == "Parameters"
}
func replaceWithHasProperties(field *parse.FieldNode) *parse.FieldNode {
// Clone the identifier slice and replace the last element
newIdent := make([]string, len(field.Ident))
copy(newIdent, field.Ident)
newIdent[len(newIdent)-1] = "HasProperties"
return &parse.FieldNode{
NodeType: parse.NodeField,
Ident: newIdent,
Pos: field.Pos,
}
}