use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
use super::node::{Expr, Node, Template};
use super::pipe;
use super::{TemplateError, Value};
const MAX_DEPTH: usize = 64;
struct Scope {
bind: Option<String>,
value: Value,
}
struct Partials<'a> {
resolve: &'a dyn Fn(&str) -> Option<String>,
cache: RefCell<BTreeMap<String, Option<Rc<Template>>>>,
}
impl<'a> Partials<'a> {
fn new(resolve: &'a dyn Fn(&str) -> Option<String>) -> Self {
Self {
resolve,
cache: RefCell::new(BTreeMap::new()),
}
}
fn get(&self, name: &str) -> Result<Option<Rc<Template>>, TemplateError> {
if let Some(cached) = self.cache.borrow().get(name) {
return Ok(cached.clone());
}
let parsed = match (self.resolve)(name) {
Some(source) => Some(Rc::new(Template::parse(
source.strip_suffix('\n').unwrap_or(&source),
)?)),
None => None,
};
self.cache
.borrow_mut()
.insert(name.to_owned(), parsed.clone());
Ok(parsed)
}
}
#[derive(Default)]
struct Sink {
buf: String,
absorb_newline: bool,
}
impl Sink {
fn push_literal(&mut self, text: &str) {
let text = self.take_absorbed(text);
self.buf.push_str(text);
self.absorb_newline = false;
}
fn push_value(&mut self, text: &str) {
let text = self.take_absorbed(text);
let ends_with_newline = text.ends_with('\n');
let indent = self.current_indent();
if indent == 0 || !text.contains('\n') {
self.buf.push_str(text);
} else {
let pad = " ".repeat(indent);
let mut lines = text.split('\n');
if let Some(first) = lines.next() {
self.buf.push_str(first);
}
for line in lines {
self.buf.push('\n');
if !line.is_empty() {
self.buf.push_str(&pad);
self.buf.push_str(line);
}
}
}
self.absorb_newline = ends_with_newline;
}
fn take_absorbed<'a>(&self, text: &'a str) -> &'a str {
if self.absorb_newline {
text.trim_start_matches('\n')
} else {
text
}
}
fn current_indent(&self) -> usize {
let line = match self.buf.rfind('\n') {
Some(k) => self.buf.get(k + 1..).unwrap_or(""),
None => self.buf.as_str(),
};
if !line.is_empty() && line.bytes().all(|b| b == b' ') {
line.len()
} else {
0
}
}
}
impl Template {
pub fn render(
&self,
context: &Value,
resolve_partial: &dyn Fn(&str) -> Option<String>,
) -> Result<String, TemplateError> {
let partials = Partials::new(resolve_partial);
let mut sink = Sink::default();
let mut scopes = Vec::new();
render_nodes(&self.nodes, context, &mut scopes, &partials, 0, &mut sink)?;
Ok(sink.buf)
}
}
fn render_nodes(
nodes: &[Node],
ctx: &Value,
scopes: &mut Vec<Scope>,
partials: &Partials<'_>,
depth: usize,
out: &mut Sink,
) -> Result<(), TemplateError> {
for node in nodes {
render_node(node, ctx, scopes, partials, depth, out)?;
}
Ok(())
}
fn render_node(
node: &Node,
ctx: &Value,
scopes: &mut Vec<Scope>,
partials: &Partials<'_>,
depth: usize,
out: &mut Sink,
) -> Result<(), TemplateError> {
match node {
Node::Literal(text) => out.push_literal(text),
Node::Var(expr) => {
if let Some(value) = eval(expr, ctx, scopes) {
out.push_value(&pipe::stringify(&value));
} else if !expr.pipes.is_empty() {
let mut value = Value::Str(String::new());
for filter in &expr.pipes {
value = pipe::apply(&value, filter);
}
out.push_value(&pipe::stringify(&value));
}
}
Node::If {
branches,
otherwise,
} => {
for (cond, body) in branches {
if eval(cond, ctx, scopes)
.as_deref()
.is_some_and(Value::is_truthy)
{
return render_nodes(body, ctx, scopes, partials, depth, out);
}
}
render_nodes(otherwise, ctx, scopes, partials, depth, out)?;
}
Node::For {
expr,
bind,
body,
sep,
} => render_for(
expr,
bind.as_ref(),
body,
sep,
ctx,
scopes,
partials,
depth,
out,
)?,
Node::Partial {
name,
map_over,
sep,
} => render_partial(
name,
map_over.as_ref(),
sep.as_ref(),
ctx,
scopes,
partials,
depth,
out,
)?,
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn render_for(
expr: &Expr,
bind: Option<&String>,
body: &[Node],
sep: &[Node],
ctx: &Value,
scopes: &mut Vec<Scope>,
partials: &Partials<'_>,
depth: usize,
out: &mut Sink,
) -> Result<(), TemplateError> {
let Some(items) = eval(expr, ctx, scopes).map(into_items) else {
return Ok(());
};
for (i, item) in items.into_iter().enumerate() {
if i > 0 {
render_nodes(sep, ctx, scopes, partials, depth, out)?;
}
scopes.push(Scope {
bind: bind.cloned(),
value: item,
});
let result = render_nodes(body, ctx, scopes, partials, depth, out);
scopes.pop();
result?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn render_partial(
name: &str,
map_over: Option<&Expr>,
sep: Option<&String>,
ctx: &Value,
scopes: &mut Vec<Scope>,
partials: &Partials<'_>,
depth: usize,
out: &mut Sink,
) -> Result<(), TemplateError> {
if depth >= MAX_DEPTH {
return Ok(());
}
let Some(template) = partials.get(name)? else {
return Err(TemplateError::new(format!(
"partial `{name}` could not be found"
)));
};
match map_over {
None => {
let rendered = render_to_string(&template.nodes, ctx, scopes, partials, depth + 1)?;
out.push_value(&rendered);
}
Some(expr) => {
let Some(items) = eval(expr, ctx, scopes).map(into_items) else {
return Ok(());
};
let separator = sep.cloned().unwrap_or_default();
let mut pieces = Vec::new();
for item in items {
scopes.push(Scope {
bind: None,
value: item,
});
let result = render_to_string(&template.nodes, ctx, scopes, partials, depth + 1);
scopes.pop();
pieces.push(result?);
}
out.push_value(&pieces.join(&separator));
}
}
Ok(())
}
fn render_to_string(
nodes: &[Node],
ctx: &Value,
scopes: &mut Vec<Scope>,
partials: &Partials<'_>,
depth: usize,
) -> Result<String, TemplateError> {
let mut sink = Sink::default();
render_nodes(nodes, ctx, scopes, partials, depth, &mut sink)?;
Ok(sink.buf)
}
fn into_items(value: Cow<'_, Value>) -> Vec<Value> {
match value.into_owned() {
Value::List(items) => items,
other => vec![other],
}
}
fn eval<'a>(expr: &Expr, ctx: &'a Value, scopes: &'a [Scope]) -> Option<Cow<'a, Value>> {
let base = lookup(&expr.path, ctx, scopes)?;
if expr.pipes.is_empty() {
return Some(Cow::Borrowed(base));
}
let mut value = Cow::Borrowed(base);
for filter in &expr.pipes {
value = Cow::Owned(pipe::apply(value.as_ref(), filter));
}
Some(value)
}
fn lookup<'a>(path: &[String], ctx: &'a Value, scopes: &'a [Scope]) -> Option<&'a Value> {
let (head, rest) = path.split_first()?;
let base = if head == "it"
&& let Some(scope) = scopes.last()
{
&scope.value
} else if let Some(scope) = scopes
.iter()
.rev()
.find(|s| s.bind.as_deref() == Some(head.as_str()))
{
&scope.value
} else if let Value::Map(map) = ctx {
map.get(head)?
} else {
return None;
};
let mut current = base;
for segment in rest {
match current {
Value::Map(map) => current = map.get(segment)?,
_ => return None,
}
}
Some(current)
}