cookie_cutter_core 0.1.0

A feature-rich template engine with context aware escaping and both runtime and compiletime compilation
Documentation
use crate::{
    rendertime::WRONG_TYPE_MESSAGE,
    type_checking::ir::{Call, Expression, Literal},
    Escaper,
};
use std::{
    collections::BTreeMap,
    fmt::{self, Write},
    iter,
    marker::PhantomData,
};

use itertools::{Itertools, Position};

use crate::{type_checking::ir::TemplateContent, Templates};

use super::{param_matches_ty, Error, Value};

struct Context<'a, 'b, D, W, Esc> {
    _w: PhantomData<W>,
    drain: D,
    tmpls: &'a Templates<Esc>,
    vars: &'b mut [Option<Value>],
}

pub(crate) trait TemplateContentDrain<W: fmt::Write> {
    fn next_line(&mut self, newline: &str) -> fmt::Result;
    fn current(&mut self) -> &mut W;
}

impl<W: fmt::Write> TemplateContentDrain<W> for W {
    fn next_line(&mut self, newline: &str) -> fmt::Result {
        self.write_str(newline)
    }
    fn current(&mut self) -> &mut W {
        self
    }
}

#[derive(Debug, Default)]
struct VecDrain(Vec<Value>);

impl TemplateContentDrain<String> for &mut VecDrain {
    fn next_line(&mut self, _newline: &str) -> fmt::Result {
        self.0.push(Value::Text(String::new()));
        Ok(())
    }

    fn current(&mut self) -> &mut String {
        if self.0.is_empty() {
            self.0.push(Value::Text(String::new()));
        }

        let last = self.0.len() - 1;
        let Value::Text(ref mut current) = &mut self.0[last] else {
            panic!("the vec should only contain texts");
        };
        current
    }
}

