diff --git a/themes/manager.go b/themes/manager.go index eed5f4f..46fbd49 100644 --- a/themes/manager.go +++ b/themes/manager.go @@ -1,6 +1,7 @@ package themes import ( + "embed" "errors" "fmt" "html/template" @@ -9,228 +10,155 @@ import ( "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"` + Version string `yaml:"version"` 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 + mu sync.RWMutex + currentTheme string + templates map[string]*template.Template + baseTemplates embed.FS // 可选:嵌入基础模板 + themesDir string } -// NewManager 创建主题管理器实例 -func NewManager(themesDir, defaultTheme string) (*ThemeManager, error) { - m := &ThemeManager{ +// 创建新主题管理器 +func NewManager(themesDir string) *ThemeManager { + return &ThemeManager{ + templates: make(map[string]*template.Template), 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) - - // 原子化更新状态 +// 核心方法:加载主题 +func (m *ThemeManager) LoadTheme(themeName string) error { m.mu.Lock() defer m.mu.Unlock() - m.currentTheme = name + // 1. 验证主题目录结构 + themePath := filepath.Join(m.themesDir, themeName) + if !isValidTheme(themePath) { + return fmt.Errorf("invalid theme structure: %s", themeName) + } + + // 2. 加载模板文件 + tpls, err := m.parseTemplates(themePath) + if err != nil { + return fmt.Errorf("template parsing failed: %w", err) + } + + // 3. 更新当前主题 + m.currentTheme = themeName 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 (m *ThemeManager) CurrentTheme() string { + m.mu.RLock() + defer m.mu.RUnlock() + return m.currentTheme } -// 加载主题元数据 -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") + return m.templates[name] } -// RegisterStatic 注册静态资源路由 -func (m *ThemeManager) RegisterStatic(r *gin.Engine) { - m.mu.RLock() - defer m.mu.RUnlock() - - r.StaticFS("/theme/static", m.staticFS) +// 注册静态文件路由(Gin框架) +func (m *ThemeManager) RegisterStaticRoutes(router *gin.Engine) { + staticPath := filepath.Join(m.themesDir, m.currentTheme, "static") + router.StaticFS("/theme/static", gin.Dir(staticPath, false)) } -// CurrentThemeInfo 返回当前主题信息 -func (m *ThemeManager) CurrentThemeInfo() ThemeMeta { - m.mu.RLock() - defer m.mu.RUnlock() +// 校验主题完整性 +func isValidTheme(themePath string) bool { + requiredFiles := []string{ + "theme.yaml", + // "templates/home.html", + // "templates/post.html", + "templates/index.html", + } - return m.meta + for _, f := range requiredFiles { + if _, err := os.Stat(filepath.Join(themePath, f)); os.IsNotExist(err) { + return false + } + } + return true } -// ListThemes 获取可用主题列表 +// 解析模板文件(支持布局继承) +func (m *ThemeManager) parseTemplates(themePath string) (map[string]*template.Template, error) { + templates := make(map[string]*template.Template) + + // 加载公共基础模板(可选) + baseTpl := template.New("base").Funcs(template.FuncMap{ + "safeHTML": func(s string) template.HTML { return template.HTML(s) }, + }) + + // 从主题目录加载模板 + tplDir := filepath.Join(themePath, "templates") + err := filepath.WalkDir(tplDir, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(path, ".html") { + return nil + } + + // 读取模板内容 + content, err := os.ReadFile(path) + if err != nil { + return err + } + + // 生成模板名称(相对路径) + name := strings.TrimPrefix(path, tplDir+string(filepath.Separator)) + name = strings.TrimSuffix(name, filepath.Ext(name)) + + // 克隆基础模板并解析 + tpl := template.Must(baseTpl.Clone()) + tpl, err = tpl.Parse(string(content)) + if err != nil { + return fmt.Errorf("parse error in %s: %w", name, err) + } + + templates[name] = tpl + return nil + }) + + if err != nil { + return nil, err + } + + if len(templates) == 0 { + return nil, errors.New("no valid templates found") + } + + return templates, nil +} + +// 获取可用主题列表 func (m *ThemeManager) ListThemes() ([]string, error) { - entries, err := os.ReadDir(m.themesDir) + dirs, 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()) - } + for _, d := range dirs { + if d.IsDir() && isValidTheme(filepath.Join(m.themesDir, d.Name())) { + themes = append(themes, d.Name()) } } return themes, nil