neutralts/
template.rs

1use std::fs;
2use std::path::Path;
3use std::time::{Duration, Instant};
4use serde_json::{json, Value};
5use regex::Regex;
6use crate::{
7    constants::*,
8    default_json::*,
9    utils::*,
10    shared::Shared,
11    block_parser::BlockParser,
12    block_parser::BlockInherit
13};
14
15pub struct Template<'a> {
16    raw: String,
17    file_path: &'a str,
18    schema: Value,
19    shared: Shared,
20    time_start: Instant,
21    time_elapsed: Duration,
22    out: String,
23}
24
25/// A struct representing a template that can be rendered.
26///
27/// This struct is used to handle the rendering of templates.
28impl<'a> Template<'a> {
29    /// Constructs a new `Template` instance with default settings.
30    ///
31    /// It allows you to set up a template and schema with different types.
32    pub fn new() -> Result<Self, String> {
33        let default_schema: Value = match serde_json::from_str(DEFAULT) {
34            Ok(value) => value,
35            Err(_) => return Err("const DEFAULT is not a valid JSON string".to_string()),
36        };
37        let shared = Shared::new(default_schema.clone());
38
39        Ok(Template {
40            raw: String::new(),
41            file_path: "",
42            schema: default_schema,
43            shared,
44            time_start: Instant::now(),
45            time_elapsed: Instant::now().elapsed(),
46            out: String::new(),
47        })
48    }
49
50    /// Constructs a new `Template` instance from a file path and a JSON schema.
51    ///
52    /// # Arguments
53    ///
54    /// * `file_path` - A reference to the path of the file containing the template content.
55    /// * `schema` - A JSON value representing the custom schema to be used with the template.
56    ///
57    /// # Returns
58    ///
59    /// A `Result` containing the new `Template` instance or an error message if:
60    /// - The file cannot be read.
61    pub fn from_file_value(file_path: &'a str, schema: Value) -> Result<Self, String> {
62        let raw: String = match fs::read_to_string(file_path) {
63            Ok(s) => s,
64            Err(e) => {
65                eprintln!("Cannot be read: {}", file_path);
66                return Err(e.to_string());
67            }
68        };
69        let mut default_schema: Value = match serde_json::from_str(DEFAULT) {
70            Ok(value) => value,
71            Err(_) => {
72                eprintln!("Internal error in const DEFAULT {}, line: {}", file!(), line!());
73                return Err("const DEFAULT is not a valid JSON string".to_string());
74            }
75        };
76
77        update_schema(&mut default_schema, &schema);
78        let shared = Shared::new(default_schema.clone());
79
80        Ok(Template {
81            raw,
82            file_path,
83            schema: default_schema,
84            shared,
85            time_start: Instant::now(),
86            time_elapsed: Instant::now().elapsed(),
87            out: String::new(),
88        })
89    }
90
91    /// Sets the source path of the template.
92    ///
93    /// # Arguments
94    ///
95    /// * `file_path` - A reference to the path of the file containing the template content.
96    ///
97    /// # Returns
98    ///
99    /// A `Result` indicating success or an error message if the file cannot be read
100    pub fn set_src_path(&mut self, file_path: &'a str) -> Result<(), String> {
101        self.file_path = file_path;
102        self.raw = match fs::read_to_string(file_path) {
103            Ok(s) => s,
104            Err(e) => {
105                eprintln!("Cannot be read: {}", file_path);
106                return Err(e.to_string());
107            }
108        };
109
110        Ok(())
111    }
112
113    /// Sets the content of the template from a string.
114    ///
115    /// # Arguments
116    ///
117    /// * `source` - A reference to the new string content to be set as the raw content.
118    pub fn set_src_str(&mut self, source: &str) {
119        self.raw = source.to_string();
120    }
121
122    /// Merges the schema from a file with the current template schema.
123    ///
124    /// # Arguments
125    ///
126    /// * `schema_path` - A reference to the path of the file containing the schema content.
127    ///
128    /// # Returns
129    ///
130    /// A `Result` indicating success or an error message if:
131    /// - The file cannot be read.
132    /// - The file's content is not a valid JSON string.
133    pub fn merge_schema_path(&mut self, schema_path: &str) -> Result<(), String> {
134        let schema_str: String = match fs::read_to_string(schema_path) {
135            Ok(s) => s,
136            Err(e) => {
137                eprintln!("Cannot be read: {}", schema_path);
138                return Err(e.to_string());
139            }
140        };
141        let schema_value: Value = match serde_json::from_str(&schema_str) {
142            Ok(value) => value,
143            Err(_) => {
144                return Err("Is not a valid JSON file".to_string());
145            }
146        };
147        update_schema(&mut self.schema, &schema_value);
148
149        Ok(())
150    }
151
152    /// Merges the schema from a JSON string with the current template schema.
153    ///
154    /// # Arguments
155    ///
156    /// * `schema` - A reference to the JSON string of the schema content.
157    ///
158    /// # Returns
159    ///
160    /// A `Result` indicating success or an error message if:
161    /// - The file's content is not a valid JSON string.
162    pub fn merge_schema_str(&mut self, schema: &str) -> Result<(), String> {
163        let schema_value: Value = match serde_json::from_str(schema) {
164            Ok(value) => value,
165            Err(_) => {
166                return Err("Is not a valid JSON string".to_string());
167            }
168        };
169        update_schema(&mut self.schema, &schema_value);
170
171        Ok(())
172    }
173
174    /// Merges the provided JSON value with the current schema.
175    ///
176    /// # Arguments
177    ///
178    /// * `schema` - The JSON Value to be merged with the current schema.
179    pub fn merge_schema_value(&mut self, schema: Value) {
180        update_schema(&mut self.schema, &schema);
181    }
182
183    /// Renders the template content.
184    ///
185    /// This function initializes the rendering process.
186    /// The resulting output is returned as a string.
187    ///
188    /// # Returns
189    ///
190    /// The rendered template content as a string.
191    pub fn render(&mut self) -> String {
192        let inherit = self.init_render();
193        self.out = BlockParser::new(&mut self.shared, &inherit).parse(&self.raw, "");
194
195        while self.out.contains("{:!cache;") {
196            let out;
197            out = BlockParser::new(&mut self.shared, &inherit).parse(&self.out, "!cache");
198            self.out = out;
199        }
200
201        self.ends_render();
202
203        self.out.clone()
204    }
205
206    // Restore vars for render
207    fn init_render(&mut self) -> BlockInherit {
208        self.time_start = Instant::now();
209        self.shared = Shared::new(self.schema.clone());
210
211        if self.shared.comments.contains("remove") {
212            self.raw = remove_comments(&self.raw);
213        }
214
215        // init inherit
216        let mut inherit = BlockInherit::new();
217        let indir = inherit.create_block_schema(&mut self.shared);
218        self.shared.schema["__moveto"] = json!({});
219        self.shared.schema["__error"] = json!([]);
220        self.shared.schema["__indir"] = json!({});
221        self.shared.schema["__indir"][&indir] = self.shared.schema["inherit"].clone();
222        inherit.current_file = self.file_path.to_string();
223
224        // Escape CONTEXT values
225        filter_value(&mut self.shared.schema["data"]["CONTEXT"]);
226
227        // Escape CONTEXT keys names
228        filter_value_keys(&mut self.shared.schema["data"]["CONTEXT"]);
229
230        if !self.file_path.is_empty() {
231            let path = Path::new(&self.file_path);
232
233            if let Some(parent) = path.parent() {
234                inherit.current_dir = parent.display().to_string();
235            }
236        } else {
237            inherit.current_dir = self.shared.working_dir.clone();
238        }
239
240        if !self.shared.debug_file.is_empty() {
241            eprintln!("WARNING: config->debug_file is not empty: {} (Remember to remove this in production)", self.shared.debug_file);
242        }
243
244        inherit
245    }
246
247    // Rendering ends
248    fn ends_render(&mut self) {
249        self.set_moveto();
250        self.replacements();
251        self.set_status_code();
252        self.time_elapsed = self.time_start.elapsed();
253    }
254
255    fn set_status_code(&mut self) {
256        let status_code = self.shared.status_code.as_str();
257
258        if ("400"..="599").contains(&status_code) {
259            self.out = format!("{} {}", self.shared.status_code, self.shared.status_text);
260
261            return;
262        }
263
264        if status_code == "301"
265            || status_code == "302"
266            || status_code == "303"
267            || status_code == "307"
268            || status_code == "308"
269        {
270            self.out = format!(
271                "{} {}\n{}",
272                self.shared.status_code, self.shared.status_text, self.shared.status_param
273            );
274
275            return;
276        }
277
278        if !self.shared.redirect_js.is_empty() {
279            self.out = self.shared.redirect_js.clone();
280        }
281    }
282
283    fn set_moveto(&mut self) {
284        if let Value::Object(data_map) = &self.shared.schema["__moveto"] {
285            for (_key, value) in data_map {
286                if let Value::Object(inner_map) = value {
287                    for (inner_key, inner_value) in inner_map {
288                        let mut tag;
289
290                        // although it should be "<tag" or "</tag" it also supports
291                        // "tag", "/tag", "<tag>" and "</tag>
292                        if !inner_key.starts_with("<") {
293                            tag = format!("<{}", inner_key);
294                        } else {
295                            tag = inner_key.to_string();
296                        }
297                        if tag.ends_with(">") {
298                            tag = tag[..tag.len() - 1].to_string();
299                        }
300
301                        // if it does not find it, it does nothing
302                        let position = find_tag_position(&self.out, &tag);
303                        if let Some(pos) = position {
304                            let mut insert = inner_value.as_str().unwrap().to_string();
305                            insert = insert.to_string();
306                            self.out.insert_str(pos, &insert);
307                        }
308                    }
309                }
310            }
311        }
312    }
313
314    fn replacements(&mut self) {
315        let pattern = format!(r"\s*{}", BACKSPACE);
316        let re = Regex::new(&pattern).expect("Failed to create regex with constant pattern");
317        self.out = re.replace_all(&self.out, "").to_string();
318
319        // UNPRINTABLE should be substituted after BACKSPACE
320        self.out = self.out.replace(UNPRINTABLE, "");
321    }
322
323    /// Retrieves the status code.
324    ///
325    /// The status code is "200" unless "exit", "redirect" is used or the
326    /// template contains a syntax error, which will return a status code
327    /// of "500". Although the codes are numeric, a string is returned.
328    ///
329    /// # Returns
330    ///
331    /// A reference to the status code as a string.
332    pub fn get_status_code(&self) -> &String {
333        &self.shared.status_code
334    }
335
336    /// Retrieves the status text.
337    ///
338    /// It will correspond to the one set by the HTTP protocol.
339    ///
340    /// # Returns
341    ///
342    /// A reference to the status text as a string.
343    pub fn get_status_text(&self) -> &String {
344        &self.shared.status_text
345    }
346
347    /// Retrieves the status parameter.
348    ///
349    /// Some statuses such as 301 (redirect) may contain additional data, such
350    /// as the destination URL, and in similar cases “param” will contain
351    /// that value.
352    ///
353    /// # Returns
354    ///
355    /// A reference to the status parameter as a string.
356    pub fn get_status_param(&self) -> &String {
357        &self.shared.status_param
358    }
359
360    /// Checks if there is an error.
361    ///
362    /// If any error has occurred, in the parse or otherwise, it will return true.
363    ///
364    /// # Returns
365    ///
366    /// A boolean indicating whether there is an error.
367    pub fn has_error(&self) -> bool {
368        self.shared.has_error
369    }
370
371    /// Get bifs errors list
372    ///
373    /// # Returns
374    ///
375    /// * `Value`: A clone of the value with the list of errors in the bifs during rendering.
376    pub fn get_error(&self) -> Value {
377        self.shared.schema["__error"].clone()
378    }
379
380    /// Retrieves the time duration for template rendering.
381    ///
382    /// # Returns
383    ///
384    /// The time duration elapsed .
385    pub fn get_time_duration(&self) -> Duration {
386        let duration: std::time::Duration = self.time_elapsed;
387
388        duration
389    }
390}