package server import ( "bytes" "database/sql" "fmt" "html/template" "net/http" "path/filepath" "time" "git.runcible.io/learning/ratchet/internal/model" "github.com/alexedwards/scs/v2" ) // Define a templateData type to act as the holding structure for // any dynamic data that we want to pass to our HTML templates. // At the moment it only contains one field, but we'll add more // to it as the build progresses. type templateData struct { CurrentYear int Snippet model.Snippet Snippets []model.Snippet Form any Flash string } // newTemplateData is useful to inject default values. Example CSRF tokens for forms. func newTemplateData(r *http.Request, sm *scs.SessionManager) templateData { return templateData{CurrentYear: time.Now().Year(), Flash: sm.PopString(r.Context(), "flash")} } // TEMPLATE FUNCTIONS // Custom template functions can accept as many parameters as they need to, but they must return one value only. // The only exception to this is if you want to return an error as the second value, in which case that’s OK too. // Create a humanDate function which returns a nicely formatted string // representation of a time.Time object. func humanDate(t time.Time) string { return t.Format("02 Jan 2006 at 15:04") } // TEMPLATE FILTERS var templateFuncMap = template.FuncMap{ // This is a trivial example because you can simply use {{ .Content.String }} to call the sql.NullString.String() function. "nullStringToStr": func(ns sql.NullString) string { if ns.Valid { return ns.String } return "" }, "humanDate": humanDate, } // parseTemplateFiles parses the provided template files and extends the resulting // template with additional custom template functions (filters) defined in the // templateFuncMap. // // This function serves as a wrapper around template.ParseFiles, allowing the // inclusion of reusable template functions to enhance the template's capabilities. // // Parameters: // - files: A variadic list of file paths to the template files to be parsed. // // Returns: // - (*template.Template): The parsed template with custom functions injected. // - (error): An error if the template files cannot be parsed. func parseTemplateFiles(files ...string) (*template.Template, error) { tmpl := template.New("").Funcs(templateFuncMap) tmpl, err := tmpl.ParseFiles(files...) if err != nil { return nil, err } return tmpl, nil } // TODO use an go:embed FS instead of file paths type TemplateCache map[string]*template.Template func InitTemplateCache() (*TemplateCache, error) { cache := TemplateCache{} pages, err := filepath.Glob("./ui/html/pages/*.tmpl") if err != nil { return nil, err } for _, page := range pages { name := filepath.Base(page) tmpl := template.New(name).Funcs(templateFuncMap) // Parse the base template file into a template set. tmpl, err = tmpl.ParseFiles("./ui/html/base.go.tmpl") if err != nil { return nil, err } // Call ParseGlob() *on this template set* to add any partials. tmpl, err = tmpl.ParseGlob("./ui/html/partials/*.tmpl") if err != nil { return nil, err } // Call ParseFiles() *on this template set* to add the page template. tmpl, err = tmpl.ParseFiles(page) if err != nil { return nil, err } cache[name] = tmpl } return &cache, nil } func renderTemplate(w http.ResponseWriter, r *http.Request, tc *TemplateCache, status int, page string, data templateData) { cache := *tc ts, ok := cache[page] if !ok { err := fmt.Errorf("the template %s does not exist", page) serverError(w, r, err) return } // Write the template results to a buffer to capture templating errors before writing // to ResponseWriter buf := new(bytes.Buffer) err := ts.ExecuteTemplate(buf, "base", data) if err != nil { serverError(w, r, err) return } w.Header().Set("Content-Length", "this isn't an integer!") w.WriteHeader(status) buf.WriteTo(w) }