#[derive(Clone, Copy, Debug)]
pub struct Cursor<'a> {
src: &'a str,
pos: u32,
}
impl<'a> Cursor<'a> {
pub fn new(src: &'a str) -> Self {
debug_assert!(
u32::try_from(src.len()).is_ok(),
"Cursor source length must fit in u32; tokenize() guards this",
);
Self { src, pos: 0 }
}
pub fn src(&self) -> &'a str {
self.src
}
pub fn pos(&self) -> u32 {
self.pos
}
pub fn set_pos(&mut self, pos: u32) {
debug_assert!(
(pos as usize) <= self.src.len(),
"set_pos must stay within the source",
);
self.pos = pos;
}
pub fn is_eof(&self) -> bool {
self.pos as usize >= self.src.len()
}
pub fn peek(&self) -> Option<u8> {
self.src.as_bytes().get(self.pos as usize).copied()
}
pub fn peek_nth(&self, n: u32) -> Option<u8> {
let index = self.pos as usize + n as usize;
self.src.as_bytes().get(index).copied()
}
pub fn rest(&self) -> &'a [u8] {
&self.src.as_bytes()[self.pos as usize..]
}
pub fn bump(&mut self) -> Option<u8> {
let byte = self.peek()?;
self.pos += 1;
Some(byte)
}
pub fn advance_bytes(&mut self, n: u32) {
debug_assert!(
self.pos as usize + n as usize <= self.src.len(),
"advance_bytes must stay within the source",
);
self.pos += n;
}
pub fn eat_while(&mut self, mut pred: impl FnMut(u8) -> bool) {
while let Some(byte) = self.peek() {
if !pred(byte) {
break;
}
self.pos += 1;
}
}
pub fn char_at(&self, n: u32) -> Option<char> {
self.src
.get(self.pos as usize + n as usize..)?
.chars()
.next()
}
pub fn bump_char(&mut self) -> Option<char> {
let ch = self.char_at(0)?;
self.pos += ch.len_utf8() as u32;
Some(ch)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn peek_and_bump_walk_bytes_and_track_offset() {
let mut cursor = Cursor::new("ab");
assert_eq!(cursor.pos(), 0);
assert!(!cursor.is_eof());
assert_eq!(cursor.peek(), Some(b'a'));
assert_eq!(cursor.peek_nth(1), Some(b'b'));
assert_eq!(cursor.peek_nth(2), None);
assert_eq!(cursor.bump(), Some(b'a'));
assert_eq!(cursor.pos(), 1);
assert_eq!(cursor.bump(), Some(b'b'));
assert_eq!(cursor.pos(), 2);
assert!(cursor.is_eof());
assert_eq!(cursor.bump(), None);
assert_eq!(cursor.pos(), 2);
}
#[test]
fn eat_while_stops_at_first_non_match_and_at_eof() {
let mut cursor = Cursor::new("123abc");
cursor.eat_while(|b| b.is_ascii_digit());
assert_eq!(cursor.pos(), 3);
assert_eq!(cursor.peek(), Some(b'a'));
cursor.eat_while(|b| b.is_ascii_alphabetic());
assert_eq!(cursor.pos(), 6);
assert!(cursor.is_eof());
}
#[test]
fn rest_returns_remaining_bytes_from_offset() {
let mut cursor = Cursor::new("a$tag$");
cursor.bump();
assert_eq!(cursor.rest(), b"$tag$");
}
#[test]
fn char_at_and_bump_char_walk_whole_characters() {
let mut cursor = Cursor::new("aé🎉");
assert_eq!(cursor.char_at(0), Some('a'));
assert_eq!(cursor.char_at(1), Some('é'));
assert_eq!(cursor.bump_char(), Some('a'));
assert_eq!(cursor.pos(), 1);
assert_eq!(cursor.bump_char(), Some('é'));
assert_eq!(cursor.pos(), 3); assert_eq!(cursor.bump_char(), Some('🎉'));
assert_eq!(cursor.pos(), 7); assert_eq!(cursor.bump_char(), None);
let cursor = Cursor::new("é");
assert_eq!(cursor.char_at(1), None);
}
}