RustForge

rustforge

RustForge: the read-only, cgit-style git web host serving this site

rustforge / src / highlight.rs
3751 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//! Syntax highlighting for the blob view, using syntect.
//!
//! We emit `<span class="hl-...">` tags instead of inline colours. The colours
//! come from two generated stylesheets (light + dark), so the browser picks the
//! right one via `prefers-color-scheme` without any JavaScript.

use syntect::highlighting::ThemeSet;
use syntect::html::{css_for_theme_with_class_style, ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;

const CLASS_STYLE: ClassStyle = ClassStyle::SpacedPrefixed { prefix: "hl-" };

/// Files bigger than this are shown without colours (highlighting is slow-ish).
const MAX_HIGHLIGHT_BYTES: usize = 512 * 1024;

pub struct Highlighter {
    syntaxes: SyntaxSet,
    /// CSS for both themes, served at /static/highlight.css
    pub css: String,
}

impl Highlighter {
    pub fn new() -> Highlighter {
        let syntaxes = SyntaxSet::load_defaults_newlines();
        let themes = ThemeSet::load_defaults();
        let light =
            css_for_theme_with_class_style(&themes.themes["InspiredGitHub"], CLASS_STYLE).unwrap_or_default();
        let dark = css_for_theme_with_class_style(&themes.themes["base16-ocean.dark"], CLASS_STYLE)
            .unwrap_or_default();
        let css = format!("{light}\n@media (prefers-color-scheme: dark) {{\n{dark}\n}}\n");
        Highlighter { syntaxes, css }
    }

    /// Return highlighted HTML for `code`. The result is already escaped and
    /// safe to insert into a page. Falls back to plain escaped text when the
    /// file type is unknown or the file is too large.
    pub fn highlight(&self, file_name: &str, code: &str) -> String {
        let ext = file_name.rsplit('.').next().unwrap_or("");
        let syntax = self
            .syntaxes
            .find_syntax_by_extension(ext)
            .or_else(|| self.syntaxes.find_syntax_by_extension(file_name))
            .or_else(|| self.syntaxes.find_syntax_by_first_line(code.lines().next().unwrap_or("")));
        let syntax = match syntax {
            Some(s) if code.len() <= MAX_HIGHLIGHT_BYTES => s,
            _ => return escape(code),
        };
        let mut gen = ClassedHTMLGenerator::new_with_class_style(syntax, &self.syntaxes, CLASS_STYLE);
        for line in LinesWithEndings::from(code) {
            if gen.parse_html_for_line_which_includes_newline(line).is_err() {
                return escape(code);
            }
        }
        gen.finalize()
    }
}

impl Default for Highlighter {
    fn default() -> Self {
        Highlighter::new()
    }
}

/// Minimal HTML escaping for text placed inside an element.
pub fn escape(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '&' => out.push_str("&amp;"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '"' => out.push_str("&quot;"),
            '\'' => out.push_str("&#39;"),
            c => out.push(c),
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn escapes() {
        assert_eq!(escape("<a href=\"x\">&'"), "&lt;a href=&quot;x&quot;&gt;&amp;&#39;");
    }

    #[test]
    fn highlights_rust_and_escapes() {
        let h = Highlighter::new();
        let out = h.highlight("main.rs", "fn main() { let s = \"<b>\"; }\n");
        assert!(out.contains("hl-"), "expected class spans: {out}");
        assert!(!out.contains("<b>"));
        assert!(out.contains("&lt;b&gt;"));
        assert!(h.css.contains("prefers-color-scheme: dark"));
    }

    #[test]
    fn unknown_type_is_plain_escaped() {
        let h = Highlighter::new();
        assert_eq!(h.highlight("data.zzzunknown", "<x>"), "&lt;x&gt;");
    }
}