render/render.go

172 lines
3.7 KiB
Go

package render
import (
"html/template"
"io"
"io/fs"
"log/slog"
"net/http"
"path/filepath"
"slices"
"strings"
)
type Manager interface {
GetPage(page string, username string) Renderer
GetBlock(page string, block string) Renderer
GetComponent(path string, name string) Renderer
FromRequest(r *http.Request, page string, username string) Renderer
}
type RendererManager struct {
pages map[string]Renderer
components map[string]Renderer
}
func ParseTemplates(f fs.FS, fm template.FuncMap) (*RendererManager, error) {
rm := RendererManager{
pages: make(map[string]Renderer),
components: make(map[string]Renderer),
}
var components []string
err := fs.WalkDir(f, ".", func(path string, d fs.DirEntry, err error) error {
if strings.HasPrefix(path, "pages") {
return fs.SkipDir
}
if strings.HasSuffix(path, ".gohtml") {
components = append(components, path)
if strings.HasPrefix(path, "components") {
p := strings.TrimSuffix(strings.TrimPrefix(path, "components/"), ".gohtml")
tmpl, err := template.New("").Funcs(fm).ParseFS(f, path)
if err != nil {
return err
}
slog.Debug("registering new component", "name", p, "path", path)
rm.components[p] = Renderer{
tmpl: tmpl,
}
}
}
return nil
})
if err != nil {
return nil, err
}
err = fs.WalkDir(f, "pages", func(path string, d fs.DirEntry, err error) error {
if strings.HasSuffix(path, ".gohtml") {
var cs []string
parent := strings.Split(filepath.Dir(path), "/")
for _, c := range components {
if !strings.HasPrefix(c, "components") {
cs = append(cs, c)
continue
}
if strings.Count(c, "/") == 1 {
cs = append(cs, c)
continue
}
parts := strings.Split(filepath.Dir(c), "/")
if slices.Equal(parent[1:], parts[1:]) {
cs = append(cs, c)
}
}
tmpl, err := template.New("").Funcs(fm).ParseFS(f, append(cs, path)...)
if err != nil {
return err
}
name := strings.TrimPrefix(strings.TrimSuffix(path, ".gohtml"), "pages/")
slog.Debug("registering new page", "name", name, "path", path)
rm.pages[name] = Renderer{
tmpl: tmpl,
}
}
return err
})
if err != nil {
return nil, err
}
return &rm, nil
}
func (rm RendererManager) GetPage(page, username string) Renderer {
p, ok := rm.pages[page]
if !ok {
slog.Error("template not found", "page", page)
}
p.username = username
return p
}
func (rm RendererManager) GetBlock(page, block string) Renderer {
p, ok := rm.pages[page]
if !ok {
slog.Error("template not found", "page", page)
}
p.target = block
return p
}
func (rm RendererManager) GetComponent(path, name string) Renderer {
c, ok := rm.components[path]
if !ok {
slog.Error("component not found", "path", path)
}
c.target = name
return c
}
func (rm RendererManager) FromRequest(r *http.Request, page string, username string) Renderer {
p, ok := rm.pages[page]
if !ok {
slog.Error("template not found", "page", page)
}
p.username = username
if r.Header.Get("HX-Boosted") == "true" {
p.target = "content"
} else if target := r.Header.Get("HX-Target"); target != "" {
p.target = target
}
return p
}
type Renderer struct {
tmpl *template.Template
target string
username string
}
func (r Renderer) Render(w io.Writer, data any) error {
var err error
if r.target != "" {
// TODO: allow client to use it's own template for auth
// I use it like this since this is what I do for all my projects for now. (and it's not the best)
err = r.tmpl.ExecuteTemplate(w, r.target, data)
} else {
err = r.tmpl.ExecuteTemplate(w, "page", layoutData{Data: data, Username: r.username})
}
if err != nil {
slog.Error("failed to render template", "error", err)
}
return err
}
type layoutData struct {
Username string
Data any
}