Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/web/templates.go

Line attribution on main · 106 line(s)

653e0cfc
Keenan Nemetz
18d ago
1 package web
2
f09ef39e
Keenan Nemetz
15d ago
3 // Templates and static assets are embedded so an instance is still one file.
c717d4eb
Keenan Nemetz
13d ago
4 //
5 // Page templates are split by surface under templates/, one file per destination
6 // (issues.html, requests.html, code.html). A page that HTMX can replace in part
7 // keeps its fragments in the same file, so a fragment and the element it swaps
8 // are read together. Four files hold what more than one surface uses:
9 //
10 // - chrome.html: the document shell, global bar, repository header, error page
11 // - components.html: the label/assignee token input, the commit list, the
12 // file tree, and the pager
13 // - diff.html: the diff table, including its inline review comments
14 // - role-picker.html: the per-account role control the settings surfaces use
15 //
16 // All page templates are parsed into one namespace at startup (web.New), so a
17 // template name must be unique across the whole set: prefix a fragment with the
18 // surface it belongs to (issue_, request_, code_, review_) rather than naming it
19 // after the element it renders. A fragment that both surfaces render keeps the
20 // shared name instead (comment_thread, comment_card, tree_nodes, pager).
21 //
ed03b3d1
Keenan Nemetz
3h ago
22 // Static assets are served from /-/static/ and never inlined into a template. The
c717d4eb
Keenan Nemetz
13d ago
23 // passkey script is a static asset too; it runs under the same CSP as the rest.
796ac8c0
Keenan Nemetz
11d ago
24 // theme.js is loaded before the stylesheet on purpose: it applies the saved
25 // Everforest variant, or the system light/dark medium default, to <html> before
26 // the first paint. The account page owns the picker.
c8fa5759
Keenan Nemetz
13d ago
27 //
306f36fc
Keenan Nemetz
12d ago
28 // Scripts vendored beside the assets that load them: htmx.min.js is loaded by the
1b8675b1
Keenan Nemetz
8d ago
29 // Shell on every page; prism.min.js, prism-manifest.js and the grammars under
30 // prism-languages/ are loaded by prism-worker.js, which app.js starts only on a
31 // page with a blob to colour, and which imports a grammar only when a blob names
8032d15a
Keenan Nemetz
6d ago
32 // that language.
f09ef39e
Keenan Nemetz
15d ago
33
34 import (
306f36fc
Keenan Nemetz
12d ago
35 "bytes"
36 "crypto/sha256"
f09ef39e
Keenan Nemetz
15d ago
37 "embed"
306f36fc
Keenan Nemetz
12d ago
38 "encoding/hex"
f09ef39e
Keenan Nemetz
15d ago
39 "io/fs"
40 "net/http"
306f36fc
Keenan Nemetz
12d ago
41 "path"
42 "strings"
43 "time"
f09ef39e
Keenan Nemetz
15d ago
44 )
45
46 //go:embed templates/*.html
47 var templateFS embed.FS
48
49 //go:embed static/*
50 var staticFS embed.FS
51
796ac8c0
Keenan Nemetz
11d ago
52 // Static serves the embedded stylesheet, favicon, fonts, and scripts. Assets are
f09ef39e
Keenan Nemetz
15d ago
53 // compiled into the binary, so there is no directory to deploy beside it.
796ac8c0
Keenan Nemetz
11d ago
54 // Lexend and JetBrains Mono live under static/fonts with their OFL licenses.
306f36fc
Keenan Nemetz
12d ago
55 //
56 // Each file carries a strong ETag derived from its bytes. An embedded file has
57 // no modification time, so without a validator a browser re-downloads the
58 // stylesheet and the Prism bundle on every code page; with one it revalidates
59 // for a 304. Cache-Control is no-cache rather than immutable because a URL does
60 // not change when the binary is upgraded.
6ce635a8
Keenan Nemetz
1d ago
61 func Static() http.Handler { return embeddedStatic }
62
63 var embeddedStatic = newStatic()
64
65 func newStatic() http.Handler {
f09ef39e
Keenan Nemetz
15d ago
66 sub, err := fs.Sub(staticFS, "static")
67 if err != nil {
68 panic(err)
69 }
306f36fc
Keenan Nemetz
12d ago
70 files := map[string]staticFile{}
71 err = fs.WalkDir(sub, ".", func(p string, d fs.DirEntry, err error) error {
72 if err != nil || d.IsDir() {
73 return err
74 }
75 body, err := fs.ReadFile(sub, p)
76 if err != nil {
77 return err
78 }
79 sum := sha256.Sum256(body)
80 files[p] = staticFile{body: body, etag: `"` + hex.EncodeToString(sum[:12]) + `"`}
81 return nil
82 })
83 if err != nil {
84 panic(err)
85 }
86 return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ed03b3d1
Keenan Nemetz
3h ago
87 f, ok := files[strings.TrimPrefix(r.URL.Path, "/-/static/")]
306f36fc
Keenan Nemetz
12d ago
88 if !ok {
89 // No directory listing and no path traversal: the embedded set is the
90 // whole surface, and a miss is a 404.
91 http.NotFound(rw, r)
92 return
93 }
94 rw.Header().Set("ETag", f.etag)
95 rw.Header().Set("Cache-Control", "no-cache")
96 // ServeContent answers If-None-Match from the ETag above and picks the
97 // content type from the name.
98 http.ServeContent(rw, r, path.Base(r.URL.Path), time.Time{}, bytes.NewReader(f.body))
99 })
100 }
101
102 // staticFile is one embedded asset and the validator that names its bytes.
103 type staticFile struct {
104 body []byte
105 etag string
653e0cfc
Keenan Nemetz
18d ago
106 }