Skip to main content

use_regex/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use regex::Regex;
5
6/// A reusable owned regex pattern wrapper.
7#[derive(Clone, Debug, Default, PartialEq, Eq)]
8pub struct RegexPattern {
9    pub pattern: String,
10}
11
12/// Optional flags that can be applied when constructing a regex.
13#[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/// A captured value and its byte span.
21#[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
51/// Escapes regex metacharacters in a literal string.
52pub fn escape_regex(input: &str) -> String {
53    regex::escape(input)
54}
55
56/// Returns `true` when the input appears to contain regex syntax.
57pub 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
73/// Returns `true` when the pattern compiles successfully.
74pub fn is_valid_regex(input: &str) -> bool {
75    compile_regex(input).is_some()
76}
77
78/// Returns `true` when the pattern matches any part of the input.
79pub 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
85/// Counts matches for a valid pattern.
86pub fn count_regex_matches(pattern: &str, input: &str) -> Option<usize> {
87    compile_regex(pattern).map(|regex| regex.find_iter(input).count())
88}
89
90/// Extracts matched substrings for a valid pattern.
91pub 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
100/// Extracts capture groups across all matches for a valid pattern.
101pub 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
125/// Replaces all matches for a valid pattern.
126pub 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
130/// Returns `true` when a valid pattern matches at the beginning of the input.
131pub 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
137/// Returns `true` when a valid pattern matches at the end of the input.
138pub 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}