You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

188 lines
4.8 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package server
import (
"bytes"
"database/sql"
"fmt"
"html/template"
"io/fs"
"net/http"
"path/filepath"
"time"
"git.runcible.io/learning/ratchet/internal/model"
"git.runcible.io/learning/ratchet/ui"
"github.com/alexedwards/scs/v2"
"github.com/justinas/nosurf"
)
// 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
IsAuthenticated bool
CSRFToken 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"),
IsAuthenticated: isAuthenticated(r),
// added to every page because the form for logout can appear on every page
CSRFToken: nosurf.Token(r),
}
}
// 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 thats OK too.
// Create a humanDate function which returns a nicely formatted string
// representation of a time.Time object.
func humanDate(t time.Time) string {
if t.IsZero() {
return ""
}
// return t.Format("02 Jan 2006 at 15:04")
// always return in UTC
return t.UTC().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 InitFSTemplateCache() (*TemplateCache, error) {
cache := TemplateCache{}
pages, err := fs.Glob(ui.Files, "html/pages/*.tmpl")
if err != nil {
return nil, err
}
for _, page := range pages {
name := filepath.Base(page)
// Create a slice container the filepath patterns for the templates we
// want to parse
patterns := []string{
"html/base.go.tmpl",
"html/partials/*.tmpl",
page,
}
tmpl, err := template.New(name).Funcs(templateFuncMap).ParseFS(ui.Files, patterns...)
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.WriteHeader(status)
buf.WriteTo(w)
}