A minimal self-hosted git browser on Rust axum: bare repos rendered as a website with commits, diffs, syntax-highlighted blobs, atom feeds, and clone over HTTPS.
axumdockergitgit-browsergitoxidegixrustself-hosted
1use std::sync::OnceLock;
2use syntect::highlighting::{Theme, ThemeSet};
3use syntect::html::{styled_line_to_highlighted_html, IncludeBackground};
4use syntect::parsing::SyntaxSet;
5use syntect::util::LinesWithEndings;
6
7fn syntax_set() -> &'static SyntaxSet {
8 static CELL: OnceLock<SyntaxSet> = OnceLock::new();
9 CELL.get_or_init(SyntaxSet::load_defaults_newlines)
10}
11
12fn theme() -> &'static Theme {
13 static CELL: OnceLock<Theme> = OnceLock::new();
14 CELL.get_or_init(|| {
15 let mut ts = ThemeSet::load_defaults();
16 // base16-eighties.dark reads well on the dark palette and ships
17 // with syntect's default themes, so no theme files to vendor.
18 ts.themes
19 .remove("base16-eighties.dark")
20 .or_else(|| ts.themes.remove("Solarized (dark)"))
21 .unwrap_or_else(|| ts.themes.into_values().next().expect("at least one theme"))
22 })
23}
24
25/// `filename` is used only to pick the syntax; pass the blob basename.
26pub fn highlight(source: &str, filename: &str) -> String {
27 let ss = syntax_set();
28 let theme = theme();
29
30 // Never find_syntax_for_file here: it opens the named file on the
31 // server's filesystem, but `filename` is a blob basename from the URL,
32 // not a real local file. Match on the extension (which for files like
33 // "Makefile" can be the whole name), then sniff the first line
34 // (shebangs, XML declarations) from the blob itself.
35 let ext = std::path::Path::new(filename)
36 .extension()
37 .and_then(|e| e.to_str())
38 .unwrap_or("");
39 let syntax = ss
40 .find_syntax_by_extension(ext)
41 .or_else(|| ss.find_syntax_by_extension(filename))
42 .or_else(|| source.lines().next().and_then(|l| ss.find_syntax_by_first_line(l)))
43 .unwrap_or_else(|| ss.find_syntax_plain_text());
44
45 let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme);
46 let mut out = String::from("<pre class=\"hl\"><code>");
47 for (i, line) in LinesWithEndings::from(source).enumerate() {
48 let ranges = highlighter
49 .highlight_line(line, ss)
50 .unwrap_or_else(|_| vec![(Default::default(), line)]);
51 out.push_str(&format!(
52 "<span class=\"ln\" data-n=\"{}\"></span>",
53 i + 1
54 ));
55 out.push_str("<span class=\"l\">");
56 let html = styled_line_to_highlighted_html(&ranges, IncludeBackground::No)
57 .unwrap_or_else(|_| line.to_string());
58 out.push_str(&html);
59 out.push_str("</span>");
60 }
61 out.push_str("</code></pre>");
62 out
63}