146 lines
3.6 KiB
Go
146 lines
3.6 KiB
Go
// 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 ...string) []string { return items },
|
|
"json": jsonAttr,
|
|
"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,
|
|
"urlquery": url.QueryEscape,
|
|
}
|
|
|
|
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)
|
|
}
|