oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
4 "bytes"
5 "embed"
6 "fmt"
7 "html/template"
8 "io/fs"
9 "net/http"
10 "path"
11
12 "amadan.net/oficina/estilo.git/chrome"
13 "amadan.net/oficina/estilo.git/credits"
14 "amadan.net/oficina/estilo.git/menu"
15 "amadan.net/oficina/estilo.git/sky"
16 "amadan.net/rastrillo/rastrillo/ui"
17)
18
19//go:embed templates/*.html
20var templatesFS embed.FS
21
22// layoutFile is the app's own shell. It defines "layout" and executes
23// {{template "content" .}}, which is the contract rastrillo's shipped
24// shells use, so swapping in one of those later is a file copy rather
25// than a rewrite.
26const layoutFile = "templates/layout.html"
27
28// renderer holds one parsed tree per page.
29//
30// Parsed once, at construction, so a broken template is a boot failure
31// rather than a 500 on the one page nobody visits in testing. Minimal on
32// purpose: no asset pipeline, no theme, no vendored static files. The
33// scriptless sweep and the assets land with the task that adds them.
34type renderer struct {
35 pages map[string]*template.Template
36}
37
38func newRenderer() (*renderer, error) {
39 files, err := fs.Glob(templatesFS, "templates/*.html")
40 if err != nil {
41 return nil, err
42 }
43 r := &renderer{pages: make(map[string]*template.Template, len(files))}
44 for _, f := range files {
45 name := path.Base(f)
46 if f == layoutFile {
47 continue
48 }
49 // The layout first, then rastrillo's partials, then the page —
50 // so a page may use a partial, and a page redefining a block
51 // wins over the layout's default.
52 // ui.Funcs supplies the helpers rastrillo's partials call —
53 // "T" among them — so the tree must carry them before those
54 // partials are parsed, not after.
55 isDoor, isHop := doorPages[name], hopPages[name]
56 t, err := template.New(name).Funcs(ui.Funcs()).Funcs(homeFuncs).Funcs(template.FuncMap{
57 "door": func() bool { return isDoor },
58 "hop": func() bool { return isHop },
59 }).ParseFS(templatesFS, layoutFile)
60 if err != nil {
61 return nil, fmt.Errorf("parse %s: %w", layoutFile, err)
62 }
63 if t, err = t.ParseFS(ui.Templates(), "*.html"); err != nil {
64 return nil, fmt.Errorf("parse rastrillo partials: %w", err)
65 }
66 if t, err = t.ParseFS(credits.Templates(), "templates/*.html"); err != nil {
67 return nil, fmt.Errorf("parse credits partials: %w", err)
68 }
69 if t, err = t.ParseFS(sky.Templates(), "templates/*.html"); err != nil {
70 return nil, fmt.Errorf("parse sky partials: %w", err)
71 }
72 // The shared chrome: the bar, the account menu, the door and the
73 // error page (chrome.go).
74 if t, err = t.ParseFS(menu.Templates(), "templates/*.html"); err != nil {
75 return nil, fmt.Errorf("parse menu partials: %w", err)
76 }
77 if t, err = t.ParseFS(chrome.Templates(), "templates/*.html"); err != nil {
78 return nil, fmt.Errorf("parse chrome partials: %w", err)
79 }
80 if t, err = t.ParseFS(templatesFS, f); err != nil {
81 return nil, fmt.Errorf("parse %s: %w", f, err)
82 }
83 r.pages[name] = t
84 }
85 if len(r.pages) == 0 {
86 return nil, fmt.Errorf("no page templates found")
87 }
88 return r, nil
89}
90
91// render writes one page in the request's chrome (chrome.go).
92func (a *App) render(w http.ResponseWriter, r *http.Request, status int, page string, data any) {
93 a.renderPage(w, r, status, page, data)
94}
95
96// renderPage writes one page.
97//
98// It renders to a buffer first. A template that fails halfway through
99// would otherwise have already written a 200 and half a page, and the
100// error handler could only append to it — so the status code would say
101// the opposite of what happened. A page that fails is answered with the
102// 500 page; if that one fails too, with plain text.
103func (a *App) renderPage(w http.ResponseWriter, r *http.Request, status int, name string, data any) {
104 t, ok := a.renderer.pages[name]
105 if !ok {
106 a.logger.Error("no such page template", "page", name)
107 a.renderFailed(w, r, name)
108 return
109 }
110 var buf bytes.Buffer
111 if err := t.ExecuteTemplate(&buf, "layout", a.pageFor(r, status, data, doorPages[name], hopPages[name])); err != nil {
112 a.logger.Error("render", "page", name, "err", err)
113 a.renderFailed(w, r, name)
114 return
115 }
116 w.Header().Set("Content-Type", "text/html; charset=utf-8")
117 w.WriteHeader(status)
118 if _, err := buf.WriteTo(w); err != nil {
119 a.logger.Warn("write response", "page", name, "err", err)
120 }
121}
122
123// renderFailed answers a page that could not be drawn: the 500 page,
124// unless the page that failed was the error page itself.
125func (a *App) renderFailed(w http.ResponseWriter, r *http.Request, name string) {
126 if name == "error.html" || r == nil {
127 http.Error(w, "internal error", http.StatusInternalServerError)
128 return
129 }
130 a.serverError(w, r)
131}
132