t4rust_derive/
lib.rs

1//! # About
2//! t4rust is a minimal templating engine, inspired by the [T4](https://docs.microsoft.com/en-us/visualstudio/modeling/code-generation-and-t4-text-templates) syntax.
3//!
4//! # Example
5//! A simple example how to create a template.
6//!
7//! ```
8//! use t4rust_derive::Template;
9//!
10//! // Add this attribute to use a template
11//! #[derive(Template)]
12//! // Specify the path to the template file here
13//! #[TemplatePath = "./examples/doc_example1.tt"]
14//! // Add this attribute if you want to get debug parsing information
15//! // This also enables writing temporary files, you might get better error messages.
16//! //#[TemplateDebug]
17//! struct Example {
18//!     // Add fields to the struct you want to use in the template
19//!     name: String,
20//!     food: String,
21//!     num: i32,
22//! }
23//!
24//! fn main() {
25//!     // Generate your template by formating it.
26//!     let result = format!("{}", Example { name: "Splamy".into(), food: "Cake".into(), num: 3 });
27//!     println!("{}", result);
28//!#    assert_eq!(result, "Hello From Template!\nMy Name is: Splamy\nI like to eat Cake.\nNum:1\nNum:2\nNum:3\n\n");
29//! }
30//! ```
31//!
32//! `doc_example1.tt`:
33//! ```text
34//! Hello From Template!
35//! My Name is: <# write!(_fmt, "{}", self.name)?; #>
36//! I like to eat <#= self.food #>.
37//! <# for num in 0..self.num { #>Num:<#= num + 1 #>
38//! <# } #>
39//! ```
40//!
41//! Output:
42//! ```text
43//! Hello From Template!
44//! My Name is: Splamy
45//! I like to eat Cake.
46//! Num:1
47//! Num:2
48//! Num:3
49//! ```
50//!
51//! # Syntax
52//!
53//! You can simply write rust code within code blocks.
54//!
55//! Code is written within `<#` and `#>` blocks.
56//! If you want to write a `<#` in template text without starting a code block
57//! simply write it twice: `<#<#`. Same goes for the `#>` in code blocks.
58//! You dont need to duplicate the `<#` within code blocks and `#>` not in
59//! template text blocks.
60//!
61//! You can use `<#= expr #>` to print out a single expression.
62//!
63//! Maybe you noticed the magical `_fmt` in the template. This variable gives you
64//! access to the formatter and e.g. enables you to write functions in your
65//! template. `<# write!(_fmt, "{}", self.name)?; #>` is equal to `<#= self.name #>`.
66//!
67//! **Warning**: Make sure to never create a variable called `_fmt`! You will get
68//! weird compiler errors.
69//!
70//! # Features
71//!
72//! ## Auto-escaping
73//!
74//! Use the `escape` directive in your .tt file:
75//! ```text
76//! <#@ escape function="escape_html" #>`
77//! ```
78//!
79//! And a function with this signature in your code:
80//! ```rust
81//! fn escape_html(s: &str) -> String {
82//!     todo!(); /* Your escaping code here */
83//! }
84//! ```
85//!
86//! All expression blocks (e.g. `<#= self.name #>`) will call the escape
87//! function before inserted.
88//!
89//! You can redeclare this directive as many times and where you want in your
90//! template to change or disable (with `function=""`) the escape function.
91
92extern crate proc_macro;
93
94use std::collections::hash_map::DefaultHasher;
95use std::fs::File;
96use std::hash::Hasher;
97use std::io::prelude::*;
98use std::option::Option;
99use std::path::Path;
100use std::path::PathBuf;
101use std::result::Result;
102use std::vec::Vec;
103
104use nom::{
105	branch::alt,
106	bytes::complete::{
107		escaped_transform, is_not, tag, take, take_until, take_while,
108	},
109	character::complete::{alphanumeric1, line_ending, space0},
110	combinator::{map, not, opt, peek},
111	multi::many0,
112	sequence::tuple,
113	IResult,
114};
115use quote::quote;
116use syn::Meta::*;
117use syn::*;
118
119use crate::TemplatePart::*;
120
121macro_rules! dbg_println {
122	($inf:ident) => { if $inf.debug_print { println!(); } };
123	($inf:ident, $fmt:expr) => { if $inf.debug_print { println!($fmt); } };
124	($inf:ident, $fmt:expr, $($arg:tt)*) => { if $inf.debug_print { println!($fmt, $($arg)*); } };
125}
126
127macro_rules! dbg_print {
128	($inf:ident) => { if $inf.debug_print { print!(); } };
129	($inf:ident, $fmt:expr) => { if $inf.debug_print { print!($fmt); } };
130	($inf:ident, $fmt:expr, $($arg:tt)*) => { if $inf.debug_print { print!($fmt, $($arg)*); } };
131}
132
133const TEMPLATE_PATH_MACRO: &str = "TemplatePath";
134const TEMPLATE_DEBUG_MACRO: &str = "TemplateDebug";
135
136#[proc_macro_derive(Template, attributes(TemplatePath, TemplateDebug))]
137pub fn transform_template(
138	input: proc_macro::TokenStream,
139) -> proc_macro::TokenStream {
140	let macro_input = parse_macro_input!(input as DeriveInput);
141
142	let mut path: Option<String> = None;
143	let mut info = TemplateInfo::default();
144
145	for attr in &macro_input.attrs {
146		match &attr.meta {
147			NameValue(MetaNameValue {
148				path: p,
149				value: syn::Expr::Lit(ExprLit {attrs: _, lit: Lit::Str(lit_str)}),
150				..
151			}) => {
152				if p.get_ident().expect("Attribute with no name")
153					== TEMPLATE_PATH_MACRO
154				{
155					path = Some(lit_str.value());
156				}
157			}
158			Path(name) => {
159				if name.get_ident().expect("Attribute with no name")
160					== TEMPLATE_DEBUG_MACRO
161				{
162					info.debug_print = true;
163				}
164			}
165			_ => {}
166		}
167	}
168
169	// Get template path
170	let mut path_absolute =
171		PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
172	path_absolute.push(&path.unwrap_or_else(|| {
173		panic!(
174			"Please specify a #[{}=\"<path>\"] atribute with the template \
175			 file path.",
176			TEMPLATE_PATH_MACRO
177		)
178	}));
179	let path =
180		&path_absolute.canonicalize().expect("Could not canonicalize path");
181	dbg_println!(
182		info,
183		"Looking for template in \"{}\"",
184		path.to_str().unwrap()
185	);
186
187	// Read template file
188	let read = read_from_file(path).expect("Could not read file");
189
190	// Parse template file
191	let mut data = match parse_all(&mut info, &read) {
192		Ok(data) => data,
193		Err(e) => {
194			return syn::Error::new_spanned(macro_input, format!("Parse error: {}, reason: {}", e.index, e.reason))
195				.into_compile_error()
196				.into()
197		}
198	};
199
200	if info.debug_print {
201		debug_to_file(path, &data);
202	}
203
204	parse_postprocess(&mut data);
205
206	let data = parse_optimize(data);
207
208	// Build code from template
209	info = TemplateInfo::default();
210	let mut builder = String::new();
211	for part in data {
212		match part {
213			Text(x) => {
214				builder.push_str(generate_save_str_print(&x).as_ref());
215			}
216			Code(x) => {
217				builder.push_str(x.as_ref());
218			}
219			Expr(x) => {
220				builder.push_str(generate_expression_print(&x, &info).as_ref());
221			}
222			Directive(dir) => {
223				apply_directive(&mut info, &dir);
224			}
225		}
226	}
227
228	dbg_println!(info, "Generated Code:\n{}", builder);
229
230	let tokens: proc_macro2::TokenStream =
231		builder.parse().expect("Parsing template code failed!");
232
233	// Build frame and insert
234	let (impl_generics, ty_generics, where_clause) =
235		macro_input.generics.split_for_impl();
236	let name = &macro_input.ident;
237	let path_str = path.to_str().expect("Invalid path");
238
239	let frame = quote! {
240		impl #impl_generics ::std::fmt::Display for #name #ty_generics #where_clause {
241			fn fmt(&self, _fmt: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
242				let _ = include_bytes!(#path_str);
243				#tokens
244				Ok(())
245			}
246		}
247	};
248
249	// We could return the code now. The problem is that span information are
250	// missing and the error messages are awful.
251	// So instead, we write to a file and include! this file, which still does
252	// not give us nice errors but at least includes source code.
253	if !info.debug_print {
254		proc_macro::TokenStream::from(frame)
255	} else {
256		// Unfortunately we have no access to OUT_DIR like build scripts so we
257		// try to emulate that partially.
258
259		// Use hash of template path as filename
260		let mut hasher = DefaultHasher::new();
261		hasher.write(path_str.as_bytes());
262
263		let out_dir = if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR")
264		{
265			PathBuf::from(target_dir)
266		} else {
267			let dir = std::env::var("CARGO_MANIFEST_DIR")
268				.expect("CARGO_MANIFEST_DIR not set");
269			PathBuf::from(dir).join("target")
270		};
271
272		let code_path = out_dir
273			.join("t4rust")
274			.join(&hasher.finish().to_string())
275			.with_extension("rs");
276
277		std::fs::create_dir_all(code_path.parent().unwrap())
278			.expect("Failed to create output path");
279
280		// Write file
281		std::fs::write(&code_path, frame.to_string().as_bytes())
282			.expect("Failed to write compiled template");
283
284		let code_path_str = code_path.to_str();
285		proc_macro::TokenStream::from(quote! { include!(#code_path_str); })
286	}
287}
288
289fn generate_expression_print(print_expr: &str, info: &TemplateInfo) -> String {
290	if info.print_postprocessor.is_empty() {
291		format!("write!(_fmt, \"{{}}\", {})?;\n", print_expr)
292	} else {
293		format!(
294			"{{
295			let _s = format!(\"{{}}\", {});
296			let _s_transfomed = {}(&_s);
297			_fmt.write_str(&_s_transfomed)?;
298			}}\n",
299			print_expr, info.print_postprocessor
300		)
301	}
302}
303
304fn generate_save_str_print(print_str: &str) -> String {
305	let mut max_sharp_count = 0;
306	let mut cur_sharp_count = 0;
307
308	for c in print_str.chars() {
309		if c == '#' {
310			cur_sharp_count += 1;
311			max_sharp_count = std::cmp::max(max_sharp_count, cur_sharp_count);
312		} else {
313			cur_sharp_count = 0;
314		}
315	}
316
317	let sharps = "#".repeat(max_sharp_count + 1);
318	format!("_fmt.write_str(r{1}\"{0}\"{1})?;\n", print_str, sharps)
319}
320
321fn read_from_file(path: &Path) -> Result<String, std::io::Error> {
322	let mut file = File::open(path)?;
323	let mut contents = String::new();
324	file.read_to_string(&mut contents)?;
325	Ok(contents)
326}
327
328fn debug_to_file(path: &Path, data: &[TemplatePart]) {
329	let mut pathbuf = PathBuf::new();
330	pathbuf.push(path);
331	pathbuf.set_extension("tt.out");
332	let writepath = pathbuf.as_path();
333	if let Ok(mut file) = File::create(writepath) {
334		for var in data {
335			match *var {
336				Code(ref x) => {
337					write!(file, "Code:").unwrap();
338					file.write_all(x.as_bytes()).unwrap();
339				}
340				Text(ref x) => {
341					write!(file, "Text:").unwrap();
342					file.write_all(x.as_bytes()).unwrap();
343				}
344				Expr(ref x) => {
345					write!(file, "Expr:").unwrap();
346					file.write_all(x.as_bytes()).unwrap();
347				}
348				Directive(ref dir) => {
349					write!(file, "Dir:{:?}", dir).unwrap();
350				}
351			}
352			writeln!(file).unwrap();
353		}
354	}
355}
356
357/// Transforms template code into an intermediate representation
358fn parse_all(
359	info: &mut TemplateInfo,
360	input: &str,
361) -> Result<Vec<TemplatePart>, TemplateError>
362{
363	let mut builder: Vec<TemplatePart> = Vec::new();
364	let mut cur = input;
365
366	dbg_println!(info, "Reading template");
367
368	while !cur.is_empty() {
369		let (crest, content) = parse_text(info, cur)?;
370		builder.push(Text(content));
371		cur = crest;
372		dbg_println!(info, "");
373
374		// Read code block
375		if let Ok((rest, _)) = expression_start(cur) {
376			dbg_print!(info, " expression start");
377			let (crest, content) = parse_code(info, rest)?;
378			builder.push(Expr(content));
379			cur = crest;
380		} else if let Ok((rest, _)) = template_directive_start(cur) {
381			dbg_print!(info, " directive start");
382			let (crest, content) = parse_code(info, rest)?;
383			let dir = parse_directive(&content);
384			dbg_println!(info, " Directive: {:?}", dir);
385			match dir {
386				Ok((_, dir)) => {
387					apply_directive(info, &dir);
388					builder.push(Directive(dir));
389				}
390				Err(_) => {
391					println!("Malformed directive: {}", &content);
392					return Err(TemplateError {
393						index: 0,
394						reason: format!(
395							"Could not understand the directive: {}",
396							&content
397						),
398					});
399				}
400			}
401			cur = crest;
402		} else if let Ok((rest, _)) = code_start(cur) {
403			dbg_print!(info, " code start");
404			let (crest, content) = parse_code(info, rest)?;
405			builder.push(Code(content));
406			cur = crest;
407		}
408
409		dbg_println!(info, " Rest: {:?}", &cur);
410	}
411
412	dbg_println!(info, "\nTemplate ok!");
413
414	Result::Ok(builder)
415}
416
417fn parse_text<'a>(
418	info: &TemplateInfo,
419	input: &'a str,
420) -> Result<(&'a str, String), TemplateError>
421{
422	let mut content = String::new();
423	let mut cur = input;
424
425	loop {
426		let read = read_text(cur);
427		match read {
428			Ok((rest, done)) => {
429				content.push_str(&done);
430				if rest.is_empty() {
431					return Ok((rest, content));
432				}
433				cur = rest;
434				dbg_print!(info, " take text: {:?}", &done);
435
436				if let Ok((rest, _)) = double_code_start(cur) {
437					dbg_print!(info, " double-escape");
438					content.push_str("<#");
439
440					if rest.is_empty() {
441						return Ok((rest, content));
442					}
443					cur = rest;
444				} else if done.is_empty() {
445					return Ok((rest, content));
446				}
447			}
448			Err(_) => {
449				if let Ok((rest, done)) = till_end(cur) {
450					if rest.is_empty() {
451						content.push_str(&done);
452						return Ok((rest, content));
453					}
454				}
455				panic!(
456					"Reached unknown parsing state (!read_text > !till_end)"
457				);
458			}
459		}
460
461		dbg_println!(info, " Rest: {:?}", &cur);
462	}
463}
464
465fn parse_code<'a>(
466	info: &TemplateInfo,
467	input: &'a str,
468) -> Result<(&'a str, String), TemplateError>
469{
470	let mut content = String::new();
471	let mut cur = input;
472
473	loop {
474		match read_code(cur) {
475			Ok((rest, done)) => {
476				dbg_print!(info, " take code: {:?}", &done);
477				content.push_str(&done);
478				cur = rest;
479
480				if let Ok((rest, _)) = code_end(cur) {
481					dbg_print!(info, " code end");
482					return Ok((rest, content));
483				} else if let Ok((rest, _)) = double_code_end(cur) {
484					dbg_print!(info, " double-escape");
485					content.push_str("#>");
486					cur = rest;
487				} else {
488					panic!("Nothing, i guess?");
489				}
490			}
491			Err(err) => {
492				dbg_println!(info, "Error at code {:?}", err);
493				return Err(TemplateError {
494					index: 0,
495					reason: "Unclosed code or expression block".into(),
496				});
497			}
498		}
499	}
500}
501
502/// Merges multiple identical Parts into one
503fn parse_optimize(data: Vec<TemplatePart>) -> Vec<TemplatePart> {
504	let mut last_type = TemplatePartType::None;
505	let mut combined = Vec::<TemplatePart>::new();
506	let mut tmp_build = String::new();
507	for item in data {
508		match item {
509			Code(u) => {
510				if u.is_empty() {
511					continue;
512				}
513				if last_type != TemplatePartType::Code {
514					if !tmp_build.is_empty() {
515						match last_type {
516							TemplatePartType::None | TemplatePartType::Code => {
517								panic!()
518							}
519							TemplatePartType::Text => {
520								combined.push(Text(tmp_build))
521							}
522							TemplatePartType::Expr => {
523								combined.push(Expr(tmp_build))
524							}
525						}
526					}
527					tmp_build = String::new();
528					last_type = TemplatePartType::Code;
529				}
530				tmp_build.push_str(&u);
531			}
532			Text(u) => {
533				if u.is_empty() {
534					continue;
535				}
536				if last_type != TemplatePartType::Text {
537					if !tmp_build.is_empty() {
538						match last_type {
539							TemplatePartType::None | TemplatePartType::Text => {
540								panic!()
541							}
542							TemplatePartType::Code => {
543								combined.push(Code(tmp_build))
544							}
545							TemplatePartType::Expr => {
546								combined.push(Expr(tmp_build))
547							}
548						}
549					}
550					tmp_build = String::new();
551					last_type = TemplatePartType::Text;
552				}
553				tmp_build.push_str(&u);
554			}
555			Expr(u) => {
556				if !tmp_build.is_empty() {
557					match last_type {
558						TemplatePartType::None => panic!(),
559						TemplatePartType::Code => {
560							combined.push(Code(tmp_build))
561						}
562						TemplatePartType::Text => {
563							combined.push(Text(tmp_build))
564						}
565						TemplatePartType::Expr => {
566							combined.push(Expr(tmp_build))
567						}
568					}
569				}
570				tmp_build = String::new();
571				last_type = TemplatePartType::Expr;
572				tmp_build.push_str(&u);
573			}
574			Directive(d) => {
575				combined.push(Directive(d));
576			}
577		}
578	}
579	if !tmp_build.is_empty() {
580		match last_type {
581			TemplatePartType::None => {}
582			TemplatePartType::Code => combined.push(Code(tmp_build)),
583			TemplatePartType::Text => combined.push(Text(tmp_build)),
584			TemplatePartType::Expr => combined.push(Expr(tmp_build)),
585		}
586	}
587	combined
588}
589
590/// Applies template directives like 'cleanws' and modifies the input
591/// accordingly.
592fn parse_postprocess(data: &mut Vec<TemplatePart>) {
593	let mut info = TemplateInfo::default();
594	let mut was_b_clean = None;
595	let mut clean_index = 0;
596
597	// if there are less than 3 blocks available we can't do any transformations
598	if data.len() < 3 {
599		return;
600	}
601
602	for i in 0..(data.len() - 2) {
603		let tri = data[i..(i + 3)].as_mut();
604		if let Directive(ref dir) = tri[1] {
605			apply_directive(&mut info, dir);
606		}
607
608		if !info.clean_whitespace
609			|| !tri[0].is_text()
610			|| !tri[1].should_trim_whitespace()
611			|| !tri[2].is_text()
612		{
613			continue;
614		}
615
616		let mut res_a = None;
617		if clean_index == i && was_b_clean.is_some() {
618			res_a = was_b_clean;
619		} else if let Text(ref text_a) = tri[0] {
620			let rev_txt: String = text_a.chars().rev().collect();
621			if let Ok((_, a_len)) = is_ws_till_newline(&rev_txt) {
622				res_a = Some(a_len);
623			} else if i == 0 && text_a.is_empty() {
624				// Start of file
625				res_a = Some((0, 0));
626			} else {
627				continue;
628			}
629		}
630
631		let mut res_b = None;
632		if let Text(ref text_b) = tri[2] {
633			if let Ok((_, b_len)) = is_ws_till_newline(&text_b) {
634				res_b = Some(b_len);
635			} else {
636				continue;
637			}
638		}
639
640		// start trimming
641
642		if let Text(ref mut text_a) = tri[0] {
643			let res_a = res_a.unwrap();
644			let len = text_a.len();
645			text_a.drain((len - (res_a.0))..len);
646		}
647
648		if let Text(ref mut text_b) = tri[2] {
649			let rev_txt: String = text_b.chars().rev().collect();
650			if let Ok((_, b_len)) = is_ws_till_newline(&rev_txt) {
651				was_b_clean = Some(b_len);
652				clean_index = i + 2;
653			}
654
655			let res_b = res_b.unwrap();
656			text_b.drain(0..(res_b.0 + res_b.1));
657		}
658	}
659}
660
661fn apply_directive(info: &mut TemplateInfo, directive: &TemplateDirective) {
662	for (key, value) in directive
663		.params
664		.iter()
665		.map(|p| ((directive.name.as_str(), p.0.as_str()), p.1.as_str()))
666	{
667		match key {
668			("template", "debug") => {
669				info.debug_print = value.parse::<bool>().unwrap()
670			}
671			("template", "cleanws") | ("template", "clean_whitespace") => {
672				info.clean_whitespace = value.parse::<bool>().unwrap()
673			}
674			("escape", "function") => {
675				info.print_postprocessor = value.to_string()
676			}
677			_ => println!(
678				"Unrecognized template parameter \"{}\" in \"{}\"",
679				key.0, key.1
680			),
681		}
682	}
683}
684
685// NOM DECLARATIONS ===========================================================
686
687fn expression_start(s: &str) -> IResult<&str, &str> { tag("<#=")(s) }
688fn template_directive_start(s: &str) -> IResult<&str, &str> { tag("<#@")(s) }
689fn read_text(s: &str) -> IResult<&str, &str> { take_until("<#")(s) }
690
691fn code_start(s: &str) -> IResult<&str, &str> {
692	let (s, r) = tag("<#")(s)?;
693	not(tag("<#"))(s)?;
694	Ok((s, r))
695}
696fn double_code_start(s: &str) -> IResult<&str, &str> { tag("<#<#")(s) }
697
698fn code_end(s: &str) -> IResult<&str, &str> {
699	let (s, r) = tag("#>")(s)?;
700	not(tag("#>"))(s)?;
701	Ok((s, r))
702}
703fn double_code_end(s: &str) -> IResult<&str, &str> { tag("#>#>")(s) }
704
705fn read_code(s: &str) -> IResult<&str, &str> { take_until("#>")(s) }
706
707fn till_end(s: &str) -> IResult<&str, &str> { take_while(|_| true)(s) }
708
709fn parse_directive(s: &str) -> IResult<&str, TemplateDirective> {
710	map(
711		tuple((space0, alphanumeric1, many0(parse_directive_param), at_end)),
712		|t| TemplateDirective { name: t.1.to_string(), params: t.2 },
713	)(s)
714}
715
716fn at_end(s: &str) -> IResult<&str, ()> { not(peek(take(1usize)))(s) }
717
718fn parse_directive_param(s: &str) -> IResult<&str, (String, String)> {
719	map(
720		tuple((
721			space0,
722			alphanumeric1,
723			space0,
724			tag("="),
725			space0,
726			tag("\""),
727			opt(escaped_transform(
728				is_not("\\\""),
729				'\\',
730				alt((tag_transform("\\", "\\"), tag_transform("\"", "\""))),
731			)),
732			tag("\""),
733			space0,
734		)),
735		|t| (t.1.to_string(), t.6.unwrap_or_else(|| "".to_string())),
736	)(s)
737}
738
739fn is_ws_till_newline(s: &str) -> IResult<&str, (usize, usize)> {
740	map(
741		tuple((space0, line_ending)),
742		|t: (&str, &str)| (t.0.len(), t.1.len()),
743	)(s)
744}
745
746fn tag_transform<'a>(
747	s: &'a str,
748	t: &'a str,
749) -> impl Fn(&'a str) -> IResult<&str, &str>
750{
751	move |i: &'a str| {
752		let (r, _) = tag(s)(i)?;
753		Ok((r, t))
754	}
755}
756
757// NOM END ====================================================================
758
759#[derive(Debug)]
760struct TemplateError {
761	reason: String,
762	index: usize,
763}
764
765#[derive(Debug)]
766struct TemplateDirective {
767	name: String,
768	params: Vec<(String, String)>,
769}
770
771#[derive(Debug)]
772enum TemplatePart {
773	Text(String),
774	Code(String),
775	Expr(String),
776	Directive(TemplateDirective),
777}
778
779impl TemplatePart {
780	fn is_text(&self) -> bool { matches!(self, Text(_)) }
781
782	/// Whitespace should only be trimmed for code and directive blocks, we want to keep it for
783	/// expressions.
784	fn should_trim_whitespace(&self) -> bool { matches!(self, Code(_) | Directive(_)) }
785}
786
787#[derive(PartialEq)]
788enum TemplatePartType {
789	None,
790	Code,
791	Text,
792	Expr,
793}
794
795#[derive(Debug)]
796struct TemplateInfo {
797	debug_print: bool,
798	clean_whitespace: bool,
799	print_postprocessor: String,
800}
801
802impl TemplateInfo {
803	fn default() -> Self {
804		Self {
805			debug_print: false,
806			clean_whitespace: false,
807			print_postprocessor: "".into(),
808		}
809	}
810}