#[cfg(test)]
#[path = "test.rs"]
mod test;
use chumsky::error::Rich;
use chumsky::prelude::*;
#[derive(Debug, Clone, PartialEq)]
pub struct Path<'a> {
pub segments: Vec<Segment<'a>>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BracketContent {
pub singles: Vec<char>,
pub ranges: Vec<(char, char)>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Segment<'a> {
Literal(&'a str), SingleStar, DoubleStar, QuestionMark(char), Plus(char), Bracket(BracketContent), }
macro_rules! define_starters {
($($name:ident => $val:expr),* $(,)?) => {
$(const $name: char = $val;)*
const SEGMENT_STARTERS: &[char] = &[$($name),*];
};
}
define_starters! {
STAR => '*',
BRACKET_OPEN => '[',
QUESTION_MARK => '?',
PLUS => '+',
}
pub fn parse<'a>(path: &'a str) -> Result<Path<'a>, Vec<Rich<'a, char>>> {
let literal = none_of(SEGMENT_STARTERS).repeated().at_least(1).to_slice();
let literal_mod = literal
.then(just(QUESTION_MARK).or(just(PLUS)).or_not())
.map(|(lit, op): (&str, Option<char>)| match (op, lit.chars().next_back()) {
(Some(QUESTION_MARK), Some(last)) => {
let prefix = &lit[..lit.len() - last.len_utf8()];
if prefix.is_empty() {
vec![Segment::QuestionMark(last)]
} else {
vec![Segment::Literal(prefix), Segment::QuestionMark(last)]
}
}
(Some(PLUS), Some(last)) => {
let prefix = &lit[..lit.len() - last.len_utf8()];
if prefix.is_empty() {
vec![Segment::Plus(last)]
} else {
vec![Segment::Literal(prefix), Segment::Plus(last)]
}
}
_ => vec![Segment::Literal(lit)],
})
.boxed();
let double_star = just(STAR).then(just(STAR)).map(|_| vec![Segment::DoubleStar]);
let single_star = just(STAR).map(|_| vec![Segment::SingleStar]);
let bracket_inner = any::<&'a str, chumsky::extra::Full<Rich<'a, char>, (), ()>>()
.filter(|c| *c != ']')
.repeated()
.to_slice()
.validate(|inner: &str, map_extra: &mut _, emitter: &mut _| {
let span = map_extra.span();
let content: Vec<(usize, char)> = inner.char_indices().collect();
let mut singles = Vec::new();
let mut ranges = Vec::new();
if content.is_empty() {
emitter.emit(Rich::custom(span, "empty bracket"));
return BracketContent { singles, ranges };
}
let mut iter = content.iter().peekable();
while let Some(&(pos_a, a)) = iter.next() {
let is_range = iter.peek().map(|&(_, ch)| ch) == Some(&'-') && iter.clone().nth(1).is_some();
if is_range {
iter.next();
if let Some(&(pos_b, b)) = iter.next() {
if a > b {
let abs_start = span.start + pos_a;
let abs_end = span.start + pos_b + b.len_utf8();
let bad_span = abs_start..abs_end;
emitter.emit(Rich::custom(bad_span.into(), format!("invalid bracket range {a}-{b}")));
}
ranges.push((a, b));
}
} else {
singles.push(a);
}
}
BracketContent { singles, ranges }
});
let bracket = just(BRACKET_OPEN)
.ignore_then(bracket_inner)
.then_ignore(just(']'))
.map(|content: BracketContent| vec![Segment::Bracket(content)]);
let segment = choice((double_star, single_star, bracket, literal_mod));
let parser = segment
.repeated()
.collect::<Vec<_>>()
.map(|vecs: Vec<Vec<Segment>>| vecs.into_iter().flatten().collect::<Vec<_>>());
let (maybe_out, errs) = parser.parse(path).into_output_errors();
if !errs.is_empty() {
return Err(errs);
}
let segments = maybe_out.unwrap_or_default();
Ok(Path { segments })
}