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.

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/highlight.go +141−32

@@ -1,41 +1,150 @@
1 1 package web
2 2
3 // Server-side syntax highlighting for source blobs. Chroma is a library, not a
4 // browser asset: the page ships coloured HTML and no highlighting script, which
5 // keeps code browsing fast and works with JavaScript disabled.
3 // The code browser does not highlight; it names a blob's language and the
4 // browser tokenises it (static/prism-worker.js). So the server carries no lexer
5 // and does no lexing work per blob, and a file whose language is not in the
6 // table below is served as plain text with its line numbers — the honest result
7 // for a file we cannot classify.
6 8
7 9 import (
8 "bytes"
9 "html/template"
10
11 "github.com/alecthomas/chroma/v2"
12 "github.com/alecthomas/chroma/v2/formatters/html"
13 "github.com/alecthomas/chroma/v2/lexers"
14 "github.com/alecthomas/chroma/v2/styles"
10 "path"
11 "strings"
15 12 )
16 13
17 // highlightCode returns coloured HTML for a source file, or "" when no lexer
18 // recognises it. The caller falls back to a plain, line-numbered view, which is
19 // the honest result for a file we cannot classify.
20 func highlightCode(path, src string) template.HTML {
21 lexer := lexers.Match(path)
22 if lexer == nil {
23 lexer = lexers.Analyse(src)
24 }
25 if lexer == nil {
26 return ""
27 }
28 lexer = chroma.Coalesce(lexer)
29 it, err := lexer.Tokenise(nil, src)
30 if err != nil {
31 return ""
14 // prismLanguages maps a lower-case file extension (without the dot) or a whole
15 // lower-case file name to the Prism language that colours it. Prism has no file
16 // extension table of its own, so this is the work its grammars cannot do: every
17 // language in static/prism-languages.min.js is reachable from here, and a name
18 // this table does not claim stays plain.
19 //
20 // Names are matched before extensions because a Dockerfile has no extension and
21 // nginx.conf would otherwise be read as an ini file. Where two languages want
22 // the same extension the more common one on a forge wins: `.v` is Verilog,
23 // `.m` is Objective-C, `.h` is C, `.pp` is Puppet, `.cls` is LaTeX.
24 var prismLanguages = map[string]string{
25 // Systems and compiled languages.
26 "go": "go",
27 "c": "c", "h": "c",
28 "cc": "cpp", "cpp": "cpp", "cxx": "cpp", "hpp": "cpp", "hh": "cpp", "hxx": "cpp", "ino": "cpp",
29 "cs": "csharp",
30 "m": "objectivec", "mm": "objectivec",
31 "java": "java", "kt": "kotlin", "kts": "kotlin", "scala": "scala", "sc": "scala",
32 "groovy": "groovy", "gvy": "groovy", "gradle": "gradle",
33 "rs": "rust", "zig": "zig", "swift": "swift", "nix": "nix",
34 "d": "d", "nim": "nim", "nims": "nim", "odin": "odin", "cr": "crystal",
35 "ada": "ada", "adb": "ada", "ads": "ada",
36 "pas": "pascal", "p": "pascal", "dpr": "pascal", "pp": "puppet",
37 "f": "fortran", "for": "fortran", "f77": "fortran", "f90": "fortran", "f95": "fortran", "f03": "fortran",
38 "fs": "fsharp", "fsx": "fsharp", "fsi": "fsharp",
39 "ml": "ocaml", "mli": "ocaml",
40 "hs": "haskell", "lhs": "haskell",
41 "vala": "vala", "vapi": "vala",
42 "cobol": "cobol", "cbl": "cobol", "cpy": "cobol",
43 "eiffel": "eiffel", "e": "eiffel",
44 "dart": "dart", "elm": "elm", "purs": "purescript", "pure": "purebasic",
45 "hx": "haxe", "v": "verilog", "sv": "verilog", "svh": "verilog",
46 "vhd": "vhdl", "vhdl": "vhdl",
47 "wat": "wasm", "wasm": "wasm", "ll": "llvm", "cl": "opencl",
48 "glsl": "glsl", "vert": "glsl", "frag": "glsl", "geom": "glsl", "comp": "glsl",
49 "hlsl": "hlsl", "wgsl": "wgsl", "sol": "solidity",
50 "s": "armasm", "asm": "nasm", "nasm": "nasm",
51 "basic": "basic", "bb": "bbj", "vba": "vba", "bas": "visual-basic",
52
53 // Scripting and dynamic languages.
54 "js": "javascript", "mjs": "javascript", "cjs": "javascript", "jsx": "javascript",
55 "ts": "typescript", "tsx": "typescript", "mts": "typescript", "cts": "typescript",
56 "py": "python", "pyw": "python", "pyi": "python", "py3": "python",
57 "rb": "ruby", "rake": "ruby", "gemspec": "ruby",
58 "php": "php", "phtml": "php", "php3": "php", "php4": "php", "php5": "php", "php7": "php", "php8": "php",
59 "pl": "perl", "pm": "perl", "t": "perl",
60 "lua": "lua", "tcl": "tcl", "tm": "tcl",
61 "sh": "bash", "bash": "bash", "zsh": "bash", "ksh": "bash",
62 "fish": "bash", "bat": "batch", "cmd": "batch",
63 "ps1": "powershell", "psm1": "powershell", "psd1": "powershell",
64 "ex": "elixir", "exs": "elixir", "erl": "erlang", "hrl": "erlang",
65 "clj": "clojure", "cljs": "clojure", "cljc": "clojure", "edn": "clojure",
66 "scm": "scheme", "ss": "scheme", "rkt": "racket",
67 "lisp": "lisp", "lsp": "lisp", "el": "elisp",
68 "jl": "julia", "r": "r", "mata": "stata", "do": "stata", "ado": "stata", "sas": "sas",
69 "awk": "awk", "gawk": "awk", "coffee": "coffeescript", "ls": "livescript",
70 "vim": "vim", "sml": "sml", "sig": "sml", "fun": "sml",
71 "re": "reason", "rei": "reason", "res": "rescript", "resi": "rescript",
72 "robot": "robot", "rpy": "renpy", "gd": "gdscript", "nut": "squirrel",
73 "ahk": "autohotkey", "au3": "autoit", "applescript": "applescript", "scpt": "applescript",
74 "pro": "prolog", "prolog": "prolog", "rego": "rego", "dhall": "dhall",
75 "agda": "agda", "idr": "idris", "miz": "mizar", "oz": "oz", "factor": "factor",
76 "jolie": "jolie", "ol": "jolie", "iol": "jolie", "bbj": "bbj",
77 "bsl": "bsl", "os": "oscript", "jq": "jq", "gml": "gml",
78 "monkey": "monkey", "moon": "moonscript", "lol": "lolcode",
79 "bf": "brainfuck", "bqn": "bqn", "cook": "cooklang",
80 "inform7": "inform7", "ni": "inform7", "gcode": "gcode", "nc": "gcode",
81 "cue": "cue", "bicep": "bicep", "as": "actionscript",
82
83 // Markup, data, and configuration.
84 "html": "markup", "htm": "markup", "xhtml": "markup", "svg": "markup",
85 "xml": "markup", "xsl": "markup", "xslt": "markup", "xsd": "markup",
86 "rss": "markup", "atom": "markup", "plist": "markup", "mathml": "markup", "ssml": "markup",
87 "csproj": "markup", "fsproj": "markup", "vbproj": "markup", "props": "markup", "targets": "markup",
88 "nuspec": "markup", "rdf": "markup",
89 "css": "css", "scss": "scss", "sass": "sass", "less": "less", "styl": "stylus",
90 "graphql": "graphql", "gql": "graphql", "http": "http", "rest": "rest",
91 "json": "json", "json5": "json5", "jsonp": "jsonp", "webmanifest": "webmanifest",
92 "yaml": "yaml", "yml": "yaml", "toml": "toml",
93 "ini": "ini", "cfg": "ini", "conf": "ini", "properties": "properties",
94 "editorconfig": "editorconfig", "env": "ini",
95 "hcl": "hcl", "tf": "hcl", "tfvars": "hcl",
96 "proto": "protobuf", "avdl": "avdl", "yang": "yang",
97 "csv": "csv", "tsv": "csv", "po": "gettext", "pot": "gettext",
98 "diff": "diff", "patch": "diff",
99 "sql": "sql", "plsql": "plsql", "pks": "plsql", "pkb": "plsql",
100 "cypher": "cypher", "sparql": "sparql", "rq": "sparql", "ttl": "turtle",
101 "dns": "dns-zone-file", "zone": "dns-zone-file",
102 "md": "markdown", "markdown": "markdown", "mdown": "markdown", "mkd": "markdown",
103 "rst": "rest", "adoc": "asciidoc", "asciidoc": "asciidoc", "textile": "textile", "wiki": "wiki",
104 "tex": "latex", "latex": "latex", "sty": "latex", "cls": "latex",
105 "puml": "plant-uml", "plantuml": "plant-uml", "iuml": "plant-uml", "mmd": "mermaid",
106 "dot": "dot", "gv": "dot", "feature": "gherkin", "sln": "sln",
107 "tt": "t4", "tt2": "tt2", "tpl": "smarty",
108 "ejs": "ejs", "hbs": "handlebars", "mustache": "mustache", "pug": "pug", "jade": "pug",
109 "haml": "haml", "erb": "erb", "twig": "twig", "liquid": "liquid",
110 "jinja": "jinja2", "jinja2": "jinja2", "j2": "jinja2",
111 "cshtml": "cshtml", "razor": "razor",
112 "vm": "velocity", "vtl": "velocity", "soy": "soy", "ftl": "ftl",
113 "xq": "xquery", "xquery": "xquery", "xqy": "xquery",
114 "uri": "uri", "url": "uri", "regex": "regex", "webidl": "webidl", "idl": "webidl",
115 "log": "log", "spl": "splunk-spl", "promql": "promql", "kql": "kusto",
116
117 // Build, packaging, and tool configuration.
118 "mk": "makefile", "mak": "makefile", "cmake": "cmake",
119 "nsi": "nsis", "nsh": "nsis", "phpdoc": "phpdoc", "javadoc": "javadoc",
120 "gitignore": "gitignore", "npmignore": "npmignore", "hgignore": "hgignore",
121 "service": "systemd", "socket": "systemd", "timer": "systemd",
122
123 // Whole names, matched before the extension.
124 "dockerfile": "docker", "containerfile": "docker",
125 "makefile": "makefile", "gnumakefile": "makefile", "bsdmakefile": "makefile", "makefile.am": "makefile",
126 "justfile": "makefile", "pkgbuild": "bash",
127 "cmakelists.txt": "cmake",
128 "gemfile": "ruby", "rakefile": "ruby", "vagrantfile": "ruby", "brewfile": "ruby",
129 "podfile": "ruby", "fastfile": "ruby", "jenkinsfile": "groovy",
130 "go.mod": "go-module", "go.sum": "go-module",
131 "cargo.lock": "toml", "poetry.lock": "toml", "pipfile": "toml",
132 "nginx.conf": "nginx", "httpd.conf": "apacheconf", ".htaccess": "apacheconf",
133 ".gitignore": "gitignore", ".npmignore": "npmignore", ".hgignore": "hgignore",
134 ".dockerignore": "ignore", ".eslintignore": "ignore",
135 ".editorconfig": "editorconfig", ".env": "ini",
136 ".babelrc": "json", ".eslintrc": "json", ".prettierrc": "json",
137 }
138
139 // prismLanguage returns the Prism language for a path, or "" when the file is
140 // one the highlighter does not know.
141 func prismLanguage(name string) string {
142 base := strings.ToLower(path.Base(name))
143 if lang, ok := prismLanguages[base]; ok {
144 return lang
32 145 }
33 var buf bytes.Buffer
34 // Classes rather than inline styles: the palette lives in vole.css beside the
35 // rest of the theme and changes with it.
36 f := html.New(html.WithClasses(true), html.WithLineNumbers(true))
37 if err := f.Format(&buf, styles.Get("github-dark"), it); err != nil {
38 return ""
146 if lang, ok := prismLanguages[strings.TrimPrefix(path.Ext(base), ".")]; ok {
147 return lang
39 148 }
40 return template.HTML(buf.String())
149 return ""
41 150 }