159 lines
3.9 KiB
Go
159 lines
3.9 KiB
Go
package HTMLTemplate
|
|
|
|
import (
|
|
// External
|
|
werr "git.gibonuddevalla.se/go/wrappederror"
|
|
|
|
// Standard
|
|
"fmt"
|
|
"html/template"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
)
|
|
|
|
type Engine struct {
|
|
parsedTemplates map[string]*template.Template
|
|
viewFS fs.FS
|
|
staticEmbeddedFS http.Handler
|
|
staticLocalFS http.Handler
|
|
componentFilenames []string
|
|
DevMode bool
|
|
}
|
|
|
|
func NewEngine(viewFS, staticFS fs.FS, devmode bool) (e Engine, err error) { // {{{
|
|
e.parsedTemplates = make(map[string]*template.Template)
|
|
e.viewFS = viewFS
|
|
e.DevMode = devmode
|
|
|
|
e.componentFilenames, err = e.getComponentFilenames()
|
|
|
|
// Set up fileservers for static resources.
|
|
// The embedded FS is using the embedded files intented for production use.
|
|
// The local FS is for development of Javascript to avoid server rebuild (devmode).
|
|
var staticSubFS fs.FS
|
|
staticSubFS, err = fs.Sub(staticFS, "static")
|
|
if err != nil {
|
|
return
|
|
}
|
|
e.staticEmbeddedFS = http.FileServer(http.FS(staticSubFS))
|
|
e.staticLocalFS = http.FileServer(http.Dir("static"))
|
|
|
|
return
|
|
} // }}}
|
|
|
|
func (e *Engine) getComponentFilenames() (files []string, err error) { // {{{
|
|
files = []string{}
|
|
if err := fs.WalkDir(e.viewFS, "views/components", func(path string, d fs.DirEntry, err error) error {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
files = append(files, path)
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return files, nil
|
|
} // }}}
|
|
|
|
func (e *Engine) ReloadTemplates() { // {{{
|
|
e.parsedTemplates = make(map[string]*template.Template)
|
|
} // }}}
|
|
|
|
func (e *Engine) StaticResource(w http.ResponseWriter, r *http.Request) { // {{{
|
|
var err error
|
|
|
|
// URLs with pattern /(css|images)/v1.0.0/foobar are stripped of the version.
|
|
// To get rid of problems with cached content in browser on a new version release,
|
|
// while also not disabling cache altogether.
|
|
if r.URL.Path == "/favicon.ico" {
|
|
e.staticEmbeddedFS.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
rxp := regexp.MustCompile("^/(css|images|js|fonts)/v[0-9]+/(.*)$")
|
|
if comp := rxp.FindStringSubmatch(r.URL.Path); comp != nil {
|
|
w.Header().Add("Pragma", "public")
|
|
w.Header().Add("Cache-Control", "max-age=604800")
|
|
|
|
r.URL.Path = fmt.Sprintf("/%s/%s", comp[1], comp[2])
|
|
if e.DevMode {
|
|
p := fmt.Sprintf("static/%s/%s", comp[1], comp[2])
|
|
_, err = os.Stat(p)
|
|
if err == nil {
|
|
e.staticLocalFS.ServeHTTP(w, r)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
e.staticEmbeddedFS.ServeHTTP(w, r)
|
|
} // }}}
|
|
func (e *Engine) getPage(layout, page string) (tmpl *template.Template, err error) { // {{{
|
|
layoutFilename := fmt.Sprintf("views/layouts/%s.gotmpl", layout)
|
|
pageFilename := fmt.Sprintf("views/pages/%s.gotmpl", page)
|
|
|
|
if tmpl, found := e.parsedTemplates[page]; found {
|
|
return tmpl, nil
|
|
}
|
|
|
|
funcMap := template.FuncMap{
|
|
/*
|
|
"format_time": func(t time.Time) template.HTML {
|
|
return template.HTML(
|
|
t.In(smonConfig.Timezone()).Format(`<span class="date">2006-01-02</span> <span class="time">15:04:05<span class="seconds">:05</span></span>`),
|
|
)
|
|
},
|
|
*/
|
|
}
|
|
|
|
filenames := []string{layoutFilename, pageFilename}
|
|
filenames = append(filenames, e.componentFilenames...)
|
|
|
|
if e.DevMode {
|
|
tmpl, err = template.New(layout+".gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...)
|
|
} else {
|
|
tmpl, err = template.New(layout+".gotmpl").Funcs(funcMap).ParseFS(e.viewFS, filenames...)
|
|
}
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
return
|
|
}
|
|
|
|
e.parsedTemplates[page] = tmpl
|
|
return
|
|
} // }}}
|
|
func (e *Engine) Render(p Page, w http.ResponseWriter, r *http.Request) (err error) { // {{{
|
|
if e.DevMode {
|
|
e.ReloadTemplates()
|
|
}
|
|
|
|
var tmpl *template.Template
|
|
tmpl, err = e.getPage(p.GetLayout(), p.GetPage())
|
|
if err != nil {
|
|
err = werr.Wrap(err)
|
|
return
|
|
}
|
|
|
|
data := map[string]any{
|
|
"VERSION": p.GetVersion(),
|
|
"LAYOUT": p.GetLayout(),
|
|
"PAGE": p.GetPage(),
|
|
"ERROR": r.URL.Query().Get("_err"),
|
|
"Data": p.GetData(),
|
|
}
|
|
|
|
err = tmpl.Execute(w, data)
|
|
if err != nil {
|
|
err = werr.Wrap(err)
|
|
}
|
|
return
|
|
} // }}}
|
|
|
|
// vim: foldmethod=marker
|