1use super::{
2 CaptureGroup, CompiledRegex, EngineError, EngineFlags, EngineKind, EngineResult, Match,
3 RegexEngine,
4};
5
6pub struct FancyRegexEngine;
7
8impl RegexEngine for FancyRegexEngine {
9 fn kind(&self) -> EngineKind {
10 EngineKind::FancyRegex
11 }
12
13 fn compile(&self, pattern: &str, flags: &EngineFlags) -> EngineResult<Box<dyn CompiledRegex>> {
14 let mut flag_prefix = String::new();
15 if flags.case_insensitive {
16 flag_prefix.push('i');
17 }
18 if flags.multi_line {
19 flag_prefix.push('m');
20 }
21 if flags.dot_matches_newline {
22 flag_prefix.push('s');
23 }
24 if flags.unicode {
25 flag_prefix.push('u');
26 }
27 if flags.extended {
28 flag_prefix.push('x');
29 }
30
31 let full_pattern = if flag_prefix.is_empty() {
32 pattern.to_string()
33 } else {
34 format!("(?{flag_prefix}){pattern}")
35 };
36
37 let re = fancy_regex::Regex::new(&full_pattern)
38 .map_err(|e| EngineError::CompileError(e.to_string()))?;
39
40 Ok(Box::new(FancyCompiledRegex { re }))
41 }
42}
43
44struct FancyCompiledRegex {
45 re: fancy_regex::Regex,
46}
47
48impl CompiledRegex for FancyCompiledRegex {
49 fn find_matches(&self, text: &str) -> EngineResult<Vec<Match>> {
50 let mut matches = Vec::new();
51
52 for result in self.re.captures_iter(text) {
53 let caps = result.map_err(|e| EngineError::MatchError(e.to_string()))?;
54 let overall = caps.get(0).unwrap();
55 let mut captures = Vec::new();
56
57 for (i, name) in self.re.capture_names().enumerate() {
58 if i == 0 {
59 continue;
60 }
61 if let Some(m) = caps.get(i) {
62 captures.push(CaptureGroup {
63 index: i,
64 name: name.map(String::from),
65 start: m.start(),
66 end: m.end(),
67 text: m.as_str().to_string(),
68 });
69 }
70 }
71
72 matches.push(Match {
73 start: overall.start(),
74 end: overall.end(),
75 text: overall.as_str().to_string(),
76 captures,
77 });
78 }
79
80 Ok(matches)
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn test_simple_match() {
90 let engine = FancyRegexEngine;
91 let flags = EngineFlags::default();
92 let compiled = engine.compile(r"\d+", &flags).unwrap();
93 let matches = compiled.find_matches("abc 123 def 456").unwrap();
94 assert_eq!(matches.len(), 2);
95 assert_eq!(matches[0].text, "123");
96 }
97
98 #[test]
99 fn test_lookahead() {
100 let engine = FancyRegexEngine;
101 let flags = EngineFlags::default();
102 let compiled = engine.compile(r"\w+(?=@)", &flags).unwrap();
103 let matches = compiled.find_matches("user@example.com").unwrap();
104 assert_eq!(matches.len(), 1);
105 assert_eq!(matches[0].text, "user");
106 }
107
108 #[test]
109 fn test_named_captures() {
110 let engine = FancyRegexEngine;
111 let flags = EngineFlags::default();
112 let compiled = engine
113 .compile(r"(?P<user>\w+)@(?P<domain>\w+)", &flags)
114 .unwrap();
115 let matches = compiled.find_matches("user@example").unwrap();
116 assert_eq!(matches.len(), 1);
117 assert_eq!(matches[0].captures.len(), 2);
118 assert_eq!(matches[0].captures[0].name, Some("user".to_string()));
119 assert_eq!(matches[0].captures[0].text, "user");
120 assert_eq!(matches[0].captures[1].name, Some("domain".to_string()));
121 assert_eq!(matches[0].captures[1].text, "example");
122 }
123
124 #[test]
125 fn test_lookbehind() {
126 let engine = FancyRegexEngine;
127 let flags = EngineFlags::default();
128 let compiled = engine.compile(r"(?<=@)\w+", &flags).unwrap();
129 let matches = compiled.find_matches("user@example.com").unwrap();
130 assert_eq!(matches.len(), 1);
131 assert_eq!(matches[0].text, "example");
132 }
133}