package web import ( "embed" "html/template" "io" "io/fs" "path/filepath" "strings" ) //go:embed templates/* var templateFS embed.FS // Templates holds the parsed templates type Templates struct { templates map[string]*template.Template } // Template helper functions var templateFuncs = template.FuncMap{ "divFloat": func(a int64, b int) float64 { return float64(a) / float64(b) }, "add": func(a, b int) int { return a + b }, "sub": func(a, b int) int { return a - b }, "mul": func(a, b int) int { return a * b }, "min": func(a, b int) int { if a < b { return a } return b }, } // NewTemplates creates and parses all templates func NewTemplates() (*Templates, error) { t := &Templates{ templates: make(map[string]*template.Template), } // Load all layout and partial templates first layoutFiles, err := fs.Glob(templateFS, "templates/layouts/*.html") if err != nil { return nil, err } partialFiles, err := fs.Glob(templateFS, "templates/partials/*.html") if err != nil { return nil, err } // Load page templates pageFiles, err := fs.Glob(templateFS, "templates/pages/*.html") if err != nil { return nil, err } // Combine layouts and partials baseFiles := append(layoutFiles, partialFiles...) // Parse each page template with layouts and partials for _, pageFile := range pageFiles { files := append([]string{pageFile}, baseFiles...) // Read and parse all files tmpl := template.New(filepath.Base(pageFile)).Funcs(templateFuncs) for _, file := range files { content, err := templateFS.ReadFile(file) if err != nil { return nil, err } _, err = tmpl.Parse(string(content)) if err != nil { return nil, err } } // Extract page name (e.g., "login" from "templates/pages/login.html") name := strings.TrimSuffix(filepath.Base(pageFile), ".html") t.templates[name] = tmpl } // Also parse partials standalone for htmx partial responses for _, partialFile := range partialFiles { content, err := templateFS.ReadFile(partialFile) if err != nil { return nil, err } tmpl, err := template.New(filepath.Base(partialFile)).Funcs(templateFuncs).Parse(string(content)) if err != nil { return nil, err } name := "partial:" + strings.TrimSuffix(filepath.Base(partialFile), ".html") t.templates[name] = tmpl } return t, nil } // RenderPage renders a full page template func (t *Templates) RenderPage(w io.Writer, name string, data interface{}) error { tmpl, ok := t.templates[name] if !ok { return &ErrTemplateNotFound{Name: name} } return tmpl.ExecuteTemplate(w, "base", data) } // RenderPartial renders a partial template (for htmx responses) func (t *Templates) RenderPartial(w io.Writer, name string, data interface{}) error { tmpl, ok := t.templates["partial:"+name] if !ok { return &ErrTemplateNotFound{Name: name} } return tmpl.ExecuteTemplate(w, name, data) } // ErrTemplateNotFound is returned when a template is not found type ErrTemplateNotFound struct { Name string } func (e *ErrTemplateNotFound) Error() string { return "template not found: " + e.Name }