pochoir-common 0.12.2

Common utilities for the pochoir template engine
Documentation
use std::{
    ops::Range,
    path::{Path, PathBuf},
};

use crate::{Error, Result, Spanned};

#[derive(Debug, Clone)]
pub struct StreamParser<'a> {
    file_path: PathBuf,
    value: &'a str,
    index: usize,
}

impl<'a> StreamParser<'a> {
    pub fn new<T: AsRef<Path>>(file_path: T, value: &'a str) -> Self {
        Self {
            file_path: file_path.as_ref().into(),
            value,
            index: 0,
        }
    }

    /// Get the file path containing the current text.
    pub fn file_path(&self) -> &Path {
        &self.file_path
    }

    /// Get the current index.
    pub fn index(&self) -> usize {
        self.index
    }

    /// Set the current index.
    pub fn set_index(&mut self, index: usize) {
        self.index = index;
    }

    /// Returns the next byte without advancing the index.
    ///
    /// # Errors
    ///
    /// Returns an error if EOI is found.
    pub fn next(&self) -> Result<char> {
        self.value.as_bytes().get(self.index).copied().map_or_else(
            || {
                Err(Spanned::new(Error::UnexpectedEoi)
                    .with_span(if self.value.is_empty() {
                        0..0
                    } else {
                        self.value.len() - 1..self.value.len()
                    })
                    .with_file_path(&self.file_path))
            },
            |v| Ok(v as char),
        )
    }

    /// Returns the next byte while advancing the index.
    ///
    /// # Errors
    ///
    /// Returns an error if EOI is found.
    #[allow(clippy::missing_panics_doc)]
    pub fn take_next(&mut self) -> Result<char> {
        if self.index < self.value.len() {
            self.index += 1;

            Ok(self.value.as_bytes().get(self.index - 1).copied().unwrap() as char)
        } else {
            Err(Spanned::new(Error::UnexpectedEoi)
                .with_span(if self.value.is_empty() {
                    0..0
                } else {
                    self.value.len() - 1..self.value.len()
                })
                .with_file_path(&self.file_path))
        }
    }

