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  ///
38  /// # Errors
39  ///
40  /// Currently never returns an error, but returns a Result for API
41  /// consistency.
42  pub fn new() -> SyntaxResult<Self> {
43    let language_set = Arc::new(LanguageSetImpl::new());
44
45    let mut themes = HashMap::new();
46
47    // Load all available themes
48    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  /// Add a custom theme
64  pub fn add_theme(&mut self, name: String, theme: ResolvedTheme) {
65    self.themes.insert(name, theme);
66  }
67
68  /// Set the default theme
69  pub fn set_default_theme(&mut self, theme: ResolvedTheme) {
70    self.default_theme = theme;
71  }
72
73  /// Convert a language string to a Lang enum
74  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, // use fallback for plain text
103      _ => None,
104    }
105  }
106
107  /// Get the theme by name, falling back to default
108  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    // Create a processor for this highlighting operation
188    let mut processor = Processor::new(self.language_set.as_ref());
189
190    // Process the code to get highlights
191    let highlights = processor
192      .process(code, lang)
193      .map_err(|e| SyntaxError::HighlightingFailed(e.to_string()))?;
194
195    // Render to HTML
196    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
233/// Create a Syntastica-based syntax manager with default configuration.
234///
235/// Syntastica provides modern tree-sitter based syntax highlighting with
236/// excellent language support including native Nix highlighting.
237///
238/// # Errors
239///
240/// Returns an error if the Syntastica highlighter fails to initialize.
241pub 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}