diff --git a/html_template/page.go b/html_template/page.go new file mode 100644 index 0000000..25696aa --- /dev/null +++ b/html_template/page.go @@ -0,0 +1,31 @@ +package HTMLTemplate + +type Page interface { + GetVersion() string + GetLayout() string + GetPage() string + GetData() any +} + +type SimplePage struct { + Version string + Layout string + Page string + Data any +} + +func (s SimplePage) GetVersion() string { + return s.Version +} + +func (s SimplePage) GetLayout() string { + return s.Layout +} + +func (s SimplePage) GetPage() string { + return s.Page +} + +func (s SimplePage) GetData() any { + return s.Data +} diff --git a/html_template/pkg.go b/html_template/pkg.go new file mode 100644 index 0000000..4140f89 --- /dev/null +++ b/html_template/pkg.go @@ -0,0 +1,158 @@ +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