--- /dev/null
+use std::str::Chars;
+
+/// Peekable iterator over a char sequence.
+///
+/// Next characters can be peeked via `nth_char` method,
+/// and position can be shifted forward via `bump` method.
+pub(crate) struct Cursor<'a> {
+ initial_len: usize,
+ chars: Chars<'a>,
+ #[cfg(debug_assertions)]
+ prev: char,
+}
+
+pub(crate) const EOF_CHAR: char = '\0';
+
+impl<'a> Cursor<'a> {
+ pub(crate) fn new(input: &'a str) -> Cursor<'a> {
+ Cursor {
+ initial_len: input.len(),
+ chars: input.chars(),
+ #[cfg(debug_assertions)]
+ prev: EOF_CHAR,
+ }
+ }
+
+ /// Returns the last eaten symbol (or `'\0'` in release builds).
+ /// (For debug assertions only.)
+ pub(crate) fn prev(&self) -> char {
+ #[cfg(debug_assertions)]
+ {
+ self.prev
+ }
+
+ #[cfg(not(debug_assertions))]
+ {
+ EOF_CHAR
+ }
+ }
+
+ /// Returns nth character relative to the current cursor position.
+ /// If requested position doesn't exist, `EOF_CHAR` is returned.
+ /// However, getting `EOF_CHAR` doesn't always mean actual end of file,
+ /// it should be checked with `is_eof` method.
+ fn nth_char(&self, n: usize) -> char {
+ self.chars().nth(n).unwrap_or(EOF_CHAR)
+ }
+
+ /// Peeks the next symbol from the input stream without consuming it.
+ pub(crate) fn first(&self) -> char {
+ self.nth_char(0)
+ }
+
+ /// Peeks the second symbol from the input stream without consuming it.
+ pub(crate) fn second(&self) -> char {
+ self.nth_char(1)
+ }
+
+ /// Checks if there is nothing more to consume.
+ pub(crate) fn is_eof(&self) -> bool {
+ self.chars.as_str().is_empty()
+ }
+
+ /// Returns amount of already consumed symbols.
+ pub(crate) fn len_consumed(&self) -> usize {
+ self.initial_len - self.chars.as_str().len()
+ }
+
+ /// Returns a `Chars` iterator over the remaining characters.
+ fn chars(&self) -> Chars<'a> {
+ self.chars.clone()
+ }
+
+ /// Moves to the next character.
+ pub(crate) fn bump(&mut self) -> Option<char> {
+ let c = self.chars.next()?;
+
+ #[cfg(debug_assertions)]
+ {
+ self.prev = c;
+ }
+
+ Some(c)
+ }
+}