feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
98
internal/webapp/templates.go
Обычный файл
98
internal/webapp/templates.go
Обычный файл
@@ -0,0 +1,98 @@
|
||||
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)
|
||||
}
|
||||
Ссылка в новой задаче
Block a user