package themes import ( "errors" "fmt" "html/template" "io/fs" "os" "path/filepath" "strings" "sync" "time" "github.com/gin-gonic/gin" "golang.org/x/exp/slog" "gopkg.in/yaml.v3" ) const ( defaultLayout = "base.html" themeConfigFile = "theme.yaml" requiredTemplates = "home.html,post.html" ) // ThemeMeta 主题元数据 type ThemeMeta struct { Name string `yaml:"name"` Version string `yaml:"version"` Author string `yaml:"author"` Description string `yaml:"description"` License string `yaml:"license"` } // ThemeManager 主题管理器 type ThemeManager struct { mu sync.RWMutex themesDir string currentTheme string templates *template.Template staticFS gin.StaticFS meta ThemeMeta funcMap template.FuncMap } // NewManager 创建主题管理器实例 func NewManager(themesDir, defaultTheme string) (*ThemeManager, error) { m := &ThemeManager{ themesDir: themesDir, funcMap: defaultFuncMap(), } if err := m.LoadTheme(defaultTheme); err != nil { return nil, fmt.Errorf("初始化默认主题失败: %w", err) } return m, nil } // 默认模板函数 func defaultFuncMap() template.FuncMap { return template.FuncMap{ "safeHTML": func(s string) template.HTML { return template.HTML(s) }, "dateFmt": func(t time.Time) string { return t.Format("2006-01-02") }, "assetPath": func(name string) string { return fmt.Sprintf("/theme/static/%s?t=%d", name, time.Now().Unix()) }, } } // LoadTheme 加载指定主题 func (m *ThemeManager) LoadTheme(name string) error { start := time.Now() themePath := filepath.Join(m.themesDir, name) // 验证主题完整性 if err := validateTheme(themePath); err != nil { return fmt.Errorf("主题验证失败: %w", err) } // 加载元数据 meta, err := loadThemeMeta(themePath) if err != nil { return fmt.Errorf("元数据加载失败: %w", err) } // 解析模板 tpls, err := parseTemplates(themePath, m.funcMap) if err != nil { return fmt.Errorf("模板解析失败: %w", err) } // 准备静态文件系统 staticPath := filepath.Join(themePath, "static") staticFS := gin.Dir(staticPath, false) // 原子化更新状态 m.mu.Lock() defer m.mu.Unlock() m.currentTheme = name m.templates = tpls m.staticFS = staticFS m.meta = meta slog.Info("主题加载成功", "theme", name, "templates", len(tpls.Templates()), "duration", time.Since(start), ) return nil } // 验证主题完整性 func validateTheme(themePath string) error { // 检查必须文件 requiredFiles := []string{ themeConfigFile, filepath.Join("templates", "base.html"), } for _, f := range strings.Split(requiredTemplates, ",") { requiredFiles = append(requiredFiles, filepath.Join("templates", f)) } var missing []string for _, f := range requiredFiles { if _, err := os.Stat(filepath.Join(themePath, f)); err != nil { if os.IsNotExist(err) { missing = append(missing, f) } } } if len(missing) > 0 { return fmt.Errorf("缺失必要文件: %v", missing) } return nil } // 加载主题元数据 func loadThemeMeta(themePath string) (ThemeMeta, error) { configPath := filepath.Join(themePath, themeConfigFile) data, err := os.ReadFile(configPath) if err != nil { return ThemeMeta{}, err } var meta ThemeMeta if err := yaml.Unmarshal(data, &meta); err != nil { return ThemeMeta{}, err } if meta.Name == "" { return ThemeMeta{}, errors.New("主题名称不能为空") } return meta, nil } // 解析模板文件 func parseTemplates(themePath string, funcMap template.FuncMap) (*template.Template, error) { tplDir := filepath.Join(themePath, "templates") baseFile := filepath.Join(tplDir, defaultLayout) // 先解析基础模板 baseTpl, err := template.New("base"). Funcs(funcMap). ParseFiles(baseFile) if err != nil { return nil, fmt.Errorf("基础模板解析失败: %w", err) } // 遍历模板目录 return filepath.WalkDir(tplDir, func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return nil } if path == baseFile { // 已处理基础模板 return nil } if filepath.Ext(path) != ".html" { return nil } // 克隆基础模板并解析 _, err = baseTpl.Clone(). ParseFiles(path) if err != nil { return fmt.Errorf("解析 %s 失败: %w", filepath.Base(path), err) } return nil }) } // GetTemplate 获取指定模板 func (m *ThemeManager) GetTemplate(name string) *template.Template { m.mu.RLock() defer m.mu.RUnlock() return m.templates.Lookup(name + ".html") } // RegisterStatic 注册静态资源路由 func (m *ThemeManager) RegisterStatic(r *gin.Engine) { m.mu.RLock() defer m.mu.RUnlock() r.StaticFS("/theme/static", m.staticFS) } // CurrentThemeInfo 返回当前主题信息 func (m *ThemeManager) CurrentThemeInfo() ThemeMeta { m.mu.RLock() defer m.mu.RUnlock() return m.meta } // ListThemes 获取可用主题列表 func (m *ThemeManager) ListThemes() ([]string, error) { entries, err := os.ReadDir(m.themesDir) if err != nil { return nil, err } var themes []string for _, entry := range entries { if entry.IsDir() { if validateTheme(filepath.Join(m.themesDir, entry.Name())) == nil { themes = append(themes, entry.Name()) } } } return themes, nil }