ndg_commonmark/syntax/
types.rs1use std::collections::HashMap;
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.supported_languages()
25 .iter()
26 .any(|lang| lang.eq_ignore_ascii_case(language))
27 }
28
29 fn has_theme(&self, theme: &str) -> bool {
31 self.available_themes()
32 .iter()
33 .any(|t| t.eq_ignore_ascii_case(theme))
34 }
35
36 fn highlight(&self, code: &str, language: &str, theme: Option<&str>) -> SyntaxResult<String>;
48
49 fn language_from_extension(&self, extension: &str) -> Option<String>;
51
52 fn language_from_filename(&self, filename: &str) -> Option<String> {
54 std::path::Path::new(filename)
55 .extension()
56 .and_then(|ext| ext.to_str())
57 .and_then(|ext| self.language_from_extension(ext))
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct SyntaxConfig {
64 pub default_theme: Option<String>,
66
67 pub language_aliases: HashMap<String, String>,
69
70 pub fallback_to_plain: bool,
72}
73
74impl Default for SyntaxConfig {
75 fn default() -> Self {
76 let mut language_aliases = HashMap::new();
77
78 language_aliases.insert("js".to_string(), "javascript".to_string());
80 language_aliases.insert("ts".to_string(), "typescript".to_string());
81 language_aliases.insert("py".to_string(), "python".to_string());
82 language_aliases.insert("rb".to_string(), "ruby".to_string());
83 language_aliases.insert("sh".to_string(), "bash".to_string());
84 language_aliases.insert("shell".to_string(), "bash".to_string());
85 language_aliases.insert("yml".to_string(), "yaml".to_string());
86 language_aliases.insert("nixos".to_string(), "nix".to_string());
87 language_aliases.insert("md".to_string(), "markdown".to_string());
88
89 Self {
90 default_theme: None,
91 language_aliases,
92 fallback_to_plain: true,
93 }
94 }
95}
96
97pub struct SyntaxManager {
102 highlighter: Box<dyn SyntaxHighlighter>,
103 config: SyntaxConfig,
104}
105
106impl SyntaxManager {
107 pub fn new(highlighter: Box<dyn SyntaxHighlighter>, config: SyntaxConfig) -> Self {
109 Self {
110 highlighter,
111 config,
112 }
113 }
114
115 pub fn with_highlighter(highlighter: Box<dyn SyntaxHighlighter>) -> Self {
117 Self::new(highlighter, SyntaxConfig::default())
118 }
119
120 pub fn highlighter(&self) -> &dyn SyntaxHighlighter {
122 self.highlighter.as_ref()
123 }
124
125 pub fn config(&self) -> &SyntaxConfig {
127 &self.config
128 }
129
130 pub fn set_config(&mut self, config: SyntaxConfig) {
132 self.config = config;
133 }
134
135 pub fn resolve_language(&self, language: &str) -> String {
137 self.config
138 .language_aliases
139 .get(language)
140 .cloned()
141 .unwrap_or_else(|| language.to_string())
142 }
143
144 pub fn highlight_code(
146 &self,
147 code: &str,
148 language: &str,
149 theme: Option<&str>,
150 ) -> SyntaxResult<String> {
151 let resolved_language = self.resolve_language(language);
152 let theme = theme.or(self.config.default_theme.as_deref());
153
154 if self.highlighter.supports_language(&resolved_language) {
156 return self.highlighter.highlight(code, &resolved_language, theme);
157 }
158
159 if self.config.fallback_to_plain {
161 if self.highlighter.supports_language("text") {
162 return self.highlighter.highlight(code, "text", theme);
163 }
164 if self.highlighter.supports_language("plain") {
165 return self.highlighter.highlight(code, "plain", theme);
166 }
167 }
168
169 Err(SyntaxError::UnsupportedLanguage(resolved_language))
170 }
171
172 pub fn highlight_from_filename(
174 &self,
175 code: &str,
176 filename: &str,
177 theme: Option<&str>,
178 ) -> SyntaxResult<String> {
179 if let Some(language) = self.highlighter.language_from_filename(filename) {
180 self.highlight_code(code, &language, theme)
181 } else if self.config.fallback_to_plain {
182 self.highlight_code(code, "text", theme)
183 } else {
184 Err(SyntaxError::UnsupportedLanguage(format!(
185 "from filename: {}",
186 filename
187 )))
188 }
189 }
190}