1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use regex::Regex;
5
6#[derive(Clone, Debug, Default, PartialEq, Eq)]
8pub struct RegexPattern {
9 pub pattern: String,
10}
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14pub struct RegexFlags {
15 pub case_insensitive: bool,
16 pub multi_line: bool,
17 pub dot_matches_new_line: bool,
18}
19
20#[derive(Clone, Debug, Default, PartialEq, Eq)]
22pub struct RegexCapture {
23 pub name: Option<String>,
24 pub value: String,
25 pub start: usize,
26 pub end: usize,
27}
28
29fn compile_regex(pattern: &str) -> Option<Regex> {
30 Regex::new(pattern).ok()
31}
32
33fn compile_anchored(pattern: &str, anchor_start: bool, anchor_end: bool) -> Option<Regex> {
34 let mut anchored = String::new();
35
36 if anchor_start {
37 anchored.push('^');
38 }
39
40 anchored.push_str("(?:");
41 anchored.push_str(pattern);
42 anchored.push(')');
43
44 if anchor_end {
45 anchored.push('$');
46 }
47
48 compile_regex(&anchored)
49}
50
51pub fn escape_regex(input: &str) -> String {
53 regex::escape(input)
54}
55
56pub fn looks_like_regex(input: &str) -> bool {
58 let trimmed = input.trim();
59
60 if trimmed.is_empty() {
61 return false;
62 }
63
64 if trimmed.starts_with('/') && trimmed[1..].contains('/') {
65 return true;
66 }
67
68 trimmed.contains([
69 '\\', '^', '$', '.', '|', '?', '*', '+', '(', ')', '[', ']', '{', '}',
70 ])
71}
72
73pub fn is_valid_regex(input: &str) -> bool {
75 compile_regex(input).is_some()
76}
77
78pub fn has_regex_match(pattern: &str, input: &str) -> bool {
80 compile_regex(pattern)
81 .map(|regex| regex.is_match(input))
82 .unwrap_or(false)
83}
84
85pub fn count_regex_matches(pattern: &str, input: &str) -> Option<usize> {
87 compile_regex(pattern).map(|regex| regex.find_iter(input).count())
88}
89
90pub fn extract_regex_matches(pattern: &str, input: &str) -> Option<Vec<String>> {
92 compile_regex(pattern).map(|regex| {
93 regex
94 .find_iter(input)
95 .map(|item| item.as_str().to_string())
96 .collect()
97 })
98}
99
100pub fn extract_regex_captures(pattern: &str, input: &str) -> Option<Vec<RegexCapture>> {
102 let regex = compile_regex(pattern)?;
103 let capture_names: Vec<Option<String>> = regex
104 .capture_names()
105 .map(|name| name.map(ToOwned::to_owned))
106 .collect();
107 let mut values = Vec::new();
108
109 for captures in regex.captures_iter(input) {
110 for index in 1..captures.len() {
111 if let Some(capture) = captures.get(index) {
112 values.push(RegexCapture {
113 name: capture_names.get(index).cloned().flatten(),
114 value: capture.as_str().to_string(),
115 start: capture.start(),
116 end: capture.end(),
117 });
118 }
119 }
120 }
121
122 Some(values)
123}
124
125pub fn replace_regex_all(pattern: &str, input: &str, replacement: &str) -> Option<String> {
127 compile_regex(pattern).map(|regex| regex.replace_all(input, replacement).into_owned())
128}
129
130pub fn regex_starts_with(pattern: &str, input: &str) -> bool {
132 compile_anchored(pattern, true, false)
133 .map(|regex| regex.is_match(input))
134 .unwrap_or(false)
135}
136
137pub fn regex_ends_with(pattern: &str, input: &str) -> bool {
139 compile_anchored(pattern, false, true)
140 .map(|regex| regex.is_match(input))
141 .unwrap_or(false)
142}