use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::{fs, io};
use regex::Regex;
use thiserror::Error;
pub enum Filling {
Text(String),
List(Vec<Filling>),
Template(HashMap<String, Filling>),
}
impl Filling {
pub fn insert(&mut self, variable: String, to_insert: Filling) -> Result<(), &'static str> {
match self {
Filling::Template(ref mut map) => {
map.insert(variable, to_insert);
Ok(())
}
_ => Err("Cannot insert into non-Template variant"),
}
}
pub fn push(&mut self, to_push: Filling) -> Result<(), &'static str> {
match self {
Filling::List(ref mut list) => {
list.push(to_push);
Ok(())
}
_ => Err("Cannot push into non-List variant"),
}
}
}
#[derive(Error, Debug)]
pub enum TemplateNestError {
#[error("expected template directory at `{0}`")]
TemplateDirNotFound(String),
#[error("expected template file at `{0}`")]
TemplateFileNotFound(String),
#[error("error reading: `{0}`")]
TemplateFileReadError(#[from] io::Error),
#[error("encountered hash with no name label (name label: `{0}`)")]
NoNameLabel(String),
#[error("encountered hash with invalid name label type (name label: `{0}`)")]
InvalidNameLabel(String),
#[error("bad params in template hash, variable not present in template file: `{0}`")]
BadParams(String),
}
pub struct TemplateNest<'a> {
pub delimiters: (&'a str, &'a str),
pub label: &'a str,
pub extension: &'a str,
pub directory: PathBuf,
pub show_labels: bool,
pub comment_delimiters: (&'a str, &'a str),
pub fixed_indent: bool,
pub die_on_bad_params: bool,
pub token_escape_char: &'a str,
pub defaults: HashMap<String, Filling>,
}
struct TemplateFileIndex {
contents: String,
variables: Vec<TemplateFileVariable>,
variable_names: HashSet<String>,
}
struct TemplateFileVariable {
name: String,
start_position: usize,
end_position: usize,
indent_level: usize,
escaped_token: bool,
}
impl Default for TemplateNest<'_> {
fn default() -> Self {
TemplateNest {
label: "TEMPLATE",
extension: "html",
show_labels: false,
fixed_indent: false,
die_on_bad_params: false,
directory: "templates".into(),
delimiters: ("<!--%", "%-->"),
comment_delimiters: ("<!--", "-->"),
token_escape_char: "",
defaults: HashMap::new(),
}
}
}
impl TemplateNest<'_> {
pub fn new(directory_str: &str) -> Result<Self, TemplateNestError> {
let directory = PathBuf::from(directory_str);
if !directory.is_dir() {
return Err(TemplateNestError::TemplateDirNotFound(
directory_str.to_string(),
));
}
Ok(Self {
directory,
..Default::default()
})
}
fn index(&self, template_name: &str) -> Result<TemplateFileIndex, TemplateNestError> {
let file = self
.directory
.join(format!("{}.{}", template_name, self.extension));
if !file.is_file() {
return Err(TemplateNestError::TemplateFileNotFound(
file.display().to_string(),
));
}
let contents = match fs::read_to_string(&file) {
Ok(file_contents) => file_contents,
Err(err) => {
return Err(TemplateNestError::TemplateFileReadError(err));
}
};
let mut variable_names = HashSet::new();
let mut variables = vec![];
let re = Regex::new(&format!("{}(.+?){}", self.delimiters.0, self.delimiters.1)).unwrap();
for cap in re.captures_iter(&contents) {
let whole_capture = cap.get(0).unwrap();
let start_position = whole_capture.start();
if !self.token_escape_char.is_empty() && start_position > self.token_escape_char.len() {
let escape_char_start = start_position - self.token_escape_char.len();
if &contents[escape_char_start..start_position] == self.token_escape_char {
variables.push(TemplateFileVariable {
indent_level: 0,
name: "".to_string(),
escaped_token: true,
start_position: escape_char_start,
end_position: escape_char_start + self.token_escape_char.len(),
});
continue;
}
}
let indent_level = match self.fixed_indent {
true => {
let newline_position = &contents[..start_position].rfind('\n').unwrap_or(0);
start_position - newline_position - 1
}
false => 0,
};
let variable_name = cap[1].trim();
variable_names.insert(variable_name.to_string());
variables.push(TemplateFileVariable {
indent_level,
start_position,
end_position: whole_capture.end(),
name: variable_name.to_string(),
escaped_token: false,
});
}
let file_index = TemplateFileIndex {
variable_names,
contents,
variables,
};
Ok(file_index)
}
pub fn render(&self, filling: &Filling) -> Result<String, TemplateNestError> {
match filling {
Filling::Text(text) => Ok(text.to_string()),
Filling::List(list) => {
let mut render = "".to_string();
for f in list {
render.push_str(&self.render(f)?);
}
Ok(render)
}
Filling::Template(template_hash) => {
let template_label: &Filling = template_hash
.get(self.label)
.ok_or(TemplateNestError::NoNameLabel(self.label.to_string()))?;
if let Filling::Text(name) = template_label {
let template_index = self.index(name)?;
if self.die_on_bad_params {
for name in template_hash.keys() {
if !template_index.variable_names.contains(name) && name != self.label {
return Err(TemplateNestError::BadParams(name.to_string()));
}
}
}
let mut rendered = String::from(&template_index.contents);
for var in template_index.variables.iter().rev() {
if var.escaped_token {
rendered.replace_range(var.start_position..var.end_position, "");
continue;
}
let mut render = "".to_string();
if let Some(value) = template_hash
.get(&var.name)
.or_else(|| self.defaults.get(&var.name))
{
let mut r: String = self.render(value)?;
if self.fixed_indent && var.indent_level != 0 {
let replacement = format!("\n{}", " ".repeat(var.indent_level));
r = r.replace('\n', &replacement);
}
render.push_str(&r);
}
rendered.replace_range(var.start_position..var.end_position, &render);
}
if self.show_labels {
rendered.replace_range(
0..0,
&format!(
"{} BEGIN {} {}\n",
self.comment_delimiters.0, name, self.comment_delimiters.1
),
);
rendered.replace_range(
rendered.len()..rendered.len(),
&format!(
"{} END {} {}\n",
self.comment_delimiters.0, name, self.comment_delimiters.1
),
);
}
let len_withoutcrlf = rendered.trim_end().len();
rendered.truncate(len_withoutcrlf);
Ok(rendered)
} else {
Err(TemplateNestError::InvalidNameLabel(self.label.to_string()))
}
}
}
}
}
#[macro_export]
macro_rules! filling_list {
[] => { "".to_string() };
[@ITEM($( $i:expr, )*) $item:tt, $( $cont:tt )+] => {
$crate::filling_list!(
@ITEM($( $i, )* $crate::filling_text!($item), )
$( $cont )*
)
};
(@ITEM($( $i:expr, )*) $item:tt,) => ({
$crate::filling_list!(@END $( $i, )* $crate::filling_text!($item), )
});
(@ITEM($( $i:expr, )*) $item:tt) => ({
$crate::filling_list!(@END $( $i, )* $crate::filling_text!($item), )
});
[@ITEM($( $i:expr, )*) $item:expr, $( $cont:tt )+] => {
$crate::filling_list!(
@ITEM($( $i, )* $crate::filling_text!($item), )
$( $cont )*
)
};
(@ITEM($( $i:expr, )*) $item:expr,) => ({
$crate::filling_list!(@END $( $i, )* $crate::filling_text!($item), )
});
(@ITEM($( $i:expr, )*) $item:expr) => ({
$crate::filling_list!(@END $( $i, )* $crate::filling_text!($item), )
});
(@END $( $i:expr, )*) => ({
let size = 0 $( + {let _ = &$i; 1} )*;
let mut vec: Vec<Filling> = Vec::with_capacity(size);
$(
vec.push($i);
)*
$crate::Filling::List( vec )
});
($( $cont:tt )+) => {
$crate::filling_list!(@ITEM() $($cont)*)
};
}
#[macro_export]
macro_rules! filling_text {
( null ) => { "".to_string() };
( [$( $token:tt )*] ) => {
$crate::filling_list![ $( $token )* ]
};
( {$( $token:tt )*} ) => {
$crate::filling!{ $( $token )* }
};
{ $value:expr } => { $crate::Filling::Text($value.to_string()) };
}
#[macro_export]
macro_rules! filling {
{} => { "".to_string() };
(@ENTRY($( $k:expr => $v:expr, )*) $key:ident: $( $cont:tt )*) => {
$crate::filling!(@ENTRY($( $k => $v, )*) stringify!($key) => $($cont)*)
};
(@ENTRY($( $k:expr => $v:expr, )*) $key:literal: $( $cont:tt )*) => {
$crate::filling!(@ENTRY($( $k => $v, )*) $key => $($cont)*)
};
(@ENTRY($( $k:expr => $v:expr, )*) [$key:expr]: $( $cont:tt )*) => {
$crate::filling!(@ENTRY($( $k => $v, )*) $key => $($cont)*)
};
(@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:tt, $( $cont:tt )+) => {
$crate::filling!(
@ENTRY($( $k => $v, )* $key => $crate::filling_text!($value), )
$( $cont )*
)
};
(@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:tt,) => ({
$crate::filling!(@END $( $k => $v, )* $key => $crate::filling_text!($value), )
});
(@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:tt) => ({
$crate::filling!(@END $( $k => $v, )* $key => $crate::filling_text!($value), )
});
(@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:expr, $( $cont:tt )+) => {
$crate::filling!(
@ENTRY($( $k => $v, )* $key => $crate::filling_text!($value), )
$( $cont )*
)
};
(@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:expr,) => ({
$crate::filling!(@END $( $k => $v, )* $key => $crate::filling_text!($value), )
});
(@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:expr) => ({
$crate::filling!(@END $( $k => $v, )* $key => $crate::filling_text!($value), )
});
(@END $( $k:expr => $v:expr, )*) => ({
let mut params : HashMap<String, Filling> = Default::default();
$(
params.insert($k.to_string(), $v);
)*
let template = $crate::Filling::Template( params );
template
});
($key:tt: $( $cont:tt )+) => {
$crate::filling!(@ENTRY() $key: $($cont)*)
};
($( $k:expr => $v:expr, )*) => {
$crate::filling!(@END $( $k => $crate::filling_text!($v), )*)
};
($( $k:expr => $v:expr ),*) => {
$crate::filling!(@END $( $k => $crate::filling_text!($v), )*)
};
}