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
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(
50 &self,
51 code: &str,
52 language: &str,
53 theme: Option<&str>,
54 ) -> SyntaxResult<String>;
55
56 fn language_from_extension(&self, extension: &str) -> Option<String>;
58
59 fn language_from_filename(&self, filename: &str) -> Option<String> {
61 std::path::Path::new(filename)
62 .extension()
63 .and_then(|ext| ext.to_str())
64 .and_then(|ext| self.language_from_extension(ext))
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct SyntaxConfig {
71 pub default_theme: Option<String>,
73
74 pub language_aliases: HashMap<String, String>,
76
77 pub fallback_to_plain: bool,
79}
80
81impl Default for SyntaxConfig {
82 fn default() -> Self {
83 let mut language_aliases = HashMap::new();
84
85 language_aliases.insert("js".to_string(), "javascript".to_string());
87 language_aliases.insert("ts".to_string(), "typescript".to_string());
88 language_aliases.insert("py".to_string(), "python".to_string());
89 language_aliases.insert("rb".to_string(), "ruby".to_string());
90 language_aliases.insert("sh".to_string(), "bash".to_string());
91 language_aliases.insert("shell".to_string(), "bash".to_string());
92 language_aliases.insert("yml".to_string(), "yaml".to_string());
93 language_aliases.insert("nixos".to_string(), "nix".to_string());
94 language_aliases.insert("md".to_string(), "markdown".to_string());
95
96 Self {
97 default_theme: None,
98 language_aliases,
99 fallback_to_plain: true,
100 }
101 }
102}
103
104pub struct SyntaxManager {
109 highlighter: Box<dyn SyntaxHighlighter>,
110 config: SyntaxConfig,
111}
112
113impl SyntaxManager {
114 #[must_use]
116 pub fn new(
117 highlighter: Box<dyn SyntaxHighlighter>,
118 config: SyntaxConfig,
119 ) -> Self {
120 Self {
121 highlighter,
122 config,
123 }
124 }
125
126 #[must_use]
128 pub fn with_highlighter(highlighter: Box<dyn SyntaxHighlighter>) -> Self {
129 Self::new(highlighter, SyntaxConfig::default())
130 }
131
132 #[must_use]
134 pub fn highlighter(&self) -> &dyn SyntaxHighlighter {
135 self.highlighter.as_ref()
136 }
137
138 #[must_use]
140 pub const fn config(&self) -> &SyntaxConfig {
141 &self.config
142 }
143
144 pub fn set_config(&mut self, config: SyntaxConfig) {
146 self.config = config;
147 }
148
149 #[must_use]
151 pub fn resolve_language(&self, language: &str) -> String {
152 self
153 .config
154 .language_aliases
155 .get(language)
156 .cloned()
157 .unwrap_or_else(|| language.to_string())
158 }
159
160 pub fn highlight_code(
162 &self,
163 code: &str,
164 language: &str,
165 theme: Option<&str>,
166 ) -> SyntaxResult<String> {
167 let resolved_language = self.resolve_language(language);
168 let theme = theme.or(self.config.default_theme.as_deref());
169
170 if self.highlighter.supports_language(&resolved_language) {
172 return self.highlighter.highlight(code, &resolved_language, theme);
173 }
174
175 if self.config.fallback_to_plain {
177 if self.highlighter.supports_language("text") {
178 return self.highlighter.highlight(code, "text", theme);
179 }
180 if self.highlighter.supports_language("plain") {
181 return self.highlighter.highlight(code, "plain", theme);
182 }
183 }
184
185 Err(SyntaxError::UnsupportedLanguage(resolved_language))
186 }
187
188 pub fn highlight_from_filename(
190 &self,
191 code: &str,
192 filename: &str,
193 theme: Option<&str>,
194 ) -> SyntaxResult<String> {
195 if let Some(language) = self.highlighter.language_from_filename(filename) {
196 self.highlight_code(code, &language, theme)
197 } else if self.config.fallback_to_plain {
198 self.highlight_code(code, "text", theme)
199 } else {
200 Err(SyntaxError::UnsupportedLanguage(format!(
201 "from filename: {filename}"
202 )))
203 }
204 }
205}