package web import ( "embed" "html/template" "log/slog" "net/http" "go.bigb.es/auxilia/scribe" "sourcecraft.dev/bigbes/sr-ht-ecore/chrome" "sourcecraft.dev/bigbes/sr-ht-ecore/pages" ) // tmplFS holds the page templates. pages.Load discovers the pages in it and // parses each one into its own set together with the shared layout, so that // per-page "content"/"scripts" defines do not collide across pages — and so // that adding templates/whatever.html is the whole registration of a page. // //go:embed templates/*.html var tmplFS embed.FS // staticFS holds the built front-end assets: the hashed bundle..js and // the hashed stylesheet, and nothing else since the favicon became chrome's // inlined one. It is served read-only under /static/. // //go:embed static var staticFS embed.FS // The globs that find this build's content-addressed artefacts in staticFS. // `make css` and the bundle build each write exactly one file, removing the // previous build's first, so a glob here has at most one match to pick. const ( cssGlob = "static/main.min.*.css" bundleGlob = "static/bundle.*.js" ) // funcMap holds the template helpers shared by every page. // // It starts from chrome.Funcs — so the shared partials find the helpers they // were written against, and so "shortsha" is the instance's one abbreviation // rule rather than this service's copy of it — and adds compare's own on top. // Adding after is deliberate: a name may then be shadowed on purpose rather than // by accident of map ordering. // The commit timestamps this service used to format with a "date" helper of its // own now go through chrome's "reltime" and "abstime": a listing says "3 days // ago" and hovers to the exact UTC stamp, which is the one spelling the whole // instance shows. var funcMap = func() template.FuncMap { m := chrome.Funcs() // statusClass maps a git file-change status letter to a CSS modifier used by // the .diff-status badge (see the diff-status rules in layout.html). Anything // unrecognized falls back to the neutral "o". m["statusClass"] = func(s string) string { if s == "" { return "o" } switch s[0] { case 'A', 'a': return "a" case 'M', 'm': return "m" case 'D', 'd': return "d" case 'R', 'r': return "r" default: return "o" } } // statusLabel spells out a status letter for the badge's tooltip. m["statusLabel"] = func(s string) string { if s == "" { return "changed" } switch s[0] { case 'A', 'a': return "added" case 'M', 'm': return "modified" case 'D', 'd': return "deleted" case 'R', 'r': return "renamed" default: return "changed" } } return m }() // render writes one page, and logs whatever pages.Render gives back. // // The log line is the whole of what a caller may do with that error: Render has // already answered the response — a 500 carrying a fixed string when the // template failed — so handing it to fail would write a second response over a // committed one, or, when the failure is in the error page itself, recurse // through the page that just broke. That is why this returns nothing. func (s *Server) render(w http.ResponseWriter, status int, page string, vd viewData) { if err := s.pages.Render(w, status, page, vd); err != nil { slog.Error("web: render", scribe.Err(err), "page", page, "status", status) } } // renderError renders the chrome-wrapped error page for a status. // // An empty message takes the instance's standard sentence for that status, so // the 404 a hidden repository produces reads exactly like the 404 of a // repository that never existed — which is the point of answering 404 rather // than 403 in the first place. A message is only ever this service's own words // about what the viewer typed (a malformed compare spec), never an error from // below. func (s *Server) renderError(w http.ResponseWriter, r *http.Request, status int, message string) { vd := s.view(r, http.StatusText(status)) vd.Data = pages.Error(status, message) s.render(w, status, pages.ErrorPage, vd) } // handleNotFound is the 404 for a route the router does not have and for an // asset path that is not a file. It is a http.HandlerFunc so it can be handed // to chi's NotFound and to assets.Handler, which both want one. func (s *Server) handleNotFound(w http.ResponseWriter, r *http.Request) { s.renderError(w, r, http.StatusNotFound, "") }