// Package templates provides HTML template rendering with custom functions. package templates import ( "errors" "fmt" "html/template" "io" "io/fs" "net/http" "net/url" "path" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/render" "go.uber.org/fx" ) // Renderer parses and renders embedded `*.gohtml` templates. type Renderer struct { templates map[string]*template.Template } var Module = fx.Options( fx.Provide(ProvideRenderer), ) func ProvideRenderer() (*Renderer, error) { funcs := template.FuncMap{ "dict": dict, "list": func(items ...any) []any { return items }, "json": jsonAttr, "browseURL": browseURL, "genresParams": genresParams, "hasGenre": hasGenre, "add": func(a, b int) int { return a + b }, "sub": func(a, b int) int { return a - b }, "mul": func(a, b float64) float64 { return a * b }, "imul": func(a, b int) int { return a * b }, "div": div, "ceilDiv": ceilDiv, "idiv": idiv, "atoi": atoi, "toFloat": func(a int) float64 { return float64(a) }, "seq": seq, "min": func(a, b int) int { return min(a, b) }, "int": toInt, "percent": percent, "formatDate": formatDate, "nextSort": nextSort, "urlquery": url.QueryEscape, "posterURL": posterURL, "episodeRangeStart": episodeRangeStart, } pages, err := fs.Glob(templateFS, "*.gohtml") if err != nil { return nil, fmt.Errorf("glob templates: %w", err) } subpages, err := fs.Glob(templateFS, "anime/*.gohtml") if err != nil { return nil, fmt.Errorf("glob anime templates: %w", err) } components, err := fs.Glob(templateFS, "components/*.gohtml") if err != nil { return nil, fmt.Errorf("glob components: %w", err) } basePath := "base.gohtml" allPages := append(pages, subpages...) r := &Renderer{templates: make(map[string]*template.Template, len(allPages))} for _, page := range allPages { name := path.Base(page) if name == path.Base(basePath) { continue } parseList := make([]string, 0, 2+len(components)) parseList = append(parseList, basePath) parseList = append(parseList, components...) parseList = append(parseList, page) tmpl := template.New(path.Base(basePath)).Funcs(funcs) parsed, err := tmpl.ParseFS(templateFS, parseList...) if err != nil { return nil, fmt.Errorf("parse template %s: %w", name, err) } r.templates[name] = parsed } if len(r.templates) == 0 { return nil, errors.New("no templates parsed") } return r, nil } func (r *Renderer) Instance(name string, data any) render.Render { return HTMLRender{ Renderer: r, Name: name, Data: data, } } type HTMLRender struct { Renderer *Renderer Name string Data any } type templateFragmentData interface { TemplateFragment() string } func (h HTMLRender) Render(w http.ResponseWriter) error { tmpl, ok := h.Renderer.templates[h.Name] if !ok { return fmt.Errorf("template %s not found", h.Name) } var block any if dataMap, ok := h.Data.(map[string]any); ok { block = dataMap["_fragment"] } else if ginH, ok := h.Data.(gin.H); ok { block = ginH["_fragment"] } else if fragmentData, ok := h.Data.(templateFragmentData); ok { block = fragmentData.TemplateFragment() } if blockStr, ok := block.(string); ok && blockStr != "" { return tmpl.ExecuteTemplate(w, blockStr, h.Data) } return tmpl.ExecuteTemplate(w, "base.gohtml", h.Data) } func (h HTMLRender) WriteContentType(w http.ResponseWriter) { w.Header().Set("Content-Type", "text/html; charset=utf-8") } func (r *Renderer) ExecuteFragment(w io.Writer, name string, block string, data any) error { tmpl, ok := r.templates[name] if !ok { return fmt.Errorf("template %s not found", name) } return tmpl.ExecuteTemplate(w, block, data) }