ndg_commonmark/syntax/
mod.rs

1//! Provides a trait-based architecture for syntax highlighting that allows
2//! multiple backends to be plugged in.
3//! Currently supported backends:
4//! - **Syntastica** - Modern tree-sitter based highlighting with 60+ themes
5//! - **Syntect** - Uses Sublime Text syntax definitions, with two-face added
6//!    for extended syntax definitions
7
8pub mod error;
9pub mod types;
10
11// Re-export commonly used types
12pub use error::{SyntaxError, SyntaxResult};
13pub use types::{SyntaxConfig, SyntaxHighlighter, SyntaxManager};
14
15// Compile-time check for mutually exclusive backends
16#[cfg(all(feature = "syntastica", feature = "syntect"))]
17compile_error!(
18    "Cannot enable both 'syntastica' and 'syntect' features simultaneously. They are mutually exclusive."
19);
20
21// Syntastica backend implementation
22#[cfg(feature = "syntastica")]
23mod syntastica;
24#[cfg(feature = "syntastica")]
25pub use syntastica::*;
26
27// Syntect backend implementation
28#[cfg(feature = "syntect")]
29mod syntect;
30#[cfg(feature = "syntect")]
31pub use syntect::*;
32
33/// Create the default syntax manager based on available features.
34///
35/// This function will:
36/// - Use **Syntastica** if the 'syntastica' feature is enabled
37/// - Use **Syntect** if the 'syntect' feature is enabled
38/// - Return an error if both are enabled (mutual exclusivity check)
39/// - Return an error if neither is enabled
40///
41/// **Note**: While the SYntect feature is enabled, the two-face crate
42/// will also be pulled to provide additional Syntax highlighting.
43pub fn create_default_manager() -> SyntaxResult<SyntaxManager> {
44    // Runtime check for mutual exclusivity (backup to compile-time check)
45    #[cfg(all(feature = "syntastica", feature = "syntect"))]
46    {
47        return Err(SyntaxError::MutuallyExclusiveBackends);
48    }
49
50    #[cfg(feature = "syntastica")]
51    {
52        return create_syntastica_manager();
53    }
54
55    #[cfg(feature = "syntect")]
56    {
57        return create_syntect_manager();
58    }
59
60    #[cfg(not(any(feature = "syntastica", feature = "syntect")))]
61    {
62        Err(SyntaxError::NoBackendAvailable)
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::{types::*, *};
69
70    #[test]
71    fn test_syntax_config_default() {
72        let config = SyntaxConfig::default();
73        assert!(config.fallback_to_plain);
74        assert!(config.language_aliases.contains_key("js"));
75        assert_eq!(config.language_aliases["js"], "javascript");
76    }
77
78    #[cfg(feature = "syntect")]
79    #[test]
80    fn test_syntect_highlighter() {
81        let highlighter = SyntectHighlighter::default();
82        assert_eq!(highlighter.name(), "Syntect");
83        assert!(!highlighter.supported_languages().is_empty());
84        assert!(!highlighter.available_themes().is_empty());
85    }
86
87    #[cfg(feature = "syntect")]
88    #[test]
89    fn test_syntect_highlight_simple() {
90        let highlighter = SyntectHighlighter::default();
91        let result = highlighter.highlight("fn main() {}", "rust", None);
92        assert!(result.is_ok());
93        let html = result.unwrap();
94        assert!(html.contains("main"));
95    }
96
97    #[cfg(feature = "syntastica")]
98    #[test]
99    fn test_syntastica_highlighter() {
100        let highlighter = SyntasticaHighlighter::new().unwrap();
101        assert_eq!(highlighter.name(), "Syntastica");
102        assert!(!highlighter.supported_languages().is_empty());
103        assert!(!highlighter.available_themes().is_empty());
104    }
105
106    #[cfg(feature = "syntastica")]
107    #[test]
108    fn test_syntastica_highlight_simple() {
109        let highlighter = SyntasticaHighlighter::new().unwrap();
110        let result = highlighter.highlight("fn main() {}", "rust", None);
111        assert!(result.is_ok());
112        let html = result.unwrap();
113        assert!(html.contains("main"));
114    }
115
116    #[cfg(any(feature = "syntastica", feature = "syntect"))]
117    #[test]
118    fn test_syntax_manager() {
119        let manager = create_default_manager().unwrap();
120        assert!(!manager.highlighter().supported_languages().is_empty());
121
122        let resolved = manager.resolve_language("js");
123        assert_eq!(resolved, "javascript");
124    }
125
126    #[cfg(any(feature = "syntastica", feature = "syntect"))]
127    #[test]
128    fn test_language_resolution() {
129        let manager = create_default_manager().unwrap();
130
131        // Test alias resolution
132        assert_eq!(manager.resolve_language("js"), "javascript");
133        assert_eq!(manager.resolve_language("py"), "python");
134        assert_eq!(manager.resolve_language("ts"), "typescript");
135
136        // Test non-alias languages
137        assert_eq!(manager.resolve_language("rust"), "rust");
138        assert_eq!(manager.resolve_language("nix"), "nix");
139    }
140
141    #[test]
142    fn test_modular_access_to_syntax_types() {
143        // Test that we can access types from submodules
144        use super::{
145            error::{SyntaxError, SyntaxResult},
146            types::SyntaxConfig,
147        };
148
149        // Test error type creation
150        let error = SyntaxError::UnsupportedLanguage("test".to_string());
151        assert!(matches!(error, SyntaxError::UnsupportedLanguage(_)));
152
153        // Test result type
154        let result: SyntaxResult<String> = Err(SyntaxError::NoBackendAvailable);
155        assert!(result.is_err());
156
157        // Test config creation
158        let config = SyntaxConfig::default();
159        assert!(config.fallback_to_plain);
160        assert!(config.language_aliases.contains_key("js"));
161
162        // Test that re-exports work at the module level
163        let _config2: SyntaxConfig = SyntaxConfig::default();
164        let _error2: SyntaxError = SyntaxError::BackendError("test".to_string());
165    }
166
167    #[cfg(any(feature = "syntastica", feature = "syntect"))]
168    #[test]
169    fn test_extended_theme_availability() {
170        let manager = create_default_manager().unwrap();
171        let themes = manager.highlighter().available_themes();
172
173        // Verify we have loaded like a lot of themes
174        assert!(
175            themes.len() > 30,
176            "Expected > 30 themes, got {}",
177            themes.len()
178        );
179
180        // Check for specific themes that should be available with our enhancements
181        #[cfg(feature = "syntastica")]
182        {
183            assert!(
184                themes.contains(&"github::dark".to_string()),
185                "Expected github::dark theme"
186            );
187            assert!(
188                themes.contains(&"gruvbox::dark".to_string()),
189                "Expected gruvbox::dark theme"
190            );
191            assert!(
192                themes.contains(&"nord::nord".to_string()),
193                "Expected nord::nord theme"
194            );
195            assert!(
196                themes.contains(&"dracula::dracula".to_string()),
197                "Expected dracula::dracula theme"
198            );
199        }
200
201        #[cfg(feature = "syntect")]
202        {
203            assert!(
204                themes.contains(&"Nord".to_string()),
205                "Expected Nord theme from two-face"
206            );
207            assert!(
208                themes.contains(&"Dracula".to_string()),
209                "Expected Dracula theme from two-face"
210            );
211            assert!(
212                themes.contains(&"GruvboxDark".to_string()),
213                "Expected GruvboxDark theme from two-face"
214            );
215            assert!(
216                themes.contains(&"VisualStudioDarkPlus".to_string()),
217                "Expected VisualStudioDarkPlus theme from two-face"
218            );
219        }
220
221        println!("Available themes ({}):", themes.len());
222        for theme in &themes {
223            println!("  - {}", theme);
224        }
225    }
226
227    #[cfg(feature = "syntect")]
228    #[test]
229    fn test_nix_language_support() {
230        let manager = create_default_manager().unwrap();
231        let languages = manager.highlighter().supported_languages();
232
233        // Verify that Nix is supported via two-face
234        assert!(
235            languages.contains(&"nix".to_string()),
236            "Expected Nix language support via two-face"
237        );
238
239        // Test highlighting Nix code
240        // Without two-face Nix is not highlighted, so what we are *really* trying
241        // to test here is whether two-face integration worked.
242        let nix_code = r#"
243{ pkgs ? import <nixpkgs> {} }:
244
245pkgs.stdenv.mkDerivation rec {
246  pname = "hello";
247  version = "2.12";
248
249  src = pkgs.fetchurl {
250    url = "mirror://gnu/hello/${pname}-${version}.tar.gz";
251    sha256 = "1ayhp9v4m4rdhjmnl2bq3cibrbqqkgjbl3s7yk2nhlh8vj3ay16g";
252  };
253}
254"#;
255
256        let result = manager.highlight_code(nix_code, "nix", Some("Nord"));
257        assert!(
258            result.is_ok(),
259            "Failed to highlight Nix code: {:?}",
260            result.err()
261        );
262    }
263}