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

@ -10,7 +10,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/julienschmidt/httprouter"
"github.com/rs/zerolog"
@ -32,7 +32,7 @@ type Admin struct {
Policy *policy.Policy
Templating *templates.Templating
Templating *templating.Templating
Purger *purger.Purger
}

View file

@ -10,75 +10,71 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
"github.com/julienschmidt/httprouter"
)
//go:embed "html/components.html"
var componentsHTML []byte
var componentsTemplate = templates.Parse[componentContext]("components.html", componentsHTML, assets.AssetsAdmin)
//go:embed "html/anal.html"
var analHTML []byte
var analTemplate = templating.Parse[analContext](
"anal.html", analHTML, nil,
type componentContext struct {
templates.BaseContext
templating.Assets(assets.AssetsAdmin),
)
type analContext struct {
templating.RuntimeFlags
Analytics lazy.PoolAnalytics
}
func (admin *Admin) components(ctx context.Context) http.Handler {
tpl := componentsTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
Crumbs: []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Components", Path: "/admin/components/"},
},
})
tpl := analTemplate.Prepare(
admin.Dependencies.Templating,
templating.Crumbs(
component.MenuItem{Title: "Admin", Path: "/admin/"},
component.MenuItem{Title: "Components", Path: "/admin/components/"},
),
templating.Title("Components"),
)
return tpl.HTMLHandler(func(r *http.Request) (cp componentContext, err error) {
cp.Analytics = *admin.Analytics
return tpl.HTMLHandler(func(r *http.Request) (ac analContext, err error) {
ac.Analytics = *admin.Analytics
return
})
}
//go:embed "html/ingredients.html"
var ingredientsHTML []byte
var ingredientsTemplate = templates.Parse[ingredientsContext]("ingredients.html", ingredientsHTML, assets.AssetsAdmin)
type ingredientsContext struct {
templates.BaseContext
Instance models.Instance
Analytics *lazy.PoolAnalytics
}
func (admin *Admin) ingredients(ctx context.Context) http.Handler {
tpl := ingredientsTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
Crumbs: []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Instance", Path: "* to be updated *"},
{Title: "Ingredients", Path: "* to be updated *"},
},
})
tpl := analTemplate.Prepare(
admin.Dependencies.Templating,
templating.Crumbs(
component.MenuItem{Title: "Admin", Path: "/admin/"},
component.DummyMenuItem,
component.DummyMenuItem,
),
)
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (ic ingredientsContext, err error) {
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ac analContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
gaps.Crumbs[1] = component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}
gaps.Crumbs[2] = component.MenuItem{Title: "Ingredients", Path: template.URL("/admin/instance/" + slug + "/ingredients/")}
// find the instance itself!
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug)
if err == instances.ErrWissKINotFound {
return ic, httpx.ErrNotFound
return ac, nil, httpx.ErrNotFound
}
if err != nil {
return ic, err
return ac, nil, err
}
funcs = []templating.FlagFunc{
templating.ReplaceCrumb(1, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}),
templating.ReplaceCrumb(2, component.MenuItem{Title: "Ingredients", Path: template.URL("/admin/instance/" + slug + "/ingredients/")}),
templating.Title(instance.Name() + " - Ingredients"),
}
ic.Instance = instance.Instance
// and get the components
ic.Analytics = instance.Info().Analytics
ac.Analytics = *instance.Info().Analytics
return
})

View file

