package template import ( "fmt" "os" "path/filepath" "reflect" "strings" "sync" "time" "github.com/valyala/fasthttp" ) type Cache struct { mu sync.RWMutex templates map[string]*Template basePath string } type Template struct { name string content string modTime time.Time filePath string } func NewCache(basePath string) *Cache { if basePath == "" { exe, err := os.Executable() if err != nil { basePath = "." } else { basePath = filepath.Dir(exe) } } return &Cache{ templates: make(map[string]*Template), basePath: basePath, } } func (c *Cache) Load(name string) (*Template, error) { c.mu.RLock() tmpl, exists := c.templates[name] c.mu.RUnlock() if exists { if err := c.checkAndReload(tmpl); err != nil { return nil, err } return tmpl, nil } return c.loadFromFile(name) } func (c *Cache) loadFromFile(name string) (*Template, error) { filePath := filepath.Join(c.basePath, "templates", name) info, err := os.Stat(filePath) if err != nil { return nil, fmt.Errorf("template file not found: %s", name) } content, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("failed to read template: %w", err) } tmpl := &Template{ name: name, content: string(content), modTime: info.ModTime(), filePath: filePath, } c.mu.Lock() c.templates[name] = tmpl c.mu.Unlock() return tmpl, nil } func (c *Cache) checkAndReload(tmpl *Template) error { info, err := os.Stat(tmpl.filePath) if err != nil { return err } if info.ModTime().After(tmpl.modTime) { content, err := os.ReadFile(tmpl.filePath) if err != nil { return err } c.mu.Lock() tmpl.content = string(content) tmpl.modTime = info.ModTime() c.mu.Unlock() } return nil } func (t *Template) RenderPositional(args ...any) string { result := t.content for i, arg := range args { placeholder := fmt.Sprintf("{%d}", i) result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", arg)) } return result } func (t *Template) RenderNamed(data map[string]any) string { result := t.content for key, value := range data { placeholder := fmt.Sprintf("{%s}", key) result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value)) } result = t.replaceDotNotation(result, data) return result } func (t *Template) replaceDotNotation(content string, data map[string]any) string { result := content 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] 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 } } start = endIdx + 1 } return result } func (t *Template) getNestedValue(data map[string]any, path string) any { keys := strings.Split(path, ".") current := data for i, key := range keys { if i == len(keys)-1 { return current[key] } next, ok := current[key] if !ok { return nil } switch v := next.(type) { case map[string]any: current = v case map[any]any: newMap := make(map[string]any) for k, val := range v { newMap[fmt.Sprintf("%v", k)] = val } current = newMap default: rv := reflect.ValueOf(next) 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() } current = newMap } else { return nil } } } return nil } func (t *Template) WriteTo(ctx *fasthttp.RequestCtx, data any) { var result string switch v := data.(type) { case map[string]any: result = t.RenderNamed(v) case []any: result = 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() } result = t.RenderPositional(args...) } else { result = t.RenderPositional(data) } } ctx.SetContentType("text/html; charset=utf-8") ctx.WriteString(result) }