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