package web import ( "embed" "fmt" "html/template" "io" "io/fs" "os" "path/filepath" "runtime" "strings" "sync" "time" "github.com/gramanas/blogspace/internal/i18n" "github.com/gramanas/blogspace/internal/store" ) //go:embed templates static var assets embed.FS // templates compiles each page together with its layout, once per language // (the translating funcs are closed over the language, so templates just // write {{t "…"}}). In dev mode it re-reads from disk on every render so // edits show up without a restart. type templates struct { dev bool fs fs.FS mu sync.Mutex cache map[string]*template.Template // "lang/dir/file.html" } func newTemplates(dev bool) *templates { t := &templates{dev: dev, cache: map[string]*template.Template{}} t.fs = assets if dev { // locate the package directory so `go run` from anywhere still finds the files _, file, _, _ := runtime.Caller(0) dir := filepath.Dir(file) if _, err := os.Stat(filepath.Join(dir, "templates")); err == nil { t.fs = os.DirFS(dir) } } return t } // AssetsFS returns the static files (embedded or on disk in dev mode). func (t *templates) AssetsFS() fs.FS { sub, _ := fs.Sub(t.fs, "static") return sub } func layoutFor(name string) string { if strings.HasPrefix(name, "blog/") { return "layouts/blog.html" } return "layouts/dashboard.html" } func (t *templates) get(lang, name string) (*template.Template, error) { key := lang + "/" + name if !t.dev { t.mu.Lock() if tpl, ok := t.cache[key]; ok { t.mu.Unlock() return tpl, nil } t.mu.Unlock() } tpl, err := template.New("").Funcs(funcsFor(lang)).ParseFS(t.fs, "templates/"+layoutFor(name), "templates/partials/*.html", "templates/"+name) if err != nil { return nil, fmt.Errorf("parse %s: %w", name, err) } if !t.dev { t.mu.Lock() t.cache[key] = tpl t.mu.Unlock() } return tpl, nil } func (t *templates) render(w io.Writer, lang, name string, data any) error { tpl, err := t.get(lang, name) if err != nil { return err } return tpl.ExecuteTemplate(w, "layout", data) } // funcsFor is the shared funcs plus the ones that speak the page's language. func funcsFor(lang string) template.FuncMap { m := make(template.FuncMap, len(funcs)+6) for k, v := range funcs { m[k] = v } // t translates fixed text; tf also formats: {{tf "Page %d of %d" .N .Last}} m["t"] = func(key string) string { return i18n.T(lang, key) } m["tf"] = func(key string, args ...any) string { return i18n.Tf(lang, key, args...) } // date is for the dashboard; postdate follows the theme's date style m["date"] = func(t time.Time) string { return i18n.FormatDate(lang, "long", t) } m["postdate"] = func(style string, t time.Time) string { return i18n.FormatDate(lang, style, t) } m["month"] = func(mo time.Month) string { return i18n.Month(lang, mo) } m["modsummary"] = func(mod store.Module) string { return moduleSummary(lang, mod) } return m } var funcs = template.FuncMap{ "rfc": func(t time.Time) string { return t.Format(time.RFC1123Z) }, "html": func(s string) template.HTML { return template.HTML(s) }, "css": func(s string) template.CSS { return template.CSS(s) }, "kb": func(n int) string { return fmt.Sprintf("%.0f KB", float64(n)/1024) }, "add": func(a, b int) int { return a + b }, "sub": func(a, b int) int { return a - b }, "deref": func(p *string) string { if p == nil { return "" } return *p }, "deref64": func(p *int64) int64 { if p == nil { return 0 } return *p }, // split turns a plain-text field into lines: {{range split .Body "\n"}}

{{.}}

{{end}} "split": strings.Split, "lower": strings.ToLower, // layout modules: whether the kind has an edit page (modsummary is per language) "modsettings": moduleHasSettings, // hasPrefix marks the current dashboard tab: {{if hasPrefix .Path "/b/x/posts"}} "hasPrefix": strings.HasPrefix, // dict builds a map for passing several values to a partial: {{template "x" (dict "a" 1 "b" 2)}} "dict": func(kv ...any) (map[string]any, error) { if len(kv)%2 != 0 { return nil, fmt.Errorf("dict: odd number of arguments") } m := make(map[string]any, len(kv)/2) for i := 0; i < len(kv); i += 2 { k, ok := kv[i].(string) if !ok { return nil, fmt.Errorf("dict: key %v is not a string", kv[i]) } m[k] = kv[i+1] } return m, nil }, }