impl<D, W: fmt::Write, Esc: Escaper> Context<'_, '_, D, W, Esc>
where
    D: TemplateContentDrain<W>,
    W: fmt::Write,
{
    fn run_template_body(&mut self, body: &[TemplateContent<Esc>]) -> Result<(), Error> {
        fn write_value<W: Write>(
            drain: &mut impl TemplateContentDrain<W>,
            value: Value,
            indentation: &str,
            newline: &str,
        ) -> fmt::Result {
            match value {
                Value::Text(text) => drain.current().write_str(&text),
                Value::Int(v) => write!(drain.current(), "{v}"),
                Value::Bool(v) => write!(drain.current(), "{v}"),
                Value::Float(v) => write!(drain.current(), "{v}"),
                Value::ArrayOrTuple(values) => {
                    for (pos, value) in values.into_iter().with_position() {
                        if !matches!(pos, Position::Only | Position::First) {
                            drain.current().write_str(indentation)?;
                        }

                        write_value(drain, value, indentation, newline)?;

                        if !matches!(pos, Position::Only | Position::Last) {
                            drain.next_line(newline)?;
                        }
                    }

                    Ok(())
                }
                Value::Struct(_) => {
                    panic!("Cannot write {value:?}; {WRONG_TYPE_MESSAGE}")
                }
            }
        }

        for content in body {
            match content {
                TemplateContent::OriginalTemplateNewline(newline) => {
                    self.drain.next_line(newline).map_err(Error::Fmt)?;
                }
                TemplateContent::Static(static_str) => {
                    self.drain
                        .current()
                        .write_str(static_str)
                        .map_err(Error::Fmt)?;
                }
                TemplateContent::Assignment(var_index, expr) => {
                    self.vars[*var_index] = Some(self.eval_expression(expr)?);
                }
                TemplateContent::BodyExpression {
                    indentation,
                    expression,
                    newline,
                } => {
                    let value = self.eval_expression(expression)?;
                    assert!(value.is_printable(), "Wrong type of ({value:?}); expected array of printable values for body array expression; {WRONG_TYPE_MESSAGE}");
                    write_value(&mut self.drain, value, indentation, newline)
                        .map_err(Error::Fmt)?;
                }
            }
        }

        Ok(())
    }

    fn eval_literal(&mut self, literal: &Literal<Esc>) -> Result<Value, Error> {
        Ok(match literal {
            Literal::Int(v) => Value::Int(*v),
            Literal::Float(v) => Value::Float(*v),
            Literal::Bool(v) => Value::Bool(*v),
            Literal::String(str) => Value::Text(str.clone()),
            Literal::Template(body) => {
                let mut result = VecDrain::default();

                Context {
                    _w: PhantomData,
                    drain: &mut result,
                    tmpls: self.tmpls,
                    vars: self.vars,
                }
                .run_template_body(body)?;

                Value::ArrayOrTuple(result.0)
            }
            Literal::ArrayOrTuple(values) => Value::ArrayOrTuple(
                values
                    .iter()
                    .map(|expr| self.eval_expression(expr))
                    .collect::<Result<_, _>>()?,
            ),
            Literal::Struct(values) => Value::Struct(
                values
                    .iter()
                    .map(|(name, expr)| Ok((name.clone(), self.eval_expression(expr)?)))
                    .collect::<Result<_, _>>()?,
            ),
        })
    }

    /// returns the value, as well as the information whether indentation has already been factored
    /// in
    #[allow(clippy::too_many_lines)]
    fn eval_expression(&mut self, expr: &Expression<Esc>) -> Result<Value, Error> {
        Ok(match expr {
            Expression::Escaped(inner, escer) => escer
                .escape(self.eval_expression(&**inner)?)
                .map_err(Error::Escape)?,
            Expression::Literal(literal) => self.eval_literal(literal)?,
            Expression::Variable(var_index) => self.vars[*var_index]
                .as_ref()
                .expect("type checking should ensure that unitialized variables are not read")
                .clone(),
            Expression::Call(Call::Function(f, args)) => f
                .run(
                    args.iter()
                        .map(|arg| self.eval_expression(arg))
                        .collect::<Result<_, _>>()?,
                )
                .map_err(Error::Function)?,
            Expression::Call(Call::Template(tmpl_index, arg)) => {
                let mut res = VecDrain::default();
                write_tmpl(
                    &mut res,
                    self.tmpls,
                    *tmpl_index,
                    arg.iter()
                        .map(|(name, expr)| {
                            Ok::<_, Error>((name.clone(), self.eval_expression(expr)?))
                        })
                        .collect::<Result<_, _>>()?,
                )?;
                Value::ArrayOrTuple(res.0)
            }
            Expression::If {
                condition,
                then,
                otherwise,
            } => {
                let condition = self.eval_expression(condition)?;
                self.eval_expression(match condition {
                    Value::Bool(true) => then,
                    Value::Bool(false) => otherwise,
                    _ => panic!(
                        "non bool condition ({condition:?}) in if expression; {WRONG_TYPE_MESSAGE}"
                    ),
                })?
            }
            Expression::MemberAccess { expression, access } => {
                let mut value = self.eval_expression(expression)?;

                match value {
                        Value::ArrayOrTuple(ref mut values) => {
                            assert!(*access < values.len(), "value ({value:?}) in member access expression did not have the requested value ({access}); {WRONG_TYPE_MESSAGE}");
                            values.swap_remove(*access)
                                }
                        Value::Struct(ref values) => {
                            values.values().nth(*access).unwrap_or_else(|| panic!("value ({value:?}) in member access expression did not have the requested value ({access}); {WRONG_TYPE_MESSAGE}")).clone()
                                }
                        _ => panic!("value ({value:?}) has wrong type for member access expression; {WRONG_TYPE_MESSAGE}"),
                    }
            }
            Expression::Each {
                assignment_variable,
                array_expression,
                body,
            } => {
                let array_values = self.eval_expression(array_expression)?;

                match array_values {
                        Value::ArrayOrTuple(values) => {
                            let mut result_values = Vec::with_capacity(values.len());

                            for value in values {
                                self.vars[*assignment_variable] = Some(value);
                                result_values.push(self.eval_expression(body)?);
                            }

                            Value::ArrayOrTuple(result_values)
                                }
                        _ => panic!("value ({array_values:?}) has wrong type for each expression; {WRONG_TYPE_MESSAGE}"),
                    }
            }
        })
    }
}

pub(crate) fn write_tmpl<W: fmt::Write, Esc: Escaper>(
    writer: impl TemplateContentDrain<W>,
    tmpls: &Templates<Esc>,
    index: usize,
    params: BTreeMap<String, Value>,
) -> Result<(), Error> {
    let tmpl = &tmpls.tmpls[index];

    if !param_matches_ty(&Value::Struct(params.clone()), &tmpl.params) {
        return Err(Error::WrongParameters {
            got: Value::Struct(params.clone()),
            expected: tmpl.params.clone(),
        });
    }

    let remaining_var_count = tmpl.variable_count - params.len();
    let mut vars = params
        .into_values()
        .map(Some)
        .chain(iter::repeat_n(None, remaining_var_count))
        .collect_vec();

    Context {
        _w: PhantomData,
        drain: writer,
        tmpls,
        vars: vars.as_mut_slice(),
    }
    .run_template_body(&tmpl.body)
}