Skip to main content

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