Refactor html templates

This commit entirely refactors the use of html templates. Instead of
inheriting from a shared template, we insert the results into a base
template.
This commit is contained in:
Tom Wiesing 2023-01-20 14:42:37 +01:00
parent 6ede99d7c6
commit d235ee4e5c
No known key found for this signature in database
59 changed files with 869 additions and 777 deletions

View file

@ -0,0 +1,193 @@
package templating
import (
"context"
_ "embed"
"fmt"
"html/template"
"net/http"
"reflect"
"strings"
"time"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/gorilla/csrf"
"github.com/rs/zerolog"
)
//go:embed "src/base.html"
var baseHTML string
var baseTemplate = template.Must(template.New("base.html").Parse(baseHTML))
// Tempalte represents an executable template.
type Template[C any] struct {
templating *Templating
p *Parsed[C]
}
// Template returns a template that, if executed together with the context by the Context method, produces the desired result.
func (tpl *Template[C]) Template() *template.Template {
return baseTemplate
}
// Context generates the context to pass to an instance of the template returned by Template.
func (tpl *Template[C]) Context(r *http.Request, c C, funcs ...FlagFunc) (ctx *tContext[C]) {
// create a new context
ctx = new(tContext[C])
// setup the basic properties
ctx.ctx = r.Context()
ctx.Runtime.RequestURI = r.URL.RequestURI()
ctx.Runtime.GeneratedAt = time.Now().UTC()
ctx.Runtime.CSRF = csrf.TemplateField(r)
ctx.Runtime.Menu = tpl.templating.buildMenu(r)
// generate the rest of the options
ctx.Runtime.Flags = ctx.Runtime.Flags.Apply(r, tpl.p.funcs...)
ctx.Runtime.Flags = ctx.Runtime.Flags.Apply(r, funcs...)
// if the context has a runtime flags embed, then set the field properly
if tpl.p.hasRuntimeFlagsEmbed {
reflect.ValueOf(&c).Elem().
FieldByName(runtimeFlagsName).
Set(reflect.ValueOf(ctx.Runtime))
}
// the main template
ctx.cMain = c
ctx.tMain = tpl.p.tpl
// the footer template
ctx.tFooter = tpl.templating.GetCustomizable(footerTemplate)
ctx.cFooter = ctx.Runtime
return
}
// ParseForm is like Parse[BaseFormContext]
var ParseForm = Parse[FormContext]
type FormContext struct {
httpx.FormContext
RuntimeFlags
}
// NewFormContext returns a new FormContext from an underlying context
func NewFormContext(context httpx.FormContext) FormContext {
return FormContext{FormContext: context}
}
// FormTemplateContext returns a new handler for a form with the given base context
func FormTemplateContext(tw *Template[FormContext]) func(ctx httpx.FormContext, r *http.Request) any {
// TODO: Is this needed?
return func(ctx httpx.FormContext, r *http.Request) any {
return tw.Context(r, FormContext{FormContext: ctx})
}
}
// Hander returns a function that returns a context for the given template
func (tw *Template[C]) Handler(f func(r *http.Request) (C, error)) func(r *http.Request) (any, error) {
// TODO: Should this one be removed?
return tw.HandlerWithFlags(func(r *http.Request) (C, []FlagFunc, error) {
c, err := f(r)
return c, nil, err
})
}
// HTMLHandler returns a new HTMLHandler for this request
func (tw *Template[C]) HTMLHandler(f func(r *http.Request) (C, error)) httpx.HTMLHandler[any] {
return httpx.HTMLHandler[any]{
Handler: tw.Handler(f),
Template: tw.Template(),
}
}
// HandlerWithFlags works like handler, but additionally receive funcs to generate flags
func (tw *Template[C]) HandlerWithFlags(f func(r *http.Request) (C, []FlagFunc, error)) func(r *http.Request) (any, error) {
return func(r *http.Request) (any, error) {
c, funcs, err := f(r)
if err != nil {
return nil, err
}
return tw.Context(r, c, funcs...), nil
}
}
func (tw *Template[C]) HTMLHandlerWithFlags(f func(r *http.Request) (C, []FlagFunc, error)) httpx.HTMLHandler[any] {
return httpx.HTMLHandler[any]{
Handler: tw.HandlerWithFlags(f),
Template: tw.Template(),
}
}
// tContext is passed to the underlying template.
//
// Callers may not retain references beyond the invocation of the template.
// Callers must not rely on the internal structure of this tContext.
type tContext[C any] struct {
Runtime RuntimeFlags // underlying flags
ctx context.Context // underlying context for render
// the main template and context
tMain *template.Template
cMain C
// the footer template and context
tFooter *template.Template
cFooter RuntimeFlags
}
// Main renders the main template.
func (ctx *tContext[C]) Main() (template.HTML, error) {
return ctx.renderSafe("main", ctx.tMain, ctx.cMain)
}
// Footer renders the footer template
func (ctx *tContext[C]) Footer() (template.HTML, error) {
return ctx.renderSafe("footer", ctx.tFooter, ctx.cFooter)
}
const renderSafeError = "Error displaying page. See server log for details. "
func (ctx *tContext[C]) renderSafe(name string, t *template.Template, c any) (template.HTML, error) {
// already done
if err := ctx.ctx.Err(); err != nil {
return "", err
}
value, err, panicked := func() (value template.HTML, err error, panicked bool) {
var builder strings.Builder
defer func() {
if panicked {
r := recover()
zerolog.Ctx(ctx.ctx).Error().
Str("uri", ctx.Runtime.RequestURI).
Str("name", name).
Str("panic", fmt.Sprint(r)).
Msg("templating.Main(): template panic()ed")
}
}()
panicked = true
err = t.Execute(&builder, c)
panicked = false
if err != nil {
zerolog.Ctx(ctx.ctx).Err(err).
Str("uri", ctx.Runtime.RequestURI).
Str("name", name).
Msg("template errored")
}
return template.HTML(builder.String()), err, false
}()
if err != nil || panicked {
return renderSafeError, httpx.ErrInternalServerError
}
return value, nil
}