1use crate::{PrayError, PrayResult};
2use std::collections::BTreeMap;
3
4const PLACEHOLDER_PREFIX: &str = "((pray:";
7const PLACEHOLDER_SUFFIX: &str = "))";
8
9pub fn is_pray_symbol_key(key: &str) -> bool {
10 if key.is_empty() {
11 return false;
12 }
13 key.chars().all(|character| {
14 character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '/' | '-')
15 })
16}
17
18pub fn substitute_pray_symbols(
19 text: &str,
20 symbols: &BTreeMap<String, String>,
21) -> PrayResult<String> {
22 let mut output = String::with_capacity(text.len());
23 let mut rest = text;
24
25 while let Some(start) = rest.find(PLACEHOLDER_PREFIX) {
26 output.push_str(&rest[..start]);
27 let after_prefix = &rest[start + PLACEHOLDER_PREFIX.len()..];
28 let Some(end) = after_prefix.find(PLACEHOLDER_SUFFIX) else {
29 return Err(PrayError::Render(
30 "unclosed ((pray:...) placeholder".to_string(),
31 ));
32 };
33 let path = &after_prefix[..end];
34 if !is_pray_symbol_key(path) {
35 return Err(PrayError::Render(format!(
36 "invalid ((pray:...)) path `{path}`"
37 )));
38 }
39 let Some(value) = symbols.get(path) else {
40 return Err(PrayError::Render(format!(
41 "unknown pray symbol `{path}`; declare it in `pray do ... end`"
42 )));
43 };
44 output.push_str(value);
45 rest = &after_prefix[end + PLACEHOLDER_SUFFIX.len()..];
46 }
47
48 output.push_str(rest);
49 Ok(output)
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn replaces_known_symbols() {
58 let mut symbols = BTreeMap::new();
59 symbols.insert("support_email".to_string(), "a@example.com".to_string());
60 symbols.insert("security_email".to_string(), "b@example.com".to_string());
61 let text = "write ((pray:support_email)) or ((pray:security_email))";
62 assert_eq!(
63 substitute_pray_symbols(text, &symbols).expect("ok"),
64 "write a@example.com or b@example.com"
65 );
66 }
67
68 #[test]
69 fn rejects_unknown_symbol() {
70 let symbols = BTreeMap::new();
71 let error = substitute_pray_symbols("((pray:missing))", &symbols).unwrap_err();
72 assert!(error.to_string().contains("unknown pray symbol"));
73 }
74
75 #[test]
76 fn ignores_spaced_forms() {
77 let mut symbols = BTreeMap::new();
78 symbols.insert("email".to_string(), "a@example.com".to_string());
79 let text = "(( pray:email )) ((pray : email))";
80 assert_eq!(substitute_pray_symbols(text, &symbols).expect("ok"), text);
81 }
82}