1use std::path::Path;
2
3use indexmap::IndexMap;
4use itertools::Itertools as _;
5
6use crate::{filters, functions, markdown};
7
8macro_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
24macro_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}