coverage~bigbes/sr-ht-compare9720ccc2web/templates.go

Coverage
73.1% 19/26 statements
Δ
Blob
37fe60a
1 package web
2
3 import (
4 "embed"
5 "html/template"
6 "log/slog"
7 "net/http"
8
9 "go.bigb.es/auxilia/scribe"
10 "sourcecraft.dev/bigbes/sr-ht-ecore/chrome"
11 "sourcecraft.dev/bigbes/sr-ht-ecore/pages"
12 )
13
14 // tmplFS holds the page templates. pages.Load discovers the pages in it and
15 // parses each one into its own set together with the shared layout, so that
16 // per-page "content"/"scripts" defines do not collide across pages — and so
17 // that adding templates/whatever.html is the whole registration of a page.
18 //
19 //go:embed templates/*.html
20 var tmplFS embed.FS
21
22 // staticFS holds the built front-end assets: the hashed bundle.<hash>.js and
23 // the hashed stylesheet, and nothing else since the favicon became chrome's
24 // inlined one. It is served read-only under /static/.
25 //
26 //go:embed static
27 var staticFS embed.FS
28
29 // The globs that find this build's content-addressed artefacts in staticFS.
30 // `make css` and the bundle build each write exactly one file, removing the
31 // previous build's first, so a glob here has at most one match to pick.
32 const (
33 cssGlob = "static/main.min.*.css"
34 bundleGlob = "static/bundle.*.js"
35 )
36
37 // funcMap holds the template helpers shared by every page.
38 //
39 // It starts from chrome.Funcs — so the shared partials find the helpers they
40 // were written against, and so "shortsha" is the instance's one abbreviation
41 // rule rather than this service's copy of it — and adds compare's own on top.
42 // Adding after is deliberate: a name may then be shadowed on purpose rather than
43 // by accident of map ordering.
44 // The commit timestamps this service used to format with a "date" helper of its
45 // own now go through chrome's "reltime" and "abstime": a listing says "3 days
46 // ago" and hovers to the exact UTC stamp, which is the one spelling the whole
47 // instance shows.
48 1 var funcMap = func() template.FuncMap {
49 1 m := chrome.Funcs()
50 1
51 1 // statusClass maps a git file-change status letter to a CSS modifier used by
52 1 // the .diff-status badge (see the diff-status rules in layout.html). Anything
53 1 // unrecognized falls back to the neutral "o".
54 13 m["statusClass"] = func(s string) string {
55 13 if s == "" {
56 0 return "o"
57 0 }
58 13 switch s[0] {
59 8 case 'A', 'a':
60 8 return "a"
61 4 case 'M', 'm':
62 4 return "m"
63 1 case 'D', 'd':
64 1 return "d"
65 0 case 'R', 'r':
66 0 return "r"
67 0 default:
68 0 return "o"
69 }
70 }
71
72 // statusLabel spells out a status letter for the badge's tooltip.
73 13 m["statusLabel"] = func(s string) string {
74 13 if s == "" {
75 0 return "changed"
76 0 }
77 13 switch s[0] {
78 8 case 'A', 'a':
79 8 return "added"
80 4 case 'M', 'm':
81 4 return "modified"
82 1 case 'D', 'd':
83 1 return "deleted"
84 0 case 'R', 'r':
85 0 return "renamed"
86 0 default:
87 0 return "changed"
88 }
89 }
90
91 1 return m
92 }()
93
94 // render writes one page, and logs whatever pages.Render gives back.
95 //
96 // The log line is the whole of what a caller may do with that error: Render has
97 // already answered the response — a 500 carrying a fixed string when the
98 // template failed — so handing it to fail would write a second response over a
99 // committed one, or, when the failure is in the error page itself, recurse
100 // through the page that just broke. That is why this returns nothing.
101 32 func (s *Server) render(w http.ResponseWriter, status int, page string, vd viewData) {
102 32 if err := s.pages.Render(w, status, page, vd); err != nil {
103 0 slog.Error("web: render", scribe.Err(err), "page", page, "status", status)
104 0 }
105 }
106
107 // renderError renders the chrome-wrapped error page for a status.
108 //
109 // An empty message takes the instance's standard sentence for that status, so
110 // the 404 a hidden repository produces reads exactly like the 404 of a
111 // repository that never existed — which is the point of answering 404 rather
112 // than 403 in the first place. A message is only ever this service's own words
113 // about what the viewer typed (a malformed compare spec), never an error from
114 // below.
115 14 func (s *Server) renderError(w http.ResponseWriter, r *http.Request, status int, message string) {
116 14 vd := s.view(r, http.StatusText(status))
117 14 vd.Data = pages.Error(status, message)
118 14 s.render(w, status, pages.ErrorPage, vd)
119 14 }
120
121 // handleNotFound is the 404 for a route the router does not have and for an
122 // asset path that is not a file. It is a http.HandlerFunc so it can be handed
123 // to chi's NotFound and to assets.Handler, which both want one.
124 3 func (s *Server) handleNotFound(w http.ResponseWriter, r *http.Request) {
125 3 s.renderError(w, r, http.StatusNotFound, "")
126 3 }