use std::borrow::Cow;
use regex::Captures;
const CAPTURE_LENGTH_GUESS: usize = 8;
#[derive(Debug, Clone)]
pub(crate) enum Replacement {
Literal(String),
Capture(usize),
Template {
segments: Vec<Segment>,
capacity: usize,
},
}
#[derive(Debug, Clone)]
pub(crate) enum Segment {
Literal(String),
Capture(usize),
}
impl Replacement {
pub(crate) fn new(text: &str) -> Replacement {
if let [b'$', index] = text.as_bytes()
&& index.is_ascii_digit()
{
return Replacement::Capture((index - b'0') as usize);
}
match Self::parse(text) {
Some(segments) => {
let capacity = segments
.iter()
.map(|segment| match segment {
Segment::Literal(text) => text.len(),
Segment::Capture(_) => CAPTURE_LENGTH_GUESS,
})
.sum();
Replacement::Template {
segments,
capacity,
}
},
None => Replacement::Literal(text.trim().to_string()),
}
}
fn parse(text: &str) -> Option<Vec<Segment>> {
let bytes = text.as_bytes();
let mut segments = Vec::new();
let mut literal_start = 0;
let mut i = 0;
while i + 1 < bytes.len() {
if bytes[i] == b'$' && bytes[i + 1].is_ascii_digit() {
if literal_start < i {
segments.push(Segment::Literal(text[literal_start..i].to_string()));
}
segments.push(Segment::Capture((bytes[i + 1] - b'0') as usize));
i += 2;
literal_start = i;
} else {
i += 1;
}
}
if segments.is_empty() {
return None;
}
if literal_start < bytes.len() {
segments.push(Segment::Literal(text[literal_start..].to_string()));
}
Some(segments)
}
pub(crate) fn resolve<'a>(&'a self, captures: &Captures<'a>) -> Option<Cow<'a, str>> {
match self {
Replacement::Literal(text) if text.is_empty() => None,
Replacement::Literal(text) => Some(Cow::from(text.as_str())),
Replacement::Capture(index) => capture_str(*index, captures).map(Cow::from),
Replacement::Template {
segments,
capacity,
} => {
let mut result = String::with_capacity(*capacity);
for segment in segments {
match segment {
Segment::Literal(text) => result.push_str(text),
Segment::Capture(index) => {
result.push_str(captures.get(*index).map_or("", |m| m.as_str()))
},
}
}
let trimmed_end = result.trim_end().len();
result.truncate(trimmed_end);
let trimmed_start = result.len() - result.trim_start().len();
result.drain(..trimmed_start);
if result.is_empty() { None } else { Some(Cow::from(result)) }
},
}
}
}
#[inline]
pub(crate) fn resolve<'a>(
index: usize,
replacement: Option<&'a Replacement>,
captures: &Captures<'a>,
) -> Option<Cow<'a, str>> {
match replacement {
Some(replacement) => replacement.resolve(captures),
None => capture_str(index, captures).map(Cow::from),
}
}
#[inline]
pub(crate) fn capture_str<'a>(index: usize, captures: &Captures<'a>) -> Option<&'a str> {
let s = captures.get(index)?.as_str().trim();
if s.is_empty() { None } else { Some(s) }
}