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, Option<usize>)> {
14 if pattern.is_empty() {
15 return Ok(vec![]);
16 }
17
18 let ast = Parser::new().parse(pattern).map_err(|e| {
19 let offset = pattern[..e.span().start.offset].chars().count();
20 (format!("Parse error: {e}"), Some(offset))
21 })?;
22
23 let mut visitor = ExplainVisitor::new();
24 visitor.visit(&ast);
25 Ok(visitor.into_nodes())
26}
27
28#[cfg(test)]
29mod tests {
30 use super::*;
31
32 #[test]
33 fn test_empty_pattern() {
34 let result = explain("").unwrap();
35 assert!(result.is_empty());
36 }
37
38 #[test]
39 fn test_simple_literal() {
40 let result = explain("hello").unwrap();
41 assert!(!result.is_empty());
42 }
43
44 #[test]
45 fn test_digit_class() {
46 let result = explain(r"\d+").unwrap();
47 assert!(!result.is_empty());
48 let text: String = result.iter().map(|n| n.description.clone()).collect();
49 assert!(text.to_lowercase().contains("digit"));
50 }
51
52 #[test]
53 fn test_capture_group() {
54 let result = explain(r"(\w+)@(\w+)").unwrap();
55 assert!(!result.is_empty());
56 let text: String = result.iter().map(|n| n.description.clone()).collect();
57 assert!(text.to_lowercase().contains("group"));
58 }
59
60 #[test]
61 fn test_invalid_pattern() {
62 let result = explain(r"(unclosed");
63 assert!(result.is_err());
64 let (msg, offset) = result.unwrap_err();
65 assert!(msg.contains("Parse error"));
66 assert!(offset.is_some());
67 }
68}