punktf_lib/template/
mod.rs

1//! Everything related to parsing/resolving templates is located in this module or it's submodules.
2//!
3//! # Syntax
4//!
5//! The syntax is heavily inspired by <https://handlebarsjs.com/>.
6//!
7//! ## Comment blocks
8//!
9//! Document template blocks. Will not be copied over to final output.
10//!
11//! ### Syntax
12//!
13//! `{{!-- This is a comment --}}`
14//!
15//! ## Escape blocks
16//!
17//! Everything inside will be copied over as is. It can be used to  copied over `{{` or `}}` without it being interpreted as a template block.
18//!
19//! ### Syntax
20//!
21//! `{{{ This will be copied over {{ as is }} even with the "{{" inside }}}`
22//!
23//! ## Variable blocks
24//!
25//! Define a variable which will be inserted instead of the block. The value of the variable can be gotten from three different environments which can be defined by specifying a prefix:
26//!
27//! 1) `$`: System environment
28//! 2) `#`: Variables defined in the `profile` section
29//! 2) `&`: Variables defined in the `profile.dotfile` section
30//!
31//! To search in more than one environment, these prefixes can be combined. The order they appear in is important, as they will be searched in order of appearance. If one environment does not have a value set for the variable, the next one is searched.
32//!
33//! If no prefixes are defined, it will default to `&#`.
34//!
35//! Valid symbols/characters for a variable name are: `(a..z|A..Z|0-9|_)`
36//!
37//! ### Syntax
38//!
39//! `{{$&#OS}}`
40//!
41//! ## Print blocks
42//!
43//! Print blocks will simply print everything contained within the block to the command line. The content of the print block **won't** be resolved, meaning it will be printed 1 to 1 (e.g. no variables are resolved).
44//!
45//! ### Syntax
46//!
47//! `{{@print Hello World}}`
48//!
49//! ## If blocks
50//!
51//! Supported are `if`, `elif`, `else` and `fi`. Each `if` block must have a `fi` block as a final closing block.
52//! In between the `if` and `fi` block can be zero or multiple `elif` blocks with a final optional `else` block.
53//! Each if related block must be prefixed with `{{@` and end with `}}`.
54//!
55//! Currently the only supported if syntax is:
56//!
57//! - Check if the value of a variable is (not) equal to the literal given: `{{VAR}} (==|!=) "LITERAL"`
58//! - Check if a value for a variable exists: `{{VAR}}`
59//!
60//! Other blocks can be nested inside the `if`, `elif` and `else` bodies.
61//!
62//! ### Syntax
63//!
64//! ```text
65//! {{@if {{OS}}}}
66//!         {{@if {{&OS}} != "windows"}}
67//!             print("OS is not windows")
68//!         {{@elif {{OS}} == "windows"}}
69//!             {{{!-- This is a nested comment. Below it is a nested variable block. --}}}
70//!             print("OS is {{OS}}")
71//!         {{@else}}
72//!             {{{!-- This is a nested comment. --}}}
73//!             print("Can never get here. {{{ {{OS}} is neither `windows` nor not `windows`. }}}")
74//!         {{@fi}}
75//! {{@else}}
76//!     print("No value for variable `OS` set")
77//! {{@fi}}
78//! ```
79//!
80//! # Copyright Notice
81//!
82//! The code for error/diagnostics and source input handling is heavily inspired by
83//! [rust's](https://github.com/rust-lang/rust) compiler, which is licensed under the MIT license.
84//! While some code is adapted for use with `punktf`, some of it is also a plain copy of it. If a
85//! portion of code was copied/adapted from the Rust project there will be an explicit notices
86//! above it. For further information and the license please see the `COPYRIGHT` file in the root
87//! of this project.
88//!
89//! Specifically but not limited to:
90//! - <https://github.com/rust-lang/rust/blob/master/compiler/rustc_span/src/lib.rs>
91//! - <https://github.com/rust-lang/rust/blob/master/compiler/rustc_span/src/analyze_source_file.rs>
92//! - <https://github.com/rust-lang/rust/blob/master/compiler/rustc_parse/src/parser/diagnostics.rs>
93//! - <https://github.com/rust-lang/rust/blob/master/compiler/rustc_errors/src/diagnostic.rs>
94//! - <https://github.com/rust-lang/rust/blob/master/compiler/rustc_errors/src/diagnostic_builder.rs>
95//! - <https://github.com/rust-lang/rust/blob/master/compiler/rustc_errors/src/emitter.rs>
96
97mod block;
98mod diagnostic;
99mod parse;
100mod resolve;
101mod session;
102pub mod source;
103mod span;
104
105use color_eyre::eyre::Result;
106
107use self::block::Block;
108use self::parse::Parser;
109use self::resolve::Resolver;
110use self::source::Source;
111use crate::profile::variables::Vars;
112
113/// A `Template` is a file from the Source folder that is not yet deployed. It might contain statements and variables.
114#[derive(Debug, Clone)]
115pub struct Template<'a> {
116	/// The source from which the template was parsed from.
117	source: Source<'a>,
118
119	/// All parsed blocks contained in `source`.
120	///
121	/// These are sorted in the order they occur in `source`.
122	blocks: Vec<Block>,
123}
124
125impl<'a> Template<'a> {
126	/// Parses the source file and returns a `Template` object.
127	pub fn parse(source: Source<'a>) -> Result<Self> {
128		Parser::new(source).parse()
129	}
130
131	/// Resolves the variables in the template and returns a `Template` object.
132	pub fn resolve<PV: Vars, DV: Vars>(
133		&self,
134		profile_vars: Option<&PV>,
135		dotfile_vars: Option<&DV>,
136	) -> Result<String> {
137		Resolver::new(self, profile_vars, dotfile_vars).resolve()
138	}
139}
140
141#[cfg(test)]
142mod tests {
143	use std::collections::HashMap;
144
145	use super::*;
146	use crate::profile::variables::Variables;
147
148	#[test]
149	fn parse_template() -> Result<()> {
150		crate::tests::setup_test_env();
151
152		let content = r#"
153			[some settings]
154			var = 2
155			foo = "bar"
156			fizz = {{BUZZ}}
157			escaped = {{{42}}}
158
159			{{!--
160				Sets the message of the day for a specific operating system
161				If no os matches it defaults to a generic one.
162			--}}
163			{{@print Writing motd...}}
164			{{@if {{&OS}} == "linux" }}
165			{{@print Linux Motd!}}
166			[linux]
167			motd = "very nice"
168			{{@elif {{&#OS}} == "windows" }}
169			[windows]
170			motd = "nice"
171			{{@else}}
172			[other]
173			motd = "who knows"
174			{{@fi}}
175
176			{{!-- Check if not windows --}}
177			{{@if {{&OS}} != "windows"}}
178			windows = false
179			{{@fi}}
180
181			[last]
182			num = 23
183			threads = 1337
184			os_str = "_unknown"
185			"#;
186
187		let source = Source::anonymous(content);
188		let template = Template::parse(source)?;
189
190		// println!("{:#?}", template);
191
192		let mut vars = HashMap::new();
193		vars.insert(String::from("BUZZ"), String::from("Hello World"));
194		vars.insert(String::from("OS"), String::from("linux"));
195		let vars = Variables { inner: vars };
196
197		println!("{}", template.resolve(Some(&vars), Some(&vars))?);
198
199		Ok(())
200	}
201
202	#[test]
203	fn parse_template_vars() -> Result<()> {
204		crate::tests::setup_test_env();
205
206		// Default
207		let content = r#"{{OS}}"#;
208		let source = Source::anonymous(content);
209		let template = Template::parse(source)?;
210
211		let profile_vars = Variables::from_items(vec![("OS", "windows")]);
212		let item_vars = Variables::from_items(vec![("OS", "unix")]);
213		std::env::set_var("OS", "macos");
214
215		assert_eq!(
216			template.resolve(Some(&profile_vars), Some(&item_vars))?,
217			"unix"
218		);
219
220		// Profile
221		let content = r#"{{#OS}}"#;
222		let source = Source::anonymous(content);
223		let template = Template::parse(source)?;
224
225		let profile_vars = Variables::from_items(vec![("OS", "windows")]);
226		let item_vars = Variables::from_items(vec![("OS", "unix")]);
227		std::env::set_var("OS", "macos");
228
229		assert_eq!(
230			template.resolve(Some(&profile_vars), Some(&item_vars))?,
231			"windows"
232		);
233
234		// Item
235		let content = r#"{{&OS}}"#;
236		let source = Source::anonymous(content);
237		let template = Template::parse(source)?;
238
239		let profile_vars = Variables::from_items(vec![("OS", "windows")]);
240		let item_vars = Variables::from_items(vec![("OS", "unix")]);
241		std::env::set_var("OS", "macos");
242
243		assert_eq!(
244			template.resolve(Some(&profile_vars), Some(&item_vars))?,
245			"unix"
246		);
247
248		// Env
249		let content = r#"{{$OS}}"#;
250		let source = Source::anonymous(content);
251		let template = Template::parse(source)?;
252
253		let profile_vars = Variables::from_items(vec![("OS", "windows")]);
254		let item_vars = Variables::from_items(vec![("OS", "unix")]);
255		std::env::set_var("OS", "macos");
256
257		assert_eq!(
258			template.resolve(Some(&profile_vars), Some(&item_vars))?,
259			"macos"
260		);
261
262		// Mixed - First
263		let content = r#"{{$#OS}}"#;
264		let source = Source::anonymous(content);
265		let template = Template::parse(source)?;
266
267		let profile_vars = Variables::from_items(vec![("OS", "windows")]);
268		let item_vars = Variables::from_items(vec![("OS", "unix")]);
269		std::env::set_var("OS", "macos");
270
271		assert_eq!(
272			template.resolve(Some(&profile_vars), Some(&item_vars))?,
273			"macos"
274		);
275
276		// Mixed - Last
277		let content = r#"{{$&OS}}"#;
278		let source = Source::anonymous(content);
279		let template = Template::parse(source)?;
280
281		let profile_vars = Variables::from_items(vec![("OS", "windows")]);
282		let item_vars = Variables::from_items(vec![("OS", "unix")]);
283		std::env::remove_var("OS");
284
285		assert_eq!(
286			template.resolve(Some(&profile_vars), Some(&item_vars))?,
287			"unix"
288		);
289
290		Ok(())
291	}
292}