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