Skip to content

Commit

Permalink
Implemented next line, next line terminator, and into scanner
Browse files Browse the repository at this point in the history
  • Loading branch information
vallentin committed Jul 9, 2023
1 parent 334683c commit a7434d2
Showing 1 changed file with 167 additions and 4 deletions.
171 changes: 167 additions & 4 deletions text-scanner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
pub mod ext;

pub mod prelude {
pub use super::{ScanResult, Scanner, ScannerItem, ScannerResult};
pub use super::{IntoScanner, ScanResult, Scanner, ScannerItem, ScannerResult};
}

mod private {
Expand Down Expand Up @@ -257,6 +257,8 @@ impl<'text> Scanner<'text> {
/// Advances the scanner cursor and returns the next
/// [`char`] and its [`Range`], if any.
///
/// See also [`next_str()`] and [`next_line()`].
///
/// # Example
///
/// ```rust
Expand All @@ -275,6 +277,9 @@ impl<'text> Scanner<'text> {
///
/// assert_eq!(scanner.remaining_text(), "");
/// ```
///
/// [`next_str()`]: Self::next_str
/// [`next_line()`]: Self::next_line
#[inline]
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> ScannerResult<'text, char> {
Expand All @@ -286,7 +291,8 @@ impl<'text> Scanner<'text> {
/// Returns the next [`char`] and its [`Range`], if any,
/// without advancing the cursor position.
///
/// See also [`peek_str()`], [`peek_nth()`], and [`peek_iter()`].
/// See also [`peek_str()`], [`peek_line()`], [`peek_nth()`],
/// and [`peek_iter()`].
///
/// # Example
///
Expand All @@ -306,6 +312,7 @@ impl<'text> Scanner<'text> {
/// ```
///
/// [`peek_str()`]: Self::peek_str
/// [`peek_line()`]: Self::peek_line
/// [`peek_nth()`]: Self::peek_nth
/// [`peek_iter()`]: Self::peek_iter
#[inline]
Expand All @@ -320,7 +327,7 @@ impl<'text> Scanner<'text> {
/// Returns the `n`th [`char`] and its [`Range`], if any,
/// without advancing the cursor position.
///
/// See also [`peek_str()`] and [`peek_iter()`].
/// See also [`peek_str()`], [`peek_line()`] and, [`peek_iter()`].
///
/// # Example
///
Expand All @@ -340,6 +347,7 @@ impl<'text> Scanner<'text> {
/// ```
///
/// [`peek_str()`]: Self::peek_str
/// [`peek_line()`]: Self::peek_line
/// [`peek_iter()`]: Self::peek_iter
#[inline]
pub fn peek_nth(&self, n: usize) -> ScannerResult<'text, char> {
Expand All @@ -355,7 +363,7 @@ impl<'text> Scanner<'text> {
/// **Note:** This has the same lifetime as the original `text`,
/// so the scanner can continue to be used while this exists.
///
/// See also [`peek_str()`].
/// See also [`peek_str()`] and [`peek_line()`].
///
/// # Example
///
Expand All @@ -378,6 +386,7 @@ impl<'text> Scanner<'text> {
/// ```
///
/// [`peek_str()`]: Self::peek_str
/// [`peek_line()`]: Self::peek_line
#[inline]
pub fn peek_iter(&self) -> CharRangesOffset<'text> {
self.remaining_text().char_ranges().offset(self.cursor)
Expand All @@ -388,6 +397,8 @@ impl<'text> Scanner<'text> {
/// remaining, then [`Err`] is returned, with the [remaining text],
/// if any, without advancing the cursor.
///
/// See also [`next_line()`].
///
/// **Note:** The returned string slice has the same lifetime as
/// the original `text`, so the scanner can continue to be used
/// while this exists.
Expand Down Expand Up @@ -431,6 +442,7 @@ impl<'text> Scanner<'text> {
/// ```
///
/// [remaining text]: Self::remaining_text
/// [`next_line()`]: Self::next_line
/// [chars()]: str::chars
/// [count()]: Iterator::count()
/// [`len()`]: str::len
Expand Down Expand Up @@ -506,6 +518,136 @@ impl<'text> Scanner<'text> {
Ok(self.ranged_text(r))
}

/// Advances the scanner cursor and returns `Ok` with the `&'text str`
/// and its [`Range`], of the next line, i.e. all the following characters
/// until the next line terminator.
/// If there are no more [remaining characters], then `Err` is returned.
///
/// Line terminators are excluded from the returned line.
///
/// See [`next_line_terminator()`] for information about line terminators.
///
/// **Note:** The returned string slice has the same lifetime as
/// the original `text`, so the scanner can continue to be used
/// while this exists.
///
/// # Example
///
/// Calling [`next_line()`] followed by [`next_line_terminator()`] in a loop
/// matches the behavior of [`str::lines()`].
///
/// ```rust
/// # use text_scanner::Scanner;
/// let mut scanner = Scanner::new("foo\nbar\r\n\n\nbaz\rqux\r\r\nend");
/// // ^^^ ^^^ ^^^^^^^^^^ ^^^
///
/// assert_eq!(scanner.next_line(), Ok((0..3, "foo")));
/// assert_eq!(scanner.next_line_terminator(), Ok((3..4, "\n")) );
///
/// assert_eq!(scanner.next_line(), Ok((4..7, "bar")));
/// assert_eq!(scanner.next_line_terminator(), Ok((7..9, "\r\n")));
///
/// assert_eq!(scanner.next_line(), Ok((9..9, "")));
/// assert_eq!(scanner.next_line_terminator(), Ok((9..10, "\n")));
///
/// assert_eq!(scanner.next_line(), Ok((11..19, "baz\rqux\r")));
/// assert_eq!(scanner.next_line_terminator(), Ok((19..21, "\r\n")));
///
/// assert_eq!(scanner.next_line(), Ok((21..24, "end")));
/// assert_eq!(scanner.next_line_terminator(), Err((24..24, "")));
///
/// assert_eq!(scanner.next_line(), Err((24..24, "")));
/// assert_eq!(scanner.next_line_terminator(), Err((24..24, "")));
///
/// # assert_eq!(scanner.remaining_text(), "");
/// ```
///
/// [remaining characters]: Self::remaining_text
/// [`next_line()`]: Self::next_line
/// [`next_line_terminator()`]: Self::next_line_terminator
#[inline]
pub fn next_line(&mut self) -> ScannerResult<'text, &'text str> {
let start = self.cursor;

if !self.has_remaining_text() {
return Err((start..start, ""));
}

loop {
_ = self.skip_until_char_any(&['\n', '\r']);
if self.peek_line_terminator().is_ok() || !self.has_remaining_text() {
break;
} else {
_ = self.next();
}
}

let r = start..self.cursor;
Ok(self.ranged_text(r))
}

/// Returns the next line, if any, without advancing
/// the cursor position.
///
/// See [`next_line()`] for more information.
///
/// [`next_line()`]: Self::next_line
#[inline]
pub fn peek_line(&self) -> ScannerResult<'text, &'text str> {
self.clone().next_line()
}

/// Advances the scanner cursor and returns `Ok` with the `&'text str`
/// and its [`Range`], if the following character(s) is a line terminator.
///
/// Line Terminators:
/// - `\n` (line feed)
/// - `\r\n` (carriage return + line feed)
///
/// Any carriage return (`\r`) **not** followed by a line feed (`\n`)
/// is **not** considered a line terminator.
///
/// **Note:** The returned string slice has the same lifetime as
/// the original `text`, so the scanner can continue to be used
/// while this exists.
///
/// # Example
///
/// ```rust
/// use text_scanner::{IntoScanner, Scanner};
///
/// assert_eq!("\n".into_scanner().next_line_terminator(), Ok((0..1, "\n")));
/// assert_eq!("\r\n".into_scanner().next_line_terminator(), Ok((0..2, "\r\n")));
///
/// assert_eq!("\r".into_scanner().next_line_terminator(), Err((0..0, "")));
/// assert_eq!("\r\r\n".into_scanner().next_line_terminator(), Err((0..0, "")));
/// ```
#[inline]
pub fn next_line_terminator(&mut self) -> ScannerResult<'text, &'text str> {
let (r, s) = self.peek_line_terminator()?;
self.cursor = r.end;
Ok((r, s))
}

/// Returns the next line terminator, if any, without advancing
/// the cursor position.
///
/// See [`next_line_terminator()`] for more information.
///
/// [`next_line_terminator()`]: Self::next_line_terminator
#[inline]
pub fn peek_line_terminator(&self) -> ScannerResult<'text, &'text str> {
let cursor = self.cursor;
let text = self.remaining_text();
if text.starts_with('\n') {
Ok(self.ranged_text(cursor..(cursor + 1)))
} else if text.starts_with("\r\n") {
Ok(self.ranged_text(cursor..(cursor + 2)))
} else {
Err((cursor..cursor, ""))
}
}

/// Advances the scanner cursor and returns the next
/// [`char`] and its [`Range`], if `f(c)` returns `true`
/// where `c` is the next character.
Expand Down Expand Up @@ -1483,6 +1625,27 @@ impl<'text> Scanner<'text> {
}
}

pub trait IntoScanner<'text> {
fn into_scanner(self) -> Scanner<'text>;
}

impl<'text> IntoScanner<'text> for &'text str {
#[inline]
fn into_scanner(self) -> Scanner<'text> {
Scanner::new(self)
}
}

impl<'text, T> From<T> for Scanner<'text>
where
T: IntoScanner<'text>,
{
#[inline]
fn from(value: T) -> Self {
value.into_scanner()
}
}

// Currently not publicly exported, as using e.g. `accept_if()` with a
// closure would require specifying types more often than desired.
pub(crate) trait ScanOne<Args> {
Expand Down

0 comments on commit a7434d2

Please sign in to comment.