#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Token {
Separator,
Elided,
Barrier,
}
#[derive(Clone, Copy, Debug)]
pub struct Match {
pub token: Token,
pub start: usize,
pub end: usize,
}
impl Match {
#[must_use]
pub const fn new(token: Token, start: usize, end: usize) -> Self {
Self { token, start, end }
}
#[must_use]
pub const fn at(token: Token, at: usize, end: usize) -> Self {
Self::new(token, at, end)
}
}
pub type Syntax = fn(&[u8], usize) -> Option<Match>;
#[must_use]
pub fn strip(gap: &str, syntax: Syntax) -> String {
let bytes = gap.as_bytes();
let mut out = String::with_capacity(gap.len());
let mut i = 0;
while i < bytes.len() {
let Some(found) = syntax(bytes, i) else {
let start = i;
loop {
i = next_boundary(bytes, i);
if i >= bytes.len() || syntax(bytes, i).is_some() {
break;
}
}
out.push_str(&gap[start..i]);
continue;
};
match found.token {
Token::Separator => out.push(' '),
Token::Elided => {}
Token::Barrier => out.push_str(&gap[i..found.end]),
}
i = found.end;
}
out
}
pub fn exclusions(gap: &str, offset: usize, syntax: Syntax, out: &mut Vec<(usize, usize)>) {
let bytes = gap.as_bytes();
let mut i = 0;
let mut previous_end = 0;
while i < bytes.len() {
let Some(found) = syntax(bytes, i) else {
i = next_boundary(bytes, i);
continue;
};
let start = found.start.max(previous_end);
if start < found.end {
out.push((offset + start, offset + found.end));
previous_end = found.end;
}
i = found.end;
}
}
const fn next_boundary(bytes: &[u8], i: usize) -> usize {
let mut j = i + 1;
while j < bytes.len() && bytes[j] & 0b1100_0000 == 0b1000_0000 {
j += 1;
}
j
}
#[cfg(test)]
mod tests {
use super::*;
fn toy(bytes: &[u8], i: usize) -> Option<Match> {
Some(match bytes[i..] {
[b'$', ..] => Match::at(
Token::Separator,
i,
super::super::shared::close_at(bytes, i + 1, b"$", None),
),
[b'\\', _, ..] => Match::at(Token::Elided, i, i + 2),
[b'|', ..] => Match::at(Token::Barrier, i, i + 1),
_ => return None,
})
}
#[test]
fn strip_applies_one_rule_per_token_kind() {
assert_eq!(strip("a $x$ b", toy), "a b");
assert_eq!(strip("a \\q b", toy), "a b");
assert_eq!(strip("a | b", toy), "a | b");
}
#[test]
fn strip_keeps_non_ascii_text_intact() {
assert_eq!(strip("ä ö $x$ ü", toy), "ä ö ü");
}
#[test]
fn exclusions_are_sorted_and_disjoint() {
let mut out = Vec::new();
exclusions("a $x$ \\q |", 100, toy, &mut out);
assert_eq!(out, vec![(102, 105), (106, 108), (109, 110)]);
}
#[test]
fn exclusions_clamp_a_token_that_reaches_backwards() {
fn greedy(bytes: &[u8], i: usize) -> Option<Match> {
match bytes[i..] {
[b'!', ..] => Some(Match::new(Token::Separator, i.saturating_sub(1), i + 1)),
_ => None,
}
}
let mut out = Vec::new();
exclusions("!!", 0, greedy, &mut out);
assert_eq!(
out,
vec![(0, 1), (1, 2)],
"the second token must not reach back into the first"
);
}
#[test]
fn an_unterminated_token_consumes_the_rest() {
assert_eq!(strip("a $x", toy), "a ");
}
}