ndg_commonmark/syntax/
types.rs

1//! Core types and traits for syntax highlighting.
2
3use std::collections::HashMap;
4
5use super::error::{SyntaxError, SyntaxResult};
6
7/// Trait for syntax highlighting backends.
8///
9/// Allows different syntax highlighting implementations to be used
10/// interchangeably. Implementations should handle language detection, theme
11/// management, and the actual highlighting process.
12pub trait SyntaxHighlighter: Send + Sync {
13  /// Get the name of this highlighter backend
14  fn name(&self) -> &'static str;
15
16  /// Get a list of supported languages
17  fn supported_languages(&self) -> Vec<String>;
18
19  /// Get a list of available themes
20  fn available_themes(&self) -> Vec<String>;
21
22  /// Check if a language is supported
23  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  /// Check if a theme is available
31  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  /// Highlight code with the specified language and theme.
39  ///
40  /// # Arguments
41  ///
42  /// * `code` - The source code to highlight
43  /// * `language` - The programming language (case-insensitive)
44  /// * `theme` - The theme name (case-insensitive, optional)
45  ///
46  /// # Returns
47  ///
48  /// Highlighted HTML string on success
49  fn highlight(
50    &self,
51    code: &str,
52    language: &str,
53    theme: Option<&str>,
54  ) -> SyntaxResult<String>;
55
56  /// Detect language from a file extension
57  fn language_from_extension(&self, extension: &str) -> Option<String>;
58
59  /// Detect language from a filename
60  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/// Configuration for syntax highlighting
69#[derive(Debug, Clone)]
70pub struct SyntaxConfig {
71  /// Default theme to use when none is specified
72  pub default_theme: Option<String>,
73
74  /// Language aliases for mapping common names to supported languages
75  pub language_aliases: HashMap<String, String>,
76
77  /// Whether to fall back to plain text for unsupported languages
78  pub fallback_to_plain: bool,
79}
80
81impl Default for SyntaxConfig {
82  fn default() -> Self {
83    let mut language_aliases = HashMap::new();
84
85    // Common aliases
86    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
104/// High-level syntax highlighting manager.
105///
106/// Manages a syntax highlighting backend and provides a convenient
107/// interface for highlighting code with configuration options.
108pub struct SyntaxManager {
109  highlighter: Box<dyn SyntaxHighlighter>,
110  config:      SyntaxConfig,
111}
112
113impl SyntaxManager {
114  /// Create a new syntax manager with the given highlighter and config
115  #[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  /// Create a new syntax manager with the default configuration
127  #[must_use]
128  pub fn with_highlighter(highlighter: Box<dyn SyntaxHighlighter>) -> Self {
129    Self::new(highlighter, SyntaxConfig::default())
130  }
131
132  /// Get the underlying highlighter
133  #[must_use]
134  pub fn highlighter(&self) -> &dyn SyntaxHighlighter {
135    self.highlighter.as_ref()
136  }
137
138  /// Get the configuration
139  #[must_use]
140  pub const fn config(&self) -> &SyntaxConfig {
141    &self.config
142  }
143
144  /// Update the configuration
145  pub fn set_config(&mut self, config: SyntaxConfig) {
146    self.config = config;
147  }
148
149  /// Resolve a language name using aliases
150  #[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  /// Highlight code with automatic language resolution and fallback
161  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    // Try to highlight with the resolved language
171    if self.highlighter.supports_language(&resolved_language) {
172      return self.highlighter.highlight(code, &resolved_language, theme);
173    }
174
175    // If language is not supported and fallback is enabled, try plain text
176    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  /// Highlight code from a filename
189  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}