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.
scripts/vendor-prism.py +160
| @@ -0,0 +1,160 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | """Regenerate the vendored Prism assets in server/web/static. | ||
| 3 | |||
| 4 | Vole's code browser highlights in the browser (server/web/static/prism-worker.js) | ||
| 5 | and names the language on the server (server/web/highlight.go). The grammars | ||
| 6 | themselves are Prism's, concatenated into two files so an instance is still one | ||
| 7 | binary and there is no npm step in this repository: | ||
| 8 | |||
| 9 | prism.min.js the core build: markup, css, clike, javascript | ||
| 10 | prism-languages.min.js every other language Prism ships, in dependency order | ||
| 11 | |||
| 12 | The order is not alphabetical: it comes from the "require" graph in Prism's own | ||
| 13 | components.json, so markup-templating precedes php, c precedes cpp and zig, java | ||
| 14 | precedes scala, and css precedes scss. Cycles or unsatisfied requirements are | ||
| 15 | errors rather than a subtly broken bundle. | ||
| 16 | |||
| 17 | scripts/vendor-prism.py write the files | ||
| 18 | scripts/vendor-prism.py --check compare, exit 1 if they differ | ||
| 19 | |||
| 20 | Bumping PRISM_VERSION is the whole upgrade: rerun, then run the tests, which hold | ||
| 21 | the server's extension table (highlight.go) to the grammars in the bundle. | ||
| 22 | """ | ||
| 23 | |||
| 24 | import argparse | ||
| 25 | import json | ||
| 26 | import os | ||
| 27 | import sys | ||
| 28 | import urllib.request | ||
| 29 | |||
| 30 | PRISM_VERSION = "1.30.0" | ||
| 31 | |||
| 32 | CDN = "https://cdnjs.cloudflare.com/ajax/libs/prism/{version}" | ||
| 33 | RAW = "https://raw.githubusercontent.com/PrismJS/prism/v{version}" | ||
| 34 | |||
| 35 | # Languages that ship inside prism.min.js (Prism's "default" build). | ||
| 36 | CORE_LANGUAGES = ("markup", "css", "clike", "javascript") | ||
| 37 | |||
| 38 | STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, | ||
| 39 | "server", "web", "static") | ||
| 40 | |||
| 41 | HEADER = """/* Prism {version}, every language it ships, concatenated in dependency order from | ||
| 42 | the components published on cdnjs. Prism's core (prism.min.js) carries markup, | ||
| 43 | css, clike and javascript; these {count} extend one of those, or a dependency listed | ||
| 44 | before them here (markup-templating before php, c before cpp/csharp/zig, java | ||
| 45 | before scala, css before scss). | ||
| 46 | |||
| 47 | Vendored rather than fetched so an instance is still one file, and generated by | ||
| 48 | concatenation rather than built: there is no npm build step in this repository. | ||
| 49 | The list and its order come from Prism's components.json "require" graph; a | ||
| 50 | language added here also needs an extension in the server's table | ||
| 51 | (highlight.go), and TestPrismLanguagesAreVendored holds the two together. | ||
| 52 | Regenerate with scripts/vendor-prism.py. */ | ||
| 53 | |||
| 54 | """ | ||
| 55 | |||
| 56 | |||
| 57 | def fetch(url): | ||
| 58 | request = urllib.request.Request(url, headers={"User-Agent": "vole-vendor-prism"}) | ||
| 59 | with urllib.request.urlopen(request) as response: | ||
| 60 | return response.read() | ||
| 61 | |||
| 62 | |||
| 63 | def component(name): | ||
| 64 | return CDN.format(version=PRISM_VERSION) + "/components/prism-" + name + ".min.js" | ||
| 65 | |||
| 66 | |||
| 67 | def requirements(meta, name): | ||
| 68 | require = meta[name].get("require") | ||
| 69 | if not require: | ||
| 70 | return [] | ||
| 71 | return [require] if isinstance(require, str) else list(require) | ||
| 72 | |||
| 73 | |||
| 74 | def order_languages(meta): | ||
| 75 | """Return every non-core language id in dependency order. | ||
| 76 | |||
| 77 | Prism's metadata lists what a grammar needs; a language is emitted after its | ||
| 78 | requirements. A missing or circular requirement is a hard error: the bundle | ||
| 79 | would load but colour nothing, which is worse than failing here. | ||
| 80 | """ | ||
| 81 | ids = [name for name in meta if name not in CORE_LANGUAGES] | ||
| 82 | known = set(ids) | set(CORE_LANGUAGES) | ||
| 83 | order, done = [], set() | ||
| 84 | |||
| 85 | def visit(name, stack): | ||
| 86 | if name in done: | ||
| 87 | return | ||
| 88 | if name in stack: | ||
| 89 | raise SystemExit("prism: dependency cycle: " + " -> ".join(stack + (name,))) | ||
| 90 | for dep in requirements(meta, name): | ||
| 91 | if dep not in known: | ||
| 92 | raise SystemExit(f"prism: {name} requires {dep}, which Prism does not define") | ||
| 93 | if dep not in CORE_LANGUAGES: | ||
| 94 | visit(dep, stack + (name,)) | ||
| 95 | done.add(name) | ||
| 96 | order.append(name) | ||
| 97 | |||
| 98 | for name in ids: | ||
| 99 | visit(name, ()) | ||
| 100 | return order | ||
| 101 | |||
| 102 | |||
| 103 | def build(): | ||
| 104 | meta = json.loads(fetch(RAW.format(version=PRISM_VERSION) + "/components.json"))["languages"] | ||
| 105 | meta.pop("meta", None) | ||
| 106 | languages = order_languages(meta) | ||
| 107 | |||
| 108 | core = fetch(CDN.format(version=PRISM_VERSION) + "/prism.min.js") | ||
| 109 | license_text = fetch(RAW.format(version=PRISM_VERSION) + "/LICENSE") | ||
| 110 | bundle = [HEADER.format(version=PRISM_VERSION, count=len(languages))] | ||
| 111 | for name in languages: | ||
| 112 | bundle.append(fetch(component(name)).decode("utf-8")) | ||
| 113 | bundle.append("\n") | ||
| 114 | return core, "".join(bundle).encode("utf-8"), license_text, len(languages) | ||
| 115 | |||
| 116 | |||
| 117 | def main(): | ||
| 118 | parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) | ||
| 119 | parser.add_argument("--check", action="store_true", | ||
| 120 | help="verify the committed files instead of writing them") | ||
| 121 | args = parser.parse_args() | ||
| 122 | |||
| 123 | core, bundle, license_text, count = build() | ||
| 124 | files = { | ||
| 125 | "prism.min.js": core, | ||
| 126 | "prism-languages.min.js": bundle, | ||
| 127 | "prism.LICENSE": license_text, | ||
| 128 | } | ||
| 129 | |||
| 130 | if args.check: | ||
| 131 | stale = [] | ||
| 132 | for name, want in files.items(): | ||
| 133 | path = os.path.join(STATIC_DIR, name) | ||
| 134 | try: | ||
| 135 | with open(path, "rb") as handle: | ||
| 136 | have = handle.read() | ||
| 137 | except FileNotFoundError: | ||
| 138 | stale.append(f"{name} is missing") | ||
| 139 | continue | ||
| 140 | if have != want: | ||
| 141 | stale.append(f"{name} differs ({len(have)} bytes vendored, {len(want)} upstream)") | ||
| 142 | if stale: | ||
| 143 | for line in stale: | ||
| 144 | print("stale:", line, file=sys.stderr) | ||
| 145 | print("run scripts/vendor-prism.py to regenerate", file=sys.stderr) | ||
| 146 | return 1 | ||
| 147 | print(f"ok: Prism {PRISM_VERSION} assets match ({count} languages)") | ||
| 148 | return 0 | ||
| 149 | |||
| 150 | for name, body in files.items(): | ||
| 151 | path = os.path.join(STATIC_DIR, name) | ||
| 152 | with open(path, "wb") as handle: | ||
| 153 | handle.write(body) | ||
| 154 | print(f"wrote {os.path.relpath(path)} ({len(body)} bytes)") | ||
| 155 | print(f"Prism {PRISM_VERSION}: {count} languages") | ||
| 156 | return 0 | ||
| 157 | |||
| 158 | |||
| 159 | if __name__ == "__main__": | ||
| 160 | sys.exit(main()) | ||