Commit 0a4bb99d63
Verified · cmc ci/build: success ci/test: success ci/vuln: success
Layout: unified · split
internal/web/web.go +37 −5
| @@ -4,10 +4,13 @@ package web | |||
| 4 | 4 | ||
| 5 | import ( | 5 | import ( |
| 6 | "embed" | 6 | "embed" |
| 7 | "fmt" | ||
| 7 | "html/template" | 8 | "html/template" |
| 8 | "io" | 9 | "io" |
| 10 | "io/fs" | ||
| 9 | "reflect" | 11 | "reflect" |
| 10 | "runtime/debug" | 12 | "runtime/debug" |
| 13 | "sort" | ||
| 11 | "strconv" | 14 | "strconv" |
| 12 | "strings" | 15 | "strings" |
| 13 | "sync" | 16 | "sync" |
| @@ -225,13 +228,42 @@ var funcs = template.FuncMap{ | |||
| 225 | }, | 228 | }, |
| 226 | } | 229 | } |
| 227 | 230 | ||
| 231 | // pages holds every page template parsed once with the layout, at | ||
| 232 | // start-up: a template that does not parse fails the process before it | ||
| 233 | // serves anything, rather than the first visit to a rarely-hit page, and | ||
| 234 | // a request no longer re-parses the layout (#116). | ||
| 235 | var pages = func() map[string]*template.Template { | ||
| 236 | entries, err := fs.ReadDir(templateFS, "templates") | ||
| 237 | if err != nil { | ||
| 238 | panic(err) | ||
| 239 | } | ||
| 240 | m := map[string]*template.Template{} | ||
| 241 | for _, e := range entries { | ||
| 242 | name := e.Name() | ||
| 243 | if name == "layout.html" || !strings.HasSuffix(name, ".html") { | ||
| 244 | continue | ||
| 245 | } | ||
| 246 | layout := template.Must(template.New("layout.html").Funcs(funcs).ParseFS(templateFS, "templates/layout.html")) | ||
| 247 | m[name] = template.Must(layout.ParseFS(templateFS, "templates/"+name)) | ||
| 248 | } | ||
| 249 | return m | ||
| 250 | }() | ||
| 251 | |||
| 252 | // Pages lists the page template names, for tests that render each one. | ||
| 253 | func Pages() []string { | ||
| 254 | names := make([]string, 0, len(pages)) | ||
| 255 | for name := range pages { | ||
| 256 | names = append(names, name) | ||
| 257 | } | ||
| 258 | sort.Strings(names) | ||
| 259 | return names | ||
| 260 | } | ||
| 261 | |||
| 228 | // Render executes the named page template with the shared layout. | 262 | // Render executes the named page template with the shared layout. |
| 229 | func Render(w io.Writer, page string, data any) error { | 263 | func Render(w io.Writer, page string, data any) error { |
| 230 | t, err := template.Must( | 264 | t, ok := pages[page] |
| 231 | template.New("layout.html").Funcs(funcs).ParseFS(templateFS, "templates/layout.html"), | 265 | if !ok { |
| 232 | ).ParseFS(templateFS, "templates/"+page) | 266 | return fmt.Errorf("no page template %q", page) |
| 233 | if err != nil { | ||
| 234 | return err | ||
| 235 | } | 267 | } |
| 236 | return t.ExecuteTemplate(w, "layout", data) | 268 | return t.ExecuteTemplate(w, "layout", data) |
| 237 | } | 269 | } |
internal/web/web_test.go added +19
| @@ -0,0 +1,19 @@ | |||
| 1 | package web | ||
| 2 | |||
| 3 | import "testing" | ||
| 4 | |||
| 5 | // Every page template parses with the layout at start-up, so a broken | ||
| 6 | // template fails the process rather than the first visit to its page. | ||
| 7 | // Package init has already done the work; this asserts it covered every | ||
| 8 | // file (#116). | ||
| 9 | func TestEveryPageTemplateParses(t *testing.T) { | ||
| 10 | names := Pages() | ||
| 11 | if len(names) < 30 { | ||
| 12 | t.Fatalf("only %d page templates parsed: %v", len(names), names) | ||
| 13 | } | ||
| 14 | for _, want := range []string{"tree.html", "blob.html", "mr.html", "issue.html", "dashboard.html", "registered.html"} { | ||
| 15 | if _, ok := pages[want]; !ok { | ||
| 16 | t.Errorf("%s not parsed", want) | ||
| 17 | } | ||
| 18 | } | ||
| 19 | } | ||