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#[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 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 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 ®ions[..],
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) -> \
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}