1use std::borrow::Cow;
11use std::cell::RefCell;
12use std::collections::BTreeMap;
13use std::rc::Rc;
14
15use super::node::{Expr, Node, Template};
16use super::pipe;
17use super::{TemplateError, Value};
18
19const MAX_DEPTH: usize = 64;
21
22struct Scope {
24 bind: Option<String>,
25 value: Value,
26}
27
28struct Partials<'a> {
33 resolve: &'a dyn Fn(&str) -> Option<String>,
34 cache: RefCell<BTreeMap<String, Option<Rc<Template>>>>,
35}
36
37impl<'a> Partials<'a> {
38 fn new(resolve: &'a dyn Fn(&str) -> Option<String>) -> Self {
39 Self {
40 resolve,
41 cache: RefCell::new(BTreeMap::new()),
42 }
43 }
44
45 fn get(&self, name: &str) -> Result<Option<Rc<Template>>, TemplateError> {
48 if let Some(cached) = self.cache.borrow().get(name) {
49 return Ok(cached.clone());
50 }
51 let parsed = match (self.resolve)(name) {
52 Some(source) => Some(Rc::new(Template::parse(
55 source.strip_suffix('\n').unwrap_or(&source),
56 )?)),
57 None => None,
58 };
59 self.cache
60 .borrow_mut()
61 .insert(name.to_owned(), parsed.clone());
62 Ok(parsed)
63 }
64}
65
66#[derive(Default)]
76struct Sink {
77 buf: String,
78 absorb_newline: bool,
79}
80
81impl Sink {
82 fn push_literal(&mut self, text: &str) {
85 let text = self.take_absorbed(text);
86 self.buf.push_str(text);
87 self.absorb_newline = false;
88 }
89
90 fn push_value(&mut self, text: &str) {
94 let text = self.take_absorbed(text);
95 let ends_with_newline = text.ends_with('\n');
96 let indent = self.current_indent();
97 if indent == 0 || !text.contains('\n') {
98 self.buf.push_str(text);
99 } else {
100 let pad = " ".repeat(indent);
101 let mut lines = text.split('\n');
102 if let Some(first) = lines.next() {
103 self.buf.push_str(first);
104 }
105 for line in lines {
106 self.buf.push('\n');
107 if !line.is_empty() {
110 self.buf.push_str(&pad);
111 self.buf.push_str(line);
112 }
113 }
114 }
115 self.absorb_newline = ends_with_newline;
116 }
117
118 fn take_absorbed<'a>(&self, text: &'a str) -> &'a str {
120 if self.absorb_newline {
121 text.trim_start_matches('\n')
122 } else {
123 text
124 }
125 }
126
127 fn current_indent(&self) -> usize {
130 let line = match self.buf.rfind('\n') {
131 Some(k) => self.buf.get(k + 1..).unwrap_or(""),
132 None => self.buf.as_str(),
133 };
134 if !line.is_empty() && line.bytes().all(|b| b == b' ') {
135 line.len()
136 } else {
137 0
138 }
139 }
140}
141
142impl Template {
143 pub fn render(
150 &self,
151 context: &Value,
152 resolve_partial: &dyn Fn(&str) -> Option<String>,
153 ) -> Result<String, TemplateError> {
154 let partials = Partials::new(resolve_partial);
155 let mut sink = Sink::default();
156 let mut scopes = Vec::new();
157 render_nodes(&self.nodes, context, &mut scopes, &partials, 0, &mut sink)?;
158 Ok(sink.buf)
159 }
160}
161
162fn render_nodes(
163 nodes: &[Node],
164 ctx: &Value,
165 scopes: &mut Vec<Scope>,
166 partials: &Partials<'_>,
167 depth: usize,
168 out: &mut Sink,
169) -> Result<(), TemplateError> {
170 for node in nodes {
171 render_node(node, ctx, scopes, partials, depth, out)?;
172 }
173 Ok(())
174}
175
176fn render_node(
177 node: &Node,
178 ctx: &Value,
179 scopes: &mut Vec<Scope>,
180 partials: &Partials<'_>,
181 depth: usize,
182 out: &mut Sink,
183) -> Result<(), TemplateError> {
184 match node {
185 Node::Literal(text) => out.push_literal(text),
186 Node::Var(expr) => {
187 if let Some(value) = eval(expr, ctx, scopes) {
188 out.push_value(&pipe::stringify(&value));
189 } else if !expr.pipes.is_empty() {
190 let mut value = Value::Str(String::new());
193 for filter in &expr.pipes {
194 value = pipe::apply(&value, filter);
195 }
196 out.push_value(&pipe::stringify(&value));
197 }
198 }
199 Node::If {
200 branches,
201 otherwise,
202 } => {
203 for (cond, body) in branches {
204 if eval(cond, ctx, scopes)
205 .as_deref()
206 .is_some_and(Value::is_truthy)
207 {
208 return render_nodes(body, ctx, scopes, partials, depth, out);
209 }
210 }
211 render_nodes(otherwise, ctx, scopes, partials, depth, out)?;
212 }
213 Node::For {
214 expr,
215 bind,
216 body,
217 sep,
218 } => render_for(
219 expr,
220 bind.as_ref(),
221 body,
222 sep,
223 ctx,
224 scopes,
225 partials,
226 depth,
227 out,
228 )?,
229 Node::Partial {
230 name,
231 map_over,
232 sep,
233 } => render_partial(
234 name,
235 map_over.as_ref(),
236 sep.as_ref(),
237 ctx,
238 scopes,
239 partials,
240 depth,
241 out,
242 )?,
243 }
244 Ok(())
245}
246
247#[allow(clippy::too_many_arguments)]
248fn render_for(
249 expr: &Expr,
250 bind: Option<&String>,
251 body: &[Node],
252 sep: &[Node],
253 ctx: &Value,
254 scopes: &mut Vec<Scope>,
255 partials: &Partials<'_>,
256 depth: usize,
257 out: &mut Sink,
258) -> Result<(), TemplateError> {
259 let Some(items) = eval(expr, ctx, scopes).map(into_items) else {
260 return Ok(());
261 };
262 for (i, item) in items.into_iter().enumerate() {
263 if i > 0 {
264 render_nodes(sep, ctx, scopes, partials, depth, out)?;
265 }
266 scopes.push(Scope {
267 bind: bind.cloned(),
268 value: item,
269 });
270 let result = render_nodes(body, ctx, scopes, partials, depth, out);
271 scopes.pop();
272 result?;
273 }
274 Ok(())
275}
276
277#[allow(clippy::too_many_arguments)]
278fn render_partial(
279 name: &str,
280 map_over: Option<&Expr>,
281 sep: Option<&String>,
282 ctx: &Value,
283 scopes: &mut Vec<Scope>,
284 partials: &Partials<'_>,
285 depth: usize,
286 out: &mut Sink,
287) -> Result<(), TemplateError> {
288 if depth >= MAX_DEPTH {
289 return Ok(());
290 }
291 let Some(template) = partials.get(name)? else {
292 return Err(TemplateError::new(format!(
293 "partial `{name}` could not be found"
294 )));
295 };
296 match map_over {
297 None => {
298 let rendered = render_to_string(&template.nodes, ctx, scopes, partials, depth + 1)?;
299 out.push_value(&rendered);
300 }
301 Some(expr) => {
302 let Some(items) = eval(expr, ctx, scopes).map(into_items) else {
303 return Ok(());
304 };
305 let separator = sep.cloned().unwrap_or_default();
306 let mut pieces = Vec::new();
307 for item in items {
308 scopes.push(Scope {
309 bind: None,
310 value: item,
311 });
312 let result = render_to_string(&template.nodes, ctx, scopes, partials, depth + 1);
313 scopes.pop();
314 pieces.push(result?);
315 }
316 out.push_value(&pieces.join(&separator));
317 }
318 }
319 Ok(())
320}
321
322fn render_to_string(
325 nodes: &[Node],
326 ctx: &Value,
327 scopes: &mut Vec<Scope>,
328 partials: &Partials<'_>,
329 depth: usize,
330) -> Result<String, TemplateError> {
331 let mut sink = Sink::default();
332 render_nodes(nodes, ctx, scopes, partials, depth, &mut sink)?;
333 Ok(sink.buf)
334}
335
336fn into_items(value: Cow<'_, Value>) -> Vec<Value> {
338 match value.into_owned() {
339 Value::List(items) => items,
340 other => vec![other],
341 }
342}
343
344fn eval<'a>(expr: &Expr, ctx: &'a Value, scopes: &'a [Scope]) -> Option<Cow<'a, Value>> {
346 let base = lookup(&expr.path, ctx, scopes)?;
347 if expr.pipes.is_empty() {
348 return Some(Cow::Borrowed(base));
349 }
350 let mut value = Cow::Borrowed(base);
351 for filter in &expr.pipes {
352 value = Cow::Owned(pipe::apply(value.as_ref(), filter));
353 }
354 Some(value)
355}
356
357fn lookup<'a>(path: &[String], ctx: &'a Value, scopes: &'a [Scope]) -> Option<&'a Value> {
360 let (head, rest) = path.split_first()?;
361 let base = if head == "it"
362 && let Some(scope) = scopes.last()
363 {
364 &scope.value
365 } else if let Some(scope) = scopes
366 .iter()
367 .rev()
368 .find(|s| s.bind.as_deref() == Some(head.as_str()))
369 {
370 &scope.value
371 } else if let Value::Map(map) = ctx {
372 map.get(head)?
373 } else {
374 return None;
375 };
376 let mut current = base;
377 for segment in rest {
378 match current {
379 Value::Map(map) => current = map.get(segment)?,
380 _ => return None,
381 }
382 }
383 Some(current)
384}