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