@ -10,7 +10,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
@ -22,10 +22,14 @@ import (
//go:embed "html/grants.html"
var grantsHTML []byte
var grantsTemplate = templates.Parse[grantsContext]("grants.html", grantsHTML, assets.AssetsAdmin)
var grantsTemplate = templating.Parse[grantsContext](
"grants.html", grantsHTML, nil,
templating.Assets(assets.AssetsAdmin),
)
type grantsContext struct {
templates.BaseContext
templating.RuntimeFlags
Error string
@ -38,40 +42,43 @@ type grantsContext struct {
}
func (admin *Admin) grants(ctx context.Context) http.Handler {
tpl := grantsTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
Crumbs: []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Instance", Path: "*to be updated*"},
{Title: "Grants", Path: "*to be updated*"},
},
})
tpl := grantsTemplate.Prepare(
admin.Dependencies.Templating,
templating.Crumbs(
component.MenuItem{Title: "Admin", Path: "/admin/"},
component.DummyMenuItem,
component.DummyMenuItem,
),
)
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (grantsContext, error) {
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (grantsContext, []templating.FlagFunc, error) {
if r.Method == http.MethodGet {
return admin.getGrants(r, gaps)
return admin.getGrants(r)
} else {
return admin.postGrants(r, gaps)
return admin.postGrants(r)
}
})
}
func (admin *Admin) getGrants(r *http.Request, gaps *templates.BaseContextGaps) (gc grantsContext, err error) {
func (admin *Admin) getGrants(r *http.Request) (gc grantsContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
if err := gc.use(r, gaps, slug, admin); err != nil {
return gc, err
funcs, err = gc.use(r, slug, admin)
if err != nil {
return gc, nil, err
}
if err := gc.useGrants(r, admin); err != nil {
return gc, err
return gc, nil, err
}
return gc, nil
return gc, funcs, nil
}
func (admin *Admin) postGrants(r *http.Request, gaps *templates.BaseContextGaps) (gc grantsContext, err error) {
func (admin *Admin) postGrants(r *http.Request) (gc grantsContext, funcs []templating.FlagFunc, err error) {
// parse the form
if err := r.ParseForm(); err != nil {
return gc, err
return gc, nil, err
}
// read out the form values
@ -84,15 +91,16 @@ func (admin *Admin) postGrants(r *http.Request, gaps *templates.BaseContextGaps)
)
// set the common fields
if err := gc.use(r, gaps, slug, admin); err != nil {
return gc, err
funcs, err = gc.use(r, slug, admin)
if err != nil {
return gc, nil, err
}
if delete {
// delete the user grant
err := admin.Dependencies.Policy.Remove(r.Context(), distilleryUser, slug)
if err != nil {
return gc, err
return gc, nil, err
}
} else {
// update the grant
@ -110,26 +118,29 @@ func (admin *Admin) postGrants(r *http.Request, gaps *templates.BaseContextGaps)
// fetch the grants for the instance
if err := gc.useGrants(r, admin); err != nil {
return gc, err
return gc, nil, err
}
return gc, nil
return gc, funcs, nil
}
func (gc *grantsContext) use(r *http.Request, gaps *templates.BaseContextGaps, slug string, admin *Admin) (err error) {
gaps.Crumbs[1] = component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}
gaps.Crumbs[2] = component.MenuItem{Title: "Grants", Path: template.URL("/admin/instance/" + slug + "/grants/")}
func (gc *grantsContext) use(r *http.Request, slug string, admin *Admin) (funcs []templating.FlagFunc, err error) {
// find the instance itself
gc.instance, err = admin.Dependencies.Instances.WissKI(r.Context(), slug)
if err == instances.ErrWissKINotFound {
return httpx.ErrNotFound
return nil, httpx.ErrNotFound
}
if err != nil {
return err
return nil, err
}
gc.Instance = gc.instance.Instance
return nil
// replace the functions
funcs = []templating.FlagFunc{
templating.ReplaceCrumb(1, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}),
templating.ReplaceCrumb(2, component.MenuItem{Title: "Grants", Path: template.URL("/admin/instance/" + slug + "/grants/")}),
templating.Title(gc.Instance.Slug + " - Grants"),
}
return funcs, nil
}
func (gc *grantsContext) useGrants(r *http.Request, admin *Admin) (err error) {

View file

@ -1,6 +0,0 @@
{{ template "_base.html" . }}
{{ define "title" }}Components{{ end }}
{{ define "content" }}
{{ template "_anal.html" .Analytics }}
{{ end }}

View file

@ -1,7 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}{{ .Instance.Slug }} - Grants{{ end }}
{{ define "content" }}
{{ $csrf := .CSRF }}
{{ $slug := .Instance.Slug }}
<div class="pure-u-1-1">
@ -123,5 +119,4 @@
{{ range $unused, $drupal := .Drupals }}
<option value="{{ $drupal }}">
{{ end }}
</datalist>
{{ end }}
</datalist>

View file

@ -1,7 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}Admin{{ end }}
{{ define "content" }}
<div class="pure-u-1-1">
<h2 id="overview">Distillery Configuration</h2>
</div>
@ -256,5 +252,4 @@
</p>
</div>
</div>
{{end}}
{{ end }}
{{end}}

View file

@ -1,6 +0,0 @@
{{ template "_base.html" . }}
{{ define "title" }}{{ .Instance.Slug }} - Ingredients{{ end }}
{{ define "content" }}
{{ template "_anal.html" .Analytics }}
{{ end }}

View file

@ -1,7 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}{{ .Instance.Slug }}{{ end }}
{{ define "content" }}
<div class="pure-u-1-1">
<h2 id="overview">Info &amp; Status</h2>
</div>
@ -484,5 +480,4 @@
<button class="remote-action pure-button pure-button-danger" data-action="purge" data-param="{{ .Instance.Slug }}" data-confirm-param="#purge-confirm-slug" data-buffer="1000" data-force-reload="/admin/">Purge Instance</button>
</fieldset>
</form>
</div>
{{ end }}
</div>

View file

@ -1,4 +1,2 @@
{{ template "_form.html" . }}
{{ define "form/title" }}Create User{{ end }}
{{ template "form.html" . }}
{{ define "form/button" }}Create{{ end }}

View file

@ -1,8 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}Users{{ end }}
{{ define "content" }}
<div class="pure-u-1">
<div class="padding">
<div class="overflow">
@ -105,5 +100,3 @@
</div>
</div>
</div>
{{ end }}

View file

