pub use fancy_regex::{Captures, Error, Match, Regex};
use lazy_static::lazy_static;
use std::sync::Arc;
lazy_static! {
static ref SLASHES_RE: Regex = Regex::new(r"[\\/]+").unwrap();
static ref EXPAND_RE: Regex = Regex::new("\\\\&").unwrap();
}
pub fn normalize_pattern(pattern: &str) -> String {
let mut pattern = pattern.to_string();
if !(pattern.starts_with("RE:") || pattern.starts_with("!RE:")) {
pattern = SLASHES_RE.replace_all(pattern.as_str(), "/").to_string();
}
if pattern.len() > 1 {
pattern = pattern.trim_end_matches('/').to_string();
}
pattern
}
pub enum Replacement {
String(String),
Function(fn(&str) -> String),
Closure(Box<dyn FnMut(String) -> String + Sync + Send>),
}
pub struct Replacer {
compiled: Option<Regex>,
pats: Vec<(String, Arc<Replacement>)>,
}
impl Replacer {
pub fn new(source: Option<&Self>) -> Self {
let mut ret = Self::empty();
if let Some(source) = source {
ret.add_replacer(source);
}
ret
}
pub fn empty() -> Self {
Self {
compiled: None,
pats: Vec::new(),
}
}
pub fn add(&mut self, pat: &str, fun: Replacement) {
self.compiled = None;
self.pats.push((pat.to_string(), Arc::new(fun)));
}
pub fn add_validate(&mut self, pat: &str, fun: Replacement) -> Result<(), Error> {
Regex::new(pat)?;
self.add(pat, fun);
Ok(())
}
pub fn add_replacer(&mut self, replacer: &Replacer) {
self.compiled = None;
self.pats.extend(replacer.pats.clone());
}
pub fn replace(&mut self, text: &str) -> std::result::Result<String, Error> {
if self.pats.is_empty() {
return Ok(text.to_string());
}
if self.compiled.is_none() {
let pat_str = self
.pats
.iter()
.map(|(pat, _)| format!("({})", pat))
.collect::<Vec<_>>()
.join("|");
self.compiled = Some(Regex::new(&pat_str)?);
}
let pats = &mut self.pats;
fn expand(text: &str, rep: &str) -> String {
rep.replace("\\&", text)
}
fn sub(m: &Match, rep: &mut Arc<Replacement>) -> String {
let replacement = Arc::get_mut(rep).unwrap();
match replacement {
Replacement::String(s) => expand(m.as_str(), s.as_str()),
Replacement::Function(f) => f(m.as_str()),
Replacement::Closure(f) => f(m.as_str().to_string()),
}
}
Ok(self
.compiled
.as_ref()
.unwrap()
.replace_all(text, |caps: &Captures| {
for (index, m) in caps.iter().skip(1).enumerate() {
if let Some(m) = m {
return sub(&m, &mut pats[index].1);
}
}
unreachable!();
})
.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(text: &str) -> Replacement {
Replacement::String(text.to_string())
}
#[test]
fn test_replacer_simple() {
let mut r = Replacer::empty();
r.add("a", s("b"));
assert_eq!(r.replace("a").unwrap(), "b");
}
#[test]
fn test_replacer_function() {
let mut r = Replacer::empty();
r.add(
"a",
Replacement::Function(|m| {
assert_eq!(m, "a");
"c".to_string()
}),
);
assert_eq!(r.replace("a").unwrap(), "c");
}
#[test]
fn test_replacer_multiple() {
let mut r = Replacer::empty();
r.add("a", s("b"));
r.add("c", s("d"));
assert_eq!(r.replace("a").unwrap(), "b");
assert_eq!(r.replace("c").unwrap(), "d");
}
#[test]
fn test_replacer_none() {
let mut r = Replacer::empty();
assert_eq!(r.replace("a").unwrap(), "a");
}
#[test]
fn test_replacer_partial() {
let mut r = Replacer::empty();
r.add("a", s("b"));
assert_eq!(r.replace("ac").unwrap(), "bc");
}
#[test]
fn test_replacer_expands_ampersand() {
let mut r = Replacer::empty();
r.add("a", s("[\\&]"));
assert_eq!(r.replace("xax").unwrap(), "x[a]x");
}
#[test]
fn test_normalize_pattern_backslashes() {
assert_eq!(normalize_pattern("\\"), "/");
assert_eq!(normalize_pattern("\\\\"), "/");
assert_eq!(normalize_pattern("\\foo\\bar"), "/foo/bar");
assert_eq!(normalize_pattern("foo\\bar\\"), "foo/bar");
assert_eq!(normalize_pattern("\\\\foo\\\\bar\\\\"), "/foo/bar");
}
#[test]
fn test_normalize_pattern_forward_slashes() {
assert_eq!(normalize_pattern("/"), "/");
assert_eq!(normalize_pattern("//"), "/");
assert_eq!(normalize_pattern("/foo/bar"), "/foo/bar");
assert_eq!(normalize_pattern("foo/bar/"), "foo/bar");
assert_eq!(normalize_pattern("//foo//bar//"), "/foo/bar");
}
#[test]
fn test_normalize_pattern_mixed_slashes() {
assert_eq!(normalize_pattern("\\/\\foo//\\///bar/\\\\/"), "/foo/bar");
}
#[test]
fn test_normalize_pattern_leaves_regex_untouched() {
assert_eq!(normalize_pattern("RE:a//b"), "RE:a//b");
assert_eq!(normalize_pattern("!RE:a\\\\b"), "!RE:a\\\\b");
}
}