#[inline]
fn is_bmp(c: char) -> bool {
(c as u32) < 0x1_0000
}
#[inline]
pub fn is_white_space(c: char) -> bool {
c.is_whitespace() || matches!(c, '\u{1C}'..='\u{1F}')
}
#[inline]
pub fn is_letter(c: char) -> bool {
if c.is_ascii() {
return c.is_ascii_alphabetic();
}
is_bmp(c) && c.is_alphabetic() && !c.is_numeric() && !matches!(c, '\u{24B6}'..='\u{24E9}')
}
#[inline]
pub fn is_digit(c: char) -> bool {
if c.is_ascii() {
return c.is_ascii_digit();
}
is_bmp(c) && c.is_numeric() && !c.is_alphabetic()
}
#[inline]
fn is_connector_punctuation(c: char) -> bool {
matches!(
c,
'_' | '\u{203F}'
| '\u{2040}'
| '\u{2054}'
| '\u{FE33}'
| '\u{FE34}'
| '\u{FE4D}'..='\u{FE4F}'
| '\u{FF3F}'
)
}
#[inline]
fn is_format(c: char) -> bool {
matches!(
c,
'\u{00AD}'
| '\u{0600}'..='\u{0605}'
| '\u{061C}'
| '\u{06DD}'
| '\u{070F}'
| '\u{0890}'..='\u{0891}'
| '\u{08E2}'
| '\u{180E}'
| '\u{200B}'..='\u{200F}'
| '\u{202A}'..='\u{202E}'
| '\u{2060}'..='\u{2064}'
| '\u{2066}'..='\u{206F}'
| '\u{FEFF}'
| '\u{FFF9}'..='\u{FFFB}'
)
}
#[inline]
pub(crate) fn is_identifier_part_category(c: char) -> bool {
if c.is_ascii() {
return c.is_ascii_digit() || c == '_';
}
matches!(c, '\u{0300}'..='\u{036F}')
|| is_connector_punctuation(c)
|| is_format(c)
|| is_digit(c)
}
#[derive(Clone, Debug)]
pub struct CharacterReader<'a> {
s: &'a str,
position: i32,
}
impl<'a> CharacterReader<'a> {
#[inline]
pub fn new(s: &'a str) -> Self {
Self { s, position: 0 }
}
#[inline]
pub fn end(&self) -> bool {
self.s.is_empty()
}
#[inline]
pub fn peek(&self) -> Option<char> {
self.s.chars().next()
}
#[inline]
pub fn position(&self) -> i32 {
self.position
}
#[inline]
pub fn take(&mut self) -> Option<char> {
let taken = self.peek()?;
self.position += 1;
self.s = &self.s[taken.len_utf8()..];
Some(taken)
}
pub fn skip_whitespace(&mut self) {
let len = self.peek_whitespace().len();
self.advance(len);
}
#[inline]
pub fn take_if(&mut self, c: char) -> bool {
if self.peek() == Some(c) {
self.take();
true
} else {
false
}
}
pub fn take_if_str(&mut self, s: &str) -> bool {
if self.s.starts_with(s) {
self.advance(s.len());
true
} else {
false
}
}
#[inline]
pub fn take_if_fn(&mut self, condition: impl Fn(char) -> bool) -> bool {
match self.peek() {
Some(c) if condition(c) => {
self.take();
true
}
_ => false,
}
}
pub fn take_until(&mut self, c: char) -> &'a str {
let len = self.s.find(c).unwrap_or(self.s.len());
self.advance(len)
}
pub fn take_while(&mut self, condition: impl Fn(char) -> bool) -> &'a str {
let len = self
.s
.char_indices()
.find(|&(_, c)| !condition(c))
.map_or(self.s.len(), |(i, _)| i);
self.advance(len)
}
pub fn try_peek(&self, count: usize) -> &'a str {
match self.byte_offset(count) {
Some(len) => &self.s[..len],
None => "",
}
}
pub fn peek_whitespace(&self) -> &'a str {
let trimmed = self.s.trim_start_matches(is_white_space);
&self.s[..self.s.len() - trimmed.len()]
}
pub fn skip(&mut self, count: usize) {
match self.byte_offset(count) {
Some(len) => self.s = &self.s[len..],
None => panic!("CharacterReader::skip: count is past the end of the input"),
}
}
fn byte_offset(&self, count: usize) -> Option<usize> {
if count == 0 {
return Some(0);
}
self.s
.char_indices()
.nth(count - 1)
.map(|(i, c)| i + c.len_utf8())
}
fn advance(&mut self, len: usize) -> &'a str {
let (taken, rest) = self.s.split_at(len);
self.position += taken.chars().count() as i32;
self.s = rest;
taken
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn peek_take_and_position() {
let mut r = CharacterReader::new("aé𝒜");
assert!(!r.end());
assert_eq!(r.peek(), Some('a'));
assert_eq!(r.take(), Some('a'));
assert_eq!(r.position(), 1);
assert_eq!(r.take(), Some('é'));
assert_eq!(r.take(), Some('𝒜'));
assert_eq!(r.position(), 3);
assert!(r.end());
assert_eq!(r.peek(), None);
assert_eq!(r.take(), None);
assert_eq!(r.position(), 3);
}
#[test]
fn take_if_variants() {
let mut r = CharacterReader::new("?.ab");
assert!(!r.take_if('.'));
assert!(!r.take_if_str("?!"));
assert!(r.take_if_str("?."));
assert_eq!(r.position(), 2);
assert!(r.take_if('a'));
assert!(!r.take_if_fn(|c| c.is_ascii_digit()));
assert!(r.take_if_fn(|c| c == 'b'));
assert!(r.end());
assert!(!r.take_if('a'));
assert!(!r.take_if_fn(|_| true));
assert!(!r.take_if_str("a"));
assert!(r.take_if_str(""));
}
#[test]
fn take_until_and_take_while() {
let mut r = CharacterReader::new("żółw]rest");
assert_eq!(r.take_until(']'), "żółw");
assert_eq!(r.position(), 4);
assert_eq!(r.take(), Some(']'));
assert_eq!(r.take_while(|c| c != 's'), "re");
assert_eq!(r.take_until('#'), "st");
assert!(r.end());
assert_eq!(r.take_until('#'), "");
assert_eq!(r.take_while(|_| true), "");
assert_eq!(r.position(), 9);
}
#[test]
fn whitespace() {
let mut r = CharacterReader::new(" \t\u{00A0}\u{1F}x ");
assert_eq!(r.peek_whitespace(), " \t\u{00A0}\u{1F}");
assert_eq!(r.position(), 0);
r.skip_whitespace();
assert_eq!(r.position(), 4);
assert_eq!(r.peek(), Some('x'));
assert_eq!(r.peek_whitespace(), "");
r.skip_whitespace();
assert_eq!(r.position(), 4);
}
#[test]
fn try_peek_and_skip() {
let mut r = CharacterReader::new("aéb");
assert_eq!(r.try_peek(0), "");
assert_eq!(r.try_peek(2), "aé");
assert_eq!(r.try_peek(3), "aéb");
assert_eq!(r.try_peek(4), "");
r.skip(2);
assert_eq!(r.peek(), Some('b'));
assert_eq!(r.position(), 0);
}
#[test]
#[should_panic]
fn skip_past_end_panics() {
CharacterReader::new("a").skip(2);
}
#[test]
fn classification() {
assert!(is_letter('a') && is_letter('Z') && is_letter('ż') && is_letter('λ') && is_letter('中'));
assert!(!is_letter('_') && !is_letter('1') && !is_letter('Ⅷ') && !is_letter('Ⓐ'));
assert!(!is_letter('𝒜'));
assert!(is_digit('7') && is_digit('٣') && !is_digit('a') && !is_digit('Ⅷ'));
assert!(is_white_space(' ') && is_white_space('\u{2003}') && !is_white_space('x'));
assert!(is_identifier_part_category('\u{0301}'));
assert!(is_identifier_part_category('\u{203F}'));
assert!(is_identifier_part_category('\u{200D}'));
assert!(!is_identifier_part_category('-') && !is_identifier_part_category('a'));
}
}