1use std::{collections::HashMap, sync::Arc};
18
19use syntastica::{Processor, render, renderer::HtmlRenderer};
20use syntastica_core::theme::ResolvedTheme;
21use syntastica_parsers::{Lang, LanguageSetImpl};
22
23use super::{
24 error::{SyntaxError, SyntaxResult},
25 types::{SyntaxConfig, SyntaxHighlighter, SyntaxManager},
26};
27
28pub struct SyntasticaHighlighter {
30 language_set: Arc<LanguageSetImpl>,
31 themes: HashMap<String, ResolvedTheme>,
32 default_theme: ResolvedTheme,
33}
34
35impl SyntasticaHighlighter {
36 pub fn new() -> SyntaxResult<Self> {
43 let language_set = Arc::new(LanguageSetImpl::new());
44
45 let mut themes = HashMap::new();
46
47 for theme_name in syntastica_themes::THEMES {
49 if let Some(theme) = syntastica_themes::from_str(theme_name) {
50 themes.insert((*theme_name).to_string(), theme);
51 }
52 }
53
54 let default_theme = syntastica_themes::one::dark();
55
56 Ok(Self {
57 language_set,
58 themes,
59 default_theme,
60 })
61 }
62
63 pub fn add_theme(&mut self, name: String, theme: ResolvedTheme) {
65 self.themes.insert(name, theme);
66 }
67
68 pub fn set_default_theme(&mut self, theme: ResolvedTheme) {
70 self.default_theme = theme;
71 }
72
73 fn parse_language(language: &str) -> Option<Lang> {
75 match language.to_lowercase().as_str() {
76 "rust" | "rs" => Some(Lang::Rust),
77 "python" | "py" => Some(Lang::Python),
78 "javascript" | "js" => Some(Lang::Javascript),
79 "typescript" | "ts" => Some(Lang::Typescript),
80 "nix" => Some(Lang::Nix),
81 "bash" | "sh" | "shell" => Some(Lang::Bash),
82 "c" => Some(Lang::C),
83 "cpp" | "c++" | "cxx" => Some(Lang::Cpp),
84 "go" => Some(Lang::Go),
85 "java" => Some(Lang::Java),
86 "json" => Some(Lang::Json),
87 "yaml" | "yml" => Some(Lang::Yaml),
88 "html" => Some(Lang::Html),
89 "css" => Some(Lang::Css),
90 "markdown" | "md" => Some(Lang::Markdown),
91 "sql" => Some(Lang::Sql),
92 "lua" => Some(Lang::Lua),
93 "ruby" | "rb" => Some(Lang::Ruby),
94 "php" => Some(Lang::Php),
95 "haskell" | "hs" => Some(Lang::Haskell),
96 "ocaml" | "ml" => Some(Lang::Ocaml),
97 "scala" => Some(Lang::Scala),
98 "swift" => Some(Lang::Swift),
99 "makefile" | "make" => Some(Lang::Make),
100 "cmake" => Some(Lang::Cmake),
101 #[allow(clippy::match_same_arms, reason = "Explicit for documentation")]
102 "text" | "txt" | "plain" => None, _ => None,
104 }
105 }
106
107 fn get_theme(&self, theme_name: Option<&str>) -> &ResolvedTheme {
109 theme_name
110 .and_then(|name| self.themes.get(name))
111 .unwrap_or(&self.default_theme)
112 }
113}
114
115impl SyntaxHighlighter for SyntasticaHighlighter {
116 fn name(&self) -> &'static str {
117 "Syntastica"
118 }
119
120 fn supported_languages(&self) -> Vec<String> {
121 vec![
122 "rust",
123 "rs",
124 "python",
125 "py",
126 "javascript",
127 "js",
128 "typescript",
129 "ts",
130 "nix",
131 "bash",
132 "sh",
133 "shell",
134 "c",
135 "cpp",
136 "c++",
137 "cxx",
138 "go",
139 "java",
140 "json",
141 "yaml",
142 "yml",
143 "html",
144 "css",
145 "markdown",
146 "md",
147 "sql",
148 "lua",
149 "ruby",
150 "rb",
151 "php",
152 "haskell",
153 "hs",
154 "ocaml",
155 "ml",
156 "scala",
157 "swift",
158 "makefile",
159 "make",
160 "cmake",
161 "text",
162 "txt",
163 "plain",
164 ]
165 .into_iter()
166 .map(String::from)
167 .collect()
168 }
169
170 fn available_themes(&self) -> Vec<String> {
171 let mut themes: Vec<String> = self.themes.keys().cloned().collect();
172 themes.sort();
173 themes
174 }
175
176 fn highlight(
177 &self,
178 code: &str,
179 language: &str,
180 theme: Option<&str>,
181 ) -> SyntaxResult<String> {
182 let lang = Self::parse_language(language)
183 .ok_or_else(|| SyntaxError::UnsupportedLanguage(language.to_string()))?;
184
185 let theme = self.get_theme(theme);
186
187 let mut processor = Processor::new(self.language_set.as_ref());
189
190 let highlights = processor
192 .process(code, lang)
193 .map_err(|e| SyntaxError::HighlightingFailed(e.to_string()))?;
194
195 let mut renderer = HtmlRenderer::new();
197 let html = render(&highlights, &mut renderer, theme.clone());
198
199 Ok(html)
200 }
201
202 fn language_from_extension(&self, extension: &str) -> Option<String> {
203 match extension.to_lowercase().as_str() {
204 "rs" => Some("rust".to_string()),
205 "py" | "pyw" => Some("python".to_string()),
206 "js" | "mjs" => Some("javascript".to_string()),
207 "ts" => Some("typescript".to_string()),
208 "nix" => Some("nix".to_string()),
209 "sh" | "bash" | "zsh" | "fish" => Some("bash".to_string()),
210 "c" | "h" => Some("c".to_string()),
211 "cpp" | "cxx" | "cc" | "hpp" | "hxx" | "hh" => Some("cpp".to_string()),
212 "go" => Some("go".to_string()),
213 "java" => Some("java".to_string()),
214 "json" => Some("json".to_string()),
215 "yaml" | "yml" => Some("yaml".to_string()),
216 "html" | "htm" => Some("html".to_string()),
217 "css" => Some("css".to_string()),
218 "md" | "markdown" => Some("markdown".to_string()),
219 "sql" => Some("sql".to_string()),
220 "lua" => Some("lua".to_string()),
221 "rb" => Some("ruby".to_string()),
222 "php" => Some("php".to_string()),
223 "hs" => Some("haskell".to_string()),
224 "ml" | "mli" => Some("ocaml".to_string()),
225 "scala" => Some("scala".to_string()),
226 "swift" => Some("swift".to_string()),
227 "txt" => Some("text".to_string()),
228 _ => None,
229 }
230 }
231}
232
233pub fn create_syntastica_manager() -> SyntaxResult<SyntaxManager> {
242 let highlighter = Box::new(SyntasticaHighlighter::new()?);
243 let config = SyntaxConfig {
244 default_theme: Some("one-dark".to_string()),
245 ..Default::default()
246 };
247 Ok(SyntaxManager::new(highlighter, config))
248}