package utils import ( "embed" "errors" "fmt" "html/template" "io/fs" "os" "path/filepath" "strings" "sync" "github.com/gin-gonic/gin" ) // 主题元数据结构 type ThemeMeta struct { Name string `yaml:"name"` Author string `yaml:"author"` Version string `yaml:"version"` Description string `yaml:"description"` } // 主题管理器 type ThemeManager struct { mu sync.RWMutex currentTheme string templates map[string]*template.Template baseTemplates embed.FS // 可选:嵌入基础模板 themesDir string } // 创建新主题管理器 func NewManager(themesDir string) *ThemeManager { return &ThemeManager{ templates: make(map[string]*template.Template), themesDir: themesDir, } } // 自定义错误类型 var ( ErrInvalidThemeStructure = errors.New("invalid theme structure") ErrNoValidTemplates = errors.New("no valid templates found") ) // 核心方法:加载主题 func (themeManager *ThemeManager) LoadTheme(themeName string) error { themeManager.mu.Lock() defer themeManager.mu.Unlock() // 1. 验证主题目录结构 themePath := filepath.Join(themeManager.themesDir, themeName) if !isValidTheme(themePath) { return fmt.Errorf("%w: %s", ErrInvalidThemeStructure, themeName) } // 2. 加载模板文件 tpls, err := themeManager.parseTemplates(themePath) if err != nil { return fmt.Errorf("template parsing failed: %w", err) } // 3. 更新当前主题 themeManager.currentTheme = themeName themeManager.templates = tpls return nil } // 获取当前主题名称 func (m *ThemeManager) CurrentTheme() string { m.mu.RLock() defer m.mu.RUnlock() return m.currentTheme } // 获取编译后的模板 func (m *ThemeManager) GetTemplate(name string) *template.Template { m.mu.RLock() defer m.mu.RUnlock() return m.templates[name] } // 注册静态文件路由(Gin框架) func (m *ThemeManager) RegisterStaticRoutes(router *gin.Engine) { staticPath := filepath.Join(m.themesDir, m.currentTheme, "static") router.StaticFS("/theme/static", gin.Dir(staticPath, false)) } // 校验主题完整性 func isValidTheme(themePath string) bool { requiredBaseFiles := []string{ // 不带扩展名的基本文件名 "theme.yaml", "templates/index", // 例如: templates/index.html 或 templates/index.tmpl // "templates/post", // 如果 post 模板是必须的,也加入这里 } supportedTemplateExtensions := []string{".html", ".tmpl"} for _, baseFile := range requiredBaseFiles { found := false if strings.HasPrefix(baseFile, "templates/") { // 对于模板文件,尝试所有支持的扩展名 for _, ext := range supportedTemplateExtensions { if exists, _ := fileExists(filepath.Join(themePath, baseFile+ext)); exists { found = true break } } } else { // 对于非模板文件 (如 theme.yaml),直接检查 if exists, _ := fileExists(filepath.Join(themePath, baseFile)); exists { found = true } } if !found { // log.Printf("Required file/template %s not found in theme %s", baseFile, themePath) // 可选的调试日志 return false } } return true } // 检查文件是否存在 func fileExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return false, err } // 解析模板文件(支持布局继承) 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 { return err // 返回错误以停止 WalkDir } if d.IsDir() { return nil // 跳过目录 } // 支持 .html 和 .tmpl 扩展名 lowerPath := strings.ToLower(path) if !strings.HasSuffix(lowerPath, ".html") && !strings.HasSuffix(lowerPath, ".tmpl") { return nil // 非模板文件,跳过 } // 读取模板内容 content, err := readFile(path) if err != nil { return fmt.Errorf("failed to read template file %s: %w", path, err) } // 生成模板名称(相对路径,不含扩展名) name := strings.TrimPrefix(path, tplDir+string(filepath.Separator)) name = strings.TrimSuffix(name, filepath.Ext(name)) // 克隆基础模板并解析 tpl, err := baseTpl.Clone() if err != nil { return fmt.Errorf("failed to clone base template for %s: %w", name, err) } tpl, err = tpl.Parse(string(content)) if err != nil { return fmt.Errorf("parse error in %s (file: %s): %w", name, path, err) } templates[name] = tpl return nil }) if err != nil { return nil, err } if len(templates) == 0 { return nil, ErrNoValidTemplates } return templates, nil } // 读取文件内容 func readFile(path string) ([]byte, error) { return os.ReadFile(path) } // GetAvailableThemes 获取所有可用主题(读取 themesDir 目录下的子目录) func (m *ThemeManager) GetAvailableThemes() ([]string, error) { entries, err := os.ReadDir(m.themesDir) if err != nil { return nil, fmt.Errorf("读取主题目录失败: %w", err) } var themes []string for _, entry := range entries { if entry.IsDir() { themes = append(themes, entry.Name()) } } return themes, nil } // 加载主题下所有模板(包括子模板) func (tm *ThemeManager) LoadTemplates(themeName string) error { themePath := filepath.Join("web", "themes", themeName, "templates") // 使用通配符加载所有tmpl文件 tmpl, err := template.ParseGlob(filepath.Join(themePath, "*.tmpl")) if err != nil { return fmt.Errorf("加载主题模板失败: %w", err) } tm.templates[themeName] = tmpl return nil }