|
|
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 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 {
|
|
|
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)
|
|
|
|
|
|
}
|