Files
cv-site/internal/templates/template.go
T
2025-11-12 18:26:18 +00:00

131 lines
3.3 KiB
Go

package templates
import (
"fmt"
"html/template"
"log"
"path/filepath"
"sync"
"github.com/juanatsap/cv-site/internal/config"
)
// Manager handles template parsing and rendering
type Manager struct {
templates *template.Template
config *config.TemplateConfig
mu sync.RWMutex
}
// NewManager creates a new template manager
func NewManager(cfg *config.TemplateConfig) (*Manager, error) {
m := &Manager{
config: cfg,
}
if err := m.loadTemplates(); err != nil {
return nil, fmt.Errorf("failed to load templates: %w", err)
}
return m, nil
}
// loadTemplates parses all templates from the configured directory
func (m *Manager) loadTemplates() error {
m.mu.Lock()
defer m.mu.Unlock()
// Create template with custom functions
funcMap := template.FuncMap{
"iterate": func(count int) []int {
var result []int
for i := 0; i < count; i++ {
result = append(result, i)
}
return result
},
"eq": func(a, b string) bool {
return a == b
},
// safeHTML marks string as safe HTML to prevent escaping
// SECURITY NOTE: Only use with trusted content from CV YAML files
// Never use with user-generated content to prevent XSS attacks
"safeHTML": func(s string) template.HTML {
return template.HTML(s)
},
// dict creates a map from key-value pairs for passing to sub-templates
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, fmt.Errorf("dict requires even number of arguments")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, fmt.Errorf("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
},
}
// Parse main templates
pattern := filepath.Join(m.config.Dir, "*.html")
tmpl, err := template.New("").Funcs(funcMap).ParseGlob(pattern)
if err != nil {
return fmt.Errorf("error parsing templates from %s: %w", pattern, err)
}
// Parse partials recursively from all subdirectories
partialsPattern := filepath.Join(m.config.PartialsDir, "**", "*.html")
partialsMatches, _ := filepath.Glob(partialsPattern)
// Also match direct children
partialsDirectPattern := filepath.Join(m.config.PartialsDir, "*.html")
directMatches, _ := filepath.Glob(partialsDirectPattern)
// Combine all matches
allPartials := append(partialsMatches, directMatches...)
if len(allPartials) > 0 {
tmpl, err = tmpl.ParseFiles(allPartials...)
if err != nil {
log.Printf("Warning: error parsing partials: %v", err)
} else {
log.Printf("✓ Loaded %d partial templates", len(allPartials))
}
}
m.templates = tmpl
log.Printf("✓ Templates loaded successfully from %s", m.config.Dir)
return nil
}
// Reload reloads all templates (useful for hot-reload in development)
func (m *Manager) Reload() error {
return m.loadTemplates()
}
// Render executes a template with the given data
func (m *Manager) Render(name string) (*template.Template, error) {
// Hot reload in development mode
if m.config.HotReload {
if err := m.Reload(); err != nil {
log.Printf("Warning: template reload failed: %v", err)
// Continue with cached templates
}
}
m.mu.RLock()
defer m.mu.RUnlock()
tmpl := m.templates.Lookup(name)
if tmpl == nil {
return nil, fmt.Errorf("template %q not found", name)
}
return tmpl, nil
}