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/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 }