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")
|
||||
|
||||
fsys, err := fs.Sub(a.embeds.templates, "templates")
|
||||
fsys, err := fs.Sub(a.embeds.templates, ".")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"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/middleware"
|
||||
)
|
||||
@ -14,9 +15,11 @@ func (a *App) SetupRoutes() {
|
||||
assets := http.FileServer(a.getAssets())
|
||||
tmpls := a.getTemplates()
|
||||
|
||||
modtmpl.Init(setting, tmpls)
|
||||
// run this before declaring any handler funcs.
|
||||
handlers.InitHandlers(setting, tmpls)
|
||||
e.Renderer = handlers.Renderer
|
||||
handlers.InitHandlers(setting)
|
||||
|
||||
e.Renderer = modtmpl.Renderer
|
||||
|
||||
// keep /static/* as a compatibility fallback for /assets.
|
||||
e.GET(
|
||||
|
@ -1,22 +1,19 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
|
||||
"git.dotya.ml/mirre-mt/pcmt/app/settings"
|
||||
"git.dotya.ml/mirre-mt/pcmt/slogging"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
var (
|
||||
setting *settings.Settings
|
||||
appver string
|
||||
slogger *slogging.Logger
|
||||
log slogging.Logger
|
||||
tmplPath string
|
||||
setting *settings.Settings
|
||||
appver string
|
||||
slogger *slogging.Logger
|
||||
log slogging.Logger
|
||||
)
|
||||
|
||||
func InitHandlers(s *settings.Settings, tmpls fs.FS) {
|
||||
func InitHandlers(s *settings.Settings) {
|
||||
slogger = slogging.GetLogger()
|
||||
log = *slogger // have a local copy.
|
||||
log.Logger = log.Logger.With(
|
||||
@ -25,11 +22,5 @@ func InitHandlers(s *settings.Settings, tmpls fs.FS) {
|
||||
|
||||
setting = s
|
||||
|
||||
tmplPath = setting.TemplatesPath()
|
||||
|
||||
appver = setting.Version()
|
||||
|
||||
log.Debugf("tmpls: %+v", tmpls)
|
||||
|
||||
initTemplates(tmpls)
|
||||
}
|
||||
|
@ -1,113 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
modtmpl "git.dotya.ml/mirre-mt/pcmt/modules/template"
|
||||
"github.com/labstack/echo-contrib/session"
|
||||
"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 {
|
||||
return func(c echo.Context) error {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
|
||||
@ -123,7 +23,7 @@ func Index() echo.HandlerFunc {
|
||||
return c.Redirect(http.StatusFound, "/home")
|
||||
}
|
||||
|
||||
tpl := getTmpl("index.tmpl")
|
||||
tpl := modtmpl.Get("index.tmpl")
|
||||
csrf := c.Get("csrf").(string)
|
||||
|
||||
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