Files
worker/internal/webapp/templates.go
Gleb Tv 2c7a0236da feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
2026-07-13 17:55:14 +03:00

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)
}