Skip to main content

whiskers/
templating.rs

1use std::path::Path;
2
3use indexmap::IndexMap;
4use itertools::Itertools as _;
5
6use crate::{filters, functions, markdown};
7
8/// Allows creation of a [`FilterExample`] with the following syntax:
9///
10/// `function_example!(mix(base=base, blend=red, amount=0.5) => "#804040")`
11macro_rules! function_example {
12    ($name:ident($($key:ident = $value:tt),*) => $output:expr) => {
13        $crate::templating::FunctionExample {
14            inputs: {
15                let mut map = indexmap::IndexMap::new();
16                $(map.insert(stringify!($key).to_string(), stringify!($value).to_string());)*
17                map
18            },
19            output: $output.to_string(),
20        }
21    };
22}
23
24/// Allows creation of a [`FilterExample`] with the following syntax:
25///
26/// `filter_example!(red | add(hue=30)) => "#ff6666")`
27macro_rules! filter_example {
28    ($value:tt | $name:ident => $output:expr) => {
29        $crate::templating::FilterExample {
30            value: stringify!($value).to_string(),
31            inputs: indexmap::IndexMap::new(),
32            output: $output.to_string(),
33        }
34    };
35    ($value:tt | $name:ident($($key:ident = $arg_value:tt),*) => $output:expr) => {
36        $crate::templating::FilterExample {
37            value: stringify!($value).to_string(),
38            inputs: {
39                let mut map = indexmap::IndexMap::new();
40                $(map.insert(stringify!($key).to_string(), stringify!($arg_value).to_string());)*
41                map
42            },
43            output: $output.to_string(),
44        }
45    };
46}
47
48pub fn make_engine(template_directory: &Path) -> tera::Tera {
49    let mut tera = tera::Tera::default();
50    tera.register_filter("add", filters::add);
51    tera.register_filter("sub", filters::sub);
52    tera.register_filter("mod", filters::modify);
53    tera.register_filter("urlencode_lzma", filters::urlencode_lzma);
54    tera.register_filter("trunc", filters::trunc);
55    tera.register_filter("mix", filters::mix);
56    tera.register_filter("hex", filters::hex);
57    tera.register_filter("css_rgb", filters::css_rgb);
58    tera.register_filter("css_rgba", filters::css_rgba);
59    tera.register_filter("css_hsl", filters::css_hsl);
60    tera.register_filter("css_hsla", filters::css_hsla);
61    tera.register_function("if", functions::if_fn);
62    tera.register_function("object", functions::object);
63    tera.register_function("css_rgb", functions::css_rgb);
64    tera.register_function("css_rgba", functions::css_rgba);
65    tera.register_function("css_hsl", functions::css_hsl);
66    tera.register_function("css_hsla", functions::css_hsla);
67    tera.register_function(
68        "read_file",
69        functions::read_file_handler(template_directory.to_owned()),
70    );
71    tera
72}
73
74#[must_use]
75pub fn all_functions() -> Vec<Function> {
76    vec![
77        Function {
78            name: "if".to_string(),
79            description: "Return one value if a condition is true, and another if it's false"
80                .to_string(),
81            examples: vec![
82                function_example!(if(cond=true, t=1, f=0) => "1"),
83                function_example!(if(cond=false, t=1, f=0) => "0"),
84            ],
85        },
86        Function {
87            name: "object".to_string(),
88            description: "Create an object from the input".to_string(),
89            examples: vec![
90                function_example!(object(a=1, b=2) => "{a: 1, b: 2}"),
91                function_example!(object(a=1, b=2) => "{a: 1, b: 2}"),
92            ],
93        },
94        Function {
95            name: "css_rgb".to_string(),
96            description: "Convert a color to an RGB CSS string".to_string(),
97            examples: vec![function_example!(css_rgb(color=red) => "rgb(210, 15, 57)")],
98        },
99        Function {
100            name: "css_rgba".to_string(),
101            description: "Convert a color to an RGBA CSS string".to_string(),
102            examples: vec![function_example!(css_rgba(color=red) => "rgba(210, 15, 57, 1.00)")],
103        },
104        Function {
105            name: "css_hsl".to_string(),
106            description: "Convert a color to an HSL CSS string".to_string(),
107            examples: vec![function_example!(css_hsl(color=red) => "hsl(347, 87%, 44%)")],
108        },
109        Function {
110            name: "css_hsla".to_string(),
111            description: "Convert a color to an HSLA CSS string".to_string(),
112            examples: vec![function_example!(css_hsla(color=red) => "hsla(347, 87%, 44%, 1.00)")],
113        },
114        Function {
115            name: "rgb_array".to_string(),
116            description: "Convert a color to an array of RGB values".to_string(),
117            examples: vec![function_example!(rgb_array(color=red) => "[210, 15, 57]")],
118        },
119        Function {
120            name: "read_file".to_string(),
121            description:
122                "Read and include the contents of a file, path is relative to the template file"
123                    .to_string(),
124            examples: vec![function_example!(read_file(path="abc.txt") => "abc")],
125        },
126    ]
127}
128
129#[must_use]
130pub fn all_filters() -> Vec<Filter> {
131    vec![
132        Filter {
133            name: "add".to_string(),
134            description: "Add a value to a color".to_string(),
135            examples: vec![
136                filter_example!(red | add(hue=30) => "#ff6666"),
137                filter_example!(red | add(saturation=0.5) => "#ff6666"),
138            ],
139        },
140        Filter {
141            name: "sub".to_string(),
142            description: "Subtract a value from a color".to_string(),
143            examples: vec![
144                filter_example!(red | sub(hue=30) => "#d30f9b"),
145                filter_example!(red | sub(saturation=60) => "#8f5360"),
146            ],
147        },
148        Filter {
149            name: "mod".to_string(),
150            description: "Modify a color".to_string(),
151            examples: vec![
152                filter_example!(red | mod(lightness=80) => "#f8a0b3"),
153                filter_example!(red | mod(opacity=0.5) => "#d20f3980"),
154            ],
155        },
156        Filter {
157            name: "mix".to_string(),
158            description: "Mix two colors together".to_string(),
159            examples: vec![filter_example!(red | mix(color=base, amount=0.5) => "#e08097")],
160        },
161        Filter {
162            name: "urlencode_lzma".to_string(),
163            description: "Serialize an object into a URL-safe string with LZMA compression"
164                .to_string(),
165            examples: vec![
166                filter_example!(some_object | urlencode_lzma => "XQAAgAAEAAAAAAAAAABAqEggMAAAAA=="),
167            ],
168        },
169        Filter {
170            name: "trunc".to_string(),
171            description: "Truncate a number to a certain number of places".to_string(),
172            examples: vec![filter_example!(1.123456 | trunc(places=3) => "1.123")],
173        },
174        Filter {
175            name: "hex".to_string(),
176            description: "Fetch a colour's hex representation. Shortcut for `get(key=\"hex\")`"
177                .to_string(),
178            examples: vec![filter_example!(red | hex => "#d20f39")],
179        },
180        Filter {
181            name: "css_rgb".to_string(),
182            description: "Convert a color to an RGB CSS string".to_string(),
183            examples: vec![filter_example!(red | css_rgb => "rgb(210, 15, 57)")],
184        },
185        Filter {
186            name: "css_rgba".to_string(),
187            description: "Convert a color to an RGBA CSS string".to_string(),
188            examples: vec![filter_example!(red | css_rgba => "rgba(210, 15, 57, 1.00)")],
189        },
190        Filter {
191            name: "css_hsl".to_string(),
192            description: "Convert a color to an HSL CSS string".to_string(),
193            examples: vec![filter_example!(red | css_hsl => "hsl(347, 87%, 44%)")],
194        },
195        Filter {
196            name: "css_hsla".to_string(),
197            description: "Convert a color to an HSLA CSS string".to_string(),
198            examples: vec![filter_example!(red | css_hsla => "hsla(347, 87%, 44%, 1.00)")],
199        },
200        Filter {
201            name: "rgb_array".to_string(),
202            description: "Convert a color to an array of RGB values".to_string(),
203            examples: vec![filter_example!(red | rgb_array => "[210, 15, 57]")],
204        },
205    ]
206}
207
208#[derive(serde::Serialize)]
209pub struct Function {
210    pub name: String,
211    pub description: String,
212    pub examples: Vec<FunctionExample>,
213}
214
215#[derive(serde::Serialize)]
216pub struct Filter {
217    pub name: String,
218    pub description: String,
219    pub examples: Vec<FilterExample>,
220}
221
222#[derive(serde::Serialize)]
223pub struct FunctionExample {
224    pub inputs: IndexMap<String, String>,
225    pub output: String,
226}
227
228#[derive(serde::Serialize)]
229pub struct FilterExample {
230    pub value: String,
231    pub inputs: IndexMap<String, String>,
232    pub output: String,
233}
234
235impl markdown::TableDisplay for Function {
236    fn table_headings() -> Box<[String]> {
237        Box::new([
238            "Name".to_string(),
239            "Description".to_string(),
240            "Examples".to_string(),
241        ])
242    }
243
244    fn table_row(&self) -> Box<[String]> {
245        Box::new([
246            format!("`{}`", self.name),
247            self.description.clone(),
248            if self.examples.is_empty() {
249                "None".to_string()
250            } else {
251                self.examples.first().map_or_else(String::new, |example| {
252                    format!(
253                        "`{name}({input})` ⇒ `{output}`",
254                        name = self.name,
255                        input = example
256                            .inputs
257                            .iter()
258                            .map(|(k, v)| format!("{k}={v}"))
259                            .join(", "),
260                        output = example.output
261                    )
262                })
263            },
264        ])
265    }
266}
267
268impl markdown::TableDisplay for Filter {
269    fn table_headings() -> Box<[String]> {
270        Box::new([
271            "Name".to_string(),
272            "Description".to_string(),
273            "Examples".to_string(),
274        ])
275    }
276
277    fn table_row(&self) -> Box<[String]> {
278        Box::new([
279            format!("`{}`", self.name),
280            self.description.clone(),
281            if self.examples.is_empty() {
282                "None".to_string()
283            } else {
284                self.examples.first().map_or_else(String::new, |example| {
285                    if example.inputs.is_empty() {
286                        format!(
287                            "`{value} \\| {name}` ⇒ `{output}`",
288                            value = example.value,
289                            name = self.name,
290                            output = example.output
291                        )
292                    } else {
293                        format!(
294                            "`{value} \\| {name}({input})` ⇒ `{output}`",
295                            value = example.value,
296                            name = self.name,
297                            input = example
298                                .inputs
299                                .iter()
300                                .map(|(k, v)| format!("{k}={v}"))
301                                .join(", "),
302                            output = example.output
303                        )
304                    }
305                })
306            },
307        ])
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    #[test]
314    fn function_example_with_single_arg() {
315        let example = function_example!(mix(base=base) => "#804040");
316        assert_eq!(example.inputs["base"], "base");
317        assert_eq!(example.output, "#804040");
318    }
319
320    #[test]
321    fn function_example_with_multiple_args() {
322        let example = function_example!(mix(base=base, blend=red, amount=0.5) => "#804040");
323        assert_eq!(example.inputs["base"], "base");
324        assert_eq!(example.inputs["blend"], "red");
325        assert_eq!(example.inputs["amount"], "0.5");
326        assert_eq!(example.output, "#804040");
327    }
328
329    #[test]
330    fn filter_example_with_no_args() {
331        let example = filter_example!(red | add => "#ff6666");
332        assert_eq!(example.value, "red");
333        assert_eq!(example.inputs.len(), 0);
334        assert_eq!(example.output, "#ff6666");
335    }
336
337    #[test]
338    fn filter_example_with_single_arg() {
339        let example = filter_example!(red | add(hue=30) => "#ff6666");
340        assert_eq!(example.value, "red");
341        assert_eq!(example.inputs["hue"], "30");
342        assert_eq!(example.output, "#ff6666");
343    }
344
345    #[test]
346    fn filter_example_with_multiple_args() {
347        let example = filter_example!(red | add(hue=30, saturation=0.5) => "#ff6666");
348        assert_eq!(example.value, "red");
349        assert_eq!(example.inputs["hue"], "30");
350        assert_eq!(example.inputs["saturation"], "0.5");
351        assert_eq!(example.output, "#ff6666");
352    }
353}