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