engarde/
syntax.rs

1use std::path::Path;
2use syntect::easy::HighlightLines;
3use syntect::highlighting::ThemeSet;
4use syntect::html::{
5    IncludeBackground, append_highlighted_html_for_styled_line, start_highlighted_html_snippet,
6};
7use syntect::parsing::{SyntaxReference, SyntaxSet};
8use syntect::util::LinesWithEndings;
9
10/// Highlight code block
11#[derive(Debug)]
12#[non_exhaustive]
13pub struct Syntax {
14    syntax_set: SyntaxSet,
15    theme_set: ThemeSet,
16    default_theme: Option<String>,
17}
18
19impl Syntax {
20    pub fn new() -> Self {
21        Self {
22            syntax_set: SyntaxSet::load_defaults_newlines(),
23            theme_set: ThemeSet::load_defaults(),
24            default_theme: None,
25        }
26    }
27
28    pub fn load_custom_syntaxes(&mut self, syntaxes_path: &Path) {
29        let mut builder = self.syntax_set.clone().into_builder();
30        builder.add_from_folder(syntaxes_path, true).unwrap();
31        self.syntax_set = builder.build();
32    }
33
34    pub fn has_theme(&self, name: &str) -> bool {
35        self.theme_set.themes.contains_key(name)
36    }
37
38    pub fn themes(&self) -> impl Iterator<Item = String> + '_ {
39        self.theme_set.themes.keys().cloned()
40    }
41
42    pub fn syntaxes(&self) -> impl Iterator<Item = String> + '_ {
43        fn reference_to_string(sd: &SyntaxReference) -> String {
44            let extensions = sd.file_extensions.join(", ");
45            format!("{} [{}]", sd.name, extensions)
46        }
47
48        let mut syntaxes = self
49            .syntax_set
50            .syntaxes()
51            .iter()
52            .map(reference_to_string)
53            .collect::<Vec<_>>();
54
55        // sort alphabetically with insensitive ascii case
56        syntaxes.sort_by_key(|a| a.to_ascii_lowercase());
57
58        syntaxes.into_iter()
59    }
60
61    pub fn default_theme(&self) -> Option<&str> {
62        self.default_theme.as_deref()
63    }
64
65    pub fn set_default_theme(&mut self, theme: impl Into<String>) {
66        self.default_theme = Some(theme.into());
67    }
68
69    pub fn format(&self, code: &str, lang: Option<&str>, theme: Option<&str>) -> String {
70        if let Some(theme) = theme.or_else(|| self.default_theme()) {
71            let theme = &self.theme_set.themes[theme];
72
73            let syntax = lang
74                .and_then(|l| self.syntax_set.find_syntax_by_token(l))
75                .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
76
77            // Essentially the same as `syntect::html::highlighted_html_for_string`,
78            // but adding <code> tags between the <pre> tags
79            // See: https://docs.rs/syntect/5.0.0/src/syntect/html.rs.html#269
80            let mut highlighter = HighlightLines::new(syntax, theme);
81            let (mut output, bg) = start_highlighted_html_snippet(theme);
82            output.push_str("<code>");
83
84            for line in LinesWithEndings::from(code) {
85                let regions = highlighter.highlight_line(line, &self.syntax_set).unwrap();
86                append_highlighted_html_for_styled_line(
87                    &regions[..],
88                    IncludeBackground::IfDifferent(bg),
89                    &mut output,
90                )
91                .unwrap();
92            }
93            output.push_str("</code></pre>\n");
94            output
95        } else {
96            crate::Raw::new().format(code, lang, theme)
97        }
98    }
99}
100
101impl Default for Syntax {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107#[cfg(test)]
108mod test {
109    use super::*;
110
111    const CODEBLOCK: &str = "mod test {
112        fn hello(arg: int) -> bool {
113            \
114                                      true
115        }
116    }
117    ";
118
119    const CODEBLOCK_RENDERED: &str = "<pre style=\"background-color:#2b303b;\">\n\
120         <code><span style=\"color:#b48ead;\">mod </span>\
121         <span style=\"color:#c0c5ce;\">test {\n\
122         </span><span style=\"color:#c0c5ce;\">        </span>\
123         <span style=\"color:#b48ead;\">fn \
124         </span><span style=\"color:#8fa1b3;\">hello</span><span style=\"color:#c0c5ce;\">(\
125         </span><span style=\"color:#bf616a;\">arg</span><span style=\"color:#c0c5ce;\">: int) -&gt; \
126         </span><span style=\"color:#b48ead;\">bool </span><span style=\"color:#c0c5ce;\">{\n\
127         </span><span style=\"color:#c0c5ce;\">            \
128         </span><span style=\"color:#d08770;\">true\n\
129         </span><span style=\"color:#c0c5ce;\">        }\n\
130         </span><span style=\"color:#c0c5ce;\">    }\n\
131         </span><span style=\"color:#c0c5ce;\">    </span></code></pre>\n";
132
133    #[test]
134    fn highlight_block_renders_rust() {
135        let syntax = Syntax::new();
136        let output = syntax.format(CODEBLOCK, Some("rust"), Some("base16-ocean.dark"));
137        assert_eq!(output, CODEBLOCK_RENDERED.to_string());
138    }
139
140    const CUSTOM_CODEBLOCK: &str = "[[[]]]]";
141
142    const CUSTOM_CODEBLOCK_RENDERED: &str = "<pre style=\"background-color:#2b303b;\">\n\
143          <code><span style=\"color:#c0c5ce;\">[[[]]]</span>\
144          <span style=\"background-color:#bf616a;color:#2b303b;\">]</span>\
145          </code></pre>\n";
146
147    #[test]
148    fn highlight_custom_syntax() {
149        let mut syntax = Syntax::new();
150        let path = Path::new("./tests/fixtures/custom_syntaxes/");
151        syntax.load_custom_syntaxes(path);
152        let output = syntax.format(
153            CUSTOM_CODEBLOCK,
154            Some("brackets"),
155            Some("base16-ocean.dark"),
156        );
157        assert_eq!(output, CUSTOM_CODEBLOCK_RENDERED);
158    }
159}