use camino::Utf8Path;
use std::collections::VecDeque;
use unicode_segmentation::UnicodeSegmentation;
use crate::error::{
ExpandIdentifierError, ExpandTextError, IdentifierKeyError,
};
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
Identifier(String),
Text(String),
NewLine,
}
impl Token {
fn new_text(text: &str) -> Self {
let text = text.to_string();
Self::Text(text)
}
}
enum CapturingState {
Pre,
Enabled,
Post,
}
#[doc(hidden)]
pub struct WideChars<'a> {
wide_chars: Vec<&'a str>,
started: bool,
index: usize,
}
impl<'a> WideChars<'a> {
pub fn new(data: &'a str) -> Self {
let wide_chars = UnicodeSegmentation::graphemes(data, true)
.collect::<Vec<&str>>();
Self {
wide_chars,
started: false,
index: 0_usize,
}
}
pub fn get(
&self,
index: usize,
) -> Option<&'a str> {
match self.wide_chars.get(index) {
Some(out) => Some(*out),
None => None,
}
}
pub fn bump_index(&mut self) {
self.index += 1;
}
pub fn get_index(&self) -> usize {
self.index
}
pub fn set_index(
&mut self,
index: usize,
) {
self.index = index;
}
}
impl<'a> Iterator for WideChars<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
if self.started {
self.index += 1;
} else {
self.started = true;
}
let out = self.get(self.index);
if out.is_none() && self.index > 0 {
self.index -= 1;
}
out
}
}
pub struct TokenIterator<'a> {
wide_chars: WideChars<'a>,
token_cache: VecDeque<Token>,
line_number: usize,
}
impl<'a> TokenIterator<'a> {
pub fn new(text: &'a str) -> Self {
let wide_chars = WideChars::new(text);
let token_cache: VecDeque<Token> =
VecDeque::with_capacity(2);
Self {
wide_chars,
token_cache,
line_number: 0_usize,
}
}
fn get_identifier(
&mut self,
wide_char: &str,
) -> Result<Option<String>, IdentifierKeyError> {
if wide_char != "$" {
return Ok(None);
}
let mut index = self.wide_chars.get_index();
let next_wide_char = match self.wide_chars.get(index + 1) {
Some(nwc) => nwc,
None => return Ok(None),
};
if next_wide_char == "$" {
self.wide_chars.bump_index();
return Ok(None);
}
if next_wide_char != "{" {
return Err(IdentifierKeyError::missing_starting_curly(
index + 1,
));
}
index += 2;
let starting_curly_index = index;
let mut state = CapturingState::Pre;
let mut out = String::new();
let mut has_closing_curly = false;
let mut first_identifier_character_index = 0_usize;
while let Some(wchar) = self.wide_chars.get(index) {
if !wchar.is_ascii() {
return Err(IdentifierKeyError::non_ascii(index));
}
if wchar == "}" {
has_closing_curly = true;
break;
}
match state {
CapturingState::Pre => {
if wchar == " " {
index += 1;
continue;
}
if wchar
.chars()
.all(|x| x.is_alphanumeric() || x == '_')
{
state = CapturingState::Enabled;
out.push_str(wchar);
first_identifier_character_index = index;
index += 1;
continue;
} else {
return Err(IdentifierKeyError::non_alpha_numeric_underscore(index));
}
},
CapturingState::Enabled => {
if wchar == " " {
state = CapturingState::Post;
index += 1;
continue;
}
if !wchar
.chars()
.all(|x| x.is_alphanumeric() || x == '_')
{
return Err(IdentifierKeyError::non_alpha_numeric_underscore(index));
}
index += 1;
out.push_str(wchar);
continue;
},
CapturingState::Post => {
if wchar == " " {
index += 1;
continue;
} else {
return Err(
IdentifierKeyError::invalid_name(index),
);
}
},
}
}
if !has_closing_curly {
return Err(IdentifierKeyError::missing_closing_curly(
starting_curly_index,
));
}
if out.is_empty() {
return Err(IdentifierKeyError::empty_identifier_name(
index - 1,
));
}
let first_char = out
.chars()
.next()
.expect("Should have at lest one character");
if first_char.is_numeric() {
return Err(IdentifierKeyError::starts_with_number(
first_identifier_character_index,
));
}
self.wide_chars.set_index(index);
Ok(Some(out))
}
}
impl Iterator for TokenIterator<'_> {
type Item = Result<Token, ExpandIdentifierError>;
fn next(&mut self) -> Option<Self::Item> {
#[allow(clippy::never_loop)]
while let Some(out) = self.token_cache.pop_front() {
return Some(Ok(out));
}
let mut text = String::new();
while let Some(wide_char) = self.wide_chars.next() {
let identifier = match self.get_identifier(wide_char) {
Ok(identifier) => identifier,
Err(e) => {
return Some(Err(
ExpandIdentifierError::key_error(
self.line_number,
e,
),
));
},
};
if let Some(identifier_name) = identifier {
if !text.is_empty() {
self.token_cache
.push_back(Token::new_text(&text));
text.clear();
}
self.token_cache
.push_back(Token::Identifier(identifier_name));
break;
}
if wide_char == "\n" {
if !text.is_empty() {
self.token_cache
.push_back(Token::new_text(&text));
text.clear();
}
self.token_cache.push_back(Token::NewLine);
self.line_number += 1;
break;
}
text.push_str(wide_char);
}
if !text.is_empty() {
self.token_cache.push_back(Token::new_text(&text));
text.clear();
}
#[allow(clippy::never_loop)]
while let Some(out) = self.token_cache.pop_front() {
return Some(Ok(out));
}
None
}
}
pub struct ExpandText {
fetch_function: fn(&str) -> Option<String>,
}
impl ExpandText {
pub fn new(fetch_function: fn(&str) -> Option<String>) -> Self {
Self { fetch_function }
}
#[inline]
fn _text(
&self,
text: &str,
) -> Result<String, ExpandIdentifierError> {
let tokens = TokenIterator::new(text);
let mut out = String::new();
for token in tokens {
let token = token?;
match token {
Token::Text(text) => out.push_str(&text),
Token::Identifier(key) => {
if let Some(value) = (self.fetch_function)(&key)
{
out.push_str(&value);
} else {
out.push_str(format!("${{{key}}}").as_str());
}
},
Token::NewLine => out.push('\n'),
}
}
Ok(out)
}
pub fn text(
&self,
text: &str,
) -> Result<String, ExpandTextError> {
Ok(self._text(text)?)
}
#[inline]
fn _text_strict(
&self,
text: &str,
) -> Result<String, ExpandIdentifierError> {
let tokens = TokenIterator::new(text);
let mut out = String::new();
for token in tokens {
let token = token?;
match token {
Token::Text(text) => out.push_str(&text),
Token::Identifier(key) => {
if let Some(value) = (self.fetch_function)(&key)
{
out.push_str(&value);
} else {
return Err(
ExpandIdentifierError::ValueError(key),
);
}
},
Token::NewLine => out.push('\n'),
}
}
Ok(out)
}
pub fn text_strict(
&self,
text: &str,
) -> Result<String, ExpandTextError> {
Ok(self._text_strict(text)?)
}
pub fn file<T>(
&self,
path: &T,
) -> Result<String, ExpandTextError>
where
T: AsRef<Utf8Path> + ?Sized,
{
let path = path.as_ref();
let text = std::fs::read_to_string(path).map_err(|e| {
ExpandTextError::read_file_error(path, e)
})?;
self._text(&text).map_err(|e| {
ExpandTextError::identifier_error_in_file(path, e)
})
}
pub fn file_strict<T>(
&self,
path: &T,
) -> Result<String, ExpandTextError>
where
T: AsRef<Utf8Path> + ?Sized,
{
let path = path.as_ref();
let text = std::fs::read_to_string(path).map_err(|e| {
ExpandTextError::read_file_error(path, e)
})?;
self._text(&text).map_err(|e| {
ExpandTextError::identifier_error_in_file(path, e)
})
}
}
pub trait Fetcher {
fn fetch(
&self,
key: &str,
) -> Option<String>;
}
pub struct ExpandTextWith<'a> {
pub obj: &'a dyn Fetcher,
}
impl<'a> ExpandTextWith<'a> {
pub fn new(obj: &'a dyn Fetcher) -> Self {
Self { obj }
}
fn _text(
&self,
text: &str,
) -> Result<String, ExpandIdentifierError> {
let tokens = TokenIterator::new(text);
let mut out = String::new();
for token in tokens {
let token = token?;
match token {
Token::Text(text) => out.push_str(&text),
Token::Identifier(key) => {
if let Some(value) = self.obj.fetch(&key) {
out.push_str(&value);
} else {
out.push_str(format!("${{{key}}}").as_str());
}
},
Token::NewLine => out.push('\n'),
}
}
Ok(out)
}
pub fn text(
&self,
text: &str,
) -> Result<String, ExpandTextError> {
Ok(self._text(text)?)
}
fn _text_strict(
&self,
text: &str,
) -> Result<String, ExpandIdentifierError> {
let tokens = TokenIterator::new(text);
let mut out = String::new();
for token in tokens {
let token = token?;
match token {
Token::Text(text) => out.push_str(&text),
Token::Identifier(key) => {
if let Some(value) = self.obj.fetch(&key) {
out.push_str(&value);
} else {
return Err(
ExpandIdentifierError::ValueError(key),
);
}
},
Token::NewLine => out.push('\n'),
}
}
Ok(out)
}
pub fn text_strict(
&self,
text: &str,
) -> Result<String, ExpandTextError> {
Ok(self._text_strict(text)?)
}
pub fn file<T>(
&self,
path: &T,
) -> Result<String, ExpandTextError>
where
T: AsRef<Utf8Path> + ?Sized,
{
let path = path.as_ref();
let text = std::fs::read_to_string(path).map_err(|e| {
ExpandTextError::read_file_error(path, e)
})?;
self._text(&text).map_err(|e| {
ExpandTextError::identifier_error_in_file(path, e)
})
}
pub fn file_strict<T>(
&self,
path: &T,
) -> Result<String, ExpandTextError>
where
T: AsRef<Utf8Path> + ?Sized,
{
let path = path.as_ref();
let text = std::fs::read_to_string(path).map_err(|e| {
ExpandTextError::read_file_error(path, e)
})?;
self._text(&text).map_err(|e| {
ExpandTextError::identifier_error_in_file(path, e)
})
}
}