use regex::Regex;
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
static COMPILE_CACHE: LazyLock<Mutex<HashMap<(String, bool), Regex>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub struct PyRegex {
pub source: String,
re: Regex,
}
fn translate(source: &str) -> String {
let mut out = String::with_capacity(source.len() + 8);
let mut chars = source.chars().peekable();
let mut in_class = false;
while let Some(ch) = chars.next() {
if ch == '\\' {
out.push(ch);
if let Some(&next) = chars.peek() {
if next == 'Z' {
out.pop();
out.push_str("\\z");
} else {
out.push(next);
}
chars.next();
}
continue;
}
match ch {
'[' if !in_class => {
in_class = true;
out.push(ch);
}
']' if in_class => {
in_class = false;
out.push(ch);
}
'$' if !in_class => {
out.push_str("(?:\\n?\\z)");
}
_ => out.push(ch),
}
}
out
}
impl PyRegex {
pub fn compile(source: &str, ignore_case: bool) -> Result<Self, String> {
let effective_case = if source.contains("(?-i:") {
false
} else {
ignore_case || source.starts_with("(?i)")
};
let cache_key = (source.to_owned(), effective_case);
if let Ok(cache) = COMPILE_CACHE.lock()
&& let Some(re) = cache.get(&cache_key)
{
return Ok(Self {
source: source.to_owned(),
re: re.clone(),
});
}
let translated = translate(source);
let mut pattern = String::with_capacity(translated.len() + 8);
if effective_case && !translated.starts_with("(?i)") {
pattern.push_str("(?i)");
}
pattern.push_str(&translated);
let re = Regex::new(&pattern).map_err(|e| format!("{source}: {e}"))?;
if let Ok(mut cache) = COMPILE_CACHE.lock() {
cache.insert(cache_key, re.clone());
}
Ok(Self {
source: source.to_owned(),
re,
})
}
#[must_use]
pub const fn re(&self) -> &Regex {
&self.re
}
}
#[must_use]
pub fn cp_index(haystack: &str, byte_idx: usize) -> usize {
haystack[..byte_idx].chars().count()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Candidate {
pub start: usize,
pub end: usize,
}
impl Candidate {
#[must_use]
pub const fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
#[must_use]
pub fn text<'a>(&self, haystack: &'a str) -> &'a str {
&haystack[self.start..self.end]
}
}
#[must_use]
pub fn find_all(re: &Regex, haystack: &str) -> Vec<Candidate> {
re.find_iter(haystack)
.map(|m| Candidate::new(m.start(), m.end()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn translates_absolute_end_anchor() {
let re = PyRegex::compile(r"\A[^\n]*x\s*\Z", false).unwrap();
assert!(re.re().is_match("a x"));
assert!(!re.re().is_match("a\nx"));
}
#[test]
fn translates_python_dollar() {
let re = PyRegex::compile(r"(?:/|\s|$)", false).unwrap();
assert!(re.re().is_match("/"));
assert!(re.re().is_match("x\n"));
}
#[test]
fn scoped_case_sensitivity() {
let re = PyRegex::compile("(?i)(?-i:rO0AB)", false).unwrap();
assert!(re.re().is_match("rO0AB"));
assert!(!re.re().is_match("ro0ab"));
}
}