910 lines
20 KiB
Go
910 lines
20 KiB
Go
package template
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
sushi "git.sharkk.net/Sharkk/Sushi"
|
|
)
|
|
|
|
type Template struct {
|
|
name string
|
|
content string
|
|
modTime time.Time
|
|
filePath string
|
|
cache *TemplateCache
|
|
}
|
|
|
|
type TemplateFunc func(args ...any) any
|
|
|
|
var (
|
|
funcRegistry = make(map[string]TemplateFunc)
|
|
funcMutex sync.RWMutex
|
|
)
|
|
|
|
func init() {
|
|
// Built-in functions
|
|
RegisterFunc("upper", func(args ...any) any {
|
|
if len(args) == 0 {
|
|
return ""
|
|
}
|
|
return strings.ToUpper(fmt.Sprintf("%v", args[0]))
|
|
})
|
|
|
|
RegisterFunc("lower", func(args ...any) any {
|
|
if len(args) == 0 {
|
|
return ""
|
|
}
|
|
return strings.ToLower(fmt.Sprintf("%v", args[0]))
|
|
})
|
|
|
|
RegisterFunc("len", func(args ...any) any {
|
|
if len(args) == 0 {
|
|
return 0
|
|
}
|
|
rv := reflect.ValueOf(args[0])
|
|
switch rv.Kind() {
|
|
case reflect.Slice, reflect.Array, reflect.Map, reflect.String:
|
|
return rv.Len()
|
|
default:
|
|
return 0
|
|
}
|
|
})
|
|
}
|
|
|
|
func RegisterFunc(name string, fn TemplateFunc) {
|
|
funcMutex.Lock()
|
|
defer funcMutex.Unlock()
|
|
funcRegistry[name] = fn
|
|
}
|
|
|
|
func (t *Template) RenderPositional(args ...any) string {
|
|
result := t.content
|
|
for i, arg := range args {
|
|
result = strings.ReplaceAll(result, fmt.Sprintf("{%d}", i), fmt.Sprintf("%v", arg))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (t *Template) RenderNamed(data map[string]any) string {
|
|
if data == nil {
|
|
data = make(map[string]any)
|
|
}
|
|
|
|
blocks := make(map[string]string)
|
|
result := t.processBlocks(t.content, blocks)
|
|
result = t.processIncludes(result, data, blocks)
|
|
result = t.processYields(result, blocks, data)
|
|
result = t.processLoops(result, data)
|
|
result = t.processConditionals(result, data)
|
|
result = t.processVariables(result, data)
|
|
|
|
return result
|
|
}
|
|
|
|
func (t *Template) Render(data any) string {
|
|
switch v := data.(type) {
|
|
case map[string]any:
|
|
return t.RenderNamed(v)
|
|
case []any:
|
|
return t.RenderPositional(v...)
|
|
default:
|
|
rv := reflect.ValueOf(data)
|
|
if rv.Kind() == reflect.Slice {
|
|
args := make([]any, rv.Len())
|
|
for i := 0; i < rv.Len(); i++ {
|
|
args[i] = rv.Index(i).Interface()
|
|
}
|
|
return t.RenderPositional(args...)
|
|
} else {
|
|
return t.RenderPositional(data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Template) WriteTo(ctx sushi.Ctx, data any) error {
|
|
result := t.Render(data)
|
|
ctx.SendHTML(result)
|
|
return nil
|
|
}
|
|
|
|
func (t *Template) processBlocks(content string, blocks map[string]string) string {
|
|
result := content
|
|
|
|
for {
|
|
start := strings.Index(result, "{block ")
|
|
if start == -1 {
|
|
break
|
|
}
|
|
|
|
nameEnd := strings.Index(result[start:], "}")
|
|
if nameEnd == -1 {
|
|
break
|
|
}
|
|
nameEnd += start
|
|
|
|
blockName := strings.Trim(result[start+7:nameEnd], "\" ")
|
|
contentStart := nameEnd + 1
|
|
|
|
contentEnd := t.findMatchingEnd(result[contentStart:], "{block", "{/block}")
|
|
if contentEnd == -1 {
|
|
break
|
|
}
|
|
contentEnd += contentStart
|
|
|
|
blocks[blockName] = result[contentStart:contentEnd]
|
|
result = result[:start] + result[contentEnd+8:] // +8 for "{/block}"
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (t *Template) processIncludes(content string, data map[string]any, blocks map[string]string) string {
|
|
result := content
|
|
|
|
for {
|
|
start := strings.Index(result, "{include ")
|
|
if start == -1 {
|
|
break
|
|
}
|
|
|
|
end := strings.Index(result[start:], "}")
|
|
if end == -1 {
|
|
break
|
|
}
|
|
end += start
|
|
|
|
templateName := strings.Trim(result[start+9:end], "\" ")
|
|
|
|
if tmpl, err := t.cache.Load(templateName); err == nil {
|
|
// Process included template with same blocks context
|
|
included := tmpl.processBlocks(tmpl.content, blocks)
|
|
included = tmpl.processIncludes(included, data, blocks)
|
|
included = tmpl.processYields(included, blocks, data)
|
|
included = tmpl.processLoops(included, data)
|
|
included = tmpl.processConditionals(included, data)
|
|
included = tmpl.processVariables(included, data)
|
|
|
|
result = result[:start] + included + result[end+1:]
|
|
} else {
|
|
result = result[:start] + result[end+1:]
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (t *Template) processYields(content string, blocks map[string]string, data map[string]any) string {
|
|
result := content
|
|
|
|
// Process defined blocks
|
|
for blockName, blockContent := range blocks {
|
|
processed := t.processLoops(blockContent, data)
|
|
processed = t.processConditionals(processed, data)
|
|
processed = t.processVariables(processed, data)
|
|
|
|
result = strings.ReplaceAll(result, fmt.Sprintf(`{yield "%s"}`, blockName), processed)
|
|
}
|
|
|
|
// Remove unused named yields
|
|
start := 0
|
|
for {
|
|
yieldStart := strings.Index(result[start:], "{yield ")
|
|
if yieldStart == -1 {
|
|
break
|
|
}
|
|
yieldStart += start
|
|
|
|
yieldEnd := strings.Index(result[yieldStart:], "}")
|
|
if yieldEnd == -1 {
|
|
break
|
|
}
|
|
yieldEnd += yieldStart
|
|
|
|
result = result[:yieldStart] + result[yieldEnd+1:]
|
|
start = yieldStart
|
|
}
|
|
|
|
// Remove any remaining unnamed yields
|
|
result = strings.ReplaceAll(result, "{yield}", "")
|
|
|
|
return result
|
|
}
|
|
|
|
func (t *Template) processLoops(content string, data map[string]any) string {
|
|
result := content
|
|
|
|
for {
|
|
start := strings.Index(result, "{for ")
|
|
if start == -1 {
|
|
break
|
|
}
|
|
|
|
headerEnd := strings.Index(result[start:], "}")
|
|
if headerEnd == -1 {
|
|
break
|
|
}
|
|
headerEnd += start
|
|
|
|
header := result[start+5 : headerEnd]
|
|
contentStart := headerEnd + 1
|
|
|
|
contentEnd := t.findMatchingEnd(result[contentStart:], "{for", "{/for}")
|
|
if contentEnd == -1 {
|
|
break
|
|
}
|
|
contentEnd += contentStart
|
|
|
|
loopContent := result[contentStart:contentEnd]
|
|
expanded := t.expandLoop(header, loopContent, data)
|
|
|
|
result = result[:start] + expanded + result[contentEnd+6:] // +6 for "{/for}"
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (t *Template) expandLoop(header, content string, data map[string]any) string {
|
|
parts := strings.Split(strings.TrimSpace(header), " in ")
|
|
if len(parts) != 2 {
|
|
return ""
|
|
}
|
|
|
|
varPart := strings.TrimSpace(parts[0])
|
|
sourcePart := strings.TrimSpace(parts[1])
|
|
|
|
source := t.getNestedValue(data, sourcePart)
|
|
if source == nil {
|
|
return ""
|
|
}
|
|
|
|
var result strings.Builder
|
|
rv := reflect.ValueOf(source)
|
|
|
|
if strings.Contains(varPart, ",") {
|
|
// Key,value iteration
|
|
vars := strings.Split(varPart, ",")
|
|
keyVar := strings.TrimSpace(vars[0])
|
|
valueVar := strings.TrimSpace(vars[1])
|
|
|
|
switch rv.Kind() {
|
|
case reflect.Map:
|
|
for _, key := range rv.MapKeys() {
|
|
iterData := make(map[string]any)
|
|
maps.Copy(iterData, data)
|
|
iterData[keyVar] = key.Interface()
|
|
iterData[valueVar] = rv.MapIndex(key).Interface()
|
|
|
|
iterContent := t.processLoops(content, iterData)
|
|
iterContent = t.processConditionals(iterContent, iterData)
|
|
iterContent = t.processVariables(iterContent, iterData)
|
|
result.WriteString(iterContent)
|
|
}
|
|
case reflect.Slice, reflect.Array:
|
|
for i := 0; i < rv.Len(); i++ {
|
|
iterData := make(map[string]any)
|
|
maps.Copy(iterData, data)
|
|
iterData[keyVar] = i
|
|
iterData[valueVar] = rv.Index(i).Interface()
|
|
|
|
iterContent := t.processLoops(content, iterData)
|
|
iterContent = t.processConditionals(iterContent, iterData)
|
|
iterContent = t.processVariables(iterContent, iterData)
|
|
result.WriteString(iterContent)
|
|
}
|
|
}
|
|
} else {
|
|
// Single variable iteration
|
|
switch rv.Kind() {
|
|
case reflect.Slice, reflect.Array:
|
|
for i := 0; i < rv.Len(); i++ {
|
|
iterData := make(map[string]any)
|
|
maps.Copy(iterData, data)
|
|
iterData[varPart] = rv.Index(i).Interface()
|
|
|
|
iterContent := t.processLoops(content, iterData)
|
|
iterContent = t.processConditionals(iterContent, iterData)
|
|
iterContent = t.processVariables(iterContent, iterData)
|
|
result.WriteString(iterContent)
|
|
}
|
|
case reflect.Map:
|
|
for _, key := range rv.MapKeys() {
|
|
iterData := make(map[string]any)
|
|
maps.Copy(iterData, data)
|
|
iterData[varPart] = rv.MapIndex(key).Interface()
|
|
|
|
iterContent := t.processLoops(content, iterData)
|
|
iterContent = t.processConditionals(iterContent, iterData)
|
|
iterContent = t.processVariables(iterContent, iterData)
|
|
result.WriteString(iterContent)
|
|
}
|
|
}
|
|
}
|
|
|
|
return result.String()
|
|
}
|
|
|
|
func (t *Template) processConditionals(content string, data map[string]any) string {
|
|
result := content
|
|
|
|
for {
|
|
start := strings.Index(result, "{if ")
|
|
if start == -1 {
|
|
break
|
|
}
|
|
|
|
headerEnd := strings.Index(result[start:], "}")
|
|
if headerEnd == -1 {
|
|
break
|
|
}
|
|
headerEnd += start
|
|
|
|
condition := strings.TrimSpace(result[start+4 : headerEnd])
|
|
contentStart := headerEnd + 1
|
|
|
|
contentEnd := t.findMatchingEnd(result[contentStart:], "{if", "{/if}")
|
|
if contentEnd == -1 {
|
|
break
|
|
}
|
|
contentEnd += contentStart
|
|
|
|
ifContent := result[contentStart:contentEnd]
|
|
|
|
// Find else at top level
|
|
elsePos := t.findElseAtLevel(ifContent)
|
|
var trueContent, falseContent string
|
|
|
|
if elsePos != -1 {
|
|
trueContent = ifContent[:elsePos]
|
|
falseContent = ifContent[elsePos+6:] // Skip "{else}"
|
|
} else {
|
|
trueContent = ifContent
|
|
}
|
|
|
|
var selectedContent string
|
|
if t.evaluateCondition(condition, data) {
|
|
selectedContent = trueContent
|
|
} else {
|
|
selectedContent = falseContent
|
|
}
|
|
|
|
// Process selected content recursively
|
|
selectedContent = t.processLoops(selectedContent, data)
|
|
selectedContent = t.processConditionals(selectedContent, data)
|
|
|
|
result = result[:start] + selectedContent + result[contentEnd+5:] // +5 for "{/if}"
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (t *Template) processVariables(content string, data map[string]any) string {
|
|
result := content
|
|
|
|
// Process function calls and complex expressions first
|
|
start := 0
|
|
for {
|
|
startIdx := strings.Index(result[start:], "{")
|
|
if startIdx == -1 {
|
|
break
|
|
}
|
|
startIdx += start
|
|
|
|
endIdx := strings.Index(result[startIdx:], "}")
|
|
if endIdx == -1 {
|
|
break
|
|
}
|
|
endIdx += startIdx
|
|
|
|
placeholder := result[startIdx+1 : endIdx]
|
|
|
|
// Check for function calls
|
|
if parenIdx := strings.Index(placeholder, "("); parenIdx != -1 && strings.HasSuffix(placeholder, ")") {
|
|
value := t.callFunction(placeholder, data)
|
|
if value != nil {
|
|
result = result[:startIdx] + fmt.Sprintf("%v", value) + result[endIdx+1:]
|
|
start = startIdx + len(fmt.Sprintf("%v", value))
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Check for method calls or dot notation
|
|
if strings.Contains(placeholder, ".") {
|
|
value := t.getNestedValue(data, placeholder)
|
|
if value != nil {
|
|
result = result[:startIdx] + fmt.Sprintf("%v", value) + result[endIdx+1:]
|
|
start = startIdx + len(fmt.Sprintf("%v", value))
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Simple variable
|
|
if value, ok := data[placeholder]; ok {
|
|
result = result[:startIdx] + fmt.Sprintf("%v", value) + result[endIdx+1:]
|
|
start = startIdx + len(fmt.Sprintf("%v", value))
|
|
continue
|
|
}
|
|
|
|
start = endIdx + 1
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (t *Template) callFunction(expr string, data map[string]any) any {
|
|
parenIdx := strings.Index(expr, "(")
|
|
funcName := strings.TrimSpace(expr[:parenIdx])
|
|
argsStr := strings.TrimSpace(expr[parenIdx+1 : len(expr)-1])
|
|
|
|
funcMutex.RLock()
|
|
fn, exists := funcRegistry[funcName]
|
|
funcMutex.RUnlock()
|
|
|
|
if !exists {
|
|
return nil
|
|
}
|
|
|
|
args := t.parseArgs(argsStr, data)
|
|
return fn(args...)
|
|
}
|
|
|
|
func (t *Template) callMethod(obj any, methodCall string, data map[string]any) any {
|
|
if obj == nil {
|
|
return nil
|
|
}
|
|
|
|
parenIdx := strings.Index(methodCall, "(")
|
|
methodName := methodCall[:parenIdx]
|
|
argsStr := methodCall[parenIdx+1 : len(methodCall)-1]
|
|
|
|
rv := reflect.ValueOf(obj)
|
|
method := rv.MethodByName(methodName)
|
|
if !method.IsValid() {
|
|
return nil
|
|
}
|
|
|
|
args := t.parseArgs(argsStr, data)
|
|
methodType := method.Type()
|
|
|
|
// Convert arguments to match method signature
|
|
reflectArgs := make([]reflect.Value, len(args))
|
|
for i, arg := range args {
|
|
if i < methodType.NumIn() {
|
|
reflectArgs[i] = t.convertArg(arg, methodType.In(i))
|
|
} else {
|
|
reflectArgs[i] = reflect.ValueOf(arg)
|
|
}
|
|
}
|
|
|
|
// Handle variadic methods
|
|
if methodType.IsVariadic() && len(reflectArgs) >= methodType.NumIn()-1 {
|
|
result := method.CallSlice(reflectArgs)
|
|
if len(result) > 0 {
|
|
return result[0].Interface()
|
|
}
|
|
} else if len(reflectArgs) == methodType.NumIn() {
|
|
result := method.Call(reflectArgs)
|
|
if len(result) > 0 {
|
|
return result[0].Interface()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *Template) parseArgs(argsStr string, data map[string]any) []any {
|
|
if argsStr == "" {
|
|
return nil
|
|
}
|
|
|
|
args := []any{}
|
|
current := ""
|
|
inQuotes := false
|
|
parenLevel := 0
|
|
|
|
for _, r := range argsStr {
|
|
switch r {
|
|
case '"':
|
|
inQuotes = !inQuotes
|
|
current += string(r)
|
|
case '(':
|
|
parenLevel++
|
|
current += string(r)
|
|
case ')':
|
|
parenLevel--
|
|
current += string(r)
|
|
case ',':
|
|
if !inQuotes && parenLevel == 0 {
|
|
args = append(args, t.parseArgValue(strings.TrimSpace(current), data))
|
|
current = ""
|
|
continue
|
|
}
|
|
current += string(r)
|
|
default:
|
|
current += string(r)
|
|
}
|
|
}
|
|
|
|
if current != "" {
|
|
args = append(args, t.parseArgValue(strings.TrimSpace(current), data))
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
func (t *Template) parseArgValue(arg string, data map[string]any) any {
|
|
arg = strings.TrimSpace(arg)
|
|
|
|
// String literal
|
|
if strings.HasPrefix(arg, "\"") && strings.HasSuffix(arg, "\"") {
|
|
return arg[1 : len(arg)-1]
|
|
}
|
|
|
|
// Number
|
|
if num, err := strconv.ParseFloat(arg, 64); err == nil {
|
|
if strings.Contains(arg, ".") {
|
|
return num
|
|
}
|
|
return int(num)
|
|
}
|
|
|
|
// Boolean
|
|
if arg == "true" {
|
|
return true
|
|
}
|
|
if arg == "false" {
|
|
return false
|
|
}
|
|
|
|
// Function call
|
|
if parenIdx := strings.Index(arg, "("); parenIdx != -1 && strings.HasSuffix(arg, ")") {
|
|
return t.callFunction(arg, data)
|
|
}
|
|
|
|
// Variable or dot notation
|
|
if strings.Contains(arg, ".") {
|
|
return t.getNestedValue(data, arg)
|
|
}
|
|
|
|
// Simple variable
|
|
if value, ok := data[arg]; ok {
|
|
return value
|
|
}
|
|
|
|
return arg
|
|
}
|
|
|
|
func (t *Template) convertArg(arg any, targetType reflect.Type) reflect.Value {
|
|
argValue := reflect.ValueOf(arg)
|
|
|
|
if argValue.Type().ConvertibleTo(targetType) {
|
|
return argValue.Convert(targetType)
|
|
}
|
|
|
|
return argValue
|
|
}
|
|
|
|
func (t *Template) findMatchingEnd(content, startTag, endTag string) int {
|
|
level := 1
|
|
pos := 0
|
|
|
|
for pos < len(content) && level > 0 {
|
|
nextStart := strings.Index(content[pos:], startTag)
|
|
nextEnd := strings.Index(content[pos:], endTag)
|
|
|
|
if nextStart != -1 && (nextEnd == -1 || nextStart < nextEnd) {
|
|
level++
|
|
pos += nextStart + len(startTag)
|
|
} else if nextEnd != -1 {
|
|
level--
|
|
if level == 0 {
|
|
return pos + nextEnd
|
|
}
|
|
pos += nextEnd + len(endTag)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
func (t *Template) findElseAtLevel(content string) int {
|
|
level := 0
|
|
pos := 0
|
|
|
|
for pos < len(content) {
|
|
nextIf := strings.Index(content[pos:], "{if ")
|
|
nextElse := strings.Index(content[pos:], "{else}")
|
|
nextEnd := strings.Index(content[pos:], "{/if}")
|
|
|
|
earliest := -1
|
|
var tag string
|
|
|
|
if nextIf != -1 && (earliest == -1 || nextIf < earliest-pos) {
|
|
earliest = pos + nextIf
|
|
tag = "if"
|
|
}
|
|
if nextElse != -1 && (earliest == -1 || nextElse < earliest-pos) {
|
|
earliest = pos + nextElse
|
|
tag = "else"
|
|
}
|
|
if nextEnd != -1 && (earliest == -1 || nextEnd < earliest-pos) {
|
|
earliest = pos + nextEnd
|
|
tag = "end"
|
|
}
|
|
|
|
if earliest == -1 {
|
|
break
|
|
}
|
|
|
|
switch tag {
|
|
case "if":
|
|
level++
|
|
pos = earliest + 4
|
|
case "else":
|
|
if level == 0 {
|
|
return earliest
|
|
}
|
|
pos = earliest + 6
|
|
case "end":
|
|
level--
|
|
pos = earliest + 5
|
|
}
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
func (t *Template) getNestedValue(data map[string]any, path string) any {
|
|
keys := strings.Split(path, ".")
|
|
var current any = data
|
|
|
|
for i, key := range keys {
|
|
// Check for method call at any point in the chain
|
|
if strings.Contains(key, "(") && strings.HasSuffix(key, ")") {
|
|
result := t.callMethod(current, key, data)
|
|
if i == len(keys)-1 {
|
|
// This was the final key, return the method result
|
|
return result
|
|
}
|
|
// Continue with the method result for further chaining
|
|
current = t.convertToStringMap(result)
|
|
continue
|
|
}
|
|
|
|
if i == len(keys)-1 {
|
|
// Final key - get field/value
|
|
switch v := current.(type) {
|
|
case map[string]any:
|
|
return v[key]
|
|
default:
|
|
return t.getStructField(current, key)
|
|
}
|
|
}
|
|
|
|
// Intermediate key - get next object in chain
|
|
var next any
|
|
switch v := current.(type) {
|
|
case map[string]any:
|
|
var ok bool
|
|
next, ok = v[key]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
default:
|
|
next = t.getStructField(current, key)
|
|
if next == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
current = t.convertToStringMap(next)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *Template) getStructField(obj any, fieldName string) any {
|
|
if obj == nil {
|
|
return nil
|
|
}
|
|
|
|
rv := reflect.ValueOf(obj)
|
|
if rv.Kind() == reflect.Ptr {
|
|
if rv.IsNil() {
|
|
return nil
|
|
}
|
|
rv = rv.Elem()
|
|
}
|
|
|
|
if rv.Kind() != reflect.Struct {
|
|
return nil
|
|
}
|
|
|
|
field := rv.FieldByName(fieldName)
|
|
if !field.IsValid() {
|
|
return nil
|
|
}
|
|
|
|
return field.Interface()
|
|
}
|
|
|
|
func (t *Template) convertToStringMap(value any) any {
|
|
switch v := value.(type) {
|
|
case map[string]any:
|
|
return v
|
|
case map[any]any:
|
|
newMap := make(map[string]any)
|
|
for k, val := range v {
|
|
newMap[fmt.Sprintf("%v", k)] = val
|
|
}
|
|
return newMap
|
|
default:
|
|
rv := reflect.ValueOf(value)
|
|
if rv.Kind() == reflect.Map {
|
|
newMap := make(map[string]any)
|
|
for _, k := range rv.MapKeys() {
|
|
newMap[fmt.Sprintf("%v", k.Interface())] = rv.MapIndex(k).Interface()
|
|
}
|
|
return newMap
|
|
}
|
|
return value
|
|
}
|
|
}
|
|
|
|
func (t *Template) evaluateCondition(condition string, data map[string]any) bool {
|
|
condition = strings.TrimSpace(condition)
|
|
|
|
if strings.Contains(condition, " or ") {
|
|
for _, part := range strings.Split(condition, " or ") {
|
|
if t.evaluateCondition(strings.TrimSpace(part), data) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
if strings.Contains(condition, " and ") {
|
|
for _, part := range strings.Split(condition, " and ") {
|
|
if !t.evaluateCondition(strings.TrimSpace(part), data) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
for _, op := range []string{">=", "<=", "!=", "==", ">", "<"} {
|
|
if strings.Contains(condition, op) {
|
|
parts := strings.Split(condition, op)
|
|
if len(parts) == 2 {
|
|
left := strings.TrimSpace(parts[0])
|
|
right := strings.TrimSpace(parts[1])
|
|
return t.compareValues(t.getConditionValue(left, data), t.getConditionValue(right, data), op)
|
|
}
|
|
}
|
|
}
|
|
|
|
return t.isTruthy(t.getConditionValue(condition, data))
|
|
}
|
|
|
|
func (t *Template) getConditionValue(expr string, data map[string]any) any {
|
|
expr = strings.TrimSpace(expr)
|
|
|
|
if strings.HasPrefix(expr, "#") {
|
|
return t.getLength(t.getNestedValue(data, expr[1:]))
|
|
}
|
|
|
|
if num, err := strconv.ParseFloat(expr, 64); err == nil {
|
|
return num
|
|
}
|
|
|
|
if strings.HasPrefix(expr, "\"") && strings.HasSuffix(expr, "\"") {
|
|
return expr[1 : len(expr)-1]
|
|
}
|
|
|
|
// Function call
|
|
if parenIdx := strings.Index(expr, "("); parenIdx != -1 && strings.HasSuffix(expr, ")") {
|
|
return t.callFunction(expr, data)
|
|
}
|
|
|
|
if strings.Contains(expr, ".") {
|
|
return t.getNestedValue(data, expr)
|
|
}
|
|
|
|
if value, ok := data[expr]; ok {
|
|
return value
|
|
}
|
|
|
|
return expr
|
|
}
|
|
|
|
func (t *Template) compareValues(left, right any, op string) bool {
|
|
switch op {
|
|
case "==":
|
|
return fmt.Sprintf("%v", left) == fmt.Sprintf("%v", right)
|
|
case "!=":
|
|
return fmt.Sprintf("%v", left) != fmt.Sprintf("%v", right)
|
|
case ">", ">=", "<", "<=":
|
|
leftNum, leftOk := t.toFloat(left)
|
|
rightNum, rightOk := t.toFloat(right)
|
|
if !leftOk || !rightOk {
|
|
return false
|
|
}
|
|
switch op {
|
|
case ">":
|
|
return leftNum > rightNum
|
|
case ">=":
|
|
return leftNum >= rightNum
|
|
case "<":
|
|
return leftNum < rightNum
|
|
case "<=":
|
|
return leftNum <= rightNum
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (t *Template) toFloat(value any) (float64, bool) {
|
|
switch v := value.(type) {
|
|
case int:
|
|
return float64(v), true
|
|
case int64:
|
|
return float64(v), true
|
|
case float32:
|
|
return float64(v), true
|
|
case float64:
|
|
return v, true
|
|
case string:
|
|
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
|
return f, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func (t *Template) isTruthy(value any) bool {
|
|
if value == nil {
|
|
return false
|
|
}
|
|
|
|
switch v := value.(type) {
|
|
case bool:
|
|
return v
|
|
case int:
|
|
return v != 0
|
|
case float64:
|
|
return v != 0
|
|
case string:
|
|
return v != ""
|
|
default:
|
|
rv := reflect.ValueOf(value)
|
|
switch rv.Kind() {
|
|
case reflect.Slice, reflect.Array, reflect.Map:
|
|
return rv.Len() > 0
|
|
case reflect.Ptr:
|
|
return !rv.IsNil()
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (t *Template) getLength(value any) int {
|
|
if value == nil {
|
|
return 0
|
|
}
|
|
|
|
rv := reflect.ValueOf(value)
|
|
switch rv.Kind() {
|
|
case reflect.Slice, reflect.Array, reflect.Map, reflect.String:
|
|
return rv.Len()
|
|
default:
|
|
return 0
|
|
}
|
|
}
|