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.supported_languages()
25            .iter()
26            .any(|lang| lang.eq_ignore_ascii_case(language))
27    }
28
29    /// Check if a theme is available
30    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    /// Highlight code with the specified language and theme.
37    ///
38    /// # Arguments
39    ///
40    /// * `code` - The source code to highlight
41    /// * `language` - The programming language (case-insensitive)
42    /// * `theme` - The theme name (case-insensitive, optional)
43    ///
44    /// # Returns
45    ///
46    /// Highlighted HTML string on success
47    fn highlight(&self, code: &str, language: &str, theme: Option<&str>) -> SyntaxResult<String>;
48
49    /// Detect language from a file extension
50    fn language_from_extension(&self, extension: &str) -> Option<String>;
51
52    /// Detect language from a filename
53    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/// Configuration for syntax highlighting
62#[derive(Debug, Clone)]
63pub struct SyntaxConfig {
64    /// Default theme to use when none is specified
65    pub default_theme: Option<String>,
66
67    /// Language aliases for mapping common names to supported languages
68    pub language_aliases: HashMap<String, String>,
69
70    /// Whether to fall back to plain text for unsupported languages
71    pub fallback_to_plain: bool,
72}
73
74impl Default for SyntaxConfig {
75    fn default() -> Self {
76        let mut language_aliases = HashMap::new();
77
78        // Common aliases
79        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
97/// High-level syntax highlighting manager.
98///
99/// Manages a syntax highlighting backend and provides a convenient
100/// interface for highlighting code with configuration options.
101pub struct SyntaxManager {
102    highlighter: Box<dyn SyntaxHighlighter>,
103    config: SyntaxConfig,
104}
105
106impl SyntaxManager {
107    /// Create a new syntax manager with the given highlighter and config
108    pub fn new(highlighter: Box<dyn SyntaxHighlighter>, config: SyntaxConfig) -> Self {
109        Self {
110            highlighter,
111            config,
112        }
113    }
114
115    /// Create a new syntax manager with the default configuration
116    pub fn with_highlighter(highlighter: Box<dyn SyntaxHighlighter>) -> Self {
117        Self::new(highlighter, SyntaxConfig::default())
118    }
119
120    /// Get the underlying highlighter
121    pub fn highlighter(&self) -> &dyn SyntaxHighlighter {
122        self.highlighter.as_ref()
123    }
124
125    /// Get the configuration
126    pub fn config(&self) -> &SyntaxConfig {
127        &self.config
128    }
129
130    /// Update the configuration
131    pub fn set_config(&mut self, config: SyntaxConfig) {
132        self.config = config;
133    }
134
135    /// Resolve a language name using aliases
136    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    /// Highlight code with automatic language resolution and fallback
145    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        // Try to highlight with the resolved language
155        if self.highlighter.supports_language(&resolved_language) {
156            return self.highlighter.highlight(code, &resolved_language, theme);
157        }
158
159        // If language is not supported and fallback is enabled, try plain text
160        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    /// Highlight code from a filename
173    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}