Files
go_blog/themes/manager.go
2025-06-09 17:59:19 +08:00

232 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package themes
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 (m *ThemeManager) LoadTheme(themeName string) error {
m.mu.Lock()
defer m.mu.Unlock()
// 1. 验证主题目录结构
themePath := filepath.Join(m.themesDir, themeName)
if !isValidTheme(themePath) {
return fmt.Errorf("%w: %s", ErrInvalidThemeStructure, 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
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)
}
// ListThemes 获取所有可用主题(目录名)
func (m *ThemeManager) ListThemes() ([]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
}
// 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
}