Files
go_blog/utils/thememanager.go
2026-02-12 15:45:11 +08:00

264 lines
6.6 KiB
Go
Raw Permalink 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 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
}