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#[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 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
65pub 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 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 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 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 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#[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 pub fn render_response(&self, context: &Context) -> Result<OxiditeResponse> {
177 let html = self.render(context)?;
178 Ok(OxiditeResponse::html(html))
179 }
180}
181
182#[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}