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 methodCache = make(map[string]reflect.Method) cacheMutex 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) objType := rv.Type() // Check method cache cacheKey := fmt.Sprintf("%s.%s", objType.String(), methodName) cacheMutex.RLock() method, cached := methodCache[cacheKey] cacheMutex.RUnlock() if !cached { method = rv.MethodByName(methodName) if !method.IsValid() { return nil } cacheMutex.Lock() methodCache[cacheKey] = method cacheMutex.Unlock() } 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 i, 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 if strings.Contains(key, "(") && strings.HasSuffix(key, ")") { return t.callMethod(current, key, data) } if i == len(keys)-1 { switch v := current.(type) { case map[string]any: return v[key] default: return t.getStructField(current, key) } } 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 } }