227 lines
4.2 KiB
Go
227 lines
4.2 KiB
Go
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)
|
|
}
|