package web
import (
"bytes"
"embed"
"html/template"
"log"
"net/http"
"strings"
"sourcecraft.dev/bigbes/sr-ht-ecore/chrome"
)
// tmplFS holds the page templates. Each page is parsed together with the shared
// layout into its own template set so that per-page "content" defines do not
// collide across pages.
//
//go:embed templates/*.html
var tmplFS embed.FS
// staticFS holds the built assets: the hashed stylesheet produced by `make css`
// and the logo. It is compiled into the binary, which is why `make css` alone
// does not restyle a running daemon — see the package doc.
//
//go:embed static
var staticFS embed.FS
// funcMap holds the template helpers available to every page.
//
// It starts from chrome.Funcs — the generic helpers every custom service on
// this instance was carrying its own copy of, `shortsha` among them — and adds
// this service's own on top, after, so that a name may be shadowed
// deliberately rather than by accident of map ordering. Nothing shadows one
// today, and a helper that diverged from the shared spelling of the same name
// would be the drift ecore exists to prevent.
var funcMap = func() template.FuncMap {
m := chrome.Funcs()
// indent renders a tree depth as non-breaking space, so the space view's
// hierarchy reads as a hierarchy without a nested-list template recursion.
// A negative depth (a level-1 heading, once decremented) indents nothing.
m["indent"] = func(depth int) template.HTML {
if depth <= 0 {
return ""
}
return template.HTML(strings.Repeat(" ", depth))
}
// dec turns a 1-based heading level into a 0-based indent depth.
m["dec"] = func(n int) int { return n - 1 }
return m
}()
// pageNames are the content templates; each is parsed with layout.html.
var pageNames = []string{"index", "space", "document", "search", "error", "proposal", "inbox"}
// pages maps a page name to its parsed template set (layout + the shared
// chrome partials + local partials + that page). threads.html is parsed into
// every set rather than only into the proposal page's: it defines review-thread
// markup and nothing else, and a partial that only some sets know about is a
// lookup that fails on the page that later needs it. ecore's "srht-nav" and
// "srht-env-banner" are attached to every set for the same reason, and through
// MustAttach because a set that cannot draw the chrome is not a page this
// binary should start serving.
var pages = func() map[string]*template.Template {
m := make(map[string]*template.Template, len(pageNames))
for _, name := range pageNames {
t := chrome.MustAttach(template.New("layout.html").Funcs(funcMap))
t = template.Must(t.ParseFS(tmplFS,
"templates/layout.html", "templates/threads.html", "templates/"+name+".html"))
m[name] = t
}
return m
}()
// blockThreadsTmpl is the per-block comment markup, taken out of the proposal
// page's own set so the diff renderer — which builds its HTML in Go and cannot
// reach a page template through the usual {{template}} call — and the page
// itself cannot drift into two spellings of a thread.
//
// A missing define is a build-time mistake in this package, so it panics at
// init the way template.Must does, rather than yielding a page with the
// comments silently absent.
var blockThreadsTmpl = func() *template.Template {
t := pages["proposal"].Lookup("blockthreads")
if t == nil {
panic(`web: templates/threads.html does not define "blockthreads"`)
}
return t
}()
// render executes a page into a buffer first, so a template error yields a
// clean 500 rather than a half-written response. On success it writes the
// status and the buffered HTML.
func (s *Server) render(w http.ResponseWriter, status int, page string, vd viewData) {
t, ok := pages[page]
if !ok {
log.Printf("web: unknown template page %q", page)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
var buf bytes.Buffer
if err := t.ExecuteTemplate(&buf, "layout.html", vd); err != nil {
log.Printf("web: executing template %q: %v", page, err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
_, _ = buf.WriteTo(w)
}
// errorData is the payload of the error page.
type errorData struct {
Status int
StatusText string
Message string
}
// renderError renders the chrome-wrapped error page. It never recurses into
// render on failure (render falls back to http.Error itself).
func (s *Server) renderError(w http.ResponseWriter, r *http.Request, status int, message string) {
vd := s.view(r, http.StatusText(status))
vd.Data = errorData{
Status: status,
StatusText: http.StatusText(status),
Message: message,
}
s.render(w, status, "error", vd)
}