until.rs
raw
//! Conditional parsing with until combinators.
//!
//! This module provides parsers that consume input until a specified end condition
//! is met. It includes both byte-level ([`Until`]) and UTF-8 character-level
//! ([`Utf8Until`]) variants for different parsing needs.
//!
//! Until parsers are useful for extracting content between delimiters, parsing
//! string literals, comments, or any scenario where you need to consume input
//! up to a specific boundary condition.
use crate::{
cache::ParsingCache,
parser::{Parsable, Parser, Source},
result::{Error, ParseResult},
utf8util::read_utf8_char,
};
/// A parser that consumes characters until an end condition is met.
///
/// Until reads characters one by one from the input until the end parser
/// would match at the current position. The end parser is not consumed -
/// it's just used to determine when to stop reading.
///
/// This is useful for parsing content up to a specific delimiter without
/// consuming the delimiter itself.
///
/// # Examples
///
/// ```rust
/// use neotoma::{until::Until, literal::Literal, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Read characters until we hit "end"
/// let parser = Until::new(Literal::from_str("end"));
///
/// let mut input = Cursor::new(b"hello world end more text");
/// let mut source = Source::new(input);
/// let result = parse(parser, &mut source).unwrap();
/// assert_eq!(result, b"hello world ".to_vec());
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Until<E> {
end: E,
min_length: usize,
max_length: Option<usize>,
}
impl<E> Until<E> {
/// Create a new Until parser with default bounds (0 or more characters).
///
/// # Examples
///
/// ```rust
/// use neotoma::{until::Until, literal::Literal, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Read until we see "."
/// let parser = Until::new(Literal::from_str("."));
///
/// let mut input = Cursor::new(b"hello world.more");
/// let mut source = Source::new(input);
/// let result = parse(parser, &mut source).unwrap();
/// assert_eq!(result, b"hello world".to_vec());
/// ```
pub fn new(end: E) -> Self {
Self {
end,
min_length: 0,
max_length: None,
}
}
/// Create an Until parser that requires at least `min` characters.
///
/// # Examples
///
/// ```rust
/// use neotoma::{until::Until, literal::Literal, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Require at least 5 characters before "."
/// let parser = Until::with_min(Literal::from_str("."), 5);
///
/// let mut input1 = Cursor::new(b"hello.world");
/// let mut source1 = Source::new(input1);
/// let result1 = parse(parser, &mut source1).unwrap();
/// assert_eq!(result1, b"hello".to_vec());
///
/// // This would fail with only 2 characters
/// let parser2 = Until::with_min(Literal::from_str("."), 5);
/// let mut input2 = Cursor::new(b"hi.there");
/// let mut source2 = Source::new(input2);
/// let result2 = parse(parser2, &mut source2);
/// assert!(result2.is_err());
/// ```
pub fn with_min(end: E, min: usize) -> Self {
Self {
end,
min_length: min,
max_length: None,
}
}
/// Create an Until parser that reads at most `max` characters.
///
/// # Examples
///
/// ```rust
/// use neotoma::{until::Until, literal::Literal, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Read at most 5 characters before "."
/// let parser = Until::with_max(Literal::from_str("."), 5);
///
/// let mut input = Cursor::new(b"hello world.more");
/// let mut source = Source::new(input);
/// let result = parse(parser, &mut source).unwrap();
/// assert_eq!(result, b"hello".to_vec()); // Stops at 5 chars
/// ```
pub fn with_max(end: E, max: usize) -> Self {
Self {
end,
min_length: 0,
max_length: Some(max),
}
}
/// Create an Until parser with both minimum and maximum bounds.
///
/// # Examples
///
/// ```rust
/// use neotoma::{until::Until, literal::Literal, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Read 3-7 characters before "."
/// let parser = Until::with_bounds(Literal::from_str("."), 3, 7);
///
/// let mut input = Cursor::new(b"hello.world");
/// let mut source = Source::new(input);
/// let result = parse(parser, &mut source).unwrap();
/// assert_eq!(result, b"hello".to_vec());
/// ```
pub fn with_bounds(end: E, min: usize, max: usize) -> Self {
Self {
end,
min_length: min,
max_length: Some(max),
}
}
}
impl<E, Ctx> Parser<Ctx> for Until<E>
where
E: Parser<Ctx>,
{
type Output = Vec<u8>;
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);
self.end.id().hash(&mut hasher);
self.min_length.hash(&mut hasher);
self.max_length.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 = Vec::new();
loop {
// Check if we've hit the maximum length
if let Some(max) = self.max_length {
if result.len() >= max {
break;
}
}
// Check if the end condition matches at current position (without consuming)
source.push();
match self.end.parse(source, cache, context) {
Ok(_) => {
// End condition matched - stop here
source.pop(); // Backtrack to before the end match
break;
}
Err(Error::NoMatch) => {
// End condition doesn't match - continue reading
source.pop(); // Backtrack to before the end attempt
}
Err(err) => {
// Other error (like IO error) - propagate it
source.pop();
return Err(err);
}
}
// Try to read one byte
match source.peek1() {
Ok(byte) => {
result.push(byte);
source.advance(1);
}
Err(Error::NoMatch) => {
// End of input - we're done
break;
}
Err(err) => {
// Other error - propagate it
return Err(err);
}
}
}
// Check minimum requirement
if result.len() < self.min_length {
return Err(Error::NoMatch);
}
Ok(result)
}
}
/// A UTF-8 aware parser that consumes characters until an end condition is met.
///
/// Utf8Until reads UTF-8 characters one by one from the input until the end parser
/// would match at the current position. Unlike `Until`, this parser respects UTF-8
/// character boundaries and operates on character counts rather than byte counts.
///
/// # Examples
///
/// ```rust
/// use neotoma::{until::Utf8Until, literal::Literal, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Read UTF-8 characters until we hit "end"
/// let parser = Utf8Until::new(Literal::from_str("end"));
///
/// let mut input = Cursor::new("hello 世界 end more".as_bytes());
/// let mut source = Source::new(input);
/// let result = parse(parser, &mut source).unwrap();
/// assert_eq!(result, "hello 世界 ");
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Utf8Until<E> {
end: E,
min_chars: usize,
max_chars: Option<usize>,
}
impl<E> Utf8Until<E> {
/// Create a new Utf8Until parser with default bounds (0 or more characters).
///
/// # Examples
///
/// ```rust
/// use neotoma::{until::Utf8Until, literal::Literal, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Read until we see "."
/// let parser = Utf8Until::new(Literal::from_str("."));
///
/// let mut input = Cursor::new("hello 世界.more".as_bytes());
/// let mut source = Source::new(input);
/// let result = parse(parser, &mut source).unwrap();
/// assert_eq!(result, "hello 世界");
/// ```
pub fn new(end: E) -> Self {
Self {
end,
min_chars: 0,
max_chars: None,
}
}
/// Create a Utf8Until parser that requires at least `min` characters.
///
/// # Examples
///
/// ```rust
/// use neotoma::{until::Utf8Until, literal::Literal, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Require at least 3 characters before "."
/// let parser = Utf8Until::with_min(Literal::from_str("."), 3);
///
/// let mut input1 = Cursor::new("hello.world".as_bytes());
/// let mut source1 = Source::new(input1);
/// let result1 = parse(parser, &mut source1).unwrap();
/// assert_eq!(result1, "hello");
///
/// // This would fail with only 2 characters
/// let parser2 = Utf8Until::with_min(Literal::from_str("."), 3);
/// let mut input2 = Cursor::new("hi.there".as_bytes());
/// let mut source2 = Source::new(input2);
/// let result2 = parse(parser2, &mut source2);
/// assert!(result2.is_err());
/// ```
pub fn with_min(end: E, min: usize) -> Self {
Self {
end,
min_chars: min,
max_chars: None,
}
}
/// Create a Utf8Until parser that reads at most `max` characters.
///
/// # Examples
///
/// ```rust
/// use neotoma::{until::Utf8Until, literal::Literal, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Read at most 2 characters before "."
/// let parser = Utf8Until::with_max(Literal::from_str("."), 2);
///
/// let mut input = Cursor::new("hello 世界.more".as_bytes());
/// let mut source = Source::new(input);
/// let result = parse(parser, &mut source).unwrap();
/// // Note: this reads 2 UTF-8 characters, not 2 bytes
/// ```
pub fn with_max(end: E, max: usize) -> Self {
Self {
end,
min_chars: 0,
max_chars: Some(max),
}
}
/// Create a Utf8Until parser with both minimum and maximum character bounds.
///
/// # Examples
///
/// ```rust
/// use neotoma::{until::Utf8Until, literal::Literal, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Read 1-3 characters before "."
/// let parser = Utf8Until::with_bounds(Literal::from_str("."), 1, 3);
///
/// let mut input = Cursor::new("hello.world".as_bytes());
/// let mut source = Source::new(input);
/// let result = parse(parser, &mut source).unwrap();
/// ```
pub fn with_bounds(end: E, min: usize, max: usize) -> Self {
Self {
end,
min_chars: min,
max_chars: Some(max),
}
}
}
impl<E, Ctx> Parser<Ctx> for Utf8Until<E>
where
E: Parser<Ctx>,
{
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);
self.end.id().hash(&mut hasher);
self.min_chars.hash(&mut hasher);
self.max_chars.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 {
// Check if we've hit the maximum character count
if let Some(max) = self.max_chars {
if char_count >= max {
break;
}
}
// Check if the end condition matches at current position (without consuming)
source.push();
match self.end.parse(source, cache, context) {
Ok(_) => {
// End condition matched - stop here
source.pop(); // Backtrack to before the end match
break;
}
Err(Error::NoMatch) => {
// End condition doesn't match - continue reading
source.pop(); // Backtrack to before the end attempt
}
Err(err) => {
// Other error (like IO error) - propagate it
source.pop();
return Err(err);
}
}
// Try to read one UTF-8 character
match read_utf8_char(source) {
Ok(ch) => {
result.push(ch);
char_count += 1;
}
Err(Error::NoMatch) => {
// End of input - we're done
break;
}
Err(err) => {
// Other error - propagate it
return Err(err);
}
}
}
// Check minimum requirement
if char_count < self.min_chars {
return Err(Error::NoMatch);
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{literal::Literal, parser::parse};
use std::io::Cursor;
#[test]
fn test_id_implementation_different_until_parsers() {
// Test that Until implements proper id() method
// Different Until parsers should have different IDs to avoid cache conflicts
let until1 = Until::new(Literal::from_str("end"));
let until2 = Until::new(Literal::from_str("stop"));
// These should have different IDs because they have different end conditions
// This test will FAIL if Until uses default id() implementation
assert_ne!(
<Until<_> as crate::parser::Parser<()>>::id(&until1),
<Until<_> as crate::parser::Parser<()>>::id(&until2),
"Different Until instances should have different IDs to avoid cache collisions"
);
}
#[test]
fn test_id_implementation_same_until_parsers() {
// Test that identical Until parsers have the same ID
let until1 = Until::new(Literal::from_str("end"));
let until2 = Until::new(Literal::from_str("end"));
assert_eq!(
<Until<_> as crate::parser::Parser<()>>::id(&until1),
<Until<_> as crate::parser::Parser<()>>::id(&until2),
"Identical Until instances should have the same ID for cache efficiency"
);
}
#[test]
fn test_id_implementation_until_different_bounds() {
// Test Until parsers with different bounds
let until1 = Until::with_min(Literal::from_str("end"), 1);
let until2 = Until::with_min(Literal::from_str("end"), 2);
// These should have different IDs because they have different minimum bounds
// This test will FAIL if Until uses default id() implementation
assert_ne!(
<Until<_> as crate::parser::Parser<()>>::id(&until1),
<Until<_> as crate::parser::Parser<()>>::id(&until2),
"Until instances with different bounds should have different IDs"
);
}
#[test]
fn test_id_implementation_utf8_until_parsers() {
// Test that Utf8Until implements proper id() method
let utf8_until1 = Utf8Until::new(Literal::from_str("end"));
let utf8_until2 = Utf8Until::new(Literal::from_str("stop"));
// These should have different IDs because they have different end conditions
// This test will FAIL if Utf8Until uses default id() implementation
assert_ne!(
<Utf8Until<_> as crate::parser::Parser<()>>::id(&utf8_until1),
<Utf8Until<_> as crate::parser::Parser<()>>::id(&utf8_until2),
"Different Utf8Until instances should have different IDs to avoid cache collisions"
);
}
#[test]
fn test_until_basic() {
let parser = Until::new(Literal::from_str("end"));
let mut input = Cursor::new(b"hello world end more text");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"hello world ".to_vec());
// The "end" should still be available to parse
let end_parser = Literal::from_str("end");
let end_result = parse(end_parser, &mut source).unwrap();
assert_eq!(end_result, b"end".as_slice().into());
}
#[test]
fn test_until_single_character_end() {
let parser = Until::new(Literal::from_str("."));
let mut input = Cursor::new(b"hello.world");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"hello".to_vec());
// The "." should still be available
let dot_parser = Literal::from_str(".");
let dot_result = parse(dot_parser, &mut source).unwrap();
assert_eq!(dot_result, b".".as_slice().into());
}
#[test]
fn test_until_with_min() {
let parser = Until::with_min(Literal::from_str("."), 5);
// Should succeed with 5+ characters
let mut input1 = Cursor::new(b"hello.world");
let mut source1 = crate::parser::Source::new(&mut input1);
let result1 = parse(parser, &mut source1).unwrap();
assert_eq!(result1, b"hello".to_vec());
// Should fail with only 2 characters
let parser2 = Until::with_min(Literal::from_str("."), 5);
let mut input2 = Cursor::new(b"hi.there");
let mut source2 = crate::parser::Source::new(&mut input2);
let result2 = parse(parser2, &mut source2);
assert!(result2.is_err());
}
#[test]
fn test_until_with_max() {
let parser = Until::with_max(Literal::from_str("."), 5);
let mut input = Cursor::new(b"hello world.more");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"hello".to_vec()); // Stops at 5 characters
// Should still have " world.more" available
let remaining = source.peek(11).unwrap();
assert_eq!(remaining, b" world.more");
}
#[test]
fn test_until_with_bounds() {
let parser = Until::with_bounds(Literal::from_str("."), 3, 7);
let mut input = Cursor::new(b"hello world.more");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"hello w".to_vec()); // Stops at 7 characters (max)
}
#[test]
fn test_until_no_end_found() {
let parser = Until::new(Literal::from_str("."));
let mut input = Cursor::new(b"hello world");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"hello world".to_vec()); // Consumes all input
}
#[test]
fn test_until_immediate_end() {
let parser = Until::new(Literal::from_str("hello"));
let mut input = Cursor::new(b"hello world");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"".to_vec()); // No characters consumed before "hello"
// "hello" should still be available
let hello_parser = Literal::from_str("hello");
let hello_result = parse(hello_parser, &mut source).unwrap();
assert_eq!(hello_result, b"hello".as_slice().into());
}
#[test]
fn test_until_empty_input() {
let parser = Until::new(Literal::from_str("."));
let mut input = Cursor::new(b"");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"".to_vec()); // No matches on empty input
}
#[test]
fn test_until_multichar_delimiter() {
let parser = Until::new(Literal::from_str("end"));
let mut input = Cursor::new(b"start middle end final");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"start middle ".to_vec());
// "end" should still be available, then we can parse " final"
let end_parser = Literal::from_str("end");
let end_result = parse(end_parser, &mut source).unwrap();
assert_eq!(end_result, b"end".as_slice().into());
let final_parser = Literal::from_str(" final");
let final_result = parse(final_parser, &mut source).unwrap();
assert_eq!(final_result, b" final".as_slice().into());
}
#[test]
fn test_until_in_sequence() {
use crate::seq;
let parser = seq![
Literal::from_str("start"),
Literal::from_str("123"),
Literal::from_str("_"),
Until::new(Literal::from_str("end")),
Literal::from_str("end")
];
let mut input = Cursor::new(b"start123_abcend");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result.0, b"start".as_slice().into());
assert_eq!(result.1.0, b"123".as_slice().into());
assert_eq!(result.1.1.0, b"_".as_slice().into());
assert_eq!(result.1.1.1.0, b"abc".to_vec());
assert_eq!(result.1.1.1.1.0, b"end".as_slice().into());
}
#[test]
fn test_utf8_until_basic() {
let parser = Utf8Until::new(Literal::from_str("end"));
let mut input = Cursor::new("hello 世界 end more".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, "hello 世界 ");
// The "end" should still be available to parse
let end_parser = Literal::from_str("end");
let end_result = parse(end_parser, &mut source).unwrap();
assert_eq!(end_result, b"end".as_slice().into());
}
#[test]
fn test_utf8_until_character_boundaries() {
let parser = Utf8Until::new(Literal::from_str("."));
// Mix of ASCII and multi-byte UTF-8 characters
let mut input = Cursor::new("café 世界.more".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, "café 世界");
// The "." should still be available
let dot_parser = Literal::from_str(".");
let dot_result = parse(dot_parser, &mut source).unwrap();
assert_eq!(dot_result, b".".as_slice().into());
}
#[test]
fn test_utf8_until_with_max_chars() {
let parser = Utf8Until::with_max(Literal::from_str("."), 3);
// Should stop at 3 UTF-8 characters, not 3 bytes
let mut input = Cursor::new("café 世界 more.end".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
// "café" = 4 bytes but 4 characters, " " = 1 char, "世" = 1 char = 3 chars total
assert_eq!(result, "caf");
}
#[test]
fn test_utf8_until_with_min_chars() {
let parser = Utf8Until::with_min(Literal::from_str("."), 3);
// Should succeed with 3+ characters
let mut input1 = Cursor::new("世界测.test".as_bytes());
let mut source1 = crate::parser::Source::new(&mut input1);
let result1 = parse(parser, &mut source1).unwrap();
assert_eq!(result1, "世界测");
// Should fail with only 2 characters
let parser2 = Utf8Until::with_min(Literal::from_str("."), 3);
let mut input2 = Cursor::new("世界.test".as_bytes());
let mut source2 = crate::parser::Source::new(&mut input2);
let result2 = parse(parser2, &mut source2);
assert!(result2.is_err());
}
#[test]
fn test_utf8_until_invalid_utf8() {
let parser = Utf8Until::with_min(Literal::from_str("end"), 1);
// Create invalid UTF-8 sequence at the start (0xFF is not valid UTF-8)
let mut input = Cursor::new(b"\xFF\xFEend");
let mut source = crate::parser::Source::new(&mut input);
// Should fail when it tries to read invalid UTF-8
let result = parse(parser, &mut source);
assert!(result.is_err());
}
#[test]
fn test_utf8_until_emoji() {
let parser = Utf8Until::new(Literal::from_str("!"));
// Test with emoji (4-byte UTF-8 characters)
let mut input = Cursor::new("Hello 👋 World 🌍!".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, "Hello 👋 World 🌍");
// The "!" should still be available
let bang_parser = Literal::from_str("!");
let bang_result = parse(bang_parser, &mut source).unwrap();
assert_eq!(bang_result, b"!".as_slice().into());
}
#[test]
fn test_utf8_until_bounds() {
let parser = Utf8Until::with_bounds(Literal::from_str("."), 2, 4);
let mut input = Cursor::new("世界测试.more".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, "世界测试"); // 4 characters (max)
}
#[test]
fn test_until_complex_end_parser() {
use crate::sequence::Sequence;
// Use a sequence as the end condition
let end_parser = Sequence::new(Literal::from_str("end"), Literal::from_str("_tag"));
let parser = Until::new(end_parser);
let mut input = Cursor::new(b"some content before end_tag and after");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"some content before ".to_vec());
// The end parser should still be available
let end_parser2 = Sequence::new(Literal::from_str("end"), Literal::from_str("_tag"));
let end_result = parse(end_parser2, &mut source).unwrap();
assert_eq!(end_result.0, b"end".as_slice().into());
assert_eq!(end_result.1, b"_tag".as_slice().into());
}
#[test]
fn test_until_with_repeat_end_parser() {
use crate::repeat::Repeat;
// End condition is repeated 'x'
let end_parser = Repeat::with_min(Literal::from_str("x"), 2);
let parser = Until::new(end_parser);
let mut input = Cursor::new(b"content before xxxx and after");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"content before ".to_vec());
}
#[test]
fn test_until_multiple_potential_ends() {
// Test content with multiple instances of the end pattern
let parser = Until::new(Literal::from_str("end"));
let mut input = Cursor::new(b"start end middle end final");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"start ".to_vec()); // Stops at first "end"
// Should be positioned at the first "end"
let remaining = source.peek(20).unwrap();
assert_eq!(remaining, b"end middle end final");
}
#[test]
fn test_until_overlapping_end_patterns() {
// Test where end pattern overlaps with content
let parser = Until::new(Literal::from_str("aba"));
let mut input = Cursor::new(b"xyzababadef");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"xyz".to_vec()); // Stops when it finds "aba"
// Should be positioned at "aba"
let remaining = source.peek(8).unwrap();
assert_eq!(remaining, b"ababadef");
}
#[test]
fn test_until_boundary_edge_cases() {
// Test exactly at minimum bound
let parser = Until::with_min(Literal::from_str("."), 5);
let mut input1 = Cursor::new(b"12345.rest");
let mut source1 = crate::parser::Source::new(&mut input1);
let result1 = parse(parser, &mut source1).unwrap();
assert_eq!(result1, b"12345".to_vec());
// Test just below minimum bound
let parser2 = Until::with_min(Literal::from_str("."), 5);
let mut input2 = Cursor::new(b"1234.rest");
let mut source2 = crate::parser::Source::new(&mut input2);
let result2 = parse(parser2, &mut source2);
assert!(result2.is_err());
// Test exactly at maximum bound
let parser3 = Until::with_max(Literal::from_str("."), 3);
let mut input3 = Cursor::new(b"12345678.rest");
let mut source3 = crate::parser::Source::new(&mut input3);
let result3 = parse(parser3, &mut source3).unwrap();
assert_eq!(result3, b"123".to_vec()); // Stops at max
}
#[test]
fn test_until_very_large_content() {
// Test with reasonably large content
let large_content = b"x".repeat(10000);
let mut input_bytes = Vec::new();
input_bytes.extend_from_slice(&large_content);
input_bytes.extend_from_slice(b"END");
let parser = Until::new(Literal::from_str("END"));
let mut input = Cursor::new(&input_bytes);
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result.len(), 10000);
assert!(result.iter().all(|&b| b == b'x'));
}
#[test]
fn test_utf8_until_complex_end_condition() {
use crate::utf8class::Utf8Class;
// End condition is Unicode digits
let end_parser = Utf8Class::unicode_digits();
let parser = Utf8Until::new(end_parser);
let mut input = Cursor::new("Hello 世界 ১২৩ more text".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, "Hello 世界 ");
}
#[test]
fn test_utf8_until_mixed_content() {
// Test with mixed ASCII, Latin-1, and Unicode content
let parser = Utf8Until::new(Literal::from_str("🔚"));
let mut input = Cursor::new("ASCII café 世界 привет 🔚 end".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, "ASCII café 世界 привет ");
// Should be positioned at the emoji
let remaining_bytes = source.peek(8).unwrap();
assert_eq!(remaining_bytes, "🔚 end".as_bytes());
}
#[test]
fn test_until_position_after_parsing() {
// Verify position tracking through complex scenarios
let parser = Until::new(Literal::from_str("||"));
let mut input = Cursor::new(b"item1|item2||item3|item4");
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, b"item1|item2".to_vec());
// Should be positioned at "||"
let delimiter = source.peek(2).unwrap();
assert_eq!(delimiter, b"||");
// Advance past delimiter
source.advance(2);
// Should now be at "item3"
let remaining = source.peek(5).unwrap();
assert_eq!(remaining, b"item3");
}
#[test]
fn test_until_with_empty_end_condition() {
use crate::literal::Literal;
// Create a literal that matches empty string (this might be a special case)
let empty_literal = Literal::from_str("");
let parser = Until::new(empty_literal);
let mut input = Cursor::new(b"some content");
let mut source = crate::parser::Source::new(&mut input);
// This should immediately match at the start since empty string matches anywhere
let result = parse(parser, &mut source).unwrap();
assert_eq!(result, Vec::<u8>::new());
}
#[test]
fn test_utf8_until_character_counting_accuracy() {
// Verify character counting vs byte counting
let parser = Utf8Until::with_max(Literal::from_str("!"), 3);
// "🇺🇸" is 8 bytes but 1 grapheme cluster, "é" is 2 bytes but 1 character
let mut input = Cursor::new("🇺🇸é!more".as_bytes());
let mut source = crate::parser::Source::new(&mut input);
let result = parse(parser, &mut source).unwrap();
// Should get the flag emoji, é, and then stop due to max chars = 3
// But this might depend on how Unicode characters are counted
println!("Result: '{}', len: {}", result, result.chars().count());
}
}