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 }