1pub mod formatter;
2pub mod visitor;
3
4use regex_syntax::ast::parse::Parser;
5use visitor::ExplainVisitor;
6
7#[derive(Debug, Clone)]
8pub struct ExplainNode {
9 pub depth: usize,
10 pub description: String,
11}
12
13pub fn explain(pattern: &str) -> Result<Vec<ExplainNode>, String> {
14 if pattern.is_empty() {
15 return Ok(vec![]);
16 }
17
18 let ast = Parser::new()
19 .parse(pattern)
20 .map_err(|e| format!("Parse error: {e}"))?;
21
22 let mut visitor = ExplainVisitor::new();
23 visitor.visit(&ast);
24 Ok(visitor.into_nodes())
25}
26
27#[cfg(test)]
28mod tests {
29 use super::*;
30
31 #[test]
32 fn test_empty_pattern() {
33 let result = explain("").unwrap();
34 assert!(result.is_empty());
35 }
36
37 #[test]
38 fn test_simple_literal() {
39 let result = explain("hello").unwrap();
40 assert!(!result.is_empty());
41 }
42
43 #[test]
44 fn test_digit_class() {
45 let result = explain(r"\d+").unwrap();
46 assert!(!result.is_empty());
47 let text: String = result.iter().map(|n| n.description.clone()).collect();
48 assert!(text.to_lowercase().contains("digit"));
49 }
50
51 #[test]
52 fn test_capture_group() {
53 let result = explain(r"(\w+)@(\w+)").unwrap();
54 assert!(!result.is_empty());
55 let text: String = result.iter().map(|n| n.description.clone()).collect();
56 assert!(text.to_lowercase().contains("group"));
57 }
58
59 #[test]
60 fn test_invalid_pattern() {
61 let result = explain(r"(unclosed");
62 assert!(result.is_err());
63 }
64}