Files
database_render/internal/template/template_loader.go
2025-08-07 20:03:53 +08:00

373 lines
8.9 KiB
Go

package template
import (
"fmt"
"html/template"
"io/fs"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/gofiber/fiber/v3"
"log/slog"
)
type TemplateLoader struct {
templates map[string]*template.Template
cache map[string]*template.Template
mu sync.RWMutex
config TemplateConfig
versionManager *VersionManager
hotReload *HotReloadWatcher
configLoader *ConfigLoader
logger *slog.Logger
}
type TemplateConfig struct {
BuiltinPath string
CustomPath string
CacheEnabled bool
HotReload bool
Templates map[string]struct {
Name string
Type string
Path string
Description string
Fields map[string]string
Config map[string]interface{}
}
TemplateTypes map[string]struct {
Layout string
Fields map[string]string
Options map[string]interface{}
}
}
func NewTemplateLoader(config TemplateConfig) *TemplateLoader {
loader := &TemplateLoader{
templates: make(map[string]*template.Template),
cache: make(map[string]*template.Template),
config: config,
logger: slog.With("component", "template_loader"),
versionManager: NewVersionManager(config.CustomPath),
configLoader: NewConfigLoader("./config"),
}
// Initialize hot reload if enabled
if config.HotReload {
var err error
loader.hotReload, err = NewHotReloadWatcher(loader, config.CustomPath)
if err != nil {
loader.logger.Error("failed to initialize hot reload", "error", err)
} else {
if err := loader.hotReload.Start(); err != nil {
loader.logger.Error("failed to start hot reload", "error", err)
}
}
}
// Load configuration
if _, err := loader.configLoader.LoadTemplateConfig(); err != nil {
loader.logger.Warn("failed to load template config", "error", err)
}
return loader
}
func (tl *TemplateLoader) LoadTemplate(templateType, tableName string) (*template.Template, error) {
cacheKey := fmt.Sprintf("%s_%s", templateType, tableName)
tl.mu.RLock()
if cached, exists := tl.cache[cacheKey]; exists && tl.config.CacheEnabled {
tl.mu.RUnlock()
return cached, nil
}
tl.mu.RUnlock()
// 尝试加载自定义模板
customPath := filepath.Join(tl.config.CustomPath, tableName, fmt.Sprintf("%s.html", templateType))
if _, err := os.Stat(customPath); err == nil {
tmpl, err := tl.parseTemplate(customPath, templateType)
if err == nil {
tl.cacheTemplate(cacheKey, tmpl)
return tmpl, nil
}
}
// 回退到默认自定义模板
defaultPath := filepath.Join(tl.config.CustomPath, "_default", fmt.Sprintf("%s.html", templateType))
if _, err := os.Stat(defaultPath); err == nil {
tmpl, err := tl.parseTemplate(defaultPath, templateType)
if err == nil {
tl.cacheTemplate(cacheKey, tmpl)
return tmpl, nil
}
}
// 回退到内置模板
builtinPath := filepath.Join(tl.config.BuiltinPath, fmt.Sprintf("%s.html", templateType))
tmpl, err := tl.parseTemplate(builtinPath, templateType)
if err != nil {
return nil, fmt.Errorf("failed to load template %s: %w", templateType, err)
}
tl.cacheTemplate(cacheKey, tmpl)
return tmpl, nil
}
func (tl *TemplateLoader) parseTemplate(path, name string) (*template.Template, error) {
// 基础模板函数
funcs := template.FuncMap{
"dict": func(values ...interface{}) map[string]interface{} {
dict := make(map[string]interface{})
for i := 0; i < len(values); i += 2 {
if i+1 < len(values) {
key := fmt.Sprintf("%v", values[i])
dict[key] = values[i+1]
}
}
return dict
},
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"mul": func(a, b int) int { return a * b },
"min": func(a, b int) int {
if a < b {
return a
}
return b
},
"max": func(a, b int) int {
if a > b {
return a
}
return b
},
"json": func(v interface{}) string {
return fmt.Sprintf("%v", v)
},
"split": func(s string, sep string) []string {
return strings.Split(s, sep)
},
"until": func(n int) []int {
result := make([]int, n)
for i := 0; i < n; i++ {
result[i] = i + 1
}
return result
},
"eq": func(a, b interface{}) bool {
return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
},
"ge": func(a, b int) bool {
return a >= b
},
"gt": func(a, b int) bool {
return a > b
},
"lt": func(a, b int) bool {
return a < b
},
"le": func(a, b int) bool {
return a <= b
},
"index": func(m interface{}, key string) interface{} {
if m == nil {
return nil
}
switch v := m.(type) {
case map[string]interface{}:
return v[key]
default:
return nil
}
},
"printf": func(format string, a ...interface{}) string {
return fmt.Sprintf(format, a...)
},
}
// 解析布局模板
layoutPath := filepath.Join(tl.config.BuiltinPath, "layout.html")
if _, err := os.Stat(layoutPath); err != nil {
return nil, fmt.Errorf("layout template not found: %w", err)
}
tmpl := template.New(name).Funcs(funcs)
// 加载布局模板
layoutContent, err := os.ReadFile(layoutPath)
if err != nil {
return nil, fmt.Errorf("failed to read layout template: %w", err)
}
tmpl, err = tmpl.Parse(string(layoutContent))
if err != nil {
return nil, fmt.Errorf("failed to parse layout template: %w", err)
}
// 加载主模板
content, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read template %s: %w", path, err)
}
tmpl, err = tmpl.Parse(string(content))
if err != nil {
return nil, fmt.Errorf("failed to parse template %s: %w", path, err)
}
// 加载字段模板
fieldTemplates, err := tl.loadFieldTemplates()
if err != nil {
return nil, fmt.Errorf("failed to load field templates: %w", err)
}
for _, fieldTpl := range fieldTemplates {
tmpl, err = tmpl.Parse(fieldTpl)
if err != nil {
return nil, fmt.Errorf("failed to parse field template: %w", err)
}
}
return tmpl, nil
}
func (tl *TemplateLoader) loadFieldTemplates() ([]string, error) {
var templates []string
// 加载内置字段模板
builtinFieldPath := filepath.Join(tl.config.BuiltinPath, "field")
if err := filepath.WalkDir(builtinFieldPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && strings.HasSuffix(path, ".html") {
content, err := os.ReadFile(path)
if err != nil {
return err
}
templates = append(templates, string(content))
}
return nil
}); err != nil {
return nil, err
}
return templates, nil
}
func (tl *TemplateLoader) cacheTemplate(key string, tmpl *template.Template) {
if !tl.config.CacheEnabled {
return
}
tl.mu.Lock()
tl.cache[key] = tmpl
tl.mu.Unlock()
}
func (tl *TemplateLoader) ClearCache() {
tl.mu.Lock()
defer tl.mu.Unlock()
for k := range tl.cache {
delete(tl.cache, k)
}
}
func (tl *TemplateLoader) watchTemplates() {
if tl.config.CustomPath == "" {
return
}
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
var lastModTime time.Time
for range ticker.C {
info, err := os.Stat(tl.config.CustomPath)
if err != nil {
continue
}
if info.ModTime().After(lastModTime) {
lastModTime = info.ModTime()
tl.ClearCache()
}
}
}
// 验证模板完整性
func (tl *TemplateLoader) ValidateTemplate(templateType, tableName string) error {
_, err := tl.LoadTemplate(templateType, tableName)
return err
}
// 获取可用模板列表
func (tl *TemplateLoader) GetAvailableTemplates(tableName string) ([]string, error) {
var templates []string
// 检查自定义模板
customPath := filepath.Join(tl.config.CustomPath, tableName)
if entries, err := os.ReadDir(customPath); err == nil {
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".html") {
templates = append(templates, strings.TrimSuffix(entry.Name(), ".html"))
}
}
}
// 检查默认模板
defaultPath := filepath.Join(tl.config.CustomPath, "_default")
if entries, err := os.ReadDir(defaultPath); err == nil {
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".html") {
templateName := strings.TrimSuffix(entry.Name(), ".html")
if !contains(templates, templateName) {
templates = append(templates, templateName)
}
}
}
}
// 添加内置模板
builtinTemplates := []string{"list", "card", "timeline"}
for _, bt := range builtinTemplates {
if !contains(templates, bt) {
templates = append(templates, bt)
}
}
return templates, nil
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// Fiber模板引擎适配器
func (tl *TemplateLoader) Render(data interface{}, templateName string, c fiber.Ctx) error {
parts := strings.Split(templateName, ":")
if len(parts) != 2 {
return fmt.Errorf("invalid template format, expected 'type:table'")
}
templateType, tableName := parts[0], parts[1]
tmpl, err := tl.LoadTemplate(templateType, tableName)
if err != nil {
return err
}
c.Set("Content-Type", "text/html; charset=utf-8")
return tmpl.Execute(c.Response().BodyWriter(), data)
}