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}