99 строки
2.8 KiB
Go
99 строки
2.8 KiB
Go
package webapp
|
|
|
|
import (
|
|
"embed"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"io/fs"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
//go:embed templates/*.html static/*
|
|
var contentFS embed.FS
|
|
|
|
// staticFS exposes the embedded static assets (CSS, JS, favicon) so
|
|
// they can be served by http.FileServer. There is no CDN dependency;
|
|
// everything ships inside the worker binary.
|
|
var staticFS = mustSubFS("static")
|
|
|
|
// mustSubFS returns the named sub-filesystem inside contentFS, or
|
|
// panics if it is missing. The embed directive above guarantees the
|
|
// dir exists at build time.
|
|
func mustSubFS(name string) fs.FS {
|
|
sub, err := fs.Sub(contentFS, name)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("webapp: embedded sub %q: %v", name, err))
|
|
}
|
|
return sub
|
|
}
|
|
|
|
// Templates is a thin wrapper around html/template that lazily
|
|
// parses the embedded *.html files on first Execute. The FuncMap is
|
|
// intentionally minimal so adding global helpers does not blow up
|
|
// the per-page test surface.
|
|
type Templates struct {
|
|
mu sync.RWMutex
|
|
cache map[string]*template.Template
|
|
funcs template.FuncMap
|
|
}
|
|
|
|
// loadTemplates parses every embedded template into the cache. The
|
|
// shared layout (templates/layout.html) is parsed into every page
|
|
// so each {{template "content" .}} substitution resolves cleanly.
|
|
func loadTemplates() (*Templates, error) {
|
|
t := &Templates{
|
|
cache: map[string]*template.Template{},
|
|
funcs: template.FuncMap{
|
|
"fmtBytes": fmtBytes,
|
|
"fmtPercent": fmtPercent,
|
|
"fmtDuration": fmtDuration,
|
|
"fmtTime": fmtTime,
|
|
"fmtBool": fmtBool,
|
|
"hasPrefix": strings.HasPrefix,
|
|
"sanitizeName": sanitizeName,
|
|
},
|
|
}
|
|
pages, err := fs.ReadDir(contentFS, "templates")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("webapp: read templates: %w", err)
|
|
}
|
|
for _, p := range pages {
|
|
if p.IsDir() || !strings.HasSuffix(p.Name(), ".html") {
|
|
continue
|
|
}
|
|
if _, err := t.parse(p.Name()); err != nil {
|
|
return nil, fmt.Errorf("webapp: parse %s: %w", p.Name(), err)
|
|
}
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// parse loads the named template plus the shared layout. Page
|
|
// templates "{{define" body"}}" themselves.
|
|
func (t *Templates) parse(name string) (*template.Template, error) {
|
|
tmpl := template.New(name).Funcs(t.funcs)
|
|
if _, err := tmpl.ParseFS(contentFS, "templates/layout.html"); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := tmpl.ParseFS(contentFS, "templates/"+name); err != nil {
|
|
return nil, err
|
|
}
|
|
t.cache[name] = tmpl
|
|
return tmpl, nil
|
|
}
|
|
|
|
// Execute renders the named page template into w. The page is
|
|
// wrapped by the shared layout block via the "layout" template
|
|
// defined in templates/layout.html.
|
|
func (t *Templates) Execute(w io.Writer, name string, data interface{}) error {
|
|
t.mu.RLock()
|
|
tmpl, ok := t.cache[name]
|
|
t.mu.RUnlock()
|
|
if !ok {
|
|
return fmt.Errorf("webapp: no such template %q", name)
|
|
}
|
|
return tmpl.ExecuteTemplate(w, "layout", data)
|
|
}
|