package templates import ( "bytes" "fmt" "html/template" "io/fs" "net/http" "os" "path/filepath" "strings" "github.com/gorilla/csrf" "github.com/mattnite/forgejo-tickets/internal/auth" "github.com/mattnite/forgejo-tickets/internal/middleware" "github.com/mattnite/forgejo-tickets/internal/models" ) type Renderer struct { templates map[string]*template.Template } type PageData struct { User *models.User CSRFToken string Flash *Flash Data interface{} } type Flash struct { Type string // "success", "error", "info" Message string } func NewRenderer() (*Renderer, error) { r := &Renderer{ templates: make(map[string]*template.Template), } if err := r.parseTemplates(); err != nil { return nil, err } return r, nil } func (r *Renderer) parseTemplates() error { templateDir := "web/templates" partials, err := filepath.Glob(filepath.Join(templateDir, "partials", "*.html")) if err != nil { return fmt.Errorf("glob partials: %w", err) } err = filepath.WalkDir(filepath.Join(templateDir, "pages"), func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() || !strings.HasSuffix(path, ".html") { return nil } layout := filepath.Join(templateDir, "layouts", "base.html") if strings.Contains(path, "admin") { layout = filepath.Join(templateDir, "layouts", "admin.html") } if _, err := os.Stat(layout); os.IsNotExist(err) { return nil } files := []string{layout} files = append(files, partials...) files = append(files, path) name := strings.TrimPrefix(path, filepath.Join(templateDir, "pages")+"/") name = strings.TrimSuffix(name, ".html") tmpl, err := template.New(filepath.Base(layout)).Funcs(templateFuncs()).ParseFiles(files...) if err != nil { return fmt.Errorf("parse template %s: %w", name, err) } r.templates[name] = tmpl return nil }) if err != nil { return err } return nil } func (r *Renderer) Render(w http.ResponseWriter, req *http.Request, name string, data interface{}) { tmpl, ok := r.templates[name] if !ok { http.Error(w, fmt.Sprintf("template %q not found", name), http.StatusInternalServerError) return } pd := PageData{ User: auth.CurrentUserFromRequest(req), CSRFToken: csrf.Token(req), Data: data, } if flashType, flashMsg := middleware.GetFlash(req, w); flashMsg != "" { pd.Flash = &Flash{Type: flashType, Message: flashMsg} } var buf bytes.Buffer if err := tmpl.Execute(&buf, pd); err != nil { http.Error(w, fmt.Sprintf("render template: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") buf.WriteTo(w) } func (r *Renderer) RenderError(w http.ResponseWriter, req *http.Request, status int, message string) { w.WriteHeader(status) if tmpl, ok := r.templates["error"]; ok { pd := PageData{ User: auth.CurrentUserFromRequest(req), CSRFToken: csrf.Token(req), Data: map[string]interface{}{"Status": status, "Message": message}, } var buf bytes.Buffer if err := tmpl.Execute(&buf, pd); err == nil { w.Header().Set("Content-Type", "text/html; charset=utf-8") buf.WriteTo(w) return } } http.Error(w, message, status) }