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