1use std::{collections::HashMap, sync::Mutex};
19
20use syntastica::{Processor, render, renderer::HtmlRenderer};
21use syntastica_core::theme::ResolvedTheme;
22use syntastica_parsers::{Lang, LanguageSetImpl};
23
24use super::{
25 error::{SyntaxError, SyntaxResult},
26 types::{SyntaxConfig, SyntaxHighlighter, SyntaxManager},
27};
28
29pub struct SyntasticaHighlighter {
31 themes: HashMap<String, ResolvedTheme>,
32 default_theme: ResolvedTheme,
33 processor: Mutex<Processor<'static, LanguageSetImpl>>,
34 renderer: Mutex<HtmlRenderer>,
35}
36
37impl SyntasticaHighlighter {
38 pub fn new() -> SyntaxResult<Self> {
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 let language_set_static: &'static LanguageSetImpl =
62 Box::leak(Box::new(LanguageSetImpl::new()));
63 let processor = Processor::new(language_set_static);
64
65 Ok(Self {
66 themes,
67 default_theme,
68 processor: Mutex::new(processor),
69 renderer: Mutex::new(HtmlRenderer::new()),
70 })
71 }
72
73 pub fn add_theme(&mut self, name: String, theme: ResolvedTheme) {
75 self.themes.insert(name, theme);
76 }
77
78 pub fn set_default_theme(&mut self, theme: ResolvedTheme) {
80 self.default_theme = theme;
81 }
82
83 fn parse_language(language: &str) -> Option<Lang> {
85 match language.to_lowercase().as_str() {
86 "rust" | "rs" => Some(Lang::Rust),
87 "python" | "py" => Some(Lang::Python),
88 "javascript" | "js" => Some(Lang::Javascript),
89 "typescript" | "ts" => Some(Lang::Typescript),
90 "tsx" => Some(Lang::Tsx),
91 "nix" => Some(Lang::Nix),
92 "bash" | "sh" | "shell" => Some(Lang::Bash),
93 "c" => Some(Lang::C),
94 "cpp" | "c++" | "cxx" => Some(Lang::Cpp),
95 "c_sharp" | "csharp" | "cs" => Some(Lang::CSharp),
96 "go" => Some(Lang::Go),
97 "java" => Some(Lang::Java),
98 "json" => Some(Lang::Json),
99 "yaml" | "yml" => Some(Lang::Yaml),
100 "html" => Some(Lang::Html),
101 "css" => Some(Lang::Css),
102 "markdown" | "md" => Some(Lang::Markdown),
103 "markdown_inline" => Some(Lang::MarkdownInline),
104 "sql" => Some(Lang::Sql),
105 "lua" => Some(Lang::Lua),
106 "ruby" | "rb" => Some(Lang::Ruby),
107 "php" => Some(Lang::Php),
108 "php_only" => Some(Lang::PhpOnly),
109 "haskell" | "hs" => Some(Lang::Haskell),
110 "scala" => Some(Lang::Scala),
111 "swift" => Some(Lang::Swift),
112 "makefile" | "make" => Some(Lang::Make),
113 "cmake" => Some(Lang::Cmake),
114 "asm" | "assembly" => Some(Lang::Asm),
115 "diff" | "patch" => Some(Lang::Diff),
116 "elixir" | "ex" | "exs" => Some(Lang::Elixir),
117 "jsdoc" => Some(Lang::Jsdoc),
118 "printf" => Some(Lang::Printf),
119 "regex" | "regexp" => Some(Lang::Regex),
120 "zig" => Some(Lang::Zig),
121 #[allow(clippy::match_same_arms, reason = "Explicit for documentation")]
122 "text" | "txt" | "plain" => None, _ => None,
124 }
125 }
126
127 fn get_theme(&self, theme_name: Option<&str>) -> &ResolvedTheme {
129 theme_name
130 .and_then(|name| self.themes.get(name))
131 .unwrap_or(&self.default_theme)
132 }
133}
134
135impl SyntaxHighlighter for SyntasticaHighlighter {
136 fn name(&self) -> &'static str {
137 "Syntastica"
138 }
139
140 fn supported_languages(&self) -> Vec<String> {
141 vec![
142 "rust",
143 "rs",
144 "python",
145 "py",
146 "javascript",
147 "js",
148 "typescript",
149 "ts",
150 "tsx",
151 "nix",
152 "bash",
153 "sh",
154 "shell",
155 "c",
156 "cpp",
157 "c++",
158 "cxx",
159 "c_sharp",
160 "csharp",
161 "cs",
162 "go",
163 "java",
164 "json",
165 "yaml",
166 "yml",
167 "html",
168 "css",
169 "markdown",
170 "md",
171 "markdown_inline",
172 "sql",
173 "lua",
174 "ruby",
175 "rb",
176 "php",
177 "php_only",
178 "haskell",
179 "hs",
180 "scala",
181 "swift",
182 "makefile",
183 "make",
184 "cmake",
185 "asm",
186 "assembly",
187 "diff",
188 "patch",
189 "elixir",
190 "ex",
191 "exs",
192 "jsdoc",
193 "printf",
194 "regex",
195 "regexp",
196 "zig",
197 "text",
198 "txt",
199 "plain",
200 ]
201 .into_iter()
202 .map(String::from)
203 .collect()
204 }
205
206 fn available_themes(&self) -> Vec<String> {
207 let mut themes: Vec<String> = self.themes.keys().cloned().collect();
208 themes.sort();
209 themes
210 }
211
212 fn highlight(
213 &self,
214 code: &str,
215 language: &str,
216 theme: Option<&str>,
217 ) -> SyntaxResult<String> {
218 let lang = Self::parse_language(language)
219 .ok_or_else(|| SyntaxError::UnsupportedLanguage(language.to_string()))?;
220
221 let theme = self.get_theme(theme);
222
223 let highlights = self
225 .processor
226 .lock()
227 .map_err(|e| {
228 SyntaxError::HighlightingFailed(format!("Processor lock poisoned: {e}"))
229 })?
230 .process(code, lang)
231 .map_err(|e| SyntaxError::HighlightingFailed(e.to_string()))?;
232
233 let html = {
235 let mut renderer = self.renderer.lock().map_err(|e| {
236 SyntaxError::HighlightingFailed(format!("Renderer lock poisoned: {e}"))
237 })?;
238 render(&highlights, &mut *renderer, theme)
239 };
240
241 Ok(html)
242 }
243
244 fn language_from_extension(&self, extension: &str) -> Option<String> {
245 match extension.to_lowercase().as_str() {
246 "rs" => Some("rust".to_string()),
247 "py" | "pyw" => Some("python".to_string()),
248 "js" | "mjs" => Some("javascript".to_string()),
249 "ts" => Some("typescript".to_string()),
250 "tsx" => Some("tsx".to_string()),
251 "nix" => Some("nix".to_string()),
252 "sh" | "bash" | "zsh" | "fish" => Some("bash".to_string()),
253 "c" | "h" => Some("c".to_string()),
254 "cpp" | "cxx" | "cc" | "hpp" | "hxx" | "hh" => Some("cpp".to_string()),
255 "cs" => Some("c_sharp".to_string()),
256 "go" => Some("go".to_string()),
257 "java" => Some("java".to_string()),
258 "json" => Some("json".to_string()),
259 "yaml" | "yml" => Some("yaml".to_string()),
260 "html" | "htm" => Some("html".to_string()),
261 "css" => Some("css".to_string()),
262 "md" | "markdown" => Some("markdown".to_string()),
263 "sql" => Some("sql".to_string()),
264 "lua" => Some("lua".to_string()),
265 "rb" => Some("ruby".to_string()),
266 "php" => Some("php".to_string()),
267 "hs" => Some("haskell".to_string()),
268 "ml" | "mli" => Some("ocaml".to_string()),
269 "scala" => Some("scala".to_string()),
270 "swift" => Some("swift".to_string()),
271 "s" | "asm" => Some("asm".to_string()),
272 "diff" | "patch" => Some("diff".to_string()),
273 "ex" | "exs" => Some("elixir".to_string()),
274 "zig" => Some("zig".to_string()),
275 "txt" => Some("text".to_string()),
276 _ => None,
277 }
278 }
279}
280
281pub fn create_syntastica_manager() -> SyntaxResult<SyntaxManager> {
290 let highlighter = Box::new(SyntasticaHighlighter::new()?);
291 let config = SyntaxConfig {
292 default_theme: Some("one-dark".to_string()),
293 ..Default::default()
294 };
295 Ok(SyntaxManager::new(highlighter, config))
296}