#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
#![warn(clippy::missing_docs_in_private_items)]
use std::marker::PhantomData;
pub use far_macros::Render;
pub use far_shared::Render;
mod errors;
#[cfg(test)]
mod tests;
pub use errors::{Error, Errors};
#[derive(Debug)]
pub struct Found<R> {
inner: String,
replaces: Vec<(String, (usize, usize))>,
keys_size: usize,
_replace: PhantomData<R>,
}
#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
pub enum Mode {
Strict,
AllowMissing,
}
impl Default for Mode {
fn default() -> Self {
Self::Strict
}
}
pub fn find<S, R>(template: S) -> Result<Found<R>, Errors>
where
S: AsRef<str>,
R: Render,
{
find_with_mode(template, Default::default())
}
pub fn find_with_mode<S, R>(template: S, mode: Mode) -> Result<Found<R>, Errors>
where
S: AsRef<str>,
R: Render,
{
let template = template.as_ref();
let mut replaces = Vec::new();
let mut errors = Vec::new();
let mut cursor = 0;
while cursor < template.len() {
let start = if let Some(start) = (&template[cursor..]).find("{{") {
cursor += start + "{{".len();
cursor
} else {
break;
};
if template[cursor..].starts_with("{{}}") {
cursor += "{{}}".len();
replaces.push((
"{{".to_owned(),
(
start - "{{".len(),
start + "{{}}".len(),
),
));
continue;
} else if template[cursor..].starts_with("}}}}") {
cursor += "}}}}".len();
replaces.push((
"}}".to_owned(),
(
start - "{{".len(),
start + "}}}}".len(),
),
));
continue;
}
let end = if let Some(end) = (&template[cursor..]).find("}}") {
cursor += end + "}}".len();
cursor
} else {
errors.push(Error::Unclosed(start));
return Err(errors.into());
};
let key = template[start..(end - "}}".len())].to_owned();
replaces.push((
key,
(
start - "{{".len(),
end,
),
));
}
let mut warnings = Vec::new();
for pk in R::keys() {
if !replaces.iter().any(|(tk, (..))| tk == pk) {
if mode == Mode::AllowMissing {
warnings.push(Error::Missing(pk.to_string()));
} else {
errors.push(Error::Missing(pk.to_string()));
}
}
}
if !warnings.is_empty() && mode == Mode::AllowMissing {
let warnings = Errors::from(warnings);
tracing::debug!("{}", warnings);
}
let mut keys_size = 0;
for (tk, _) in &replaces {
if R::keys().any(|pk| (pk == tk || tk == "{{" || tk == "}}")) {
keys_size += tk.len();
} else {
errors.push(Error::Extra((*tk).to_string()));
}
}
if !errors.is_empty() {
return Err(errors.into());
}
Ok(Found {
inner: template.to_owned(),
replaces,
keys_size,
_replace: PhantomData,
})
}
impl<R> Found<R>
where
R: Render,
{
pub fn replace(&self, replacements: &R) -> String
where
R: Render,
{
let rendered = replacements.render();
let values_size = rendered.iter().fold(0, |acc, (k, v)| {
let occurrence_count =
self.replaces.iter().fold(0, |acc, (rk, _)| {
if rk == k {
acc + 1
} else {
acc
}
});
occurrence_count * (acc + v.len())
});
let final_size = (self.inner.len()
- ("{{}}".len() * self.replaces.len()))
+ values_size
- self.keys_size
+ self.replaces.iter().fold(0, |acc, (key, _)| {
match key.as_str() {
"}}" => acc + "}}".len(),
"{{" => acc + "{{".len(),
_ => acc,
}
});
let mut replaced = String::with_capacity(final_size);
let mut cursor = 0;
for (key, (start, end)) in self.replaces.iter() {
replaced.push_str(&self.inner[cursor..(*start)]);
let replacement = match key.as_str() {
"}}" => "}}",
"{{" => "{{",
key => rendered.get(key).unwrap(),
};
replaced.push_str(replacement);
cursor = *end;
}
if cursor < self.inner.len() {
replaced.push_str(&self.inner[cursor..]);
}
#[cfg(test)]
assert_eq!(replaced.len(), final_size);
replaced
}
}