ndg_commonmark/syntax/
types.rs1use std::{collections::HashMap, sync::Arc};
4
5use super::error::{SyntaxError, SyntaxResult};
6
7pub trait SyntaxHighlighter: Send + Sync {
13 fn name(&self) -> &'static str;
15
16 fn supported_languages(&self) -> Vec<String>;
18
19 fn available_themes(&self) -> Vec<String>;
21
22 fn supports_language(&self, language: &str) -> bool {
24 self
25 .supported_languages()
26 .iter()
27 .any(|lang| lang.eq_ignore_ascii_case(language))
28 }
29
30 fn has_theme(&self, theme: &str) -> bool {
32 self
33 .available_themes()
34 .iter()
35 .any(|t| t.eq_ignore_ascii_case(theme))
36 }
37
38 fn highlight(
54 &self,
55 code: &str,
56 language: &str,
57 theme: Option<&str>,
58 ) -> SyntaxResult<String>;
59
60 fn language_from_extension(&self, extension: &str) -> Option<String>;
62
63 fn language_from_filename(&self, filename: &str) -> Option<String> {
65 std::path::Path::new(filename)
66 .extension()
67 .and_then(|ext| ext.to_str())
68 .and_then(|ext| self.language_from_extension(ext))
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct SyntaxConfig {
75 pub default_theme: Option<String>,
77
78 pub language_aliases: HashMap<String, String>,
80
81 pub fallback_to_plain: bool,
83}
84
85impl Default for SyntaxConfig {
86 fn default() -> Self {
87 let mut language_aliases = HashMap::new();
88
89 language_aliases.insert("js".to_string(), "javascript".to_string());
91 language_aliases.insert("ts".to_string(), "typescript".to_string());
92 language_aliases.insert("py".to_string(), "python".to_string());
93 language_aliases.insert("rb".to_string(), "ruby".to_string());
94 language_aliases.insert("sh".to_string(), "bash".to_string());
95 language_aliases.insert("shell".to_string(), "bash".to_string());
96 language_aliases.insert("yml".to_string(), "yaml".to_string());
97 language_aliases.insert("nixos".to_string(), "nix".to_string());
98 language_aliases.insert("md".to_string(), "markdown".to_string());
99
100 Self {
101 default_theme: None,
102 language_aliases,
103 fallback_to_plain: true,
104 }
105 }
106}
107
108#[derive(Clone)]
116pub struct SyntaxManager {
117 highlighter: Arc<dyn SyntaxHighlighter>,
118 config: SyntaxConfig,
119}
120
121impl SyntaxManager {
122 #[must_use]
124 pub fn new(
125 highlighter: Box<dyn SyntaxHighlighter>,
126 config: SyntaxConfig,
127 ) -> Self {
128 Self {
129 highlighter: Arc::from(highlighter),
130 config,
131 }
132 }
133
134 #[must_use]
136 pub fn with_highlighter(highlighter: Box<dyn SyntaxHighlighter>) -> Self {
137 Self::new(highlighter, SyntaxConfig::default())
138 }
139
140 #[must_use]
142 pub fn highlighter(&self) -> &dyn SyntaxHighlighter {
143 self.highlighter.as_ref()
144 }
145
146 #[must_use]
148 pub const fn config(&self) -> &SyntaxConfig {
149 &self.config
150 }
151
152 pub fn set_config(&mut self, config: SyntaxConfig) {
154 self.config = config;
155 }
156
157 #[must_use]
159 pub fn resolve_language(&self, language: &str) -> String {
160 self
161 .config
162 .language_aliases
163 .get(language)
164 .cloned()
165 .unwrap_or_else(|| language.to_string())
166 }
167
168 pub fn highlight_code(
174 &self,
175 code: &str,
176 language: &str,
177 theme: Option<&str>,
178 ) -> SyntaxResult<String> {
179 let resolved_language = self.resolve_language(language);
180 let theme = theme.or(self.config.default_theme.as_deref());
181
182 if self.highlighter.supports_language(&resolved_language) {
184 return self.highlighter.highlight(code, &resolved_language, theme);
185 }
186
187 if self.config.fallback_to_plain {
189 if self.highlighter.supports_language("text") {
190 return self.highlighter.highlight(code, "text", theme);
191 }
192 if self.highlighter.supports_language("plain") {
193 return self.highlighter.highlight(code, "plain", theme);
194 }
195 }
196
197 Err(SyntaxError::UnsupportedLanguage(resolved_language))
198 }
199
200 #[allow(
207 clippy::option_if_let_else,
208 reason = "Clearer with explicit fallback logic"
209 )]
210 pub fn highlight_from_filename(
211 &self,
212 code: &str,
213 filename: &str,
214 theme: Option<&str>,
215 ) -> SyntaxResult<String> {
216 if let Some(language) = self.highlighter.language_from_filename(filename) {
217 self.highlight_code(code, &language, theme)
218 } else if self.config.fallback_to_plain {
219 self.highlight_code(code, "text", theme)
220 } else {
221 Err(SyntaxError::UnsupportedLanguage(format!(
222 "from filename: {filename}"
223 )))
224 }
225 }
226}