alkale 2.0.0

A simple LL(1) lexer library for Rust.
Documentation
//! Sub-module of [`common`][crate::common] used for parsing document
//! structure such as indentation and spacing.

use crate::SourceCodeScanner;

impl<'src> SourceCodeScanner<'src> {
    /// Assuming the current scanner is at the beginning of a line, this will consume whitespace
    /// to determine an "indent level."
    ///
    /// This method consumes as much whitespace as possible, (similar to
    /// [`skip_whitespace`][Self::skip_whitespace]) and returns the quotient of the amount
    /// of characters consumed and the `chars_per_level` argument.
    ///
    /// The `chars_per_level` argument can be used to define how big a single "indent level"
    /// needs to be. If it is set to 4, then a line with 1 space and one with 3 spaces
    /// are considered to both be part of "level 0".
    ///
    /// # Panics
    /// This method will panic if `chars_per_level` is 0.
    ///
    /// # Examples
    /// ```rust
    /// # use alkale::SourceCodeScanner;
    /// # use alkale::span::Span;
    /// let scanner = SourceCodeScanner::new("
    /// A
    ///     B
    ///         C
    ///         D
    ///     E
    ///      F
    /// G    
    /// ");
    ///
    /// scanner.skip_line();
    /// assert_eq!(scanner.parse_indent_level(4), 0);
    ///
    /// scanner.skip_line();
    /// assert_eq!(scanner.parse_indent_level(4), 1);
    ///
    /// scanner.skip_line();
    /// assert_eq!(scanner.parse_indent_level(4), 2);
    ///
    /// scanner.skip_line();
    /// assert_eq!(scanner.parse_indent_level(4), 2);
    ///
    /// scanner.skip_line();
    /// assert_eq!(scanner.parse_indent_level(4), 1);
    ///
    /// scanner.skip_line();
    /// assert_eq!(scanner.parse_indent_level(4), 1);
    ///
    /// scanner.skip_line();
    /// assert_eq!(scanner.parse_indent_level(4), 0);
    /// ```
    #[inline]
    pub fn parse_indent_level(&self, chars_per_level: usize) -> usize {
        let mut indent_char_count = 0usize;

        while let Some(char) = self.peek() {
            if char.is_whitespace() {
                // SAFETY: In the worst case, we have a stream of nothing but
                // spaces all the way up to usize::MAX. This would just result
                // in the number reaching usize::MAX but not reaching it.
                unsafe {
                    indent_char_count = indent_char_count.unchecked_add(1);
                }

                self.skip();
            } else {
                break;
            }
        }

        indent_char_count.div_euclid(chars_per_level)
    }

    /// Repeatedly consume whitespace until a non-whitespace character or EOF.
    ///
    /// # Examples
    /// ```rust
    /// # use alkale::SourceCodeScanner;
    /// # use alkale::span::Span;
    /// let scanner = SourceCodeScanner::new("A   C  \t\t D  ");
    ///
    /// scanner.skip_whitespace();
    /// assert_eq!(scanner.next(), Some('A'));
    ///
    /// scanner.skip_whitespace();
    /// assert_eq!(scanner.next(), Some('C'));
    ///
    /// scanner.skip_whitespace();
    /// assert_eq!(scanner.next(), Some('D'));
    ///
    /// scanner.skip_whitespace();
    /// assert_eq!(scanner.next(), None);
    /// ```
    #[inline]
    pub fn skip_whitespace(&self) {
        while let Some(char) = self.peek() {
            if char.is_whitespace() {
                self.skip();
            } else {
                break;
            }
        }
    }

    /// Skip the rest of the characters in this line. This will skip up to and including
    /// the next `\n` character. This will also skip over runs of `\r` followed by an `\n`.
    ///
    /// This method is preferred over [`skip_until`][Self::skip_until] with `\n`.
    ///
    /// # Examples
    /// ```rust
    /// # use alkale::SourceCodeScanner;
    /// # use alkale::span::Span;
    /// let scanner = SourceCodeScanner::new("A   \nC  \r\r\nD  ");
    ///
    /// scanner.skip_line();
    /// assert_eq!(scanner.next(), Some('C'));
    ///
    /// scanner.skip_line();
    /// assert_eq!(scanner.next(), Some('D'));
    ///
    /// scanner.skip_line();
    /// assert_eq!(scanner.next(), None);
    /// ```
    #[inline]
    pub fn skip_line(&self) {
        while let Some(next) = self.next() {
            if next == '\n' {
                return;
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::{span::Span, SourceCodeScanner};

    #[test]
    fn get_indent_level() {
        let code = SourceCodeScanner::new(
            "
            a
                b
                c
                    d
            e
            f
                g
                 h
                i
            j
            ",
        );

        let next_line = |num| {
            code.skip_until('\n');
            code.skip();
            assert_eq!(code.parse_indent_level(4), num);
        };

        next_line(3);
        next_line(4);
        next_line(4);
        next_line(5);
        next_line(3);
        next_line(3);
        next_line(4);
        next_line(4);
        next_line(4);
        next_line(3);
    }

    #[test]
    fn skip_whitespace() {
        let code = SourceCodeScanner::new("a       b   \n c");

        // SAFETY: Spans are all valid.
        unsafe {
            code.skip_whitespace();
            assert_eq!(code.next_span(), Some(Span::new(0, 1).wrap('a')));
            code.skip_whitespace();
            assert_eq!(code.next_span(), Some(Span::new(8, 1).wrap('b')));
            code.skip_whitespace();
            assert_eq!(code.next_span(), Some(Span::new(14, 1).wrap('c')));
            assert!(!code.has_next());
            code.skip_whitespace();
            assert!(!code.has_next());
        }
    }

    #[test]
    fn skip_line() {
        let code = SourceCodeScanner::new("a whatever  \nb   \nc   \r\r\nd etc");

        assert!(code.peek_is('a'));
        code.skip_line();
        assert!(code.peek_is('b'));
        code.skip_line();
        assert!(code.peek_is('c'));
        code.skip_line();
        assert!(code.peek_is('d'));
        code.skip_line();
        assert!(!code.has_next());
    }
}