use regex::{Regex, RegexBuilder};
#[derive(Debug, Clone)]
enum Rule {
Literal { find: String, replace: String },
Pattern {
find: Regex,
replace: String,
expand: bool,
},
}
#[derive(Debug, Clone)]
pub struct Replacements {
rules: Vec<Rule>,
}
impl Replacements {
pub fn compile(
pairs: &[(String, String)],
use_regex: bool,
ignore_case: bool,
) -> Result<Self, String> {
let mut rules = Vec::with_capacity(pairs.len());
for (find, replace) in pairs {
if find.is_empty() {
return Err("a pattern to find cannot be empty".to_string());
}
if !use_regex && !ignore_case {
rules.push(Rule::Literal {
find: find.clone(),
replace: replace.clone(),
});
continue;
}
let source = if use_regex {
find.clone()
} else {
regex::escape(find)
};
let compiled = RegexBuilder::new(&source)
.case_insensitive(ignore_case)
.multi_line(true)
.dot_matches_new_line(false)
.build()
.map_err(|error| format!("pattern '{find}' is not a valid regular expression: {error}"))?;
rules.push(Rule::Pattern {
find: compiled,
replace: replace.clone(),
expand: use_regex,
});
}
Ok(Self { rules })
}
pub fn apply(&self, text: &str) -> String {
let mut current = std::borrow::Cow::Borrowed(text);
for rule in &self.rules {
match rule {
Rule::Literal { find, replace } => {
if current.contains(find.as_str()) {
current = std::borrow::Cow::Owned(current.replace(find.as_str(), replace));
}
}
Rule::Pattern {
find,
replace,
expand,
} => {
let next = if *expand {
find.replace_all(¤t, replace.as_str())
} else {
find.replace_all(¤t, regex::NoExpand(replace.as_str()))
};
if let std::borrow::Cow::Owned(owned) = next {
current = std::borrow::Cow::Owned(owned);
}
}
}
}
current.into_owned()
}
pub fn changes(&self, text: &str) -> bool {
self.apply(text) != text
}
pub fn count(&self, text: &str) -> usize {
let mut current = text.to_string();
let mut total = 0;
for rule in &self.rules {
let (next, made) = match rule {
Rule::Literal { find, replace } => {
let made = current.matches(find.as_str()).count();
(
if made > 0 {
current.replace(find.as_str(), replace)
} else {
current.clone()
},
made,
)
}
Rule::Pattern {
find,
replace,
expand,
} => {
let made = find.find_iter(¤t).count();
let next = if *expand {
find.replace_all(¤t, replace.as_str()).into_owned()
} else {
find.replace_all(¤t, regex::NoExpand(replace.as_str()))
.into_owned()
};
(next, made)
}
};
total += made;
current = next;
}
total
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
}
#[cfg(test)]
#[path = "replacement_tests.rs"]
mod replacement_tests;