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.
45#[allow(
46  clippy::missing_const_for_fn,
47  reason = "backend-enabled builds call non-const syntax manager constructors"
48)]
49pub fn create_default_manager(
50  syntax_queries_dir: Option<&std::path::Path>,
51) -> SyntaxResult<SyntaxManager> {
52  #[cfg(not(feature = "syntastica"))]
53  let _ = syntax_queries_dir;
54
55  // Runtime check for mutual exclusivity (backup to compile-time check)
56  #[cfg(all(feature = "syntastica", feature = "syntect"))]
57  {
58    return Err(SyntaxError::MutuallyExclusiveBackends);
59  }
60
61  #[cfg(feature = "syntastica")]
62  {
63    create_syntastica_manager(syntax_queries_dir)
64  }
65
66  #[cfg(feature = "syntect")]
67  {
68    return create_syntect_manager();
69  }
70
71  #[cfg(not(any(feature = "syntastica", feature = "syntect")))]
72  {
73    Err(SyntaxError::NoBackendAvailable)
74  }
75}
76
77#[cfg(test)]
78mod tests {
79  use super::{types::*, *};
80
81  #[test]
82  fn test_syntax_config_default() {
83    let config = SyntaxConfig::default();
84    assert!(config.fallback_to_plain);
85    assert!(config.language_aliases.contains_key("js"));
86    assert_eq!(config.language_aliases["js"], "javascript");
87  }
88
89  #[test]
90  fn test_language_resolution() {
91    let manager =
92      SyntaxManager::new(Box::new(NoopHighlighter), SyntaxConfig::default());
93
94    assert_eq!(manager.resolve_language("js"), "javascript");
95    assert_eq!(manager.resolve_language("py"), "python");
96    assert_eq!(manager.resolve_language("ts"), "typescript");
97    assert_eq!(manager.resolve_language("rust"), "rust");
98    assert_eq!(manager.resolve_language("nix"), "nix");
99  }
100
101  #[test]
102  fn test_modular_access_to_syntax_types() {
103    // Test that we can access types from submodules
104    use super::{
105      error::{SyntaxError, SyntaxResult},
106      types::SyntaxConfig,
107    };
108
109    // Test error type creation
110    let error = SyntaxError::UnsupportedLanguage("test".to_string());
111    assert!(matches!(error, SyntaxError::UnsupportedLanguage(_)));
112
113    // Test result type
114    let result: SyntaxResult<String> = Err(SyntaxError::NoBackendAvailable);
115    assert!(result.is_err());
116
117    // Test config creation
118    let config = SyntaxConfig::default();
119    assert!(config.fallback_to_plain);
120    assert!(config.language_aliases.contains_key("js"));
121
122    // Test that re-exports work at the module level
123    let _config2: SyntaxConfig = SyntaxConfig::default();
124    let _error2: SyntaxError = SyntaxError::BackendError("test".to_string());
125  }
126
127  #[cfg(feature = "syntect")]
128  #[test]
129  fn test_nix_language_support() {
130    let manager = create_default_manager(None)
131      .expect("Failed to create default syntax manager");
132    let languages = manager.highlighter().supported_languages();
133
134    // Verify that Nix is supported via two-face
135    assert!(
136      languages.contains(&"nix".to_string()),
137      "Expected Nix language support via two-face"
138    );
139
140    // Test highlighting Nix code
141    // Without two-face Nix is not highlighted, so what we are *really* trying
142    // to test here is whether two-face integration worked.
143    let nix_code = r#"
144{ pkgs ? import <nixpkgs> {} }:
145
146pkgs.stdenv.mkDerivation rec {
147  pname = "hello";
148  version = "2.12";
149
150  src = pkgs.fetchurl {
151    url = "mirror://gnu/hello/${pname}-${version}.tar.gz";
152    sha256 = "1ayhp9v4m4rdhjmnl2bq3cibrbqqkgjbl3s7yk2nhlh8vj3ay16g";
153  };
154}
155"#;
156
157    let result = manager.highlight_code(nix_code, "nix", Some("Nord"));
158    assert!(
159      result.is_ok(),
160      "Failed to highlight Nix code: {:?}",
161      result.err()
162    );
163  }
164
165  struct NoopHighlighter;
166
167  impl SyntaxHighlighter for NoopHighlighter {
168    fn name(&self) -> &'static str {
169      "noop"
170    }
171
172    fn supported_languages(&self) -> Vec<String> {
173      Vec::new()
174    }
175
176    fn available_themes(&self) -> Vec<String> {
177      Vec::new()
178    }
179
180    fn highlight(
181      &self,
182      _code: &str,
183      _language: &str,
184      _theme: Option<&str>,
185    ) -> SyntaxResult<String> {
186      Ok(String::new())
187    }
188
189    fn language_from_extension(&self, _extension: &str) -> Option<String> {
190      None
191    }
192  }
193}