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.
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/compress.go +188
| @@ -0,0 +1,188 @@ | |||
| 1 | package web | ||
| 2 | |||
| 3 | // gzip response compression. | ||
| 4 | // | ||
| 5 | // The web surface sends HTML, CSS, JavaScript and source blobs, all of which | ||
| 6 | // compress to a fraction of their size: the vendored Prism bundle alone is | ||
| 7 | // 545 KB and lands near 200 KB gzipped, and a rendered page about a fifth of its | ||
| 8 | // size. There is no brotli here on purpose — it would add a dependency for a few | ||
| 9 | // points of ratio, and Go's gzip needs none. | ||
| 10 | // | ||
| 11 | // The rules are narrow, because the same handler chain also streams git | ||
| 12 | // responses, release archives and LFS objects: | ||
| 13 | // | ||
| 14 | // - only when the client sent `Accept-Encoding: gzip` with q > 0; | ||
| 15 | // - only text-like content types, never a body that is already encoded; | ||
| 16 | // - never a Range request (the compressed body has no meaningful ranges); | ||
| 17 | // - never a body the handler has already sized below compressMinBytes; | ||
| 18 | // - never a 1xx, 204, 206, 304 or redirect (nothing to gain, and 304 must | ||
| 19 | // stay bodiless). | ||
| 20 | // | ||
| 21 | // The writer is transparent otherwise: it passes Header through, keeps Flush | ||
| 22 | // working for the streaming handlers, and exposes Unwrap so a handler can reach | ||
| 23 | // the underlying ResponseWriter. | ||
| 24 | |||
| 25 | import ( | ||
| 26 | "compress/gzip" | ||
| 27 | "net/http" | ||
| 28 | "strconv" | ||
| 29 | "strings" | ||
| 30 | ) | ||
| 31 | |||
| 32 | // compressMinBytes is the smallest declared body worth a gzip header and | ||
| 33 | // trailer. Below it the compressed form is usually larger than the source. | ||
| 34 | const compressMinBytes = 256 | ||
| 35 | |||
| 36 | // compressor decides per response whether to gzip, and lazily starts the stream | ||
| 37 | // on the first write so a header-only response never opens one. | ||
| 38 | type compressor struct { | ||
| 39 | rw http.ResponseWriter | ||
| 40 | r *http.Request | ||
| 41 | gz *gzip.Writer | ||
| 42 | status int | ||
| 43 | wroteHeader bool | ||
| 44 | gzipping bool | ||
| 45 | } | ||
| 46 | |||
| 47 | func newCompressor(rw http.ResponseWriter, r *http.Request) *compressor { | ||
| 48 | return &compressor{rw: rw, r: r} | ||
| 49 | } | ||
| 50 | |||
| 51 | func (c *compressor) Header() http.Header { return c.rw.Header() } | ||
| 52 | |||
| 53 | func (c *compressor) WriteHeader(status int) { | ||
| 54 | if c.wroteHeader { | ||
| 55 | return | ||
| 56 | } | ||
| 57 | c.wroteHeader = true | ||
| 58 | c.status = status | ||
| 59 | h := c.rw.Header() | ||
| 60 | if c.acceptsGzip() && compressibleStatus(status) && compressibleHeader(h, c.r) { | ||
| 61 | c.gzipping = true | ||
| 62 | h.Set("Content-Encoding", "gzip") | ||
| 63 | h.Del("Content-Length") | ||
| 64 | h.Add("Vary", "Accept-Encoding") | ||
| 65 | } | ||
| 66 | c.rw.WriteHeader(status) | ||
| 67 | } | ||
| 68 | |||
| 69 | func (c *compressor) Write(b []byte) (int, error) { | ||
| 70 | if !c.wroteHeader { | ||
| 71 | c.WriteHeader(http.StatusOK) | ||
| 72 | } | ||
| 73 | if !c.gzipping { | ||
| 74 | return c.rw.Write(b) | ||
| 75 | } | ||
| 76 | if c.gz == nil { | ||
| 77 | c.gz = gzip.NewWriter(c.rw) | ||
| 78 | } | ||
| 79 | return c.gz.Write(b) | ||
| 80 | } | ||
| 81 | |||
| 82 | // Flush keeps streaming handlers working: the header is settled, the gzip | ||
| 83 | // buffer is emptied, and the underlying writer is flushed if it can be. | ||
| 84 | func (c *compressor) Flush() { | ||
| 85 | if !c.wroteHeader { | ||
| 86 | c.WriteHeader(http.StatusOK) | ||
| 87 | } | ||
| 88 | if c.gz != nil { | ||
| 89 | _ = c.gz.Flush() | ||
| 90 | } | ||
| 91 | if f, ok := c.rw.(http.Flusher); ok { | ||
| 92 | f.Flush() | ||
| 93 | } | ||
| 94 | } | ||
| 95 | |||
| 96 | // Close finishes the gzip stream. It is called once by the middleware after the | ||
| 97 | // handler returns, never by a handler, but it is idempotent enough for the | ||
| 98 | // panic path that may have written nothing at all. | ||
| 99 | func (c *compressor) Close() { | ||
| 100 | if c.gz != nil { | ||
| 101 | _ = c.gz.Close() | ||
| 102 | c.gz = nil | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | func (c *compressor) Unwrap() http.ResponseWriter { return c.rw } | ||
| 107 | |||
| 108 | // acceptsGzip reports whether the client asked for gzip and would accept it: | ||
| 109 | // `gzip` with a q-value of zero is a refusal, not a request. | ||
| 110 | func (c *compressor) acceptsGzip() bool { | ||
| 111 | if c.r.Method == http.MethodHead { | ||
| 112 | return false | ||
| 113 | } | ||
| 114 | for _, part := range strings.Split(c.r.Header.Get("Accept-Encoding"), ",") { | ||
| 115 | name, params, _ := strings.Cut(strings.TrimSpace(part), ";") | ||
| 116 | if !strings.EqualFold(strings.TrimSpace(name), "gzip") { | ||
| 117 | continue | ||
| 118 | } | ||
| 119 | q := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(params), "q=")) | ||
| 120 | if q == "" { | ||
| 121 | return true | ||
| 122 | } | ||
| 123 | if v, err := strconv.ParseFloat(q, 64); err == nil { | ||
| 124 | return v > 0 | ||
| 125 | } | ||
| 126 | return true | ||
| 127 | } | ||
| 128 | return false | ||
| 129 | } | ||
| 130 | |||
| 131 | // compressibleStatus covers the bodies worth compressing: a rendered page, a | ||
| 132 | // fragment, and the styled error page. Redirects are a sentence, a 304 must stay | ||
| 133 | // bodiless, and a 206 is already a byte range. | ||
| 134 | func compressibleStatus(status int) bool { | ||
| 135 | switch status { | ||
| 136 | case http.StatusNoContent, http.StatusNotModified, http.StatusPartialContent: | ||
| 137 | return false | ||
| 138 | } | ||
| 139 | return (status >= 200 && status < 300) || status >= 400 | ||
| 140 | } | ||
| 141 | |||
| 142 | // compressibleHeader applies the remaining rules: a body that is already | ||
| 143 | // encoded stays as it is, a Range request is answered with bytes, and a body | ||
| 144 | // the handler already sized as tiny is not worth compressing. | ||
| 145 | func compressibleHeader(h http.Header, r *http.Request) bool { | ||
| 146 | if h.Get("Content-Encoding") != "" || h.Get("Content-Range") != "" { | ||
| 147 | return false | ||
| 148 | } | ||
| 149 | // A handler that promised exact bytes (a raw download with its Content-Length) | ||
| 150 | // opts out with the standard directive rather than a private header. | ||
| 151 | if strings.Contains(strings.ToLower(h.Get("Cache-Control")), "no-transform") { | ||
| 152 | return false | ||
| 153 | } | ||
| 154 | if r.Header.Get("Range") != "" { | ||
| 155 | return false | ||
| 156 | } | ||
| 157 | if n, err := strconv.Atoi(h.Get("Content-Length")); err == nil && n < compressMinBytes { | ||
| 158 | return false | ||
| 159 | } | ||
| 160 | return compressibleType(h.Get("Content-Type")) | ||
| 161 | } | ||
| 162 | |||
| 163 | // compressibleType is the allow-list of types worth gzipping. Anything else — | ||
| 164 | // images, archives, packs, LFS objects, git protocol output — is already | ||
| 165 | // compressed or is a byte range where compression cannot help. | ||
| 166 | func compressibleType(contentType string) bool { | ||
| 167 | ct := strings.ToLower(strings.TrimSpace(contentType)) | ||
| 168 | if i := strings.IndexByte(ct, ';'); i >= 0 { | ||
| 169 | ct = strings.TrimSpace(ct[:i]) | ||
| 170 | } | ||
| 171 | switch ct { | ||
| 172 | case "text/event-stream": | ||
| 173 | return false | ||
| 174 | case "image/svg+xml", | ||
| 175 | "application/json", | ||
| 176 | "application/javascript", | ||
| 177 | "application/x-javascript", | ||
| 178 | "application/xml", | ||
| 179 | "application/xhtml+xml", | ||
| 180 | "application/manifest+json", | ||
| 181 | "application/rss+xml", | ||
| 182 | "application/atom+xml": | ||
| 183 | return true | ||
| 184 | } | ||
| 185 | return strings.HasPrefix(ct, "text/") || | ||
| 186 | strings.HasSuffix(ct, "+json") || | ||
| 187 | strings.HasSuffix(ct, "+xml") | ||
| 188 | } | ||