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