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) // 从主题目录加载模板 tplDir := filepath.Join(themePath, "templates") // 首先读取所有模板文件内容 type tplFile struct { name string path string content []byte } var tplFiles []tplFile err := filepath.WalkDir(tplDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } 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)) tplFiles = append(tplFiles, tplFile{name: name, path: path, content: content}) return nil }) if err != nil { return nil, err } if len(tplFiles) == 0 { return nil, ErrNoValidTemplates } // 创建包含所有模板的根模板 funcMap := template.FuncMap{ "safeHTML": func(s string) template.HTML { return template.HTML(s) }, } // 首先解析基础模板(base.tmpl 和 header.tmpl) var baseContent strings.Builder for _, tf := range tplFiles { if tf.name == "base" || tf.name == "header" { baseContent.WriteString(string(tf.content)) baseContent.WriteString("\n") } } rootTpl, err := template.New("root").Funcs(funcMap).Parse(baseContent.String()) if err != nil { return nil, fmt.Errorf("failed to parse base templates: %w", err) } // 为每个页面模板单独解析,继承基础模板 for _, tf := range tplFiles { // 跳过基础模板,它们已经在 rootTpl 中 if tf.name == "base" || tf.name == "header" { templates[tf.name] = rootTpl continue } // 克隆基础模板并添加页面特定内容 tpl, err := rootTpl.Clone() if err != nil { return nil, fmt.Errorf("failed to clone base template for %s: %w", tf.name, err) } // 解析页面特定内容,使用页面名称作为 define 名称 pageContent := string(tf.content) _, err = tpl.Parse(pageContent) if err != nil { return nil, fmt.Errorf("failed to parse template %s: %w", tf.name, err) } templates[tf.name] = tpl } 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 }