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