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