~bigbes/sr-ht-spec

ref: 53e56db27ab35117d0c2a91f15533f7dd528612c sr-ht-spec/web/templates.go -rw-r--r-- 4.7 KiB
53e56db2 — Eugene Blikh web: draw the chrome from sr-ht-ecore 9 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package web

import (
	"bytes"
	"embed"
	"html/template"
	"log"
	"net/http"
	"strings"

	"sourcecraft.dev/bigbes/sr-ht-ecore/chrome"
)

// tmplFS holds the page templates. Each page is parsed together with the shared
// layout into its own template set so that per-page "content" defines do not
// collide across pages.
//
//go:embed templates/*.html
var tmplFS embed.FS

// staticFS holds the built assets: the hashed stylesheet produced by `make css`
// and the logo. It is compiled into the binary, which is why `make css` alone
// does not restyle a running daemon — see the package doc.
//
//go:embed static
var staticFS embed.FS

// funcMap holds the template helpers available to every page.
//
// It starts from chrome.Funcs — the generic helpers every custom service on
// this instance was carrying its own copy of, `shortsha` among them — and adds
// this service's own on top, after, so that a name may be shadowed
// deliberately rather than by accident of map ordering. Nothing shadows one
// today, and a helper that diverged from the shared spelling of the same name
// would be the drift ecore exists to prevent.
var funcMap = func() template.FuncMap {
	m := chrome.Funcs()

	// indent renders a tree depth as non-breaking space, so the space view's
	// hierarchy reads as a hierarchy without a nested-list template recursion.
	// A negative depth (a level-1 heading, once decremented) indents nothing.
	m["indent"] = func(depth int) template.HTML {
		if depth <= 0 {
			return ""
		}
		return template.HTML(strings.Repeat("&nbsp;&nbsp;&nbsp;&nbsp;", depth))
	}
	// dec turns a 1-based heading level into a 0-based indent depth.
	m["dec"] = func(n int) int { return n - 1 }

	return m
}()

// pageNames are the content templates; each is parsed with layout.html.
var pageNames = []string{"index", "space", "document", "search", "error", "proposal", "inbox"}

// pages maps a page name to its parsed template set (layout + the shared
// chrome partials + local partials + that page). threads.html is parsed into
// every set rather than only into the proposal page's: it defines review-thread
// markup and nothing else, and a partial that only some sets know about is a
// lookup that fails on the page that later needs it. ecore's "srht-nav" and
// "srht-env-banner" are attached to every set for the same reason, and through
// MustAttach because a set that cannot draw the chrome is not a page this
// binary should start serving.
var pages = func() map[string]*template.Template {
	m := make(map[string]*template.Template, len(pageNames))
	for _, name := range pageNames {
		t := chrome.MustAttach(template.New("layout.html").Funcs(funcMap))
		t = template.Must(t.ParseFS(tmplFS,
			"templates/layout.html", "templates/threads.html", "templates/"+name+".html"))
		m[name] = t
	}
	return m
}()

// blockThreadsTmpl is the per-block comment markup, taken out of the proposal
// page's own set so the diff renderer — which builds its HTML in Go and cannot
// reach a page template through the usual {{template}} call — and the page
// itself cannot drift into two spellings of a thread.
//
// A missing define is a build-time mistake in this package, so it panics at
// init the way template.Must does, rather than yielding a page with the
// comments silently absent.
var blockThreadsTmpl = func() *template.Template {
	t := pages["proposal"].Lookup("blockthreads")
	if t == nil {
		panic(`web: templates/threads.html does not define "blockthreads"`)
	}
	return t
}()

// render executes a page into a buffer first, so a template error yields a
// clean 500 rather than a half-written response. On success it writes the
// status and the buffered HTML.
func (s *Server) render(w http.ResponseWriter, status int, page string, vd viewData) {
	t, ok := pages[page]
	if !ok {
		log.Printf("web: unknown template page %q", page)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	var buf bytes.Buffer
	if err := t.ExecuteTemplate(&buf, "layout.html", vd); err != nil {
		log.Printf("web: executing template %q: %v", page, err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(status)
	_, _ = buf.WriteTo(w)
}

// errorData is the payload of the error page.
type errorData struct {
	Status     int
	StatusText string
	Message    string
}

// renderError renders the chrome-wrapped error page. It never recurses into
// render on failure (render falls back to http.Error itself).
func (s *Server) renderError(w http.ResponseWriter, r *http.Request, status int, message string) {
	vd := s.view(r, http.StatusText(status))
	vd.Data = errorData{
		Status:     status,
		StatusText: http.StatusText(status),
		Message:    message,
	}
	s.render(w, status, "error", vd)
}