@ -9,7 +9,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"golang.org/x/sync/errgroup"
)
@ -80,27 +80,33 @@ func (admin *Admin) Fetch(flags component.FetcherFlags, target *status.Distiller
//go:embed "html/index.html"
var indexHTML []byte
var indexTemplate = templates.Parse[indexContext]("index.html", indexHTML, assets.AssetsAdmin)
var indexTemplate = templating.Parse[indexContext](
"index.html", indexHTML, nil,
templating.Title("Admin"),
templating.Assets(assets.AssetsAdmin),
)
type indexContext struct {
templates.BaseContext
templating.RuntimeFlags
status.Distillery
Instances []status.WissKI
}
func (admin *Admin) index(ctx context.Context) http.Handler {
tpl := indexTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
Crumbs: []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
},
Actions: []component.MenuItem{
{Title: "Users", Path: "/admin/users/"},
{Title: "Components", Path: "/admin/components/", Priority: component.SmallButton},
},
})
tpl := indexTemplate.Prepare(
admin.Dependencies.Templating,
templating.Crumbs(
component.MenuItem{Title: "Admin", Path: "/admin/"},
),
templating.Actions(
component.MenuItem{Title: "Users", Path: "/admin/users/"},
component.MenuItem{Title: "Components", Path: "/admin/components/", Priority: component.SmallButton},
),
)
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (idx indexContext, err error) {
return tpl.HTMLHandler(func(r *http.Request) (idx indexContext, err error) {
idx.Distillery, idx.Instances, err = admin.Status(r.Context(), true)
return
})

View file

@ -9,7 +9,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
@ -18,49 +18,57 @@ import (
//go:embed "html/instance.html"
var instanceHTML []byte
var instanceTemplate = templates.Parse[instanceContext]("instance.html", instanceHTML, assets.AssetsAdmin)
var instanceTemplate = templating.Parse[instanceContext](
"instance.html", instanceHTML, nil,
templating.Assets(assets.AssetsAdmin),
)
type instanceContext struct {
templates.BaseContext
templating.RuntimeFlags
Instance models.Instance
Info status.WissKI
}
func (admin *Admin) instance(ctx context.Context) http.Handler {
tpl := instanceTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
Crumbs: []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Instance", Path: "*to be replaced*"},
},
Actions: []component.MenuItem{
{Title: "Grants", Path: "*to be replaced*"},
{Title: "Ingredients", Path: "*to be replaced*", Priority: component.SmallButton},
},
})
tpl := instanceTemplate.Prepare(
admin.Dependencies.Templating,
templating.Crumbs(
component.MenuItem{Title: "Admin", Path: "/admin/"},
component.DummyMenuItem,
),
templating.Actions(
component.DummyMenuItem,
component.DummyMenuItem,
),
)
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (ic instanceContext, err error) {
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ic instanceContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
gaps.Crumbs[1] = component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}
gaps.Actions[0] = component.MenuItem{Title: "Grants", Path: template.URL("/admin/grants/" + slug)}
gaps.Actions[1] = component.MenuItem{Title: "Ingredients", Path: template.URL("/admin/ingredients/" + slug), Priority: component.SmallButton}
// find the instance itself!
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug)
if err == instances.ErrWissKINotFound {
return ic, httpx.ErrNotFound
return ic, nil, httpx.ErrNotFound
}
if err != nil {
return ic, err
return ic, nil, err
}
ic.Instance = instance.Instance
// get some more info about the wisski
ic.Info, err = instance.Info().Information(r.Context(), false)
if err != nil {
return ic, err
return ic, nil, err
}
funcs = []templating.FlagFunc{
templating.ReplaceCrumb(1, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}),
templating.ReplaceAction(0, component.MenuItem{Title: "Grants", Path: template.URL("/admin/grants/" + slug)}),
templating.ReplaceAction(1, component.MenuItem{Title: "Ingredients", Path: template.URL("/admin/ingredients/" + slug), Priority: component.SmallButton}),
templating.Title(instance.Name()),
}
return

View file

