1#![allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
26
27use caith::RollResultType;
28use minijinja::Environment;
29use rand::SeedableRng;
30use std::{
31 cmp::max,
32 collections::{HashMap, HashSet},
33};
34
35use crate::instance::SandboxInstance;
36
37pub fn prepare_renderer(env: &mut Environment, instance: &SandboxInstance) {
38 env.add_filter("bulletize", func_bulletize);
39 env.add_filter("count_identical", func_count_identical);
40
41 env.add_function("appender", func_appender);
42 env.add_function("articlize", func_articlize);
43 env.add_function("capitalize", func_capitalize);
44 env.add_function("currency", func_currency(instance));
45 env.add_function("first", func_first);
46 env.add_function("float", func_float);
47 env.add_function("hex_coords", func_hex_coords);
48 env.add_function("if_plural_else", func_if_plural_else);
49 env.add_function("int", func_int);
50 env.add_function("length", func_length);
51 env.add_function("list_to_obj", func_list_to_obj);
52 env.add_function("max", func_max);
53 env.add_function("maybe", func_maybe);
54 env.add_function("plural", func_plural);
55 env.add_function("plural_with_count", func_plural_with_count);
56 env.add_function("round", func_round);
57 env.add_function("sandbox", func_sandbox(instance));
58 env.add_function("sortby", func_sortby);
59 env.add_function("stable_dice", func_stable_dice);
60 env.add_function("sum", func_sum);
61 env.add_function("title", func_capitalize);
62 env.add_function("trim", func_trim);
63 env.add_function("unique", func_unique);
64 env.add_function("html_link", func_html_link(instance));
65 env.add_function("reroller", func_reroll);
66
67 env.add_function("begin_spoiler", func_nop_0);
69 env.add_function("end_spoiler", func_nop_0);
70 env.add_function("toc_breadcrumb", func_nop_0);
71 env.add_function("sandbox_breadcrumb", func_nop_0);
72 env.add_function("note_button", func_nop_1);
73 env.add_function("note_container", func_nop_1);
74}
75
76fn func_bulletize(value: Vec<String>, seperator: &str) -> String {
77 value.join(&format!(" &#{}; ", seperator)).to_string()
78}
79
80fn func_articlize(
81 value: minijinja::value::ViaDeserialize<serde_json::Value>,
82) -> Result<String, minijinja::Error> {
83 if let Some(noun) = value.as_str() {
84 fn is_plural(noun: &str) -> bool {
85 noun.ends_with('s') && noun != "bus" && noun != "grass" && noun != "kiss"
86 }
87 fn starts_with_vowel_sound(word: &str) -> bool {
88 let vowels = ["a", "e", "i", "o", "u"];
89 if let Some(first_char) = word.chars().next() {
90 vowels.contains(&first_char.to_lowercase().to_string().as_str())
91 } else {
92 false
93 }
94 }
95 let article = if is_plural(noun) {
96 return Ok(String::from(noun));
97 } else if starts_with_vowel_sound(noun) {
98 "an"
99 } else {
100 "a"
101 };
102 Ok(format!("{} {}", article, noun))
103 } else {
104 Err(minijinja::Error::new(
105 minijinja::ErrorKind::UndefinedError,
106 "Function articlize received a non-string value",
107 ))
108 }
109}
110
111fn func_capitalize(
112 possible_str: minijinja::value::ViaDeserialize<serde_json::Value>,
113) -> Result<minijinja::value::Value, minijinja::Error> {
114 if let Some(v) = possible_str.as_str() {
115 let mut v = v.to_string();
116 if !v.is_empty() {
117 v[0..1].make_ascii_uppercase(); }
119 Ok(minijinja::Value::from(v))
120 } else {
121 let t: serde_json::Value = possible_str.clone();
122 Ok(minijinja::value::Value::from_serialize(&t))
123 }
124}
125
126fn func_currency(
127 _instance: &SandboxInstance,
128) -> impl Fn(minijinja::value::ViaDeserialize<serde_json::Value>) -> Result<String, minijinja::Error>
129{
130 let currency_factor = 1.0;
131 move |v: minijinja::value::ViaDeserialize<serde_json::Value>| -> Result<String, minijinja::Error> {
132 if let Some(v) = v.as_f64() {
133 let v = v * currency_factor;
134 if v > 1.0 {
135 return Ok(format!("{} gp", format_with_commas(v as i64)));
136 }
137 if v > 0.1 {
138 return Ok(format!("{:.0} sp", (v * 10.0).round() as i64));
139 }
140 if v > 0.01 {
141 return Ok(format!("{:.0} cp", (v * 100.0).round() as i64));
142 }
143 return Ok(format!("{:.0} gp", v as i64));
144 }
145 Err(minijinja::Error::new(
146 minijinja::ErrorKind::UndefinedError,
147 "Currency value is not floating point",
148 ))
149 }
150}
151
152fn func_first(
153 possible_str: minijinja::value::ViaDeserialize<serde_json::Value>,
154) -> Result<minijinja::value::Value, minijinja::Error> {
155 if let Some(v) = possible_str.as_array() {
156 if let Some(first) = v.iter().next() {
157 return Ok(minijinja::Value::from_serialize(first.clone()));
158 }
159 } else if let Some(v) = possible_str.as_str() {
160 if let Some(first) = v.chars().next() {
161 return Ok(minijinja::Value::from_serialize(first));
162 }
163 }
164 Err(minijinja::Error::new(
165 minijinja::ErrorKind::UndefinedError,
166 "func_first could not pick the first item from an array",
167 ))
168}
169
170fn func_float(value: &str) -> Result<f32, minijinja::Error> {
171 if let Ok(value) = value.trim().parse::<f32>() {
172 Ok(value)
173 } else {
174 Err(minijinja::Error::new(
175 minijinja::ErrorKind::UndefinedError,
176 "Unable to convert value in func_float",
177 ))
178 }
179}
180
181fn func_int(value: &str) -> Result<i64, minijinja::Error> {
182 if let Ok(value) = value.trim().parse::<i64>() {
183 Ok(value)
184 } else {
185 Err(minijinja::Error::new(
186 minijinja::ErrorKind::UndefinedError,
187 "Unable to convert value in func_int",
188 ))
189 }
190}
191
192fn func_length(
193 c: minijinja::value::ViaDeserialize<serde_json::Value>,
194) -> Result<usize, minijinja::Error> {
195 if let Some(r) = c.as_array() {
196 Ok(r.len())
197 } else {
198 Ok(0)
199 }
200}
201
202fn func_list_to_obj(
203 list: minijinja::value::ViaDeserialize<serde_json::Value>,
204 attr_name: &str,
205) -> Result<minijinja::value::Value, minijinja::Error> {
206 let mut map = serde_json::json!({});
207 if let Some(list) = list.as_array() {
208 for item in list.iter() {
209 if let Some(key) = item.as_object().unwrap().get(attr_name) {
210 if let Some(key_str) = key.as_str() {
211 let m = &mut map.as_object_mut().unwrap();
212 if !m.contains_key(key_str) {
213 m.insert(key_str.to_string(), serde_json::json!([]));
214 }
215 m[key_str].as_array_mut().unwrap().push(item.clone());
216 }
217 }
218 }
219 }
220 Ok(minijinja::value::Value::from_serialize(map))
221}
222
223fn func_count_identical(list: Vec<String>) -> HashMap<String, i32> {
224 let mut counts = HashMap::new();
225 for item in list {
226 *counts.entry(item).or_insert(0) += 1;
227 }
228 counts
229}
230
231fn func_trim(
232 _c: minijinja::value::ViaDeserialize<serde_json::Value>,
233) -> Result<String, minijinja::Error> {
234 if let Some(value) = _c.as_str() {
235 return Ok(clean_string(value.to_string()));
236 }
237 Err(minijinja::Error::new(
238 minijinja::ErrorKind::UndefinedError,
239 "Function trim did not get a string",
240 ))
241}
242
243fn func_sortby(
244 list: minijinja::value::ViaDeserialize<serde_json::Value>,
245 attr_to_sortby: &str,
246) -> Result<minijinja::value::Value, minijinja::Error> {
247 let mut ret = serde_json::json!([]);
248 if let Some(list) = list.as_array() {
249 let mut list_to_sort = list.clone();
250 list_to_sort.sort_by(|a, b| {
251 let a_value = a.get(attr_to_sortby).and_then(|v| v.as_str()).unwrap_or("");
252 let b_value = b.get(attr_to_sortby).and_then(|v| v.as_str()).unwrap_or("");
253 a_value.cmp(b_value)
254 });
255 ret = serde_json::Value::Array(list_to_sort.to_vec());
256 }
257 Ok(minijinja::value::Value::from_serialize(ret))
258}
259
260fn func_hex_coords(
261 _: minijinja::value::ViaDeserialize<serde_json::Value>,
262) -> Result<String, minijinja::Error> {
263 Ok(String::from("TBD"))
264}
265
266fn func_maybe(
267 v: minijinja::value::ViaDeserialize<serde_json::Value>,
268) -> Result<String, minijinja::Error> {
269 if let Some(s) = v.as_str() {
270 return Ok(s.to_string());
271 }
272 Ok(String::new())
273}
274
275fn func_sandbox(instance: &SandboxInstance) -> impl Fn() -> Result<String, minijinja::Error> {
276 let sid = match instance.sid.as_ref() {
277 Some(sid) => sid.clone(),
278 None => "".to_string(),
279 };
280 move || -> Result<String, minijinja::Error> { Ok(format!("/inspect/{}", sid)) }
281}
282
283fn func_round(value: f32, _dec: f32) -> Result<f32, minijinja::Error> {
284 let y = (value * 100.0).round() / 100.0;
285 if false {
286 return Err(minijinja::Error::new(
287 minijinja::ErrorKind::UndefinedError,
288 "",
289 ));
290 }
291 Ok(y)
292}
293
294fn func_max(a: i32, b: i32) -> Result<i32, minijinja::Error> {
295 if false {
296 return Err(minijinja::Error::new(
297 minijinja::ErrorKind::UndefinedError,
298 "",
299 ));
300 }
301 Ok(max(a, b))
302}
303
304fn func_appender(parent_uid: &str, attr: &str, cls: &str) -> String {
305 format!(
306 r#"
307 <a href="/append/{parent_uid}/{attr}/{cls}">⊞</a>
308 "#
309 )
310}
311
312fn func_plural(count: f32, v: &str) -> Result<String, minijinja::Error> {
313 if count <= 1.0 {
314 return Ok(v.to_string());
315 }
316 let mut plural = v.to_string();
317 let c = v.chars().last().unwrap_or('\0');
318 let c_minus_1 = v.chars().rev().nth(1).unwrap_or('\0');
319
320 if "sxzh".contains(c) {
321 plural.push_str("es");
322 } else if c == 'y' {
323 if "aeiou".contains(c_minus_1) {
324 plural.push('s');
325 } else {
326 plural.pop();
327 plural.push_str("ies");
328 }
329 } else if v.ends_with("olf") {
330 plural.pop();
331 plural.push_str("ves");
332 } else {
333 plural.push('s');
334 }
335 Ok(plural)
336}
337
338fn func_plural_with_count(count: f32, v: &str) -> Result<String, minijinja::Error> {
339 if count <= 1.0 {
340 return Ok(v.to_string());
341 }
342 Ok(format!(
343 "{} {}",
344 count as i32,
345 func_plural(count, v).unwrap()
346 ))
347}
348
349fn func_if_plural_else(
350 check: &str,
351 ifplural: &str,
352 ifnotplural: &str,
353) -> Result<String, minijinja::Error> {
354 let check = check.to_lowercase();
355 if check.ends_with('s') || check == "teeth" || check == "wolves" {
356 Ok(ifplural.to_string())
357 } else {
358 Ok(ifnotplural.to_string())
359 }
360}
361
362fn func_sum(l: minijinja::value::ViaDeserialize<serde_json::Value>) -> f64 {
363 let mut sum = 0.0;
364 for v in l.as_array().unwrap() {
365 if let Ok(a) = v.as_str().unwrap().parse::<f64>() {
366 sum += a;
367 }
368 }
369 sum
370}
371
372fn func_unique(
373 v: minijinja::value::ViaDeserialize<serde_json::Value>,
374 attr: minijinja::value::ViaDeserialize<serde_json::Value>,
375) -> Result<minijinja::value::Value, minijinja::Error> {
376 let mut ret = serde_json::json!([]);
377 let mut unique_set = HashSet::new();
378
379 if let Some(v) = v.as_array() {
380 for e in v.iter() {
381 if let Some(value) = e.as_object().unwrap().get(attr.as_str().unwrap()) {
382 if !unique_set.contains(value) {
383 ret.as_array_mut().unwrap().push(e.clone());
384 unique_set.insert(value.clone());
385 }
386 }
387 }
388 return Ok(minijinja::value::Value::from_serialize(&ret));
389 }
390 Ok(minijinja::value::Value::from_serialize(serde_json::json!(
391 {}
392 )))
393}
394
395fn func_stable_dice(roll: &str, uid: &str, index: u64) -> Result<i32, minijinja::Error> {
396 if false {
397 return Err(minijinja::Error::new(
398 minijinja::ErrorKind::UndefinedError,
399 "",
400 ));
401 }
402 let roller = caith::Roller::new(roll).unwrap();
403 let seed = string_to_seed(uid) + index;
404 let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(seed);
405 if let RollResultType::Single(value) = roller.roll_with(&mut rng).unwrap().get_result() {
406 return Ok(value.get_total() as i32);
407 }
408 Ok(0)
409}
410
411fn func_html_link(
412 instance: &SandboxInstance,
413) -> impl Fn(&str, &str) -> Result<String, minijinja::Error> {
414 let sid = match instance.sid.as_ref() {
415 Some(sid) => sid.clone(),
416 None => "".to_string(),
417 };
418 move |uid, text| -> Result<String, minijinja::Error> {
419 Ok(format!(
420 "<a href='/inspect/{}/entity/{}'>{}</a>",
421 sid, uid, text
422 ))
423 }
424}
425
426fn func_nop_0() -> Result<String, minijinja::Error> {
427 Ok(String::new())
428}
429
430fn func_nop_1(
431 _: minijinja::value::ViaDeserialize<serde_json::Value>,
432) -> Result<String, minijinja::Error> {
433 Ok(String::new())
434}
435
436fn func_reroll(
437 uid: minijinja::value::ViaDeserialize<serde_json::Value>,
438 _: minijinja::value::ViaDeserialize<serde_json::Value>,
439 _: minijinja::value::ViaDeserialize<serde_json::Value>,
440) -> Result<String, minijinja::Error> {
441 let id = if let Some(obj) = uid.get("uuid") {
443 obj.to_string().trim_matches('"').to_string()
444 } else {
445 uid.to_string().trim_matches('"').to_string()
446 };
447 Ok(format!(
448 "<a href='/reroll/{}'>⟳</a><a href='/unroll/{}'>🗑</a>",
449 id, id
450 )
451 .to_string())
452}
453
454fn clean_string(mut s: String) -> String {
455 s = s.trim().to_string();
456 s.retain(|c| c != '\n' && c != '\r');
457 s = s
458 .chars()
459 .fold((String::new(), None), |(mut acc, prev_char), c| {
460 if c == ' ' && prev_char == Some(' ') {
461 (acc, prev_char)
462 } else {
463 acc.push(c);
464 (acc, Some(c))
465 }
466 })
467 .0;
468 s
469}
470
471fn format_with_commas(v: i64) -> String {
472 let s = v.to_string();
473 let mut formatted = String::new();
474 let mut count = 0;
475
476 for c in s.chars().rev() {
477 if count == 3 {
478 formatted.push(',');
479 count = 0;
480 }
481 formatted.push(c);
482 count += 1;
483 }
484 formatted.chars().rev().collect()
485}
486
487fn string_to_seed<S: AsRef<str>>(seed_str: S) -> u64 {
488 let mut hasher = std::hash::DefaultHasher::new();
489 std::hash::Hash::hash(&seed_str.as_ref(), &mut hasher);
490 std::hash::Hasher::finish(&hasher)
491}
492
493#[cfg(test)]
494mod tests {}