package web
import (
"bytes"
"embed"
"html/template"
"net/http"
"time"
"github.com/sirupsen/logrus"
)
// 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.
var funcMap = template.FuncMap{
// shortsha abbreviates an object id to 8 hex chars.
"shortsha": func(s string) string {
if len(s) > 8 {
return s[:8]
}
return s
},
// date formats a commit timestamp for display.
"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".
"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.
"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"
}
},
}
// 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 (layout + that page).
var pages = func() map[string]*template.Template {
m := make(map[string]*template.Template, len(pageNames))
for _, name := range pageNames {
t := 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.chrome(r)
vd.Title = http.StatusText(status)
vd.Data = errorData{
Status: status,
StatusText: http.StatusText(status),
Message: message,
}
s.render(w, status, "error", vd)
}