Skip to main content

oxidite_template/
renderer.rs

1use crate::{Context, TemplateNode, TemplateError, Result, filters::Filters, TemplateEngine, Template};
2use serde_json::Value;
3use std::collections::HashMap;
4
5/// Template renderer
6pub struct Renderer<'a> {
7    context: &'a Context,
8    filters: Filters,
9    engine: Option<&'a TemplateEngine>,
10    blocks: HashMap<String, Vec<TemplateNode>>,
11}
12
13impl<'a> Renderer<'a> {
14    pub fn new(context: &'a Context, engine: Option<&'a TemplateEngine>) -> Self {
15        Self {
16            context,
17            filters: Filters::new(),
18            engine,
19            blocks: HashMap::new(),
20        }
21    }
22
23    pub fn render(&mut self, template: &Template) -> Result<String> {
24        // Check for Extends (ignoring leading whitespace)
25        let extends_node = template.parsed.iter().find(|node| {
26            match node {
27                TemplateNode::Text(t) => !t.trim().is_empty(),
28                _ => true,
29            }
30        });
31
32        if let Some(TemplateNode::Extends(parent_name)) = extends_node {
33            // Collect blocks from current template (child)
34            // We only collect top-level blocks in the child template
35            for node in &template.parsed {
36                if let TemplateNode::Block { name, body } = node {
37                    // Only insert if not already present (child overrides parent, but we are going up)
38                    // Wait, we start at child. Child blocks should override everything.
39                    // So we insert. But if we are in a chain C -> B -> A.
40                    // We render C. C extends B. We collect C blocks. Recurse to B.
41                    // B extends A. We collect B blocks. If B defines "content" and C defined "content", C wins.
42                    // So we use entry().or_insert().
43                    self.blocks.entry(name.clone()).or_insert(body.clone());
44                }
45            }
46
47            if let Some(engine) = self.engine {
48                let parent = engine.get_template(parent_name)
49                    .ok_or_else(|| TemplateError::RenderError(format!("Parent template not found: {}", parent_name)))?;
50                return self.render(parent);
51            } else {
52                return Err(TemplateError::RenderError("Extends used without TemplateEngine".to_string()));
53            }
54        }
55
56        self.render_nodes(&template.parsed)
57    }
58
59    fn render_nodes(&mut self, nodes: &[TemplateNode]) -> Result<String> {
60        let mut output = String::new();
61
62        for node in nodes {
63            match node {
64                TemplateNode::Text(text) => {
65                    output.push_str(text);
66                }
67                TemplateNode::Variable { name, filters } => {
68                    let value = self.render_variable(name, filters)?;
69                    output.push_str(&value);
70                }
71                TemplateNode::If { condition, then_branch, else_branch } => {
72                    let value = self.render_if(condition, then_branch, else_branch)?;
73                    output.push_str(&value);
74                }
75                TemplateNode::For { item, iterable, body } => {
76                    let value = self.render_for(item, iterable, body)?;
77                    output.push_str(&value);
78                }
79                TemplateNode::Block { name, body } => {
80                    // If block is overridden, use that, else use default body
81                    if let Some(override_body) = self.blocks.get(name).cloned() {
82                        // We need to render the override body
83                        let nodes = override_body; 
84                        output.push_str(&self.render_nodes(&nodes)?);
85                    } else {
86                        output.push_str(&self.render_nodes(body)?);
87                    }
88                }
89                TemplateNode::Extends(_) => {
90                    // Should not happen inside render_nodes (only at top level)
91                    // But if it does, ignore or error?
92                    // Ignore for now.
93                }
94                TemplateNode::Include(template_name) => {
95                    if let Some(engine) = self.engine {
96                        let template = engine.get_template(template_name)
97                            .ok_or_else(|| TemplateError::RenderError(format!("Included template not found: {}", template_name)))?;
98                        
99                        // Includes are rendered in-place with current context
100                        // They do NOT inherit blocks (usually).
101                        // So we create a new renderer for the include, but share context/engine.
102                        // But we don't pass `self.blocks`?
103                        // Correct, includes are isolated from inheritance chain usually.
104                        let mut sub_renderer = Renderer::new(self.context, self.engine);
105                        output.push_str(&sub_renderer.render(template)?);
106                    } else {
107                         return Err(TemplateError::RenderError("Include used without TemplateEngine".to_string()));
108                    }
109                }
110            }
111        }
112
113        Ok(output)
114    }
115
116    fn render_variable(&self, name: &str, filter_names: &[String]) -> Result<String> {
117        let value = self.context.get(name)
118            .ok_or_else(|| TemplateError::VariableNotFound(name.to_string()))?;
119
120        let mut result = self.value_to_string(value);
121
122        // Apply filters
123        for filter_name in filter_names {
124            result = self.filters.apply(filter_name, &result)?;
125        }
126
127        // Auto-escape HTML
128        result = html_escape(&result);
129
130        Ok(result)
131    }
132
133    fn render_if(&mut self, condition: &str, then_branch: &[TemplateNode], else_branch: &Option<Vec<TemplateNode>>) -> Result<String> {
134        // Evaluate condition (simple truthy check)
135        let is_truthy = self.evaluate_condition(condition);
136
137        if is_truthy {
138            self.render_nodes(then_branch)
139        } else if let Some(else_nodes) = else_branch {
140            self.render_nodes(else_nodes)
141        } else {
142            Ok(String::new())
143        }
144    }
145
146    fn render_for(&mut self, item: &str, iterable: &str, body: &[TemplateNode]) -> Result<String> {
147        let array = self.context.get(iterable)
148            .ok_or_else(|| TemplateError::VariableNotFound(iterable.to_string()))?;
149
150        let mut output = String::new();
151
152        if let Value::Array(items) = array {
153            for item_value in items {
154                // Create new context with loop variable
155                let mut loop_context = self.context.clone();
156                loop_context.data.insert(item.to_string(), item_value.clone());
157
158                let mut renderer = Renderer::new(&loop_context, self.engine);
159                // Pass blocks to loop renderer?
160                // Loops are inside the template, so they should have access to blocks?
161                // Yes, if I use a block inside a loop?
162                renderer.blocks = self.blocks.clone();
163                output.push_str(&renderer.render_nodes(body)?);
164            }
165        }
166
167        Ok(output)
168    }
169
170    fn evaluate_condition(&self, condition: &str) -> bool {
171        if let Some(value) = self.context.get(condition) {
172            match value {
173                Value::Bool(b) => *b,
174                Value::Null => false,
175                Value::String(s) => !s.is_empty(),
176                Value::Number(_) => true,
177                Value::Array(a) => !a.is_empty(),
178                Value::Object(o) => !o.is_empty(),
179            }
180        } else {
181            false
182        }
183    }
184
185    fn value_to_string(&self, value: &Value) -> String {
186        match value {
187            Value::String(s) => s.clone(),
188            Value::Number(n) => n.to_string(),
189            Value::Bool(b) => b.to_string(),
190            Value::Null => String::new(),
191            _ => serde_json::to_string(value).unwrap_or_default(),
192        }
193    }
194}
195
196/// HTML escape for XSS protection
197fn html_escape(s: &str) -> String {
198    s.replace('&', "&amp;")
199        .replace('<', "&lt;")
200        .replace('>', "&gt;")
201        .replace('"', "&quot;")
202        .replace('\'', "&#x27;")
203}