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            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}