lib_humus/language/
text_function.rs1use std::sync::Arc;
6
7use tera::Function;
8use tera::TeraResult;
9use tera::Value;
10
11use crate::language::{LanguageEngine, UnicodeLanguageIdentifier, variable_description::Variable};
12
13pub type TextFunctionContextGetter =
16 fn(args: &tera::Kwargs, state: &tera::State<'_>) -> TeraResult<TextFunctionContext>;
17
18#[derive(Debug, Clone)]
20pub struct TextFunctionContext {
21 pub language: Option<UnicodeLanguageIdentifier>,
25
26 pub escape_function: tera::EscapeFn,
28
29 pub safe_suffix: Option<String>,
31}
32
33impl TextFunctionContext {
34 pub fn matches_safe_suffix(&self, text: &str) -> bool {
36 if let Some(safe_suffix) = &self.safe_suffix {
37 text.ends_with(safe_suffix)
38 } else {
39 false
40 }
41 }
42}
43
44pub struct TextFunction {
59 pub(crate) language_engine: Arc<LanguageEngine>,
60 pub(crate) context_getter: TextFunctionContextGetter,
61}
62
63impl TextFunction {
64 pub fn new(
68 language_engine: Arc<LanguageEngine>,
69 context_getter: TextFunctionContextGetter,
70 ) -> Self {
71 Self {
72 language_engine,
73 context_getter,
74 }
75 }
76}
77
78impl Function<tera::TeraResult<Value>> for TextFunction {
79 fn call(&self, args: tera::Kwargs, state: &tera::State<'_>) -> TeraResult<Value> {
80 let message_id: &str = args.must_get("id")?;
82
83 let context = (self.context_getter)(&args, state)?;
84
85 let is_safe_suffix = context.matches_safe_suffix(&message_id.replace("-", "_"));
86
87 let lang: UnicodeLanguageIdentifier = if let Some(lang) = args.get::<String>("lang")? {
89 lang.parse()
90 .map_err(|e| tera::Error::message(format!("Unable to parse lang argument: {e}")))?
91 } else if let Some(lang) = &context.language {
92 lang.clone()
93 } else {
94 return Err(tera::Error::message(
95 "Could neither source the language from outside the templating engine, nor was the lang argument set. If you are using the default HumusEngine this is most likely a bug. Otherwise check your `language_getter` implementation.",
96 ));
97 };
98
99 let mut variables: Vec<(String, Variable)> = vec![];
101 for (key, value) in args.iter() {
102 let Some(key) = key.as_str() else {
103 return Err(tera::Error::message(format!(
104 "Key for text() must be a string, got {key:?} instead."
105 )));
106 };
107 if matches!(key, "id" | "lang") {
108 continue;
109 }
110 if let Some(value) = value.as_bool() {
111 variables.push((key.to_owned(), Variable::Bool(value)))
112 } else if let Some(value) = value.as_number() {
113 variables.push((key.to_owned(), Variable::Number(value.as_float())))
114 } else if let Some(value) = value.as_str() {
115 if is_safe_suffix && !context.matches_safe_suffix(key) {
116 let mut escaped_value = Vec::<u8>::default();
117 (context.escape_function)(value, &mut escaped_value).map_err(|e| {
118 tera::Error::chain(
119 format!("While escaping argument {key:?} for text(id={message_id:?})."),
120 e,
121 )
122 })?;
123 variables.push((
124 key.to_owned(),
125 Variable::Text(String::from_utf8_lossy(&escaped_value).to_string()),
126 ));
127 } else {
128 variables.push((key.to_owned(), Variable::Text(value.to_owned())));
129 }
130 } else if value.is_undefined() || value.is_none() {
131 continue; } else {
133 return Err(tera::Error::message(format!(
134 "Value arguments for text() must be a bool, number, string or null got {key:?}={value:?} instead."
135 )));
136 }
137 }
138
139 match self
141 .language_engine
142 .get_text_with_args(Some(lang), message_id, variables)
143 {
144 Ok(text) => {
145 let text = text.replace("\"\u{2068}", "\"").replace("\u{2069}\"", "\"");
147 if is_safe_suffix {
148 Ok(Value::from(text).mark_safe())
149 } else {
150 Ok(Value::from(text))
151 }
152 }
153 Err(e) => Err(tera::Error::message(format!(
154 "Problem rendering message in template: {e}"
155 ))),
156 }
157 }
158
159 fn is_safe(&self) -> bool {
160 false
161 }
162}