libsubconverter/template/
template_renderer.rs1use crate::api::SubconverterQuery;
2use crate::utils::{file_exists, file_get_async};
3use crate::Settings;
4use log::{debug, error};
5use minijinja::{
6 context, escape_formatter, Environment, Error as JinjaError, ErrorKind, UndefinedBehavior,
7 Value,
8};
9use serde::Serialize;
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Default, Serialize)]
14pub struct TemplateArgs {
15 pub global_vars: HashMap<String, String>,
17
18 pub request_params: SubconverterQuery,
20
21 pub local_vars: HashMap<String, String>,
23
24 pub node_list: HashMap<String, String>,
26}
27
28pub fn render_template(
39 content: &str,
40 args: &TemplateArgs,
41 _include_scope: &str,
42) -> Result<String, Box<dyn std::error::Error>> {
43 let mut env = Environment::new();
52
53 env.set_formatter(escape_formatter);
55 env.set_undefined_behavior(UndefinedBehavior::Chainable);
56
57 env.add_filter("trim", filter_trim);
59 env.add_filter("trim_of", filter_trim_of);
60 env.add_filter("url_encode", filter_url_encode);
61 env.add_filter("url_decode", filter_url_decode);
62 env.add_filter("replace", filter_replace);
63 env.add_filter("find", filter_find);
64
65 env.add_function("getLink", fn_get_link);
66 env.add_function("startsWith", fn_starts_with);
67 env.add_function("endsWith", fn_ends_with);
68 env.add_function("bool", fn_to_bool);
69 env.add_function("string", fn_to_string);
70
71 env.add_function("default", fn_default);
72 let mut global_vars = HashMap::new();
76 for (key, value) in &args.global_vars {
77 global_vars.insert(key.clone(), value.clone());
78 }
79
80 let context = context!(
82 global => global_vars,
83 request => args.request_params,
84 local => args.local_vars,
85 node_list => args.node_list
86 );
87
88 debug!("Template context: {:?}", context);
89
90 match env.template_from_str(content) {
92 Ok(template) => match template.render(context) {
93 Ok(result) => Ok(result),
94 Err(e) => {
95 let error_msg = format!("Template render failed! Reason: {}", e);
96 error!("{}", error_msg);
97 Err(Box::new(e))
98 }
99 },
100 Err(e) => {
101 let error_msg = format!("Failed to parse template: {}", e);
102 error!("{}", error_msg);
103 Err(Box::new(e))
104 }
105 }
106}
107
108pub async fn render_template_file(
119 path: &str,
120 args: &TemplateArgs,
121 include_scope: &str,
122) -> Result<String, Box<dyn std::error::Error>> {
123 let content;
124 if file_exists(path).await {
125 content = file_get_async(
126 path,
127 if include_scope.is_empty() {
128 None
129 } else {
130 Some(include_scope)
131 },
132 )
133 .await?;
134 } else {
135 return Err(format!("Template file not found: {}", path).into());
136 }
137
138 render_template(&content, args, include_scope)
139}
140
141fn filter_trim(value: Value) -> Result<String, JinjaError> {
144 let s = value.to_string();
145 Ok(s.trim().to_string())
146}
147
148fn filter_trim_of(value: Value, chars: Value) -> Result<String, JinjaError> {
149 let s = value.to_string();
150 let chars_str = chars.to_string();
151
152 if chars_str.is_empty() {
153 return Ok(s);
154 }
155
156 let first_char = chars_str.chars().next().unwrap();
157 Ok(s.trim_matches(first_char).to_string())
158}
159
160fn filter_url_encode(value: Value) -> Result<String, JinjaError> {
161 let s = value.to_string();
162 Ok(urlencoding::encode(&s).to_string())
163}
164
165fn filter_url_decode(value: Value) -> Result<String, JinjaError> {
166 let s = value.to_string();
167 match urlencoding::decode(&s) {
168 Ok(decoded) => Ok(decoded.to_string()),
169 Err(e) => Err(JinjaError::new(
170 ErrorKind::InvalidOperation,
171 format!("URL decode error: {}", e),
172 )),
173 }
174}
175
176fn filter_replace(value: Value, pattern: Value, replacement: Value) -> Result<String, JinjaError> {
177 let s = value.to_string();
178 let pattern_str = pattern.to_string();
179 let replacement_str = replacement.to_string();
180
181 if pattern_str.is_empty() || s.is_empty() {
182 return Ok(s);
183 }
184
185 match regex::Regex::new(&pattern_str) {
187 Ok(re) => Ok(re.replace_all(&s, replacement_str.as_str()).to_string()),
188 Err(e) => Err(JinjaError::new(
189 ErrorKind::InvalidOperation,
190 format!("Invalid regex pattern: {}", e),
191 )),
192 }
193}
194
195fn filter_find(value: Value, pattern: Value) -> Result<bool, JinjaError> {
196 let s = value.to_string();
197 let pattern_str = pattern.to_string();
198
199 if pattern_str.is_empty() || s.is_empty() {
200 return Ok(false);
201 }
202
203 match regex::Regex::new(&pattern_str) {
205 Ok(re) => Ok(re.is_match(&s)),
206 Err(e) => Err(JinjaError::new(
207 ErrorKind::InvalidOperation,
208 format!("Invalid regex pattern: {}", e),
209 )),
210 }
211}
212
213fn fn_get_link(path: Value) -> Result<String, JinjaError> {
216 let path_str = path.to_string();
217 let settings = Settings::current();
218 Ok(format!("{}{}", settings.managed_config_prefix, path_str))
219}
220
221fn fn_starts_with(s: Value, prefix: Value) -> Result<bool, JinjaError> {
222 let s_str = s.to_string();
223 let prefix_str = prefix.to_string();
224 Ok(s_str.starts_with(&prefix_str))
225}
226
227fn fn_ends_with(s: Value, suffix: Value) -> Result<bool, JinjaError> {
228 let s_str = s.to_string();
229 let suffix_str = suffix.to_string();
230 Ok(s_str.ends_with(&suffix_str))
231}
232
233fn fn_to_bool(s: Value) -> Result<bool, JinjaError> {
234 let s_str = s.to_string().to_lowercase();
235 Ok(s_str == "true" || s_str == "1")
236}
237
238fn fn_to_string(n: Value) -> Result<String, JinjaError> {
239 Ok(n.to_string())
240}
241
242fn fn_default(value: Value, default: Value) -> Result<String, JinjaError> {
243 if value.is_undefined() || value.is_none() {
244 Ok(default.to_string())
245 } else {
246 Ok(value.to_string())
247 }
248}