use std::collections::HashSet;
use crate::{
cache::ParsingCache,
parser::{Parsable, Parser, Source},
result::{Error, ParseResult},
utf8util::read_utf8_char,
};
#[derive(Debug, Clone)]
pub struct Utf8Class<F = fn(char) -> bool> {
allowed: HashSet<char>,
predicate: Option<F>,
min_length: usize,
max_length: Option<usize>,
negated: bool,
}
impl<F> PartialEq for Utf8Class<F> {
fn eq(&self, other: &Self) -> bool {
self.allowed == other.allowed
&& self.min_length == other.min_length
&& self.max_length == other.max_length
&& self.negated == other.negated
}
}
impl<F> Eq for Utf8Class<F> {}
impl Utf8Class<fn(char) -> bool> {
pub fn new(chars: &str) -> Self {
Self {
allowed: chars.chars().collect(),
predicate: None,
min_length: 0,
max_length: None,
negated: false,
}
}
pub fn not_in(chars: &str) -> Utf8Class {
Utf8Class {
allowed: chars.chars().collect(),
predicate: None,
min_length: 0,
max_length: None,
negated: true,
}
}
pub fn not_in_with_min(chars: &str, min_length: usize) -> Utf8Class {
Utf8Class {
allowed: chars.chars().collect(),
predicate: None,
min_length,
max_length: None,
negated: true,
}
}
pub fn with_min(chars: &str, min_length: usize) -> Self {
Self {
allowed: chars.chars().collect(),
predicate: None,
min_length,
max_length: None,
negated: false,
}
}
pub fn with_max(chars: &str, max_length: usize) -> Self {
Self {
allowed: chars.chars().collect(),
predicate: None,
min_length: 0,
max_length: Some(max_length),
negated: false,
}
}
pub fn with_bounds(chars: &str, min_length: usize, max_length: usize) -> Self {
Self {
allowed: chars.chars().collect(),
predicate: None,
min_length,
max_length: Some(max_length),
negated: false,
}
}
pub fn digits() -> Self {
Self::with_min("0123456789", 1)
}
pub fn alpha() -> Self {
Self::with_min("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 1)
}
pub fn alphanumeric() -> Self {
Self::with_min(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
1,
)
}
pub fn whitespace() -> Self {
Self::with_min(" \t\r\n\u{0b}\u{0c}", 1)
}
pub fn hex_digits() -> Self {
Self::with_min("0123456789abcdefABCDEF", 1)
}
pub fn unicode_alpha() -> Self {
Self::from_predicate_min(char::is_alphabetic, 1)
}
pub fn unicode_digits() -> Self {
Self::from_predicate_min(char::is_numeric, 1)
}
pub fn unicode_whitespace() -> Self {
Self::from_predicate_min(char::is_whitespace, 1)
}
}
impl<F> Utf8Class<F>
where
F: Fn(char) -> bool,
{
pub fn from_predicate(predicate: F) -> Self {
Self {
allowed: HashSet::new(), predicate: Some(predicate),
min_length: 0,
max_length: None,
negated: false,
}
}
pub fn from_predicate_min(predicate: F, min_length: usize) -> Self {
Self {
allowed: HashSet::new(),
predicate: Some(predicate),
min_length,
max_length: None,
negated: false,
}
}
pub fn from_predicate_max(predicate: F, max_length: usize) -> Self {
Self {
allowed: HashSet::new(),
predicate: Some(predicate),
min_length: 0,
max_length: Some(max_length),
negated: false,
}
}
pub fn from_predicate_bounds(predicate: F, min_length: usize, max_length: usize) -> Self {
Self {
allowed: HashSet::new(),
predicate: Some(predicate),
min_length,
max_length: Some(max_length),
negated: false,
}
}
}
impl<F> Utf8Class<F> {
fn char_matches(&self, c: char) -> bool
where
F: Fn(char) -> bool,
{
let matches = if let Some(ref predicate) = self.predicate {
predicate(c)
} else {
self.allowed.contains(&c)
};
if self.negated { !matches } else { matches }
}
}
impl<F, Ctx> Parser<Ctx> for Utf8Class<F>
where
F: Fn(char) -> bool + 'static,
{
type Output = String;
fn id(&self) -> u64 {
use std::any::TypeId;
use std::hash::{DefaultHasher, Hash, Hasher};
let mut hasher = DefaultHasher::new();
TypeId::of::<Self>().hash(&mut hasher);
let mut chars: Vec<char> = self.allowed.iter().copied().collect();
chars.sort(); chars.hash(&mut hasher);
self.min_length.hash(&mut hasher);
self.max_length.hash(&mut hasher);
self.negated.hash(&mut hasher);
hasher.finish()
}
fn read<S>(
&self,
source: &mut Source<S>,
_cache: &mut impl ParsingCache,
_context: &mut Ctx,
) -> ParseResult<Self::Output>
where
S: Parsable,
{
let mut result = String::new();
let mut char_count = 0;
loop {
if let Some(max_length) = self.max_length {
if char_count >= max_length {
break;
}
}
source.push();
match read_utf8_char(source) {
Ok(c) => {
if self.char_matches(c) {
source.commit(); result.push(c);
char_count += 1;
} else {
source.pop(); break;
}
}
Err(Error::NoMatch) => {
source.pop(); break;
}
Err(err) => {
source.pop(); return Err(err);
}
}
}
if char_count < self.min_length {
return Err(Error::NoMatch);
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
use std::io::Cursor;
#[test]
fn test_id_implementation_different_utf8class_parsers() {
let class1 = Utf8Class::new("abc");
let class2 = Utf8Class::new("xyz");
assert_ne!(
<Utf8Class as crate::parser::Parser<()>>::id(&class1),
<Utf8Class as crate::parser::Parser<()>>::id(&class2),
"Different Utf8Class instances should have different IDs to avoid cache collisions"
);
}
#[test]
fn test_id_implementation_same_utf8class_parsers() {
let class1 = Utf8Class::new("abc");
let class2 = Utf8Class::new("abc");
assert_eq!(
<Utf8Class as crate::parser::Parser<()>>::id(&class1),
<Utf8Class as crate::parser::Parser<()>>::id(&class2),
"Identical Utf8Class instances should have the same ID for cache efficiency"
);
}
#[test]
fn test_id_implementation_utf8class_different_bounds() {
let class1 = Utf8Class::with_min("abc", 1);
let class2 = Utf8Class::with_min("abc", 2);
assert_ne!(
<Utf8Class as crate::parser::Parser<()>>::id(&class1),
<Utf8Class as crate::parser::Parser<()>>::id(&class2),
"Utf8Class instances with different bounds should have different IDs"
);
}
#[test]
fn test_id_implementation_utf8class_predicate_parsers() {
let class1 = Utf8Class::from_predicate(|c| c.is_alphabetic());
let class2 = Utf8Class::from_predicate(|c| c.is_numeric());
fn get_id<P: crate::parser::Parser<()>>(parser: &P) -> u64 {
parser.id()
}
assert_ne!(
get_id(&class1),
get_id(&class2),
"Utf8Class instances with different predicates should have different IDs"
);
}
#[test]
fn test_utf8class_basic_functionality() {
let class = Utf8Class::new("abc");
let mut input = Cursor::new("aabbcc123".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(class, &mut source).unwrap();
assert_eq!(result, "aabbcc");
}
#[test]
fn test_utf8class_unicode_characters() {
let emoji_class = Utf8Class::new("ππππ€£π");
let mut input = Cursor::new("πππabc".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(emoji_class, &mut source).unwrap();
assert_eq!(result, "πππ");
let mixed_class = Utf8Class::new("aΞ±Π°δΈη");
let mut input2 = Cursor::new("aΞ±δΈabc".as_bytes());
let mut source2 = crate::parser::Source::new(&mut input2);
let result2 = parse(mixed_class, &mut source2).unwrap();
assert!(result2.contains('a') && result2.contains('Ξ±') && result2.contains('δΈ'));
}
#[test]
fn test_utf8class_negated_functionality() {
let not_digits = Utf8Class::not_in("0123456789");
let mut input = Cursor::new("abc123".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(not_digits, &mut source).unwrap();
assert_eq!(result, "abc");
let not_emoji = Utf8Class::not_in("ππππ€£");
let mut input2 = Cursor::new("HelloδΈηπmore".as_bytes());
let mut source2 = crate::parser::Source::new(&mut input2);
let result2 = parse(not_emoji, &mut source2).unwrap();
assert_eq!(result2, "HelloδΈη");
}
#[test]
fn test_utf8class_boundary_conditions() {
let unicode_class = Utf8Class::with_bounds("δΈηζ΅θ―", 2, 3);
let mut input1 = Cursor::new("δΈηζ΅θ―abc".as_bytes());
let mut source1 = crate::parser::Source::new(&mut input1);
let result1 = parse(unicode_class, &mut source1).unwrap();
assert_eq!(result1, "δΈηζ΅");
let unicode_class2 = Utf8Class::with_min("δΈη", 3);
let mut input2 = Cursor::new("δΈηabc".as_bytes());
let mut source2 = crate::parser::Source::new(&mut input2);
let result2 = parse(unicode_class2, &mut source2);
assert!(result2.is_err()); }
#[test]
fn test_utf8class_predicate_functionality() {
let alpha_class = Utf8Class::from_predicate(char::is_alphabetic);
let mut input1 = Cursor::new("HelloΞ±Ξ²Ξ³δΈη123".as_bytes());
let mut source1 = crate::parser::Source::new(&mut input1);
let result1 = parse(alpha_class, &mut source1).unwrap();
assert_eq!(result1, "HelloΞ±Ξ²Ξ³δΈη");
let numeric_class = Utf8Class::from_predicate(char::is_numeric);
let mut input2 = Cursor::new("οΌοΌοΌ456abc".as_bytes()); let mut source2 = crate::parser::Source::new(&mut input2);
let result2 = parse(numeric_class, &mut source2).unwrap();
assert_eq!(result2, "οΌοΌοΌ456");
let ws_class = Utf8Class::from_predicate(char::is_whitespace);
let mut input3 = Cursor::new(" \t\u{00A0}\u{2000}abc".as_bytes());
let mut source3 = crate::parser::Source::new(&mut input3);
let result3 = parse(ws_class, &mut source3).unwrap();
assert_eq!(result3, " \t\u{00A0}\u{2000}");
}
#[test]
fn test_utf8class_convenience_constructors() {
let digits = Utf8Class::digits();
let mut input1 = Cursor::new("123abc".as_bytes());
let mut source1 = crate::parser::Source::new(&mut input1);
let result1 = parse(digits, &mut source1).unwrap();
assert_eq!(result1, "123");
let alpha = Utf8Class::alpha();
let mut input2 = Cursor::new("Hello123".as_bytes());
let mut source2 = crate::parser::Source::new(&mut input2);
let result2 = parse(alpha, &mut source2).unwrap();
assert_eq!(result2, "Hello");
let unicode_alpha = Utf8Class::unicode_alpha();
let mut input3 = Cursor::new("HelloΞ±Ξ²Ξ³δΈη123".as_bytes());
let mut source3 = crate::parser::Source::new(&mut input3);
let result3 = parse(unicode_alpha, &mut source3).unwrap();
assert_eq!(result3, "HelloΞ±Ξ²Ξ³δΈη");
let unicode_digits = Utf8Class::unicode_digits();
let mut input4 = Cursor::new("১২৩123abc".as_bytes()); let mut source4 = crate::parser::Source::new(&mut input4);
let result4 = parse(unicode_digits, &mut source4).unwrap();
assert_eq!(result4, "১২৩123");
}
#[test]
fn test_utf8class_empty_input() {
let class = Utf8Class::new("abc");
let mut input = Cursor::new("".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(class, &mut source).unwrap();
assert_eq!(result, "");
let class_min = Utf8Class::with_min("abc", 1);
let mut input2 = Cursor::new("".as_bytes());
let mut source2 = crate::parser::Source::new(&mut input2);
let result2 = parse(class_min, &mut source2);
assert!(result2.is_err());
}
#[test]
fn test_utf8class_position_tracking() {
let class = Utf8Class::new("δΈη");
let mut input = Cursor::new("δΈηζ΅XYZ".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(class, &mut source).unwrap();
assert_eq!(result, "δΈη");
let next_char = crate::utf8util::read_utf8_char(&mut source).unwrap();
assert_eq!(next_char, 'ζ΅');
}
#[test]
fn test_utf8class_complex_unicode_scenarios() {
let class = Utf8Class::new("eΓ©\u{0301}");
let mut input = Cursor::new("eΓ©e\u{0301}abc".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(class, &mut source).unwrap();
assert!(result.contains('e') && result.contains('Γ©'));
let emoji_class = Utf8Class::new("πππ");
let mut input2 = Cursor::new("ππabc".as_bytes());
let mut source2 = crate::parser::Source::new(&mut input2);
let result2 = parse(emoji_class, &mut source2).unwrap();
assert_eq!(result2, "ππ");
}
#[test]
fn test_utf8class_large_character_set() {
let large_set = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZαβγδΡ΢ηθικλμνξοΟΟΟΟΟ
ΟΟΟΟΠ°Π±Π²Π³Π΄Π΅ΠΆΠ·ΠΈΠΉΠΊΠ»ΠΌΠ½ΠΎΠΏΡΡΡΡΡΡ
ΡΡΡΡΡΡΡΡΡΡδΈηζ΅θ―ζ₯ζ¬θͺνκ΅μ΄Hello";
let class = Utf8Class::new(large_set);
let mut input = Cursor::new("HelloδΈηΞ±Ξ²νκ΅123".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(class, &mut source).unwrap();
assert!(!result.is_empty());
assert!(result.contains("Hello"));
assert!(result.contains("δΈη"));
}
#[test]
fn test_utf8class_invalid_utf8_handling() {
let class = Utf8Class::from_predicate(|_| true);
let mut invalid_input = Vec::new();
invalid_input.extend_from_slice("Hello".as_bytes());
invalid_input.push(0xFF); invalid_input.extend_from_slice("World".as_bytes());
let mut input = Cursor::new(&invalid_input);
let mut source = crate::parser::Source::new(&mut input);
let result = parse(class, &mut source).unwrap();
assert_eq!(result, "Hello"); }
#[test]
fn test_utf8class_predicate_edge_cases() {
let always_true = Utf8Class::from_predicate(|_| true);
let mut input1 = Cursor::new("HelloδΈη123!@#".as_bytes());
let mut source1 = crate::parser::Source::new(&mut input1);
let result1 = parse(always_true, &mut source1).unwrap();
assert_eq!(result1, "HelloδΈη123!@#");
let always_false = Utf8Class::from_predicate(|_| false);
let mut input2 = Cursor::new("Hello".as_bytes());
let mut source2 = crate::parser::Source::new(&mut input2);
let result2 = parse(always_false, &mut source2).unwrap();
assert_eq!(result2, "");
let vowels = Utf8Class::from_predicate(|c| {
matches!(
c,
'a' | 'e'
| 'i'
| 'o'
| 'u'
| 'A'
| 'E'
| 'I'
| 'O'
| 'U'
| 'Ξ±'
| 'Ξ΅'
| 'ΞΉ'
| 'ΞΏ'
| 'Ο
'
)
});
let mut input3 = Cursor::new("aeiouαΡβγ123".as_bytes());
let mut source3 = crate::parser::Source::new(&mut input3);
let result3 = parse(vowels, &mut source3).unwrap();
assert_eq!(result3, "aeiouΞ±Ξ΅");
}
#[test]
fn test_utf8class_character_vs_byte_counting() {
let class = Utf8Class::with_max("πππabc", 3);
let mut input = Cursor::new("πππabc".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(class, &mut source).unwrap();
assert_eq!(result, "πππ");
let class2 = Utf8Class::with_min("π", 2);
let mut input2 = Cursor::new("πabc".as_bytes()); let mut source2 = crate::parser::Source::new(&mut input2);
let result2 = parse(class2, &mut source2);
assert!(result2.is_err()); }
#[test]
fn test_utf8class_performance_large_input() {
let large_unicode = "δΈη".repeat(5000);
let class = Utf8Class::new("δΈη");
let mut input = Cursor::new(large_unicode.as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(class, &mut source).unwrap();
assert_eq!(result.chars().count(), 10000); assert!(result.chars().all(|c| c == 'δΈ' || c == 'η'));
}
#[test]
fn test_utf8class_mixed_ascii_unicode() {
let mixed_class = Utf8Class::new("HelloδΈη-_123");
let mut input = Cursor::new("Hello-δΈη_123!@#".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(mixed_class, &mut source).unwrap();
assert_eq!(result, "Hello-δΈη_123");
let remaining_bytes = source.peek(3).unwrap();
assert_eq!(remaining_bytes, "!@#".as_bytes());
}
}