use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rung {
Exact,
TrailingWs,
EdgeWs,
Normalized,
}
pub const LADDER: [Rung; 4] = [
Rung::Exact,
Rung::TrailingWs,
Rung::EdgeWs,
Rung::Normalized,
];
impl Rung {
pub fn label(self) -> &'static str {
match self {
Rung::Exact => "exact",
Rung::TrailingWs => "ignore-trailing-whitespace",
Rung::EdgeWs => "ignore-edge-whitespace",
Rung::Normalized => "unicode-normalized",
}
}
fn canon(self, s: &str) -> String {
match self {
Rung::Exact => s.to_string(),
Rung::TrailingWs => s.trim_end().to_string(),
Rung::EdgeWs => s.trim().to_string(),
Rung::Normalized => normalize(s).trim().to_string(),
}
}
}
fn normalize(s: &str) -> String {
s.chars()
.map(|c| match c {
'\u{2010}'..='\u{2015}' | '\u{2212}' => '-',
'\u{2018}'..='\u{201B}' => '\'',
'\u{201C}'..='\u{201F}' | '\u{00AB}' | '\u{00BB}' => '"',
'\u{00A0}' | '\u{2000}'..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}' => ' ',
c => c,
})
.collect()
}
#[derive(Debug, Error, PartialEq)]
pub enum Error {
#[error(
"not found at any rung (tried exact, ignore-trailing-whitespace, ignore-edge-whitespace, unicode-normalized)"
)]
NotFound,
#[error("{count} matches at the {rung} rung", rung = .rung.label())]
Ambiguous { rung: Rung, count: usize },
}
pub fn seek(
hay: &[String],
needle: &[String],
start: usize,
eof: bool,
) -> Result<(usize, Rung), Error> {
if needle.is_empty() {
let at = if eof { hay.len() } else { start };
return Ok((at, Rung::Exact));
}
let Some(last) = hay.len().checked_sub(needle.len()) else {
return Err(Error::NotFound);
};
if start > last {
return Err(Error::NotFound);
}
if eof {
for rung in LADDER {
if matches_at(hay, needle, last, rung) {
return Ok((last, rung));
}
}
}
for rung in LADDER {
let mut hits = (start..=last).filter(|&pos| matches_at(hay, needle, pos, rung));
if let Some(first) = hits.next() {
let count = 1 + hits.count();
if count > 1 {
return Err(Error::Ambiguous { rung, count });
}
return Ok((first, rung));
}
}
Err(Error::NotFound)
}
fn matches_at(hay: &[String], needle: &[String], pos: usize, rung: Rung) -> bool {
needle
.iter()
.zip(&hay[pos..pos + needle.len()])
.all(|(n, h)| rung.canon(n) == rung.canon(h))
}