package web import ( "bytes" "embed" "html/template" "net/http" "time" "github.com/sirupsen/logrus" "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"/"scripts" defines // do not collide across pages. // //go:embed templates/*.html var tmplFS embed.FS // staticFS holds the built front-end assets (the hashed bundle..js, the // hashed stylesheet, logo.svg). It is served read-only under /static/. // //go:embed static var staticFS embed.FS // 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. var funcMap = func() template.FuncMap { m := chrome.Funcs() // date formats a commit timestamp for display. m["date"] = func(t time.Time) string { return t.UTC().Format("2006-01-02 15:04 MST") } // 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 }() // pageNames are the content templates; each is parsed with layout.html. var pageNames = []string{"index", "repo", "compare", "commit", "error"} // pages maps a page name to its parsed template set: the shared chrome partials // of sr-ht-ecore, the layout, and that one page. The partials are attached to // every set rather than to a shared one, for the same reason the layout is — // each page defines its own "content", and one set would let the last parsed win. 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/"+name+".html")) m[name] = t } return m }() // 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 { logrus.WithField("page", page).Error("web: unknown template page") http.Error(w, "internal server error", http.StatusInternalServerError) return } var buf bytes.Buffer if err := t.ExecuteTemplate(&buf, "layout.html", vd); err != nil { logrus.WithError(err).WithField("page", page).Error("web: template execution failed") 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) }