vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Highlight with Prism in a worker and gzip text responses
306f36fcef89c145abcd0c5331453d0b179f37ce · Keenan Nemetz · 12d ago · parent e5542ac6
Highlight with Prism in a worker and gzip text responses
The code browser now names a blob's language and lets the browser colour it.
Chroma is gone: server/web/highlight.go is an extension table, and
static/prism-worker.js runs Prism — every language Prism ships — off the main
thread. The server sends escaped lines with their numbers and L# anchors, so a
page is complete and scrollable before any script runs and the worker only swaps
the contents of each line cell. The table covers 186 languages; a file it does
not claim stays plain rather than being coloured by a guess.
The bundle those grammars make is 545 KB, so the instance now gzips text
responses itself instead of relying on a proxy, and static assets carry a
content ETag so a browser revalidates with a 304 rather than re-downloading the
bundle on every code page. The raw-download route opts out with
Cache-Control: no-transform because it promises an exact Content-Length.
This also makes JavaScript a requirement rather than a nicety: the label and
assignee token inputs lose their noscript save buttons and their comma-separated
text fallback, and the docs say so.
Measured: release binary 26.08 MB -> 22.70 MB; /r/{repo}/code 11.1 KB -> 2.6 KB
over the wire; the Prism bundle 545 KB -> 206 KB gzipped, fetched once.
scripts/vendor-prism.py regenerates the bundle from Prism's components.json
require graph, and --check fails if the committed assets drift.
server/web/templates.go +55−6
| @@ -22,15 +22,23 @@ package web | |||
| 22 | 22 | // Static assets are served from /static/ and never inlined into a template. The | |
| 23 | 23 | // passkey script is a static asset too; it runs under the same CSP as the rest. | |
| 24 | 24 | // | |
| 25 | // Two scripts are vendored beside the assets that load them. htmx.min.js is loaded | ||
| 26 | // by the shell on every page. confetti.browser.js (canvas-confetti 1.9.3, ISC, see | ||
| 27 | // static/canvas-confetti.LICENSE) is not: app.js fetches it only on a page that a | ||
| 28 | // landing redirected to, so the rest of the instance never pays for it. | ||
| 25 | // Scripts vendored beside the assets that load them: htmx.min.js is loaded by the | ||
| 26 | // shell on every page; prism.min.js and prism-languages.min.js (every language | ||
| 27 | // Prism ships) are loaded by prism-worker.js, which app.js starts only on a page | ||
| 28 | // with a blob to colour; confetti.browser.js (canvas-confetti 1.9.3, ISC, see | ||
| 29 | // static/canvas-confetti.LICENSE) is fetched by app.js only on a page a landing | ||
| 30 | // redirected to, so the rest of the instance never pays for it. | ||
| 29 | 31 | ||
| 30 | 32 | import ( | |
| 33 | "bytes" | ||
| 34 | "crypto/sha256" | ||
| 31 | 35 | "embed" | |
| 36 | "encoding/hex" | ||
| 32 | 37 | "io/fs" | |
| 33 | 38 | "net/http" | |
| 39 | "path" | ||
| 40 | "strings" | ||
| 41 | "time" | ||
| 34 | 42 | ) | |
| 35 | 43 | ||
| 36 | 44 | //go:embed templates/*.html | |
| @@ -39,12 +47,53 @@ var templateFS embed.FS | |||
| 39 | 47 | //go:embed static/* | |
| 40 | 48 | var staticFS embed.FS | |
| 41 | 49 | ||
| 42 | // Static serves the embedded stylesheet, favicon, and small scripts. Assets are | ||
| 50 | // Static serves the embedded stylesheet, favicon, and scripts. Assets are | ||
| 43 | 51 | // compiled into the binary, so there is no directory to deploy beside it. | |
| 52 | // | ||
| 53 | // Each file carries a strong ETag derived from its bytes. An embedded file has | ||
| 54 | // no modification time, so without a validator a browser re-downloads the | ||
| 55 | // stylesheet and the Prism bundle on every code page; with one it revalidates | ||
| 56 | // for a 304. Cache-Control is no-cache rather than immutable because a URL does | ||
| 57 | // not change when the binary is upgraded. | ||
| 44 | 58 | func Static() http.Handler { | |
| 45 | 59 | sub, err := fs.Sub(staticFS, "static") | |
| 46 | 60 | if err != nil { | |
| 47 | 61 | panic(err) | |
| 48 | 62 | } | |
| 49 | return http.StripPrefix("/static/", http.FileServerFS(sub)) | ||
| 63 | files := map[string]staticFile{} | ||
| 64 | err = fs.WalkDir(sub, ".", func(p string, d fs.DirEntry, err error) error { | ||
| 65 | if err != nil || d.IsDir() { | ||
| 66 | return err | ||
| 67 | } | ||
| 68 | body, err := fs.ReadFile(sub, p) | ||
| 69 | if err != nil { | ||
| 70 | return err | ||
| 71 | } | ||
| 72 | sum := sha256.Sum256(body) | ||
| 73 | files[p] = staticFile{body: body, etag: `"` + hex.EncodeToString(sum[:12]) + `"`} | ||
| 74 | return nil | ||
| 75 | }) | ||
| 76 | if err != nil { | ||
| 77 | panic(err) | ||
| 78 | } | ||
| 79 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||
| 80 | f, ok := files[strings.TrimPrefix(r.URL.Path, "/static/")] | ||
| 81 | if !ok { | ||
| 82 | // No directory listing and no path traversal: the embedded set is the | ||
| 83 | // whole surface, and a miss is a 404. | ||
| 84 | http.NotFound(rw, r) | ||
| 85 | return | ||
| 86 | } | ||
| 87 | rw.Header().Set("ETag", f.etag) | ||
| 88 | rw.Header().Set("Cache-Control", "no-cache") | ||
| 89 | // ServeContent answers If-None-Match from the ETag above and picks the | ||
| 90 | // content type from the name. | ||
| 91 | http.ServeContent(rw, r, path.Base(r.URL.Path), time.Time{}, bytes.NewReader(f.body)) | ||
| 92 | }) | ||
| 93 | } | ||
| 94 | |||
| 95 | // staticFile is one embedded asset and the validator that names its bytes. | ||
| 96 | type staticFile struct { | ||
| 97 | body []byte | ||
| 98 | etag string | ||
| 50 | 99 | } | |