use regex::{Regex, RegexBuilder};
use crate::error::Result;
#[derive(Debug, Clone, Default)]
pub struct PatternOptions {
pub literal: bool,
pub ignore_case: bool,
pub single_line: bool,
}
#[derive(Debug, Clone)]
pub struct CompiledPattern {
regex: Regex,
replacement: String,
literal: bool,
}
impl CompiledPattern {
pub fn compile(pattern: &str, replacement: &str, opts: &PatternOptions) -> Result<Self> {
let source = if opts.literal { regex::escape(pattern) } else { pattern.to_owned() };
let regex = RegexBuilder::new(&source)
.case_insensitive(opts.ignore_case)
.dot_matches_new_line(!opts.single_line)
.multi_line(true)
.build()?;
Ok(Self { regex, replacement: replacement.to_owned(), literal: opts.literal })
}
pub fn regex(&self) -> &Regex {
&self.regex
}
pub fn replacement(&self) -> &str {
&self.replacement
}
pub fn is_convergent(&self) -> bool {
let replacement_probe = self.replacement_probe();
!self.regex.is_match(&replacement_probe)
}
fn replacement_probe(&self) -> String {
if self.literal {
return self.replacement.clone();
}
let mut out = String::with_capacity(self.replacement.len());
let bytes = self.replacement.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'$' && i + 1 < bytes.len() {
let next = bytes[i + 1];
if next == b'$' {
out.push('$');
i += 2;
continue;
}
if next.is_ascii_digit() {
i += 2;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
continue;
}
if next == b'{'
&& let Some((_, _, after)) =
crate::template_scan::scan_braced_name(&self.replacement, i)
{
i = after;
continue;
}
}
out.push(b as char);
i += 1;
}
out
}
}
#[cfg(test)]
#[path = "pattern_tests.rs"]
mod tests;