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/static/vole.css +41−57
| @@ -18,7 +18,7 @@ | |||
| 18 | 18 | repository overview summary page: main column and sidebar | |
| 19 | 19 | code tab bar and crumbs the bar above the open file | |
| 20 | 20 | code browser and file tree the persistent tree beside the pane | |
| 21 | syntax highlighting chroma classes | ||
| 21 | syntax highlighting prism token classes | ||
| 22 | 22 | repository overview sidebar compact commit and run rows | |
| 23 | 23 | run detail summary cards, step timing and lazy logs | |
| 24 | 24 | issues and changes list pages, thread, forms, metadata sidebar | |
| @@ -943,7 +943,6 @@ td.fit { width: 1%; white-space: nowrap; } | |||
| 943 | 943 | .code-tab-actions a.active { color: var(--accent); } | |
| 944 | 944 | .code-tab-actions .muted { font-size: 11.5px; } | |
| 945 | 945 | .code-tabbar + .panel, | |
| 946 | .code-tabbar + .highlight, | ||
| 947 | 946 | .code-tabbar + .filebody, | |
| 948 | 947 | .code-tabbar + .card, | |
| 949 | 948 | .code-tabbar + .readme { | |
| @@ -1062,7 +1061,6 @@ details[open] > summary .tree-chevron { transform: rotate(90deg); } | |||
| 1062 | 1061 | .code-main .filebody { max-width: none; } | |
| 1063 | 1062 | .code-main .readme > *, | |
| 1064 | 1063 | .code-main .filebody > * { max-width: none; } | |
| 1065 | .code-main .highlight, | ||
| 1066 | 1064 | .code-main .blob { max-height: calc(100vh - 240px); } | |
| 1067 | 1065 | .empty-code { margin-top: 0; } | |
| 1068 | 1066 | ||
| @@ -1078,7 +1076,6 @@ details[open] > summary .tree-chevron { transform: rotate(90deg); } | |||
| 1078 | 1076 | @media (max-width: 760px) { | |
| 1079 | 1077 | .code-browser { grid-template-columns: 1fr; } | |
| 1080 | 1078 | .file-tree { position: static; max-height: 260px; } | |
| 1081 | .code-main .highlight, | ||
| 1082 | 1079 | .code-main .blob { max-height: none; } | |
| 1083 | 1080 | } | |
| 1084 | 1081 | ||
| @@ -1127,58 +1124,45 @@ details[open] > summary .tree-chevron { transform: rotate(90deg); } | |||
| 1127 | 1124 | ||
| 1128 | 1125 | /* ---------- syntax highlighting ---------- */ | |
| 1129 | 1126 | ||
| 1130 | .highlight { | ||
| 1131 | border: 1px solid var(--border); | ||
| 1132 | border-radius: var(--radius-lg); | ||
| 1133 | background: var(--bg-panel); | ||
| 1134 | overflow: auto; | ||
| 1135 | max-height: 76vh; | ||
| 1136 | margin: 12px 0 20px; | ||
| 1137 | } | ||
| 1138 | .highlight pre.chroma { | ||
| 1139 | margin: 0; | ||
| 1140 | padding: 12px 0; | ||
| 1141 | border: 0; | ||
| 1142 | background: transparent; | ||
| 1143 | font-family: var(--mono); | ||
| 1144 | font-size: 12px; | ||
| 1145 | line-height: 19px; | ||
| 1146 | overflow: visible; | ||
| 1147 | } | ||
| 1148 | .chroma .line { display: block; } | ||
| 1149 | .chroma .ln { | ||
| 1150 | display: inline-block; | ||
| 1151 | width: 3.5rem; | ||
| 1152 | padding-right: 8px; | ||
| 1153 | margin-right: 12px; | ||
| 1154 | text-align: right; | ||
| 1155 | color: var(--fg-faint); | ||
| 1156 | user-select: none; | ||
| 1157 | border-right: 1px solid var(--border); | ||
| 1158 | } | ||
| 1159 | .chroma .cl { white-space: pre; } | ||
| 1160 | .chroma .w { color: inherit; } | ||
| 1161 | |||
| 1162 | .chroma .c, .chroma .ch, .chroma .cm, .chroma .c1, .chroma .cs, | ||
| 1163 | .chroma .cp, .chroma .cpf { color: #6f7682; font-style: italic; } | ||
| 1164 | .chroma .k, .chroma .kc, .chroma .kd, .chroma .kn, .chroma .kp, | ||
| 1165 | .chroma .kr, .chroma .kt { color: #c792ea; } | ||
| 1166 | .chroma .s, .chroma .sa, .chroma .sb, .chroma .sc, .chroma .dl, | ||
| 1167 | .chroma .sd, .chroma .s2, .chroma .se, .chroma .sh, .chroma .si, | ||
| 1168 | .chroma .sx, .chroma .sr, .chroma .s1, .chroma .ss { color: #a8d98a; } | ||
| 1169 | .chroma .m, .chroma .mb, .chroma .mf, .chroma .mh, .chroma .mi, | ||
| 1170 | .chroma .il, .chroma .mo { color: #f2a06b; } | ||
| 1171 | .chroma .nf, .chroma .fm { color: #83aaff; } | ||
| 1172 | .chroma .nc, .chroma .nn, .chroma .ne, .chroma .nd { color: #f0c674; } | ||
| 1173 | .chroma .nb, .chroma .bp { color: #7fd3e8; } | ||
| 1174 | .chroma .o, .chroma .ow { color: #7fd3e8; } | ||
| 1175 | .chroma .p, .chroma .pi { color: #a6accd; } | ||
| 1176 | .chroma .nt { color: #c792ea; } | ||
| 1177 | .chroma .na { color: #f0c674; } | ||
| 1178 | .chroma .nv, .chroma .vc, .chroma .vg, .chroma .vi, .chroma .vm { color: #e7e8ea; } | ||
| 1179 | .chroma .gd { color: var(--bad); } | ||
| 1180 | .chroma .gi { color: var(--good); } | ||
| 1181 | .chroma .err { color: var(--bad); text-decoration: underline; } | ||
| 1127 | /* static/prism-worker.js writes Prism token classes into the cells after the | ||
| 1128 | page paints. The palette is the same one the rest of the surface uses, so a | ||
| 1129 | token is a colour, never a font switch except for comments and italics. */ | ||
| 1130 | |||
| 1131 | .blob .token.comment, | ||
| 1132 | .blob .token.prolog, | ||
| 1133 | .blob .token.doctype, | ||
| 1134 | .blob .token.cdata { color: #6f7682; font-style: italic; } | ||
| 1135 | .blob .token.punctuation { color: #a6accd; } | ||
| 1136 | .blob .token.namespace { color: #a6accd; } | ||
| 1137 | .blob .token.operator, | ||
| 1138 | .blob .token.entity, | ||
| 1139 | .blob .token.url { color: #7fd3e8; } | ||
| 1140 | .blob .token.boolean, | ||
| 1141 | .blob .token.number, | ||
| 1142 | .blob .token.constant, | ||
| 1143 | .blob .token.symbol { color: #f2a06b; } | ||
| 1144 | .blob .token.string, | ||
| 1145 | .blob .token.char, | ||
| 1146 | .blob .token.attr-value, | ||
| 1147 | .blob .token.regex { color: #a8d98a; } | ||
| 1148 | .blob .token.atrule, | ||
| 1149 | .blob .token.keyword, | ||
| 1150 | .blob .token.tag, | ||
| 1151 | .blob .token.selector { color: #c792ea; } | ||
| 1152 | .blob .token.function, | ||
| 1153 | .blob .token.function-name, | ||
| 1154 | .blob .token.method { color: #83aaff; } | ||
| 1155 | .blob .token.class-name, | ||
| 1156 | .blob .token.builtin, | ||
| 1157 | .blob .token.attr-name, | ||
| 1158 | .blob .token.property, | ||
| 1159 | .blob .token.title { color: #f0c674; } | ||
| 1160 | .blob .token.variable { color: #e7e8ea; } | ||
| 1161 | .blob .token.deleted { color: var(--bad); } | ||
| 1162 | .blob .token.inserted { color: var(--good); } | ||
| 1163 | .blob .token.important, | ||
| 1164 | .blob .token.bold { font-weight: 600; } | ||
| 1165 | .blob .token.italic { font-style: italic; } | ||
| 1182 | 1166 | ||
| 1183 | 1167 | /* ---------- repository overview sidebar ---------- */ | |
| 1184 | 1168 | ||
| @@ -1613,7 +1597,7 @@ details[open] > summary .tree-chevron { transform: rotate(90deg); } | |||
| 1613 | 1597 | .reply-target strong { color: var(--fg); } | |
| 1614 | 1598 | ||
| 1615 | 1599 | /* An anchored comment can show the lines it was written about. The details | |
| 1616 | element keeps the conversation compact; nothing here needs JavaScript. */ | ||
| 1600 | element keeps the conversation compact. */ | ||
| 1617 | 1601 | .comment-snippet { | |
| 1618 | 1602 | margin: 0 14px 12px; | |
| 1619 | 1603 | border: 1px solid var(--border); | |