libsubconverter/template/
template_renderer.rs

1use 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/// Template arguments container
13#[derive(Debug, Clone, Default, Serialize)]
14pub struct TemplateArgs {
15    /// Global variables
16    pub global_vars: HashMap<String, String>,
17
18    /// Request parameters
19    pub request_params: SubconverterQuery,
20
21    /// Local variables
22    pub local_vars: HashMap<String, String>,
23
24    /// Node list variables
25    pub node_list: HashMap<String, String>,
26}
27
28/// Render a template with the given arguments
29///
30/// # Arguments
31/// * `content` - The template content
32/// * `args` - Template arguments
33/// * `include_scope` - The directory scope for included templates
34///
35/// # Returns
36/// * `Ok(String)` - The rendered template
37/// * `Err(String)` - Error message if rendering fails
38pub fn render_template(
39    content: &str,
40    args: &TemplateArgs,
41    _include_scope: &str,
42) -> Result<String, Box<dyn std::error::Error>> {
43    // let env_lock = match TEMPLATE_ENV.lock() {
44    //     Ok(env) => env,
45    //     Err(e) => {
46    //         return Err(format!("Failed to acquire template environment lock: {}", e).into());
47    //     }
48    // };
49
50    // Create a new environment for this template
51    let mut env = Environment::new();
52
53    // Copy settings from global environment
54    env.set_formatter(escape_formatter);
55    env.set_undefined_behavior(UndefinedBehavior::Chainable);
56
57    // Add the same filters and functions
58    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    // env.add_function("fetch", fn_web_get);
73
74    // Build context object
75    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    // Create full context with all variables
81    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    // Parse and render the template
91    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
108/// Render a template from a file with the given arguments
109///
110/// # Arguments
111/// * `path` - Path to the template file
112/// * `args` - Template arguments
113/// * `include_scope` - The directory scope for included templates
114///
115/// # Returns
116/// * `Ok(String)` - The rendered template
117/// * `Err(String)` - Error message if rendering fails
118pub 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
141// Filter implementations
142
143fn 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    // Use regex for replacement
186    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    // Use regex for finding
204    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
213// Function implementations
214
215fn 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}