@ -11,7 +11,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
"github.com/rs/zerolog"
@ -19,25 +19,30 @@ import (
//go:embed "html/users.html"
var usersHTML []byte
var usersTemplate = templates.Parse[usersContext]("user.html", usersHTML, assets.AssetsAdmin)
var usersTemplate = templating.Parse[usersContext](
"users.html", usersHTML, nil,
templating.Title("Users"),
templating.Assets(assets.AssetsAdmin),
)
type usersContext struct {
templates.BaseContext
templating.RuntimeFlags
Error string
Users []*auth.AuthUser
}
func (admin *Admin) users(ctx context.Context) http.Handler {
tpl := usersTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
Crumbs: []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Users", Path: "/admin/users/"},
},
Actions: []component.MenuItem{
{Title: "Create New", Path: "/admin/users/create/"},
},
})
tpl := usersTemplate.Prepare(
admin.Dependencies.Templating,
templating.Crumbs(
component.MenuItem{Title: "Admin", Path: "/admin/"},
component.MenuItem{Title: "Users", Path: "/admin/users/"},
),
templating.Actions(
component.MenuItem{Title: "Create New", Path: "/admin/users/create/"},
),
)
return tpl.HTMLHandler(func(r *http.Request) (uc usersContext, err error) {
uc.Error = r.URL.Query().Get("error")
@ -48,7 +53,12 @@ func (admin *Admin) users(ctx context.Context) http.Handler {
//go:embed "html/user_create.html"
var userCreateHTML []byte
var userCreateTemplate = templates.ParseForm("user_create.html", userCreateHTML, assets.AssetsAdmin)
var userCreateTemplate = templating.ParseForm(
"user_create.html", userCreateHTML, httpx.FormTemplate,
templating.Title("Create User"),
templating.Assets(assets.AssetsAdmin),
)
var (
errCreateInvalidUsername = errors.New("invalid username")
@ -62,13 +72,14 @@ type createUserResult struct {
}
func (admin *Admin) createUser(ctx context.Context) http.Handler {
tpl := userCreateTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
Crumbs: []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Users", Path: "/admin/users"},
{Title: "Create", Path: "/admin/users/create"},
},
})
tpl := userCreateTemplate.Prepare(
admin.Dependencies.Templating,
templating.Crumbs(
component.MenuItem{Title: "Admin", Path: "/admin/"},
component.MenuItem{Title: "Users", Path: "/admin/users"},
component.MenuItem{Title: "Create", Path: "/admin/users/create"},
),
)
return &httpx.Form[createUserResult]{
Fields: []field.Field{
@ -79,7 +90,7 @@ func (admin *Admin) createUser(ctx context.Context) http.Handler {
FieldTemplate: field.PureCSSFieldTemplate,
RenderTemplate: tpl.Template(),
RenderTemplateContext: templates.FormTemplateContext(tpl),
RenderTemplateContext: templating.FormTemplateContext(tpl),
Validate: func(r *http.Request, values map[string]string) (cu createUserResult, err error) {
cu.User, cu.Passsword, cu.Admin = values["username"], values["password"], values["admin"] == field.CheckboxChecked

View file

@ -17,32 +17,8 @@ import (
//
// Each asset group should be registered as a parameter to the 'go:generate' line.
type Assets struct {
Scripts string // <script> tags inserted by the asset
Styles string // <link> tags inserted by the asset
Scripts template.HTML // <script> tags inserted by the asset
Styles template.HTML // <link> tags inserted by the asset
}
//go:generate node build.mjs Default User Admin
// MustParse parses a new template from the given source
// and calls [RegisterAssoc] on it.
func (assets *Assets) MustParse(t *template.Template, value string) *template.Template {
t = template.Must(t.Parse(value))
assets.RegisterAssoc(t)
return t
}
// MustParseShared is like [MustParse], but creates a new SharedTemplate instead
func (assets *Assets) MustParseShared(name string, value string) *template.Template {
return assets.MustParse(NewSharedTemplate(name), value)
}
// RegisterAssoc registers two new associated templates with t.
//
// The template "scripts" will render all script tags required.
// The template "styles" will render all style tags required.
//
// If either template already exists, it will be overwritten.
func (assets *Assets) RegisterAssoc(t *template.Template) {
t.New("scripts").Parse(assets.Scripts)
t.New("styles").Parse(assets.Styles)
}

View file

@ -1,23 +0,0 @@
package assets
import (
"embed"
"html/template"
)
//go:embed "templates/*.html"
var templates embed.FS
var (
shared *template.Template = template.Must(template.ParseFS(templates, "templates/*.html"))
)
// NewSharedTemplate creates a new template with the given name.
// It will be able to make use of shared templates as well as functions.
func NewSharedTemplate(name string) *template.Template {
new := template.New(name)
for _, template := range shared.Templates() {
new.AddParseTree(template.Tree.Name, template.Tree.Copy())
}
return new
}

View file

@ -1,30 +0,0 @@
{{ template "_base.html" . }}
{{ define "title" }}{{ block "form/title" . }}Form{{ end }}{{ end }}
{{ define "content" }}
<div class="pure-u-1">
{{ block "form/extra" . }}<!-- no extra -->{{ end }}
<form class="pure-form pure-form-aligned" method="POST">
<fieldset>
<legend>{{ template "form/title" . }}</legend>
{{ block "form/message" . }}
{{ $E := .Error }}
{{ if not (eq $E "") }}
<div class="pure-form-group">
<p class="error-message">
{{ $E }}
</p>
</div>
{{ end }}
{{ end }}
{{ block "form/inside" . }}<!-- no inside -->{{ end }}
{{ .Form }}
<input type="submit" value="{{ block "form/button" .}}Submit{{ end }}" class="pure-button">
</fieldset>
</form>
</div>
{{ end }}

View file

@ -0,0 +1,5 @@
<div class="pure-u-1">
<p>
For more information, see <a href="{{ .SelfRedirect }}">{{ .SelfRedirect }}</a>.
</p>
</div>

View file

@ -7,7 +7,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
)
@ -15,7 +15,7 @@ import (
type Home struct {
component.Base
Dependencies struct {
Templating *templates.Templating
Templating *templating.Templating
Instances *instances.Instances
}

View file

@ -3,41 +3,73 @@ package home
import (
"context"
_ "embed"
"html/template"
"net/http"
"strings"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
)
//go:embed "public.html"
var publicHTML []byte
var publicTemplate = templates.Parse[publicContext]("public.html", publicHTML, assets.AssetsDefault)
var publicTemplate = templating.Parse[publicContext](
"public.html", publicHTML, nil,
type publicContext struct {
templates.BaseContext
templating.Title("WissKI Distillery"),
templating.Assets(assets.AssetsDefault),
)
//go:embed "about.html"
var aboutHTML string
var aboutTemplate = template.Must(template.New("about.html").Parse(aboutHTML))
// aboutContext is passed to about.html
type aboutContext struct {
Instances []status.WissKI
SelfRedirect string
}
// publicCOntext is passed to public.html
type publicContext struct {
templating.RuntimeFlags
aboutContext
About template.HTML
}
func (home *Home) publicHandler(ctx context.Context) http.Handler {
tpl := publicTemplate.Prepare(home.Dependencies.Templating, templates.BaseContextGaps{
Crumbs: []component.MenuItem{
{Title: "WissKI Distillery", Path: "/"},
},
})
tpl := publicTemplate.Prepare(
home.Dependencies.Templating,
templating.Crumbs(
component.MenuItem{Title: "WissKI Distillery", Path: "/"},
),
)
about := home.Dependencies.Templating.GetCustomizable(aboutTemplate)
return tpl.HTMLHandler(func(r *http.Request) (pc publicContext, err error) {
// only act on the root path!
if strings.TrimSuffix(r.URL.Path, "/") != "" {
return pc, httpx.ErrNotFound
}
pc.Instances = home.homeInstances.Get(nil)
pc.SelfRedirect = home.Config.SelfRedirect.String()
// prepare about
pc.aboutContext.Instances = home.homeInstances.Get(nil)
pc.aboutContext.SelfRedirect = home.Config.SelfRedirect.String()
// render the about template
var builder strings.Builder
if err := about.Execute(&builder, pc.aboutContext); err != nil {
return pc, nil
}
// and return about!
pc.About = template.HTML(builder.String())
return
})

View file

@ -1,14 +1,4 @@
{{ template "_base.html" . }}
{{ define "title" }}WissKI Distillery{{ end }}
{{ define "content" }}
{{ block "@custom/about" . }}
<div class="pure-u-1">
<p>
For more information, see <a href="{{ .SelfRedirect }}">{{ .SelfRedirect }}</a>.
</p>
</div>
{{ end }}
{{ .About }}
<div class="pure-u-1">
<h2>WissKIs on this Distillery</h2>
@ -33,4 +23,3 @@
</div>
{{ end }}
{{ end }}
{{ end }}

View file

@ -8,7 +8,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
_ "embed"
)
@ -17,7 +17,7 @@ type Legal struct {
component.Base
Dependencies struct {
Static *assets.Static
Templating *templates.Templating
Templating *templating.Templating
}
}
@ -27,10 +27,15 @@ var (
//go:embed "legal.html"
var legalHTML []byte
var legalTemplate = templates.Parse[legalContext]("legal.html", legalHTML, assets.AssetsDefault)
var legalTemplate = templating.Parse[legalContext](
"legal.html", legalHTML, nil,
templating.Title("Legal"),
templating.Assets(assets.AssetsDefault),
)
type legalContext struct {
templates.BaseContext
templating.RuntimeFlags
LegalNotices string
@ -49,11 +54,12 @@ func (legal *Legal) Routes() component.Routes {
}
func (legal *Legal) HandleRoute(ctx context.Context, route string) (http.Handler, error) {
tpl := legalTemplate.Prepare(legal.Dependencies.Templating, templates.BaseContextGaps{
Crumbs: []component.MenuItem{
{Title: "Legal", Path: "/legal/"},
},
})
tpl := legalTemplate.Prepare(
legal.Dependencies.Templating,
templating.Crumbs(
component.MenuItem{Title: "Legal", Path: "/legal/"},
),
)
return tpl.HTMLHandler(func(r *http.Request) (lc legalContext, err error) {
lc.LegalNotices = cli.LegalNotices

View file

@ -1,7 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}Legal{{ end }}
{{ define "content" }}
<div class="pure-u-1">
<h2 id="cookies">Cookie Usage</h2>
@ -68,5 +64,4 @@
</p>
<pre>{{ .AssetsDisclaimer }}</pre>
</div>
{{ end }}
</div>

View file

@ -11,7 +11,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/rs/zerolog"
"github.com/yuin/goldmark"
gmmeta "github.com/yuin/goldmark-meta"
@ -22,7 +22,7 @@ import (
type News struct {
component.Base
Dependencies struct {
Templating *templates.Templating
Templating *templating.Templating
}
}
@ -113,20 +113,26 @@ func Items() ([]Item, error) {
//go:embed "news.html"
var newsHTML []byte
var newsTemplate = templates.Parse[newsContext]("news.html", newsHTML, assets.AssetsDefault)
var newsTemplate = templating.Parse[newsContext](
"news.html", newsHTML, nil,
templating.Title("News"),
templating.Assets(assets.AssetsDefault),
)
type newsContext struct {
templates.BaseContext
templating.RuntimeFlags
Items []Item
}
// HandleRoute returns the handler for the requested path
func (news *News) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
tpl := newsTemplate.Prepare(news.Dependencies.Templating, templates.BaseContextGaps{
Crumbs: []component.MenuItem{
{Title: "News", Path: "/news/"},
},
})
tpl := newsTemplate.Prepare(
news.Dependencies.Templating,
templating.Crumbs(
component.MenuItem{Title: "News", Path: "/news/"},
),
)
items, itemsErr := Items()
if itemsErr != nil {

View file

@ -1,8 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}News{{ end }}
{{ define "content" }}
<div class="pure-u-1">
This page contains news items from the distillery.
</div>
@ -14,5 +9,3 @@
{{ .Content }}
</div>
{{ end }}
{{ end }}

View file

@ -7,7 +7,7 @@ import (
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/pkg/cancel"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/mux"
@ -22,7 +22,7 @@ type Server struct {
Routeables []component.Routeable
Cronables []component.Cronable
Templating *templates.Templating
Templating *templating.Templating
}
}

View file

@ -1,102 +0,0 @@
package templates
import (
"html/template"
"net/http"
"reflect"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/gorilla/csrf"
"github.com/tkw1536/goprogram/lib/reflectx"
"golang.org/x/exp/slices"
)
// baseContextName is the name of the [BaseContext] type
var baseContextName = reflectx.TypeOf[BaseContext]().Name()
// BaseContext represents a context used by templates
//
// Other invocations might cause an error at runtime.
type BaseContext struct {
inited bool // has this context been inited?
requestWasNil bool // was the passed request nil
GeneratedAt time.Time // time this page was generated at
// Menu and breadcrumbs
Menu []component.MenuItem
BaseContextGaps
CSRF template.HTML // CSRF Field
}
// constants that are used in various parts of the template to render stuff
const (
errorPrefix template.HTML = `<div style="z-index:10000;position:fixed;top:0;left:0;width:100vh;height:100vw;background:red;text-align:center;padding:10vh 10vw;font-size:xx-large;font-weight:bold">`
errorSuffix template.HTML = "</div>"
csrfError template.HTML = errorPrefix + "CSRF used but not provided" + errorSuffix
initError template.HTML = errorPrefix + "<code>BaseContext.use()</code> not called" + errorSuffix
requestNilError template.HTML = errorPrefix + "<code>BaseContext.use()</code> called with nil request" + errorSuffix
)
type BaseContextGaps struct {
Crumbs []component.MenuItem
Actions []component.MenuItem
}
func (bcg BaseContextGaps) clone() BaseContextGaps {
return BaseContextGaps{
Crumbs: slices.Clone(bcg.Crumbs),
Actions: slices.Clone(bcg.Actions),
}
}
// update updates an embedded BaseContext field in context.
func (tpl *Templating) update(context any, r *http.Request, bcg BaseContextGaps) *BaseContext {
tc := reflect.ValueOf(context).
Elem().FieldByName(baseContextName).Addr().
Interface().(*BaseContext)
tc.inited = true
tc.requestWasNil = r == nil
tc.GeneratedAt = time.Now().UTC()
// setup the CSRF field
tc.CSRF = csrfError
if r != nil {
tc.CSRF = csrf.TemplateField(r)
}
// build the menu
tc.Menu = tpl.buildMenu(r)
// build the breadcrumbs
tc.BaseContextGaps = bcg.clone()
last := len(tc.Crumbs) - 1
for i := range tc.Crumbs {
tc.Crumbs[i].Active = i == last
}
return tc
}
// DoInitCheck is called by the template to check that the BaseContext was initialized properly
func (bc BaseContext) DoInitCheck() template.HTML {
if !bc.inited {
return initError
}
if bc.requestWasNil {
return requestNilError
}
return ""
}
// BaseFormContext combines BaseContext and FormContext
type BaseFormContext struct {
BaseContext
httpx.FormContext
}

View file

@ -1,139 +0,0 @@
package templates
import (
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
)
// Parsed represents a parsed template that receives an underlying context of type C
type Parsed[C any] struct {
template *template.Template
}
// Parse creates a new Parsed from a template source.
// Parse calls panic() when parsing fails.
func Parse[C any](name string, source []byte, Assets assets.Assets) Parsed[C] {
return Parsed[C]{
template: Assets.MustParseShared(name, string(source)),
}
}
// Prepare prepares this template for use inside a concrete handler.
// gaps must either be of length 0 or length 1 and may pre-fill gaps to be used when executing the template later.
func (p *Parsed[C]) Prepare(tpl *Templating, gaps ...BaseContextGaps) *Template[C] {
wrap := Template[C]{
tpl: tpl,
template: tpl.Template(p.template),
}
if len(gaps) > 1 {
panic("WrapTemplate: must provide either 1 or no gaps")
}
if len(gaps) == 1 {
wrap.gaps = gaps[0]
}
return &wrap
}
// Tempalte represents an executable template.
type Template[C any] struct {
tpl *Templating
template *template.Template
gaps BaseContextGaps
}
// Template returns a template that, if executed together with the context by the Context method, produces the desired result.
func (tw *Template[C]) Template() *template.Template {
return tw.template
}
// Context generates a context for a given request that can be used to execute the provided template.
func (tw *Template[C]) Context(r *http.Request, c C, gaps ...BaseContextGaps) any {
// make the gaps something
if len(gaps) > 1 {
panic("Context: must provide either 1 or no gaps")
}
// update the context with gaps
{
g := tw.gaps
if len(gaps) == 1 {
g = gaps[0]
}
tw.tpl.update(&c, r, g)
}
return c
}
// ParseForm is like Parse[BaseFormContext]
var ParseForm = Parse[BaseFormContext]
// FormTemplateContext returns a new handler for a form with the given base context
func FormTemplateContext(tw *Template[BaseFormContext]) func(ctx httpx.FormContext, r *http.Request) any {
return func(ctx httpx.FormContext, r *http.Request) any {
return tw.Context(r, BaseFormContext{FormContext: ctx})
}
}
// MappedHandler returns a new handler that maps the incoming context via f
func MappedHandler[In, Out any](tw *Template[Out], f func(ctx In, r *http.Request) (Out, BaseContextGaps)) func(ctx In, r *http.Request) any {
// TODO: Should this one be removed?
return func(ctx In, r *http.Request) any {
c, g := f(ctx, r)
return tw.Context(r, c, g)
}
}
// 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.HandlerWithGaps(func(r *http.Request, gaps *BaseContextGaps) (C, error) {
return f(r)
})
}
// 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(),
}
}
// HandlerWithGaps works like handler, but additionally receives a gaps object to update.
func (tw *Template[C]) HandlerWithGaps(f func(r *http.Request, gaps *BaseContextGaps) (C, error)) func(r *http.Request) (any, error) {
// TODO: Drop this variant?
var zero C
return func(r *http.Request) (any, error) {
g := tw.gaps.clone()
c, err := f(r, &g)
if err != nil {
return zero, err
}
// update the context
return tw.Context(r, c, g), nil
}
}
func (tw *Template[C]) HTMLHandlerWithGaps(f func(r *http.Request, gaps *BaseContextGaps) (C, error)) httpx.HTMLHandler[any] {
return httpx.HTMLHandler[any]{
Handler: tw.HandlerWithGaps(f),
Template: tw.Template(),
}
}
// Execute executes this template with the given context
func (tw *Template[C]) Execute(w http.ResponseWriter, r *http.Request, c C, gaps ...BaseContextGaps) error {
return tw.ExecuteWithError(w, r, c, nil, gaps...)
}
// ExecuteWithError executes this template, or the default error handler if err != nil
func (tw *Template[C]) ExecuteWithError(w http.ResponseWriter, r *http.Request, c C, err error, gaps ...BaseContextGaps) error {
// TODO: Drop this variant?
// TODO: This should be removed!
return httpx.WriteHTML(tw.Context(r, c, gaps...), err, tw.template, "", w, r)
}

View file

@ -1,65 +0,0 @@
package templates
import (
_ "embed"
"html/template"
"text/template/parse"
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
)
const (
footerName = "@custom/footer"
aboutName = "@custom/about"
)
//go:embed "footer.html"
var footerTemplateStr string
var defaultFooterTemplate = template.Must(template.New("footer.html").Parse(footerTemplateStr))
// Template creates a copy of template with shared template parts updated accordingly.
// Any template using this should use one of the template contexts in this package.
func (tpl *Templating) Template(t *template.Template) *template.Template {
// TODO: This should not be used!
// create a clone of the template
clone := template.Must(t.Clone())
// add all the fixed parse trees
footerTree := tpl.getTemplateAsset(defaultFooterTemplate)
template.Must(clone.AddParseTree(footerName, footerTree))
// optionally add the about asset
if aboutTree := tpl.readTemplateAsset("about.html"); clone.Lookup(aboutName) != nil && aboutTree != nil {
template.Must(clone.AddParseTree(aboutName, aboutTree))
}
return clone // and return the tree
}
// getTemplateAsset returns an overridable template asset.
//
// If the asset named can successfully be parsed, it is returned.
// If it can not be parsed, the default template is returned.
func (tpl *Templating) getTemplateAsset(dflt *template.Template) *parse.Tree {
tree := tpl.readTemplateAsset(dflt.Name())
if tree == nil {
return dflt.Tree.Copy()
}
return tree
}
// readTemplateAsset is like getTemplateAssets, but takes an explicit name to read.
// when the asset does not exist, or cannot be opened, returns nil.
func (tpl *Templating) readTemplateAsset(name string) *parse.Tree {
template, err := (func() (*template.Template, error) {
data, err := environment.ReadFile(tpl.Environment, tpl.CustomAssetPath(name))
if err != nil {
return nil, err
}
return template.New(name).Parse(string(data))
})()
if err != nil {
return nil
}
return template.Tree
}

View file

@ -1,4 +1,4 @@
package templates
package templating
import (
"path/filepath"

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
}

View file

@ -0,0 +1,101 @@
package templating
import (
"html/template"
"net/http"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/tkw1536/goprogram/lib/reflectx"
"golang.org/x/exp/slices"
)
// Flags represent handle-updatable options for the base template
type Flags struct {
Title string // Title of the menu
assets.Assets // assets are the assets included in the template
Crumbs []component.MenuItem // crumbs are the breadcrumbs leading to a specific action
Actions []component.MenuItem // actions are the actions available to a specific thingy
}
// Apply applies a set of functions to this flags
func (flags Flags) Apply(r *http.Request, funcs ...FlagFunc) Flags {
for _, f := range funcs {
flags = f(flags, r)
}
return flags
}
// RuntimeFlags are passed to the template at runtime.
// Any context may e
type RuntimeFlags struct {
Flags
RequestURI string // request uri of the current page
Menu []component.MenuItem // menu at the top of the page
GeneratedAt time.Time // time the underlying data returned
CSRF template.HTML // csrf data (if any)
}
var runtimeFlagsName = reflectx.TypeOf[RuntimeFlags]().Name()
// Clone clones this flags
func (flags Flags) Clone() Flags {
flags.Crumbs = slices.Clone(flags.Crumbs)
flags.Actions = slices.Clone(flags.Actions)
return flags
}
// FlagFunc updates a flags based on a request.
// FlagFunc may not be nil.
type FlagFunc func(flags Flags, r *http.Request) Flags
// Assets sets the given assets for the given flags
func Assets(Assets assets.Assets) FlagFunc {
return func(flags Flags, r *http.Request) Flags {
flags.Assets = Assets
return flags
}
}
// Crumbs sets the crumbs
func Crumbs(crumbs ...component.MenuItem) FlagFunc {
return func(flags Flags, r *http.Request) Flags {
flags.Crumbs = crumbs
return flags
}
}
// Actions sets the actions
func Actions(actions ...component.MenuItem) FlagFunc {
return func(flags Flags, r *http.Request) Flags {
flags.Actions = actions
return flags
}
}
// ReplaceAction replaces a specific action
func ReplaceAction(index int, action component.MenuItem) FlagFunc {
return func(flags Flags, r *http.Request) Flags {
flags.Actions[index] = action
return flags
}
}
// ReplaceCrumb replaces a specific crum
func ReplaceCrumb(index int, action component.MenuItem) FlagFunc {
return func(flags Flags, r *http.Request) Flags {
flags.Crumbs[index] = action
return flags
}
}
// Title sets the title of this template
func Title(title string) FlagFunc {
return func(flags Flags, r *http.Request) Flags {
flags.Title = title
return flags
}
}

View file

@ -1,4 +1,4 @@
package templates
package templating
import (
"html/template"

View file

@ -0,0 +1,66 @@
package templating
import (
"html/template"
"reflect"
"github.com/tkw1536/goprogram/lib/reflectx"
"golang.org/x/exp/slices"
)
// Parsed represents a parsed template that takes as argument a context of type C.
type Parsed[C any] struct {
// does the context type an embed of the runtime flags type?
hasRuntimeFlagsEmbed bool
tpl *template.Template // parsed template
funcs []FlagFunc // optionally concfigured functions.
}
// Parse parses a template with the given name and source.
// If base is not nil, every template associated with the base template is copied into the given template.
// Functions will be applied on creation time to represent the context for the given template.
func Parse[C any](name string, source []byte, base *template.Template, funcs ...FlagFunc) Parsed[C] {
tp := reflectx.TypeOf[C]()
// determine if we have an embedded field in the struct
var hasEmbed bool
if tp.Kind() == reflect.Struct {
field, ok := tp.FieldByName(runtimeFlagsName)
if ok {
hasEmbed = field.Anonymous
}
}
// create a new template, and optionally inherit from the base template
new := template.New(name)
if base != nil {
for _, tree := range base.Templates() {
root := tree.Tree.Copy()
new.AddParseTree(tree.Name(), root)
}
}
return Parsed[C]{
hasRuntimeFlagsEmbed: hasEmbed,
tpl: template.Must(new.Parse(string(source))),
funcs: funcs,
}
}
// Prepare prepares this template to be used with the given templating.
func (p *Parsed[C]) Prepare(templating *Templating, funcs ...FlagFunc) *Template[C] {
pcopy := *p // make a copy of p!
wrap := Template[C]{
templating: templating,
p: &pcopy,
}
// copy the functions!
pcopy.funcs = slices.Clone(pcopy.funcs)
pcopy.funcs = append(wrap.p.funcs, funcs...)
return &wrap
}

View file

@ -4,15 +4,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<title>{{ block "title" . }}WissKI Distillery{{ end }}</title>
{{ block "styles" . }}styles{{ end }}
<title>{{ .Runtime.Flags.Title }}</title>
{{ .Runtime.Flags.Assets.Styles }}
</head>
<body>
{{ .BaseContext.DoInitCheck }}
<nav class="pure-menu pure-menu-horizontal">
<ul class="pure-menu-list" role="menubar">
{{ range .BaseContext.Menu }}
{{ range .Runtime.Menu }}
<li class="pure-menu-item{{ if .Active }} pure-menu-selected{{ end }}">
<a href="{{ .Path }}" class="pure-menu-link">{{ .Title }}</a>
</li>
@ -20,16 +19,16 @@
</ul>
</nav>
<nav class="breadcrumbs" role="navigation" aria-label="Breadcrumbs">
{{ range .BaseContext.Crumbs }}
<a class="{{ if .Active }}active{{ end }}" href="{{ .Path }}">{{ .Title }}</a>
{{ range .Runtime.Flags.Crumbs }}
<a class="{{ if .Active }}active{{ end }}" href="{{ .Path }}">{{ .Title }}</a>
{{ end }}
</nav>
<header>
<h1 id="top">{{ template "title" . }}</h1>
{{ if .BaseContext.Actions }}
<h1 id="top">{{ .Runtime.Flags.Title }}</h1>
{{ if .Runtime.Flags.Actions }}
<div class="pure-button-group" role="group" aria-label="Actions">
{{ range .BaseContext.Actions }}
{{ range .Runtime.Flags.Actions }}
<a href="{{ .Path }}" class="pure-button{{ if eq .Priority -1 }} pure-button-small{{end}}">{{ .Title }}</a>
{{ end }}
</div>
@ -37,20 +36,16 @@
</header>
<main>
<div class="pure-g">
{{ block "content" . }}content{{ end }}
{{ .Main }}
</div>
</main>
<footer>
{{ block "@custom/footer" .BaseContext }}
<div style="z-index:10000;position:fixed;top:0;left:0;width:100vh;height:100vw;background:red;text-align:center;padding:10vh 10vw;font-size:xx-large;font-weight:bold">
<code>.Templating.Template()</code> not called
</div>
{{ end }}
{{ .Footer }}
</footer>
{{ block "scripts" . }}scripts{{ end }}
{{ .Runtime.Flags.Assets.Scripts }}
</body>
</html>

View file

@ -0,0 +1,29 @@
package templating
import (
_ "embed"
"html/template"
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
)
//go:embed "src/footer.html"
var footerHTML string
var footerTemplate = template.Must(template.New("footer.html").Parse(footerHTML))
// GetCustomizable returns either a clone of dflt, or the overriden template with the same name.
func (tpl *Templating) GetCustomizable(dflt *template.Template) *template.Template {
name := dflt.Name()
custom, err := (func() (*template.Template, error) {
data, err := environment.ReadFile(tpl.Environment, tpl.CustomAssetPath(name))
if err != nil {
return nil, err
}
return template.New(name).Parse(string(data))
})()
if err != nil {
return template.Must(dflt.Clone())
}
return custom
}

View file

@ -1,4 +1,4 @@
package templates
package templating
import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"