Skip to main content

rgx/engine/
fancy.rs

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            // fancy-regex doesn't expose capture names directly, so we iterate by index
58            for i in 1..caps.len() {
59                if let Some(m) = caps.get(i) {
60                    captures.push(CaptureGroup {
61                        index: i,
62                        name: None, // fancy-regex doesn't easily expose names
63                        start: m.start(),
64                        end: m.end(),
65                        text: m.as_str().to_string(),
66                    });
67                }
68            }
69
70            matches.push(Match {
71                start: overall.start(),
72                end: overall.end(),
73                text: overall.as_str().to_string(),
74                captures,
75            });
76        }
77
78        Ok(matches)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_simple_match() {
88        let engine = FancyRegexEngine;
89        let flags = EngineFlags::default();
90        let compiled = engine.compile(r"\d+", &flags).unwrap();
91        let matches = compiled.find_matches("abc 123 def 456").unwrap();
92        assert_eq!(matches.len(), 2);
93        assert_eq!(matches[0].text, "123");
94    }
95
96    #[test]
97    fn test_lookahead() {
98        let engine = FancyRegexEngine;
99        let flags = EngineFlags::default();
100        let compiled = engine.compile(r"\w+(?=@)", &flags).unwrap();
101        let matches = compiled.find_matches("user@example.com").unwrap();
102        assert_eq!(matches.len(), 1);
103        assert_eq!(matches[0].text, "user");
104    }
105
106    #[test]
107    fn test_lookbehind() {
108        let engine = FancyRegexEngine;
109        let flags = EngineFlags::default();
110        let compiled = engine.compile(r"(?<=@)\w+", &flags).unwrap();
111        let matches = compiled.find_matches("user@example.com").unwrap();
112        assert_eq!(matches.len(), 1);
113        assert_eq!(matches[0].text, "example");
114    }
115}