Skip to main content

oxidite_template/
lib.rs

1use serde_json::Value;
2use std::collections::HashMap;
3use std::path::Path;
4use std::fs;
5
6pub mod parser;
7pub mod renderer;
8pub mod filters;
9pub mod static_files;
10
11pub use parser::{Parser, TemplateNode};
12pub use renderer::Renderer;
13pub use filters::Filters;
14pub use static_files::{StaticFiles, serve_static};
15use oxidite_core::types::OxiditeResponse;
16
17/// Template context for variable interpolation
18#[derive(Debug, Clone)]
19pub struct Context {
20    data: HashMap<String, Value>,
21}
22
23impl Context {
24    pub fn new() -> Self {
25        Self {
26            data: HashMap::new(),
27        }
28    }
29
30    pub fn set<T: serde::Serialize>(&mut self, key: impl Into<String>, value: T) {
31        if let Ok(json_value) = serde_json::to_value(value) {
32            self.data.insert(key.into(), json_value);
33        }
34    }
35
36    pub fn get(&self, key: &str) -> Option<&Value> {
37        // Support dotted notation: user.name
38        let parts: Vec<&str> = key.split('.').collect();
39        let mut current = self.data.get(parts[0])?;
40
41        for part in &parts[1..] {
42            current = current.get(part)?;
43        }
44
45        Some(current)
46    }
47
48    pub fn from_json(json: Value) -> Self {
49        let mut context = Self::new();
50        if let Value::Object(map) = json {
51            for (key, value) in map {
52                context.data.insert(key, value);
53            }
54        }
55        context
56    }
57}
58
59impl Default for Context {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65/// Template engine to manage multiple templates
66pub struct TemplateEngine {
67    templates: HashMap<String, Template>,
68}
69
70impl TemplateEngine {
71    pub fn new() -> Self {
72        Self {
73            templates: HashMap::new(),
74        }
75    }
76
77    pub fn add_template(&mut self, name: impl Into<String>, source: impl Into<String>) -> Result<()> {
78        let template = Template::new(source)?;
79        self.templates.insert(name.into(), template);
80        Ok(())
81    }
82
83    pub fn get_template(&self, name: &str) -> Option<&Template> {
84        self.templates.get(name)
85    }
86
87    pub fn render(&self, name: &str, context: &Context) -> Result<String> {
88        let template = self.get_template(name)
89            .ok_or_else(|| TemplateError::RenderError(format!("Template not found: {}", name)))?;
90        
91        let mut renderer = Renderer::new(context, Some(self));
92        renderer.render(template)
93    }
94    
95    /// Render a template as an HTML response
96    pub fn render_response(&self, name: &str, context: &Context) -> Result<OxiditeResponse> {
97        let html = self.render(name, context)?;
98        Ok(OxiditeResponse::html(html))
99    }
100    
101    /// Load all templates from a directory (recursive)
102    pub fn load_dir(&mut self, dir: impl AsRef<Path>) -> Result<usize> {
103        let dir = dir.as_ref();
104        let mut count = 0;
105        
106        if !dir.is_dir() {
107            return Err(TemplateError::RenderError(format!("Not a directory: {:?}", dir)));
108        }
109        
110        self.load_dir_recursive(dir, dir, &mut count)?;
111        Ok(count)
112    }
113    
114    fn load_dir_recursive(&mut self, base_dir: &Path, current_dir: &Path, count: &mut usize) -> Result<()> {
115        for entry in fs::read_dir(current_dir)
116            .map_err(|e| TemplateError::RenderError(format!("Failed to read directory: {}", e)))? 
117        {
118            let entry = entry.map_err(|e| TemplateError::RenderError(e.to_string()))?;
119            let path = entry.path();
120            
121            if path.is_dir() {
122                // Recursively load templates from subdirectories
123                self.load_dir_recursive(base_dir, &path, count)?;
124            } else if path.is_file() {
125                if let Some(ext) = path.extension() {
126                    if ext == "html" || ext == "htm" {
127                        let content = fs::read_to_string(&path)
128                            .map_err(|e| TemplateError::RenderError(format!("Failed to read file: {}", e)))?;
129                        
130                        // Get relative path from base_dir to preserve directory structure
131                        let relative_path = path.strip_prefix(base_dir)
132                            .map_err(|e| TemplateError::RenderError(e.to_string()))?;
133                        
134                        let name = relative_path.to_str()
135                            .ok_or_else(|| TemplateError::RenderError("Invalid filename".to_string()))?;
136                        
137                        self.add_template(name, content)?;
138                        *count += 1;
139                    }
140                }
141            }
142        }
143        
144        Ok(())
145    }
146}
147
148impl Default for TemplateEngine {
149    fn default() -> Self {
150        Self::new()
151    }
152}
153
154/// Template
155#[derive(Debug, Clone)]
156pub struct Template {
157    source: String,
158    parsed: Vec<TemplateNode>,
159}
160
161impl Template {
162    pub fn new(source: impl Into<String>) -> Result<Self> {
163        let source = source.into();
164        let parser = Parser::new(&source);
165        let parsed = parser.parse()?;
166
167        Ok(Self { source, parsed })
168    }
169
170    pub fn render(&self, context: &Context) -> Result<String> {
171        let mut renderer = Renderer::new(context, None);
172        renderer.render(self)
173    }
174    
175    /// Render the template as an HTML response
176    pub fn render_response(&self, context: &Context) -> Result<OxiditeResponse> {
177        let html = self.render(context)?;
178        Ok(OxiditeResponse::html(html))
179    }
180}
181
182/// Template errors
183#[derive(Debug, thiserror::Error)]
184pub enum TemplateError {
185    #[error("Parse error: {0}")]
186    ParseError(String),
187
188    #[error("Render error: {0}")]
189    RenderError(String),
190
191    #[error("Variable not found: {0}")]
192    VariableNotFound(String),
193
194    #[error("Filter not found: {0}")]
195    FilterNotFound(String),
196}
197
198pub type Result<T> = std::result::Result<T, TemplateError>;
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_simple_variable() {
206        let tmpl = Template::new("Hello {{ name }}!").unwrap();
207        let mut ctx = Context::new();
208        ctx.set("name", "World");
209        
210        let result = tmpl.render(&ctx).unwrap();
211        assert_eq!(result, "Hello World!");
212    }
213
214    #[test]
215    fn test_dotted_notation() {
216        let tmpl = Template::new("Hello {{ user.name }}!").unwrap();
217        let mut ctx = Context::new();
218        ctx.set("user", serde_json::json!({ "name": "Alice" }));
219        
220        let result = tmpl.render(&ctx).unwrap();
221        assert_eq!(result, "Hello Alice!");
222    }
223}