#![deny(missing_docs)]
use std::fmt::{self, Debug, Formatter, Write};
use std::ops::Range;
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct Scanner<'a> {
string: &'a str,
cursor: usize,
}
impl<'a> Scanner<'a> {
#[inline]
pub fn new(string: &'a str) -> Self {
Self { string, cursor: 0 }
}
#[inline]
pub fn string(&self) -> &'a str {
self.string
}
#[inline]
pub fn cursor(&self) -> usize {
self.cursor
}
#[inline]
pub fn done(&self) -> bool {
self.cursor == self.string.len()
}
#[inline]
pub fn before(&self) -> &'a str {
debug_assert!(self.string.is_char_boundary(self.cursor));
unsafe { self.string.get_unchecked(.. self.cursor) }
}
#[inline]
pub fn after(&self) -> &'a str {
debug_assert!(self.string.is_char_boundary(self.cursor));
unsafe { self.string.get_unchecked(self.cursor ..) }
}
#[inline]
pub fn parts(&self) -> (&'a str, &'a str) {
(self.before(), self.after())
}
#[inline]
pub fn from(&self, start: usize) -> &'a str {
let start = self.snap(start).min(self.cursor);
debug_assert!(self.string.is_char_boundary(start));
debug_assert!(self.string.is_char_boundary(self.cursor));
unsafe { self.string.get_unchecked(start .. self.cursor) }
}
#[inline]
pub fn to(&self, end: usize) -> &'a str {
let end = self.snap(end).max(self.cursor);
debug_assert!(self.string.is_char_boundary(self.cursor));
debug_assert!(self.string.is_char_boundary(end));
unsafe { self.string.get_unchecked(self.cursor .. end) }
}
#[inline]
pub fn get(&self, range: Range<usize>) -> &'a str {
let start = self.snap(range.start);
let end = self.snap(range.end).max(start);
debug_assert!(self.string.is_char_boundary(start));
debug_assert!(self.string.is_char_boundary(end));
unsafe { self.string.get_unchecked(start .. end) }
}
#[inline]
pub fn peek(&self) -> Option<char> {
self.after().chars().next()
}
#[inline]
pub fn at<T>(&self, mut pat: impl Pattern<T>) -> bool {
pat.matches(self.after()).is_some()
}
#[inline]
pub fn scout(&self, n: isize) -> Option<char> {
if n >= 0 {
self.after().chars().nth(n as usize)
} else {
self.before().chars().nth_back((-n - 1) as usize)
}
}
#[inline]
pub fn locate(&self, n: isize) -> usize {
if n >= 0 {
let mut chars = self.after().chars();
for _ in 0 .. n {
if chars.next().is_none() {
break;
}
}
self.string.len() - chars.as_str().len()
} else {
let mut chars = self.before().chars();
for _ in 0 .. -n {
if chars.next_back().is_none() {
break;
}
}
chars.as_str().len()
}
}
#[inline]
pub fn eat(&mut self) -> Option<char> {
let peeked = self.peek();
if let Some(c) = peeked {
self.cursor += c.len_utf8();
}
peeked
}
#[inline]
pub fn uneat(&mut self) -> Option<char> {
let unpeeked = self.before().chars().next_back();
if let Some(c) = unpeeked {
self.cursor -= c.len_utf8();
}
unpeeked
}
#[inline]
pub fn eat_if<T>(&mut self, mut pat: impl Pattern<T>) -> bool {
if let Some(len) = pat.matches(self.after()) {
self.cursor += len;
true
} else {
false
}
}
#[inline]
pub fn eat_while<T>(&mut self, mut pat: impl Pattern<T>) -> &'a str {
let start = self.cursor;
while let Some(len @ 1 ..) = pat.matches(self.after()) {
self.cursor += len;
}
self.from(start)
}
#[inline]
pub fn eat_until<T>(&mut self, mut pat: impl Pattern<T>) -> &'a str {
let start = self.cursor;
while !self.done() && pat.matches(self.after()).is_none() {
self.eat();
}
self.from(start)
}
#[inline]
pub fn eat_whitespace(&mut self) -> &'a str {
self.eat_while(char::is_whitespace)
}
#[inline]
#[track_caller]
pub fn expect<T>(&mut self, mut pat: impl Pattern<T>) {
if let Some(len) = pat.matches(self.after()) {
self.cursor += len;
} else {
pat.expected();
}
}
#[inline]
pub fn jump(&mut self, target: usize) {
self.cursor = self.snap(target);
}
}
impl<'a> Scanner<'a> {
#[inline]
fn snap(&self, mut index: usize) -> usize {
index = index.min(self.string.len());
while !self.string.is_char_boundary(index) {
index -= 1;
}
index
}
}
impl Debug for Scanner<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("Scanner(")?;
let (before, after) = self.parts();
if !before.is_empty() {
before.fmt(f)?;
f.write_char(' ')?;
}
f.write_char('|')?;
if !after.is_empty() {
f.write_char(' ')?;
after.fmt(f)?;
}
f.write_char(')')
}
}
pub trait Pattern<T>: Sealed<T> {}
use sealed::Sealed;
mod sealed {
pub unsafe trait Sealed<T> {
fn matches(&mut self, string: &str) -> Option<usize>;
fn expected(&self);
}
}
impl Pattern<()> for char {}
unsafe impl Sealed<()> for char {
#[inline]
fn matches(&mut self, string: &str) -> Option<usize> {
let mut buf = [0; 4];
let needle = &*self.encode_utf8(&mut buf);
string.starts_with(needle).then(|| needle.len())
}
#[cold]
fn expected(&self) {
panic!("expected {self:?}");
}
}
impl Pattern<()> for &str {}
unsafe impl Sealed<()> for &str {
#[inline]
fn matches(&mut self, string: &str) -> Option<usize> {
string.starts_with(&*self).then(|| self.len())
}
#[cold]
fn expected(&self) {
panic!("expected {self:?}");
}
}
impl Pattern<()> for &[char] {}
unsafe impl Sealed<()> for &[char] {
#[inline]
fn matches(&mut self, string: &str) -> Option<usize> {
let next = string.chars().next()?;
self.iter().any(|&c| c == next).then(|| next.len_utf8())
}
#[cold]
fn expected(&self) {
struct Or<'a>(&'a [char]);
impl Debug for Or<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let mut iter = self.0.iter();
if let Some(c) = iter.next() {
c.fmt(f)?;
for c in iter {
f.write_str(" or ")?;
c.fmt(f)?;
}
}
Ok(())
}
}
if self.is_empty() {
panic!("empty slice cannot match");
} else {
panic!("expected {:?}", Or(self));
}
}
}
impl<const N: usize> Pattern<()> for [char; N] {}
unsafe impl<const N: usize> Sealed<()> for [char; N] {
#[inline]
fn matches(&mut self, string: &str) -> Option<usize> {
self.as_slice().matches(string)
}
#[cold]
fn expected(&self) {
self.as_slice().expected();
}
}
impl<const N: usize> Pattern<()> for &[char; N] {}
unsafe impl<const N: usize> Sealed<()> for &[char; N] {
#[inline]
fn matches(&mut self, string: &str) -> Option<usize> {
self.as_slice().matches(string)
}
#[cold]
fn expected(&self) {
self.as_slice().expected();
}
}
impl<F> Pattern<char> for F where F: FnMut(char) -> bool {}
unsafe impl<F> Sealed<char> for F
where
F: FnMut(char) -> bool,
{
#[inline]
fn matches(&mut self, string: &str) -> Option<usize> {
string.chars().next().filter(|&c| self(c)).map(char::len_utf8)
}
#[cold]
fn expected(&self) {
panic!("expected closure to return `true`");
}
}
impl<F> Pattern<&char> for F where F: FnMut(&char) -> bool {}
unsafe impl<F> Sealed<&char> for F
where
F: FnMut(&char) -> bool,
{
#[inline]
fn matches(&mut self, string: &str) -> Option<usize> {
string.chars().next().filter(self).map(char::len_utf8)
}
#[cold]
fn expected(&self) {
panic!("expected closure to return `true`");
}
}
#[cfg(test)]
mod tests {
use super::Scanner;
#[test]
fn test_fmt() {
let mut s = Scanner::new("hello world");
assert_eq!(format!("{s:?}"), r#"Scanner(| "hello world")"#);
s.eat_while(char::is_alphabetic);
assert_eq!(format!("{s:?}"), r#"Scanner("hello" | " world")"#);
s.eat_while(|_| true);
assert_eq!(format!("{s:?}"), r#"Scanner("hello world" |)"#);
}
#[test]
fn test_empty() {
let mut s = Scanner::new("");
s.jump(10);
assert_eq!(s.cursor(), 0);
assert_eq!(s.done(), true);
assert_eq!(s.before(), "");
assert_eq!(s.after(), "");
assert_eq!(s.from(0), "");
assert_eq!(s.from(10), "");
assert_eq!(s.to(10), "");
assert_eq!(s.to(10), "");
assert_eq!(s.get(10 .. 20), "");
assert_eq!(s.at(""), true);
assert_eq!(s.at('a'), false);
assert_eq!(s.at(|_| true), false);
assert_eq!(s.scout(-1), None);
assert_eq!(s.scout(-1), None);
assert_eq!(s.scout(1), None);
assert_eq!(s.locate(-1), 0);
assert_eq!(s.locate(0), 0);
assert_eq!(s.locate(1), 0);
assert_eq!(s.eat(), None);
assert_eq!(s.uneat(), None);
assert_eq!(s.eat_if(""), true);
assert_eq!(s.eat_if('a'), false);
assert_eq!(s.eat_while(""), "");
assert_eq!(s.eat_while('a'), "");
assert_eq!(s.eat_until(""), "");
assert_eq!(s.eat_whitespace(), "");
}
#[test]
fn test_slice() {
let mut s = Scanner::new("zoo π¦π΄π party");
assert_eq!(s.parts(), ("", "zoo π¦π΄π party"));
assert_eq!(s.get(2 .. 9), "o π¦");
assert_eq!(s.get(2 .. 22), "o π¦π΄π party");
s.eat_while(char::is_ascii);
assert_eq!(s.parts(), ("zoo ", "π¦π΄π party"));
assert_eq!(s.from(1), "oo ");
assert_eq!(s.to(15), "π¦π΄");
assert_eq!(s.to(16), "π¦π΄π");
assert_eq!(s.to(17), "π¦π΄π ");
assert_eq!(s.to(usize::MAX), "π¦π΄π party");
s.eat_until(char::is_whitespace);
assert_eq!(s.parts(), ("zoo π¦π΄π", " party"));
assert_eq!(s.from(3), " π¦π΄π");
}
#[test]
fn test_done_and_peek() {
let mut s = Scanner::new("Γ€bc");
assert_eq!(s.done(), false);
assert_eq!(s.peek(), Some('Γ€'));
s.eat();
assert_eq!(s.done(), false);
assert_eq!(s.peek(), Some('b'));
s.eat();
assert_eq!(s.done(), false);
assert_eq!(s.peek(), Some('c'));
s.eat();
assert_eq!(s.done(), true);
assert_eq!(s.peek(), None);
}
#[test]
fn test_at() {
let mut s = Scanner::new("Π12");
assert!(s.at('Π'));
assert!(s.at(['b', 'Π', 'Π']));
assert!(s.at("Π"));
assert!(s.at("Π1"));
assert!(s.at(char::is_alphabetic));
assert!(!s.at(&['b', 'c']));
assert!(!s.at("a13"));
assert!(!s.at(char::is_numeric));
s.eat();
assert!(s.at(char::is_numeric));
assert!(s.at(char::is_ascii_digit));
}
#[test]
fn test_scout_and_locate() {
let mut s = Scanner::new("aπc1Π€");
s.eat_until(char::is_numeric);
assert_eq!(s.scout(-4), None);
assert_eq!(s.scout(-3), Some('a'));
assert_eq!(s.scout(-2), Some('π'));
assert_eq!(s.scout(-1), Some('c'));
assert_eq!(s.scout(0), Some('1'));
assert_eq!(s.scout(1), Some('Π€'));
assert_eq!(s.scout(2), None);
assert_eq!(s.locate(-4), 0);
assert_eq!(s.locate(-3), 0);
assert_eq!(s.locate(-2), 1);
assert_eq!(s.locate(-1), 5);
assert_eq!(s.locate(0), 6);
assert_eq!(s.locate(1), 7);
assert_eq!(s.locate(2), 9);
assert_eq!(s.locate(3), 9);
}
#[test]
fn test_eat_and_uneat() {
let mut s = Scanner::new("πΆπ±π");
assert_eq!(s.eat(), Some('πΆ'));
s.jump(usize::MAX);
assert_eq!(s.uneat(), Some('π'));
assert_eq!(s.uneat(), Some('π±'));
assert_eq!(s.uneat(), Some('πΆ'));
assert_eq!(s.uneat(), None);
assert_eq!(s.eat(), Some('πΆ'));
}
#[test]
fn test_conditional_and_looping() {
let mut s = Scanner::new("abc123def33");
assert_eq!(s.eat_if('b'), false);
assert_eq!(s.eat_if('a'), true);
assert_eq!(s.eat_while(['a', 'b', 'c']), "bc");
assert_eq!(s.eat_while(char::is_numeric), "123");
assert_eq!(s.eat_until(char::is_numeric), "def");
assert_eq!(s.eat_while('3'), "33");
}
#[test]
fn test_eat_whitespace() {
let mut s = Scanner::new("αα \n b\tα");
assert_eq!(s.eat_whitespace(), "");
assert_eq!(s.eat_while(char::is_alphabetic), "αα");
assert_eq!(s.eat_whitespace(), " \n ");
assert_eq!(s.eat_if('b'), true);
assert_eq!(s.eat_whitespace(), "\t");
assert_eq!(s.eat_while(char::is_alphabetic), "α");
}
#[test]
fn test_expect_okay() {
let mut s = Scanner::new("π¦12");
s.expect('π¦');
s.jump(1);
s.expect("π¦");
assert_eq!(s.after(), "12");
}
#[test]
#[should_panic(expected = "expected 'π’'")]
fn test_expect_char_fail() {
let mut s = Scanner::new("no turtle in sight");
s.expect('π’');
}
#[test]
#[should_panic(expected = "expected \"π’\"")]
fn test_expect_str_fail() {
let mut s = Scanner::new("no turtle in sight");
s.expect("π’");
}
#[test]
#[should_panic(expected = "empty slice cannot match")]
fn test_expect_empty_array_fail() {
let mut s = Scanner::new("");
s.expect([]);
}
#[test]
#[should_panic(expected = "expected 'π’' or 'π¬'")]
fn test_expect_array_fail() {
let mut s = Scanner::new("no turtle or dolphin in sight");
s.expect(['π’', 'π¬']);
}
#[test]
#[should_panic(expected = "expected closure to return `true`")]
fn test_expect_closure_fail() {
let mut s = Scanner::new("no numbers in sight");
s.expect(char::is_numeric);
}
}