ndg_commonmark/syntax/
syntastica.rs

1//! Syntastica-based syntax highlighting backend.
2//!
3//! This module provides a modern tree-sitter based syntax highlighter using the
4//! Syntastica library, which offers excellent language support including native
5//! Nix highlighting.
6//!
7//! ## Theme Support
8//!
9//! We programmaticall loads all available themes from `syntastica-themes`
10//! Some of the popular themes included are:
11//! - github (dark/light variants)
12//! - gruvbox (dark/light)
13//! - nord, dracula, catppuccin
14//! - tokyo night, solarized, monokai
15//! - And many more...
16
17use 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
28/// Syntastica-based syntax highlighter.
29pub struct SyntasticaHighlighter {
30    language_set: Arc<LanguageSetImpl>,
31    themes: HashMap<String, ResolvedTheme>,
32    default_theme: ResolvedTheme,
33}
34
35impl SyntasticaHighlighter {
36    /// Create a new Syntastica highlighter with all available themes.
37    pub fn new() -> SyntaxResult<Self> {
38        let language_set = Arc::new(LanguageSetImpl::new());
39
40        let mut themes = HashMap::new();
41
42        // Load all available themes
43        for theme_name in syntastica_themes::THEMES {
44            if let Some(theme) = syntastica_themes::from_str(theme_name) {
45                themes.insert(theme_name.to_string(), theme);
46            }
47        }
48
49        let default_theme = syntastica_themes::one::dark();
50
51        Ok(Self {
52            language_set,
53            themes,
54            default_theme,
55        })
56    }
57
58    /// Add a custom theme
59    pub fn add_theme(&mut self, name: String, theme: ResolvedTheme) {
60        self.themes.insert(name, theme);
61    }
62
63    /// Set the default theme
64    pub fn set_default_theme(&mut self, theme: ResolvedTheme) {
65        self.default_theme = theme;
66    }
67
68    /// Convert a language string to a Lang enum
69    fn parse_language(&self, language: &str) -> Option<Lang> {
70        match language.to_lowercase().as_str() {
71            "rust" | "rs" => Some(Lang::Rust),
72            "python" | "py" => Some(Lang::Python),
73            "javascript" | "js" => Some(Lang::Javascript),
74            "typescript" | "ts" => Some(Lang::Typescript),
75            "nix" => Some(Lang::Nix),
76            "bash" | "sh" | "shell" => Some(Lang::Bash),
77            "c" => Some(Lang::C),
78            "cpp" | "c++" | "cxx" => Some(Lang::Cpp),
79            "go" => Some(Lang::Go),
80            "java" => Some(Lang::Java),
81            "json" => Some(Lang::Json),
82            "yaml" | "yml" => Some(Lang::Yaml),
83            "html" => Some(Lang::Html),
84            "css" => Some(Lang::Css),
85            "markdown" | "md" => Some(Lang::Markdown),
86            "sql" => Some(Lang::Sql),
87            "lua" => Some(Lang::Lua),
88            "ruby" | "rb" => Some(Lang::Ruby),
89            "php" => Some(Lang::Php),
90            "haskell" | "hs" => Some(Lang::Haskell),
91            "ocaml" | "ml" => Some(Lang::Ocaml),
92            "scala" => Some(Lang::Scala),
93            "swift" => Some(Lang::Swift),
94            "makefile" | "make" => Some(Lang::Make),
95            "cmake" => Some(Lang::Cmake),
96            "text" | "txt" | "plain" => None, // use fallback for plain text
97            _ => None,
98        }
99    }
100
101    /// Get the theme by name, falling back to default
102    fn get_theme(&self, theme_name: Option<&str>) -> &ResolvedTheme {
103        theme_name
104            .and_then(|name| self.themes.get(name))
105            .unwrap_or(&self.default_theme)
106    }
107}
108
109impl Default for SyntasticaHighlighter {
110    fn default() -> Self {
111        Self::new().expect("Failed to create Syntastica highlighter")
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(&self, code: &str, language: &str, theme: Option<&str>) -> SyntaxResult<String> {
177        let lang = self
178            .parse_language(language)
179            .ok_or_else(|| SyntaxError::UnsupportedLanguage(language.to_string()))?;
180
181        let theme = self.get_theme(theme);
182
183        // Create a processor for this highlighting operation
184        let mut processor = Processor::new(self.language_set.as_ref());
185
186        // Process the code to get highlights
187        let highlights = processor
188            .process(code, lang)
189            .map_err(|e| SyntaxError::HighlightingFailed(e.to_string()))?;
190
191        // Render to HTML
192        let mut renderer = HtmlRenderer::new();
193        let html = render(&highlights, &mut renderer, theme.clone());
194
195        Ok(html)
196    }
197
198    fn language_from_extension(&self, extension: &str) -> Option<String> {
199        match extension.to_lowercase().as_str() {
200            "rs" => Some("rust".to_string()),
201            "py" | "pyw" => Some("python".to_string()),
202            "js" | "mjs" => Some("javascript".to_string()),
203            "ts" => Some("typescript".to_string()),
204            "nix" => Some("nix".to_string()),
205            "sh" | "bash" | "zsh" | "fish" => Some("bash".to_string()),
206            "c" | "h" => Some("c".to_string()),
207            "cpp" | "cxx" | "cc" | "hpp" | "hxx" | "hh" => Some("cpp".to_string()),
208            "go" => Some("go".to_string()),
209            "java" => Some("java".to_string()),
210            "json" => Some("json".to_string()),
211            "yaml" | "yml" => Some("yaml".to_string()),
212            "html" | "htm" => Some("html".to_string()),
213            "css" => Some("css".to_string()),
214            "md" | "markdown" => Some("markdown".to_string()),
215            "sql" => Some("sql".to_string()),
216            "lua" => Some("lua".to_string()),
217            "rb" => Some("ruby".to_string()),
218            "php" => Some("php".to_string()),
219            "hs" => Some("haskell".to_string()),
220            "ml" | "mli" => Some("ocaml".to_string()),
221            "scala" => Some("scala".to_string()),
222            "swift" => Some("swift".to_string()),
223            "txt" => Some("text".to_string()),
224            _ => None,
225        }
226    }
227}
228
229/// Create a Syntastica-based syntax manager with default configuration.
230///
231/// Syntastica provides modern tree-sitter based syntax highlighting with
232/// excellent language support including native Nix highlighting.
233pub fn create_syntastica_manager() -> SyntaxResult<SyntaxManager> {
234    let highlighter = Box::new(SyntasticaHighlighter::new()?);
235    let mut config = SyntaxConfig::default();
236    config.default_theme = Some("one-dark".to_string());
237    Ok(SyntaxManager::new(highlighter, config))
238}