    /// Peek the next `n` bytes.
    ///
    /// # Errors
    ///
    /// Returns an error if EOI is found.
    pub fn peek(&self, n: usize) -> Result<&'a str> {
        self.value.get(self.index..self.index + n).ok_or(
            Spanned::new(Error::UnexpectedEoi)
                .with_span(if self.value.is_empty() {
                    0..0
                } else {
                    self.value.len() - 1..self.value.len()
                })
                .with_file_path(&self.file_path),
        )
    }

    /// Returns if the next bytes match the provided value.
    ///
    /// If EOI is reached, `false` is returned.
    pub fn peek_exact(&self, val: &str) -> bool {
        self.value.get(self.index..self.index + val.len()) == Some(val)
    }

    /// Peek the next `n` bytes starting at `e` bytes from the current index.
    ///
    /// # Errors
    ///
    /// Returns an error if EOI is found.
    pub fn peek_early(&self, n: usize, e: usize) -> Result<&'a str> {
        self.value.get(self.index + e..self.index + e + n).ok_or(
            Spanned::new(Error::UnexpectedEoi)
                .with_span(if self.value.is_empty() {
                    0..0
                } else {
                    self.value.len() - 1..self.value.len()
                })
                .with_file_path(&self.file_path),
        )
    }

    /// Returns if the next bytes starting at `e` from the current index match the provided value.
    ///
    /// If EOI is reached, `false` is returned.
    pub fn peek_early_exact(&self, val: &str, e: usize) -> bool {
        self.value.get(self.index + e..self.index + e + val.len()) == Some(val)
    }

    /// Advance the index of the stream of `n` bytes.
    ///
    /// # Errors
    ///
    /// Returns an error if EOI is found.
    pub fn take(&mut self, n: usize) -> Result<()> {
        if self.index < self.value.len() {
            self.index += n;
            Ok(())
        } else {
            Err(Spanned::new(Error::UnexpectedEoi)
                .with_span(if self.value.is_empty() {
                    0..0
                } else {
                    self.value.len() - 1..self.value.len()
                })
                .with_file_path(&self.file_path))
        }
    }

    /// Check if the next characters match the value.
    /// If the values match, advance the current index of the length of the value and return true,
    /// otherwise return false.
    ///
    /// # Errors
    ///
    /// Returns an error if EOI is found or if the next characters does not match the string given
    /// as argument.
    #[allow(clippy::missing_panics_doc)]
    pub fn take_exact(&mut self, val: &str) -> Result<()> {
        if self.index + val.len() <= self.value.len() {
            let span = self.index
                ..self.index
                    + val.len()
                    + (0..4)
                        .find(|n| self.value.is_char_boundary(self.index + val.len() + n))
                        .expect("a unicode codepoint should at most be 4 bytes");
            let value = &self.value[span.clone()];

            if value == val {
                self.index += val.len();
                Ok(())
            } else {
                Err(Spanned::new(Error::UnexpectedInput {
                    expected: val.to_string(),
                    found: value.to_string(),
                })
                .with_span(span)
                .with_file_path(&self.file_path))
            }
        } else {
            Err(Spanned::new(Error::ExpectedFoundEoi {
                expected: val.to_string(),
            })
            .with_span(if self.value.is_empty() {
                0..0
            } else {
                self.value.len() - 1..self.value.len()
            })
            .with_file_path(&self.file_path))
        }
    }

    /// Advance the string so that the index will point to a non-space character.
    pub fn trim(&mut self) {
        self.take_while(|(_, ch)| char::is_whitespace(ch));
    }

    pub fn is_eoi(&self) -> bool {
        self.index >= self.value.len()
    }

    /// Get a range of text in the inner content.
    ///
    /// # Errors
    ///
    /// Returns an error if EOI is found.
    pub fn get_range(&self, range: Range<usize>) -> Result<&'a str> {
        self.value.get(range).ok_or_else(|| {
            Spanned::new(Error::UnexpectedEoi)
                .with_span(if self.value.is_empty() {
                    0..0
                } else {
                    self.value.len() - 1..self.value.len()
                })
                .with_file_path(&self.file_path)
        })
    }

    pub fn take_while<F: FnMut((usize, char)) -> bool>(&mut self, mut f: F) -> &'a str {
        let old_index = self.index;
        let new_index = &self.value[self.index..]
            .char_indices()
            .find_map(|(i, ch)| {
                if f((self.index + i, ch)) {
                    None
                } else {
                    Some(self.index + i)
                }
            })
            .unwrap_or(self.value.len());

        self.index = *new_index;
        &self.value[old_index..*new_index]
    }

    pub fn take_while_peekable<F: FnMut(usize, char, Option<char>) -> bool>(
        &mut self,
        mut f: F,
    ) -> &'a str {
        let old_index = self.index;
        let mut iter = self.value[self.index..].char_indices().peekable();

        while let Some(i) = iter.next() {
            if !f(i.0, i.1, iter.peek().map(|(_, ch)| *ch)) {
                self.index += i.0;
                return &self.value[old_index..self.index];
            }
        }

        self.index = self.value.len();
        &self.value[old_index..]
    }

    /// Take all the data until `val` is found.
    ///
    /// Returns the data taken.
    ///
    /// # Errors
    ///
    /// Returns an error if EOI is found.
    pub fn take_until(&mut self, val: &str) -> Result<&'a str> {
        let old_index = self.index;
        let mut new_index = old_index;

        while let Some(value) = &self.value.get(new_index..new_index + val.len()) {
            if *value == val {
                self.index = new_index;
                return Ok(&self.value[old_index..self.index]);
            }

            new_index += 1;
        }

        Err(Spanned::new(Error::ExpectedFoundEoi {
            expected: val.to_string(),
        })
        .with_span(if self.value.is_empty() {
            0..0
        } else {
            self.value.len() - 1..self.value.len()
        })
        .with_file_path(&self.file_path))
    }

    /// Take all the data until `val` is found.
    ///
    /// Returns the index of the first character of `val` in the data.
    ///
    /// # Errors
    ///
    /// Returns an error if EOI is found.
    pub fn take_until_index(&mut self, val: &str) -> Result<usize> {
        let old_index = self.index;
        let mut new_index = old_index;

        while let Some(value) = &self.value.get(new_index..new_index + val.len()) {
            if *value == val {
                self.index = new_index;
                return Ok(self.index);
            }

            new_index += 1;
        }

        Err(Spanned::new(Error::ExpectedFoundEoi {
            expected: val.to_string(),
        })
        .with_span(if self.value.is_empty() {
            0..0
        } else {
            self.value.len() - 1..self.value.len()
        })
        .with_file_path(&self.file_path))
    }

    pub fn take_until_eoi(&mut self) -> &'a str {
        let old_index = self.index;
        self.index = self.value.len();
        &self.value[old_index..]
    }
}