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(`2006-01-02 15:04:05:05`), ) }, */ } 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