1use std::collections::{HashMap, HashSet};
54use std::path::PathBuf;
55use std::{fs, io};
56
57use regex::Regex;
58use thiserror::Error;
59
60pub enum Filling {
63 Text(String),
64 List(Vec<Filling>),
65 Template(HashMap<String, Filling>),
66}
67
68impl Filling {
69 pub fn insert(&mut self, variable: String, to_insert: Filling) -> Result<(), &'static str> {
72 match self {
73 Filling::Template(ref mut map) => {
74 map.insert(variable, to_insert);
75 Ok(())
76 }
77 _ => Err("Cannot insert into non-Template variant"),
78 }
79 }
80
81 pub fn push(&mut self, to_push: Filling) -> Result<(), &'static str> {
83 match self {
84 Filling::List(ref mut list) => {
85 list.push(to_push);
86 Ok(())
87 }
88 _ => Err("Cannot push into non-List variant"),
89 }
90 }
91}
92
93#[derive(Error, Debug)]
94pub enum TemplateNestError {
95 #[error("expected template directory at `{0}`")]
96 TemplateDirNotFound(String),
97
98 #[error("expected template file at `{0}`")]
99 TemplateFileNotFound(String),
100
101 #[error("error reading: `{0}`")]
102 TemplateFileReadError(#[from] io::Error),
103
104 #[error("encountered hash with no name label (name label: `{0}`)")]
105 NoNameLabel(String),
106
107 #[error("encountered hash with invalid name label type (name label: `{0}`)")]
108 InvalidNameLabel(String),
109
110 #[error("bad params in template hash, variable not present in template file: `{0}`")]
111 BadParams(String),
112}
113
114pub struct TemplateNest<'a> {
116 pub delimiters: (&'a str, &'a str),
119
120 pub label: &'a str,
122
123 pub extension: &'a str,
125
126 pub directory: PathBuf,
128
129 pub show_labels: bool,
132
133 pub comment_delimiters: (&'a str, &'a str),
136
137 pub fixed_indent: bool,
139
140 pub die_on_bad_params: bool,
144
145 pub token_escape_char: &'a str,
151
152 pub defaults: HashMap<String, Filling>,
155}
156
157struct TemplateFileIndex {
159 contents: String,
161
162 variables: Vec<TemplateFileVariable>,
164
165 variable_names: HashSet<String>,
167}
168
169struct TemplateFileVariable {
171 name: String,
172
173 start_position: usize,
176 end_position: usize,
177
178 indent_level: usize,
180
181 escaped_token: bool,
184}
185
186impl Default for TemplateNest<'_> {
187 fn default() -> Self {
188 TemplateNest {
189 label: "TEMPLATE",
190 extension: "html",
191 show_labels: false,
192 fixed_indent: false,
193 die_on_bad_params: false,
194 directory: "templates".into(),
195 delimiters: ("<!--%", "%-->"),
196 comment_delimiters: ("<!--", "-->"),
197 token_escape_char: "",
198 defaults: HashMap::new(),
199 }
200 }
201}
202
203impl TemplateNest<'_> {
204 pub fn new(directory_str: &str) -> Result<Self, TemplateNestError> {
206 let directory = PathBuf::from(directory_str);
207 if !directory.is_dir() {
208 return Err(TemplateNestError::TemplateDirNotFound(
209 directory_str.to_string(),
210 ));
211 }
212
213 Ok(Self {
214 directory,
215 ..Default::default()
216 })
217 }
218
219 fn index(&self, template_name: &str) -> Result<TemplateFileIndex, TemplateNestError> {
223 let file = self
224 .directory
225 .join(format!("{}.{}", template_name, self.extension));
226 if !file.is_file() {
227 return Err(TemplateNestError::TemplateFileNotFound(
228 file.display().to_string(),
229 ));
230 }
231
232 let contents = match fs::read_to_string(&file) {
233 Ok(file_contents) => file_contents,
234 Err(err) => {
235 return Err(TemplateNestError::TemplateFileReadError(err));
236 }
237 };
238
239 let mut variable_names = HashSet::new();
240 let mut variables = vec![];
241 let re = Regex::new(&format!("{}(.+?){}", self.delimiters.0, self.delimiters.1)).unwrap();
243 for cap in re.captures_iter(&contents) {
244 let whole_capture = cap.get(0).unwrap();
245 let start_position = whole_capture.start();
246
247 if !self.token_escape_char.is_empty() && start_position > self.token_escape_char.len() {
254 let escape_char_start = start_position - self.token_escape_char.len();
255 if &contents[escape_char_start..start_position] == self.token_escape_char {
256 variables.push(TemplateFileVariable {
257 indent_level: 0,
258 name: "".to_string(),
259 escaped_token: true,
260 start_position: escape_char_start,
261 end_position: escape_char_start + self.token_escape_char.len(),
262 });
263 continue;
264 }
265 }
266
267 let indent_level = match self.fixed_indent {
272 true => {
273 let newline_position = &contents[..start_position].rfind('\n').unwrap_or(0);
274 start_position - newline_position - 1
275 }
276 false => 0,
277 };
278
279 let variable_name = cap[1].trim();
280 variable_names.insert(variable_name.to_string());
281 variables.push(TemplateFileVariable {
282 indent_level,
283 start_position,
284 end_position: whole_capture.end(),
285 name: variable_name.to_string(),
286 escaped_token: false,
287 });
288 }
289
290 let file_index = TemplateFileIndex {
291 variable_names,
292 contents,
293 variables,
294 };
295 Ok(file_index)
296 }
297
298 pub fn render(&self, filling: &Filling) -> Result<String, TemplateNestError> {
301 match filling {
302 Filling::Text(text) => Ok(text.to_string()),
303 Filling::List(list) => {
304 let mut render = "".to_string();
305 for f in list {
306 render.push_str(&self.render(f)?);
307 }
308 Ok(render)
309 }
310 Filling::Template(template_hash) => {
311 let template_label: &Filling = template_hash
312 .get(self.label)
313 .ok_or(TemplateNestError::NoNameLabel(self.label.to_string()))?;
314
315 if let Filling::Text(name) = template_label {
318 let template_index = self.index(name)?;
319
320 if self.die_on_bad_params {
322 for name in template_hash.keys() {
323 if !template_index.variable_names.contains(name) && name != self.label {
327 return Err(TemplateNestError::BadParams(name.to_string()));
328 }
329 }
330 }
331
332 let mut rendered = String::from(&template_index.contents);
333
334 for var in template_index.variables.iter().rev() {
337 if var.escaped_token {
339 rendered.replace_range(var.start_position..var.end_position, "");
340 continue;
341 }
342
343 let mut render = "".to_string();
346
347 if let Some(value) = template_hash
351 .get(&var.name)
352 .or_else(|| self.defaults.get(&var.name))
353 {
354 let mut r: String = self.render(value)?;
355
356 if self.fixed_indent && var.indent_level != 0 {
359 let replacement = format!("\n{}", " ".repeat(var.indent_level));
360 r = r.replace('\n', &replacement);
361 }
362
363 render.push_str(&r);
364 }
365
366 rendered.replace_range(var.start_position..var.end_position, &render);
367 }
368
369 if self.show_labels {
371 rendered.replace_range(
372 0..0,
373 &format!(
374 "{} BEGIN {} {}\n",
375 self.comment_delimiters.0, name, self.comment_delimiters.1
376 ),
377 );
378 rendered.replace_range(
379 rendered.len()..rendered.len(),
380 &format!(
381 "{} END {} {}\n",
382 self.comment_delimiters.0, name, self.comment_delimiters.1
383 ),
384 );
385 }
386
387 let len_withoutcrlf = rendered.trim_end().len();
389 rendered.truncate(len_withoutcrlf);
390
391 Ok(rendered)
392 } else {
393 Err(TemplateNestError::InvalidNameLabel(self.label.to_string()))
394 }
395 }
396 }
397 }
398}
399
400#[macro_export]
402macro_rules! filling_list {
403 [] => { "".to_string() };
405
406 [@ITEM($( $i:expr, )*) $item:tt, $( $cont:tt )+] => {
408 $crate::filling_list!(
409 @ITEM($( $i, )* $crate::filling_text!($item), )
410 $( $cont )*
411 )
412 };
413 (@ITEM($( $i:expr, )*) $item:tt,) => ({
414 $crate::filling_list!(@END $( $i, )* $crate::filling_text!($item), )
415 });
416 (@ITEM($( $i:expr, )*) $item:tt) => ({
417 $crate::filling_list!(@END $( $i, )* $crate::filling_text!($item), )
418 });
419
420 [@ITEM($( $i:expr, )*) $item:expr, $( $cont:tt )+] => {
422 $crate::filling_list!(
423 @ITEM($( $i, )* $crate::filling_text!($item), )
424 $( $cont )*
425 )
426 };
427 (@ITEM($( $i:expr, )*) $item:expr,) => ({
428 $crate::filling_list!(@END $( $i, )* $crate::filling_text!($item), )
429 });
430 (@ITEM($( $i:expr, )*) $item:expr) => ({
431 $crate::filling_list!(@END $( $i, )* $crate::filling_text!($item), )
432 });
433
434 (@END $( $i:expr, )*) => ({
436 let size = 0 $( + {let _ = &$i; 1} )*;
437 let mut vec: Vec<Filling> = Vec::with_capacity(size);
438
439 $(
440 vec.push($i);
441 )*
442
443 $crate::Filling::List( vec )
444 });
445
446 ($( $cont:tt )+) => {
448 $crate::filling_list!(@ITEM() $($cont)*)
449 };
450}
451
452#[macro_export]
455macro_rules! filling_text {
456 ( null ) => { "".to_string() };
458 ( [$( $token:tt )*] ) => {
459 $crate::filling_list![ $( $token )* ]
461 };
462 ( {$( $token:tt )*} ) => {
463 $crate::filling!{ $( $token )* }
464 };
465 { $value:expr } => { $crate::Filling::Text($value.to_string()) };
466}
467
468#[macro_export]
470macro_rules! filling {
471 {} => { "".to_string() };
472
473 (@ENTRY($( $k:expr => $v:expr, )*) $key:ident: $( $cont:tt )*) => {
475 $crate::filling!(@ENTRY($( $k => $v, )*) stringify!($key) => $($cont)*)
476 };
477 (@ENTRY($( $k:expr => $v:expr, )*) $key:literal: $( $cont:tt )*) => {
478 $crate::filling!(@ENTRY($( $k => $v, )*) $key => $($cont)*)
479 };
480 (@ENTRY($( $k:expr => $v:expr, )*) [$key:expr]: $( $cont:tt )*) => {
481 $crate::filling!(@ENTRY($( $k => $v, )*) $key => $($cont)*)
482 };
483
484 (@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:tt, $( $cont:tt )+) => {
486 $crate::filling!(
487 @ENTRY($( $k => $v, )* $key => $crate::filling_text!($value), )
488 $( $cont )*
489 )
490 };
491 (@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:tt,) => ({
492 $crate::filling!(@END $( $k => $v, )* $key => $crate::filling_text!($value), )
493 });
494 (@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:tt) => ({
495 $crate::filling!(@END $( $k => $v, )* $key => $crate::filling_text!($value), )
496 });
497
498 (@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:expr, $( $cont:tt )+) => {
500 $crate::filling!(
501 @ENTRY($( $k => $v, )* $key => $crate::filling_text!($value), )
502 $( $cont )*
503 )
504 };
505 (@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:expr,) => ({
506 $crate::filling!(@END $( $k => $v, )* $key => $crate::filling_text!($value), )
507 });
508
509 (@ENTRY($( $k:expr => $v:expr, )*) $key:expr => $value:expr) => ({
510 $crate::filling!(@END $( $k => $v, )* $key => $crate::filling_text!($value), )
511 });
512
513 (@END $( $k:expr => $v:expr, )*) => ({
515 let mut params : HashMap<String, Filling> = Default::default();
516 $(
517 params.insert($k.to_string(), $v);
518 )*
519 let template = $crate::Filling::Template( params );
520 template
521 });
522
523 ($key:tt: $( $cont:tt )+) => {
525 $crate::filling!(@ENTRY() $key: $($cont)*)
526 };
527
528 ($( $k:expr => $v:expr, )*) => {
530 $crate::filling!(@END $( $k => $crate::filling_text!($v), )*)
531 };
532 ($( $k:expr => $v:expr ),*) => {
533 $crate::filling!(@END $( $k => $crate::filling_text!($v), )*)
534 };
535}