go: refactor template rendering
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
* create pkg 'modules/template' * move template rendering code from 'handlers' to 'modules/template' * update call sites * walk the 'templates' dir to discover nested hierarchies * solidify LiveMode handling (vs embedded assets) * break out funcMap to it's own file * general clean-up
This commit is contained in:
parent
61760fa373
commit
122ea638c9
@ -59,7 +59,7 @@ func (a *App) getTemplates() fs.FS {
|
|||||||
|
|
||||||
a.logger.Info("templates loaded in embed mode")
|
a.logger.Info("templates loaded in embed mode")
|
||||||
|
|
||||||
fsys, err := fs.Sub(a.embeds.templates, "templates")
|
fsys, err := fs.Sub(a.embeds.templates, ".")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.dotya.ml/mirre-mt/pcmt/handlers"
|
"git.dotya.ml/mirre-mt/pcmt/handlers"
|
||||||
|
modtmpl "git.dotya.ml/mirre-mt/pcmt/modules/template"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
)
|
)
|
||||||
@ -14,9 +15,11 @@ func (a *App) SetupRoutes() {
|
|||||||
assets := http.FileServer(a.getAssets())
|
assets := http.FileServer(a.getAssets())
|
||||||
tmpls := a.getTemplates()
|
tmpls := a.getTemplates()
|
||||||
|
|
||||||
|
modtmpl.Init(setting, tmpls)
|
||||||
// run this before declaring any handler funcs.
|
// run this before declaring any handler funcs.
|
||||||
handlers.InitHandlers(setting, tmpls)
|
handlers.InitHandlers(setting)
|
||||||
e.Renderer = handlers.Renderer
|
|
||||||
|
e.Renderer = modtmpl.Renderer
|
||||||
|
|
||||||
// keep /static/* as a compatibility fallback for /assets.
|
// keep /static/* as a compatibility fallback for /assets.
|
||||||
e.GET(
|
e.GET(
|
||||||
|
@ -1,22 +1,19 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
|
||||||
|
|
||||||
"git.dotya.ml/mirre-mt/pcmt/app/settings"
|
"git.dotya.ml/mirre-mt/pcmt/app/settings"
|
||||||
"git.dotya.ml/mirre-mt/pcmt/slogging"
|
"git.dotya.ml/mirre-mt/pcmt/slogging"
|
||||||
"golang.org/x/exp/slog"
|
"golang.org/x/exp/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
setting *settings.Settings
|
setting *settings.Settings
|
||||||
appver string
|
appver string
|
||||||
slogger *slogging.Logger
|
slogger *slogging.Logger
|
||||||
log slogging.Logger
|
log slogging.Logger
|
||||||
tmplPath string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitHandlers(s *settings.Settings, tmpls fs.FS) {
|
func InitHandlers(s *settings.Settings) {
|
||||||
slogger = slogging.GetLogger()
|
slogger = slogging.GetLogger()
|
||||||
log = *slogger // have a local copy.
|
log = *slogger // have a local copy.
|
||||||
log.Logger = log.Logger.With(
|
log.Logger = log.Logger.With(
|
||||||
@ -25,11 +22,5 @@ func InitHandlers(s *settings.Settings, tmpls fs.FS) {
|
|||||||
|
|
||||||
setting = s
|
setting = s
|
||||||
|
|
||||||
tmplPath = setting.TemplatesPath()
|
|
||||||
|
|
||||||
appver = setting.Version()
|
appver = setting.Version()
|
||||||
|
|
||||||
log.Debugf("tmpls: %+v", tmpls)
|
|
||||||
|
|
||||||
initTemplates(tmpls)
|
|
||||||
}
|
}
|
||||||
|
@ -1,113 +1,13 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
modtmpl "git.dotya.ml/mirre-mt/pcmt/modules/template"
|
||||||
"github.com/labstack/echo-contrib/session"
|
"github.com/labstack/echo-contrib/session"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/microcosm-cc/bluemonday"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type tplMap map[string]*template.Template
|
|
||||||
|
|
||||||
var (
|
|
||||||
// tmpl *template.Template.
|
|
||||||
// tmpls = template.New("").
|
|
||||||
templateMap tplMap
|
|
||||||
tmplFS fs.FS
|
|
||||||
bluemondayPolicy = bluemonday.UGCPolicy()
|
|
||||||
)
|
|
||||||
|
|
||||||
func listAllTmpls() []string {
|
|
||||||
files, err := filepath.Glob(tmplPath + "/*.tmpl")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, v := range files {
|
|
||||||
files[i] = strings.TrimPrefix(v, tmplPath+"/")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("returning files")
|
|
||||||
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: mimic https://github.com/drone/funcmap/blob/master/funcmap.go
|
|
||||||
func setFuncMap(t *template.Template) {
|
|
||||||
t.Funcs(getFuncMap())
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFuncMap() template.FuncMap {
|
|
||||||
return template.FuncMap{
|
|
||||||
"ifIE": func() template.HTML { return template.HTML("<!--[if IE]>") },
|
|
||||||
"endifIE": func() template.HTML { return template.HTML("<![endif]>") },
|
|
||||||
"htmlSafe": func(html string) template.HTML {
|
|
||||||
return template.HTML( //nolint:gosec
|
|
||||||
bluemondayPolicy.Sanitize(html),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
"pageIs": func(want, got string) bool {
|
|
||||||
return want == got
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initTemplates(f fs.FS) {
|
|
||||||
if f != nil || f != tmplFS {
|
|
||||||
tmplFS = f
|
|
||||||
}
|
|
||||||
|
|
||||||
setFuncMap(Renderer.tmpls)
|
|
||||||
|
|
||||||
allTmpls := listAllTmpls()
|
|
||||||
|
|
||||||
// ensure this fails at compile time, if at all ("Must").
|
|
||||||
Renderer.tmpls = template.Must(Renderer.tmpls.ParseFS(tmplFS, allTmpls...))
|
|
||||||
makeTplMap(Renderer.tmpls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeTplMap(tpl *template.Template) {
|
|
||||||
allTmpls := listAllTmpls()
|
|
||||||
|
|
||||||
var tp tplMap
|
|
||||||
|
|
||||||
if templateMap == nil {
|
|
||||||
tp = make(tplMap, len(allTmpls))
|
|
||||||
} else {
|
|
||||||
tp = templateMap
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range allTmpls {
|
|
||||||
tp[v] = tpl.Lookup(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
templateMap = tp
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTmpl(name string) *template.Template {
|
|
||||||
liveMode := setting.IsLive()
|
|
||||||
|
|
||||||
if liveMode {
|
|
||||||
log.Debug("re-reading tmpls") // TODO: only re-read the tmpl in question.
|
|
||||||
|
|
||||||
tpl := template.New("")
|
|
||||||
|
|
||||||
setFuncMap(tpl)
|
|
||||||
|
|
||||||
allTmpls := listAllTmpls()
|
|
||||||
|
|
||||||
// ensure this fails at compile time, if at all ("Must").
|
|
||||||
Renderer.tmpls = template.Must(tpl.ParseFS(tmplFS, allTmpls...))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Renderer.tmpls.Lookup(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Admin() echo.HandlerFunc {
|
func Admin() echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
|
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
|
||||||
@ -123,7 +23,7 @@ func Index() echo.HandlerFunc {
|
|||||||
return c.Redirect(http.StatusFound, "/home")
|
return c.Redirect(http.StatusFound, "/home")
|
||||||
}
|
}
|
||||||
|
|
||||||
tpl := getTmpl("index.tmpl")
|
tpl := modtmpl.Get("index.tmpl")
|
||||||
csrf := c.Get("csrf").(string)
|
csrf := c.Get("csrf").(string)
|
||||||
|
|
||||||
err := tpl.Execute(c.Response().Writer,
|
err := tpl.Execute(c.Response().Writer,
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TemplateRenderer struct {
|
|
||||||
tmpls *template.Template
|
|
||||||
}
|
|
||||||
|
|
||||||
var Renderer = &TemplateRenderer{tmpls: template.New("")}
|
|
||||||
|
|
||||||
func (t *TemplateRenderer) Render(w io.Writer, name string, data any, c echo.Context) error {
|
|
||||||
c.Logger().Debugf("rendering template %s", name)
|
|
||||||
|
|
||||||
tpl := getTmpl(name)
|
|
||||||
|
|
||||||
return tpl.Execute(w, data)
|
|
||||||
}
|
|
23
modules/template/funcmap.go
Normal file
23
modules/template/funcmap.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import "html/template"
|
||||||
|
|
||||||
|
func FuncMap() template.FuncMap {
|
||||||
|
return template.FuncMap{
|
||||||
|
"ifIE": func() template.HTML { return template.HTML("<!--[if IE]>") },
|
||||||
|
"endifIE": func() template.HTML { return template.HTML("<![endif]>") },
|
||||||
|
"htmlSafe": func(html string) template.HTML {
|
||||||
|
return template.HTML( //nolint:gosec
|
||||||
|
bluemondayPolicy.Sanitize(html),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"pageIs": func(want, got string) bool {
|
||||||
|
return want == got
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: mimic https://github.com/drone/funcmap/blob/master/funcmap.go
|
||||||
|
func setFuncMap(t *template.Template) { //nolint:unused
|
||||||
|
t.Funcs(FuncMap())
|
||||||
|
}
|
71
modules/template/helper.go
Normal file
71
modules/template/helper.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// globDir lists files inside a dir that match the suffix provided.
|
||||||
|
func globDir(dir, suffix string) ([]string, error) {
|
||||||
|
files := []string{}
|
||||||
|
|
||||||
|
err := filepath.WalkDir(dir, func(path string, de fs.DirEntry, err error) error {
|
||||||
|
if de.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if filepath.Ext(path) == suffix {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return files, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// globFS lists files inside a dir of a fs that match the suffix provided.
|
||||||
|
func globFS(fspls fs.FS, dir, suffix string) ([]string, error) {
|
||||||
|
files := []string{}
|
||||||
|
|
||||||
|
err := fs.WalkDir(fspls, dir, func(path string, de fs.DirEntry, err error) error {
|
||||||
|
if de.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if filepath.Ext(path) == suffix {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return files, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// listAll returns template file names as []string and panics on error.
|
||||||
|
// uses filepath.WalkDir via glob* funcs. TODO: don't panic.
|
||||||
|
func listAll(pattern string) []string {
|
||||||
|
files := make([]string, 0)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case !setting.IsLive():
|
||||||
|
f, err := globFS(tmplFS, ".", pattern)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error: %q", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files = f
|
||||||
|
|
||||||
|
case setting.IsLive():
|
||||||
|
f, err := globDir(tmplPath, pattern)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files = f
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
22
modules/template/render.go
Normal file
22
modules/template/render.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TplRenderer struct {
|
||||||
|
tmpls *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
var Renderer = &TplRenderer{tmpls: template.New("")}
|
||||||
|
|
||||||
|
func (t *TplRenderer) Render(w io.Writer, name string, data any, c echo.Context) error {
|
||||||
|
c.Logger().Debugf("rendering template %s", name)
|
||||||
|
|
||||||
|
tpl := Get(name)
|
||||||
|
|
||||||
|
return tpl.Execute(w, data)
|
||||||
|
}
|
142
modules/template/template.go
Normal file
142
modules/template/template.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.dotya.ml/mirre-mt/pcmt/app/settings"
|
||||||
|
"git.dotya.ml/mirre-mt/pcmt/slogging"
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
"golang.org/x/exp/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tplMap map[string]*template.Template
|
||||||
|
|
||||||
|
var (
|
||||||
|
templateMap tplMap
|
||||||
|
// embedded templates.
|
||||||
|
tmplFS fs.FS
|
||||||
|
bluemondayPolicy = bluemonday.UGCPolicy()
|
||||||
|
setting *settings.Settings
|
||||||
|
// global logger.
|
||||||
|
slogger *slogging.Logger
|
||||||
|
// local logger copy.
|
||||||
|
log slogging.Logger
|
||||||
|
// path to "templates" folder (empty if LiveMode).
|
||||||
|
tmplPath string
|
||||||
|
tmplSuffix = ".tmpl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init saves settings and embedded templates to package-level state and also
|
||||||
|
// "initialises" template rendering.
|
||||||
|
func Init(s *settings.Settings, tmpls fs.FS) {
|
||||||
|
slogger = slogging.GetLogger()
|
||||||
|
log = *slogger // have a local copy.
|
||||||
|
log.Logger = log.Logger.With(
|
||||||
|
slog.Group("pcmt extra", slog.String("module", "modules/template")),
|
||||||
|
)
|
||||||
|
|
||||||
|
setting = s
|
||||||
|
|
||||||
|
tmplPath = setting.TemplatesPath() // will be empty if LiveMode.
|
||||||
|
|
||||||
|
initTemplates(tmpls)
|
||||||
|
|
||||||
|
log.Debug(Renderer.tmpls.DefinedTemplates())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a template requested either from the tmplPath (in LiveMode) or
|
||||||
|
// from the pre-compiled tmpl list set directly to Renderer.
|
||||||
|
func Get(name string) *template.Template {
|
||||||
|
liveMode := setting.IsLive()
|
||||||
|
|
||||||
|
if liveMode {
|
||||||
|
log.Debug("re-reading tmpls") // TODO: only re-read the tmpl in question.
|
||||||
|
|
||||||
|
allTmpls := listAll(tmplSuffix)
|
||||||
|
|
||||||
|
// create fresh tmpl root.
|
||||||
|
t := template.New("")
|
||||||
|
|
||||||
|
for i, v := range allTmpls {
|
||||||
|
rawfile, err := ioutil.ReadFile(v)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allTmpls[i] = strings.TrimPrefix(v, tmplPath+"/")
|
||||||
|
|
||||||
|
t = t.New(allTmpls[i]).Funcs(FuncMap())
|
||||||
|
|
||||||
|
template.Must(t.Parse(string(rawfile)))
|
||||||
|
}
|
||||||
|
|
||||||
|
Renderer.tmpls = t
|
||||||
|
}
|
||||||
|
|
||||||
|
return Renderer.tmpls.Lookup(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initTemplates(f fs.FS) {
|
||||||
|
if !setting.IsLive() {
|
||||||
|
if f != nil || f != tmplFS {
|
||||||
|
tmplFS = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allTmpls := listAll(tmplSuffix)
|
||||||
|
t := Renderer.tmpls
|
||||||
|
|
||||||
|
// ensure this fails at compile time, if at all ("Must").
|
||||||
|
if !setting.IsLive() {
|
||||||
|
for i, v := range allTmpls {
|
||||||
|
rawfile, err := fs.ReadFile(tmplFS, v)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allTmpls[i] = strings.TrimPrefix(v, "templates/")
|
||||||
|
|
||||||
|
t := Renderer.tmpls.New(allTmpls[i]).Funcs(FuncMap())
|
||||||
|
template.Must(t.Parse(string(rawfile)))
|
||||||
|
}
|
||||||
|
|
||||||
|
Renderer.tmpls = t
|
||||||
|
} else {
|
||||||
|
for i, v := range allTmpls {
|
||||||
|
rawfile, err := ioutil.ReadFile(v)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allTmpls[i] = strings.TrimPrefix(v, tmplPath+"/")
|
||||||
|
t = Renderer.tmpls.New(allTmpls[i]).Funcs(FuncMap())
|
||||||
|
|
||||||
|
template.Must(t.Parse(string(rawfile)))
|
||||||
|
}
|
||||||
|
|
||||||
|
Renderer.tmpls = t
|
||||||
|
}
|
||||||
|
|
||||||
|
makeTplMap(Renderer.tmpls, allTmpls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTplMap(tpl *template.Template, tplList []string) {
|
||||||
|
allTmpls := tplList
|
||||||
|
|
||||||
|
var tp tplMap
|
||||||
|
|
||||||
|
if templateMap == nil {
|
||||||
|
tp = make(tplMap, len(allTmpls))
|
||||||
|
} else {
|
||||||
|
tp = templateMap
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range allTmpls {
|
||||||
|
tp[v] = tpl.Lookup(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
templateMap = tp
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user