| 1 |
|
package web |
| 2 |
|
|
| 3 |
|
import ( |
| 4 |
|
"embed" |
| 5 |
|
"html/template" |
| 6 |
|
"log/slog" |
| 7 |
|
"net/http" |
| 8 |
|
"strings" |
| 9 |
|
|
| 10 |
|
"go.bigb.es/auxilia/scribe" |
| 11 |
|
"sourcecraft.dev/bigbes/sr-ht-ecore/chrome" |
| 12 |
|
"sourcecraft.dev/bigbes/sr-ht-ecore/pages" |
| 13 |
|
) |
| 14 |
|
|
| 15 |
|
// tmplFS holds the page templates. sr-ht-ecore's pages.Load discovers them: |
| 16 |
|
// layout.html is the chrome every page is executed through, a file whose name |
| 17 |
|
// starts with "_" is a partial parsed into every set, and everything else is a |
| 18 |
|
// page that must define "content". |
| 19 |
|
// |
| 20 |
|
// The glob and not the bare directory, because Go's embed excludes names |
| 21 |
|
// starting with '_' when it walks a directory and _threads.html is exactly such |
| 22 |
|
// a name. |
| 23 |
|
// |
| 24 |
|
//go:embed templates/*.html |
| 25 |
|
var tmplFS embed.FS |
| 26 |
|
|
| 27 |
|
// staticFS holds the built assets: the hashed stylesheet produced by `make css` |
| 28 |
|
// and the logo. It is compiled into the binary, which is why `make css` alone |
| 29 |
|
// does not restyle a running daemon — see the package doc. |
| 30 |
|
// |
| 31 |
|
//go:embed static |
| 32 |
|
var staticFS embed.FS |
| 33 |
|
|
| 34 |
|
// funcMap holds this service's own template helpers. |
| 35 |
|
// |
| 36 |
|
// It carries only what is ours: pages.Load merges it over chrome.Funcs — the |
| 37 |
|
// generic helpers every custom service on this instance was carrying its own |
| 38 |
|
// copy of, `shortsha` among them — in that order, so a name may be shadowed |
| 39 |
|
// deliberately rather than by accident of map ordering. Nothing shadows one |
| 40 |
|
// today, and a helper that diverged from the shared spelling of the same name |
| 41 |
|
// would be the drift ecore exists to prevent. |
| 42 |
|
var funcMap = template.FuncMap{ |
| 43 |
|
// indent renders a tree depth as non-breaking space, so the space view's |
| 44 |
|
// hierarchy reads as a hierarchy without a nested-list template recursion. |
| 45 |
|
// A negative depth (a level-1 heading, once decremented) indents nothing. |
| 46 |
35 |
"indent": func(depth int) template.HTML { |
| 47 |
35 |
if depth <= 0 { |
| 48 |
35 |
return "" |
| 49 |
35 |
} |
| 50 |
0 |
return template.HTML(strings.Repeat(" ", depth)) |
| 51 |
|
}, |
| 52 |
|
// dec turns a 1-based heading level into a 0-based indent depth. |
| 53 |
10 |
"dec": func(n int) int { return n - 1 }, |
| 54 |
|
} |
| 55 |
|
|
| 56 |
|
// blockThreadsTmpl is the per-block comment markup, parsed on its own so the |
| 57 |
|
// diff renderer — which builds its HTML in Go and cannot reach a page template |
| 58 |
|
// through the usual {{template}} call — and the proposal page cannot drift into |
| 59 |
|
// two spellings of a thread. It is the same file the page set parses as a |
| 60 |
|
// partial, so there is one spelling and not two. |
| 61 |
|
// |
| 62 |
|
// A missing define is a build-time mistake in this package, so it panics at |
| 63 |
|
// init the way template.Must does, rather than yielding a page with the |
| 64 |
|
// comments silently absent. |
| 65 |
1 |
var blockThreadsTmpl = func() *template.Template { |
| 66 |
1 |
t := template.Must(template.New("_threads.html"). |
| 67 |
1 |
Funcs(chrome.Funcs()).Funcs(funcMap). |
| 68 |
1 |
ParseFS(tmplFS, "templates/_threads.html")) |
| 69 |
1 |
blocks := t.Lookup("blockthreads") |
| 70 |
1 |
if blocks == nil { |
| 71 |
0 |
panic(`web: templates/_threads.html does not define "blockthreads"`) |
| 72 |
|
} |
| 73 |
1 |
return blocks |
| 74 |
|
}() |
| 75 |
|
|
| 76 |
|
// renderError renders the chrome-wrapped error page: this service's view struct |
| 77 |
|
// around ecore's shared error body. |
| 78 |
|
// |
| 79 |
|
// An empty message takes the standard sentence for the status, so a refusal |
| 80 |
|
// with nothing of its own to add says what every other service on this instance |
| 81 |
|
// says. It never recurses on failure — pages.Render answers the response itself |
| 82 |
|
// and hands back only a line for the log. |
| 83 |
34 |
func (s *Server) renderError(w http.ResponseWriter, r *http.Request, status int, message string) { |
| 84 |
34 |
vd := s.view(r, http.StatusText(status)) |
| 85 |
34 |
vd.Data = pages.Error(status, message) |
| 86 |
34 |
if err := s.pages.Render(w, status, pages.ErrorPage, vd); err != nil { |
| 87 |
0 |
slog.ErrorContext(r.Context(), "rendering the error page failed after it was answered", |
| 88 |
0 |
"status", status, "method", r.Method, "path", r.URL.Path, scribe.Err(err)) |
| 89 |
0 |
} |
| 90 |
|
} |