repos
/ repos-rust master

repos-rust

mirror archived upstream

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

2.5 KB · 64 lines · Rust Raw History
 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}