femstache 0.1.13

Embrace the feminine mustache to unlock your templating dreams! Or something, I don't know. :3
Documentation
use std::{
    collections::BTreeMap,
    path::{Path, PathBuf},
};

use femstache_eval::{Builtin, TemplateEnv, TemplateInputs, Value};
use femstache_loader::{TemplateLoader, TemplateRef, TemplateSource};
use femstache_parser::Template;
use femstache_types::TyContext;

use crate::{
    FemstacheError, FemstacheResult, builtins,
    output::{EscapeMode, Output},
    renderer::TemplateRenderer,
};

pub struct CompilationContext {
    pub ctx: TyContext,
    pub loader: TemplateLoader,
    pub escape: EscapeMode,
}

impl CompilationContext {
    pub fn new(loader: TemplateLoader, escape: EscapeMode) -> Self {
        let ctx = TyContext::new(loader.clone());
        Self {
            ctx,
            loader,
            escape,
        }
    }
}

pub struct CompiledTemplate<'ctx> {
    ctx: &'ctx mut CompilationContext,
    path: PathBuf,
    template: TemplateRef,
    builtins: Vec<(String, Builtin)>,
}

impl<'ctx> CompiledTemplate<'ctx> {
    fn new(ctx: &'ctx mut CompilationContext, template: TemplateRef) -> FemstacheResult<Self> {
        // TODO: don't build builtins on every render
        let builtins = builtins::add_builtins(ctx.ctx.store_mut());
        for (name, builtin) in &builtins {
            let fun_ty_id = builtin.instantiate(ctx.ctx.store_mut());
            ctx.ctx.add_builtin(name, fun_ty_id);
        }
        let path = &template.meta().path;
        ctx.ctx.infer_document(path, template.get().document())?;
        Ok(Self {
            ctx,
            path: path.to_owned(),
            template,
            builtins,
        })
    }

    pub fn render(&self, inputs: impl TemplateInputs + 'static) -> FemstacheResult<String> {
        for (name, ty_id) in self.ctx.ctx.inputs() {
            // TODO: handle optional
            let Some(value) = inputs.get(name) else {
                return Err(FemstacheError::VarNotFound(name.to_owned()));
            };
            if !value.conforms_to(self.ctx.ctx.store(), *ty_id) {
                return Err(FemstacheError::VarInvalidType(name.to_owned()));
            }
        }
        let mut env = TemplateEnv::new(inputs, self.path.clone());
        for (name, builtin) in &self.builtins {
            env.set(name, Value::Builtin(builtin.clone()));
        }
        let mut renderer = TemplateRenderer::new(env, BTreeMap::new(), self.ctx.loader.clone());
        let mut output = Output::string(self.ctx.escape);
        renderer.render_document(self.template.get().document(), &mut output)?;
        Ok(output.into_inner())
    }
}

/// Encapsulates all of the template loading and rendering logic; this is most
/// likely what you want.
#[derive(Debug)]
pub struct Femstache {
    loader: TemplateLoader,
    escape: EscapeMode,
}

impl Femstache {
    /// Create a [Femstache] instance from a [TemplateSource] and [EscapeMode]
    pub fn new(source: TemplateSource, escape: EscapeMode) -> Self {
        let loader = TemplateLoader::new(source);
        Self { loader, escape }
    }

    /// Get a new compilation context, do not reuse this
    pub fn new_ctx(&self) -> CompilationContext {
        CompilationContext::new(self.loader.clone(), self.escape)
    }

    /// Render the template at `path` in the store, with `inputs` as the inputs
    pub fn render(
        &self,
        path: impl AsRef<Path>,
        inputs: impl TemplateInputs + 'static,
    ) -> FemstacheResult<String> {
        let mut ctx = self.new_ctx();
        let compiled = self.compile(&mut ctx, path)?;
        compiled.render(inputs)
    }

    /// Directly render a template
    pub fn render_direct(
        &self,
        template: &Template,
        inputs: impl TemplateInputs + 'static,
    ) -> FemstacheResult<String> {
        let mut ctx = self.new_ctx();
        let compiled = self.compile_direct(&mut ctx, template)?;
        compiled.render(inputs)
    }

    /// Compile a template from a path
    pub fn compile<'ctx>(
        &self,
        ctx: &'ctx mut CompilationContext,
        path: impl AsRef<Path>,
    ) -> FemstacheResult<CompiledTemplate<'ctx>> {
        let path = path.as_ref();
        let template_ref = self.loader.load(path)?;
        CompiledTemplate::new(ctx, template_ref)
    }

    /// Compile a template directly
    pub fn compile_direct<'ctx>(
        &self,
        ctx: &'ctx mut CompilationContext,
        template: &Template,
    ) -> FemstacheResult<CompiledTemplate<'ctx>> {
        let template_ref = self.loader.intern(template.clone())?;
        CompiledTemplate::new(ctx, template_ref)
    }
}

/// Render a template using the specified input
///
/// ```rust
/// # use std::collections::BTreeMap;
/// # use femstache::Template;
/// let tpl = Template::parse("hello, {{place}}! :3c").unwrap();
///
/// let mut map = BTreeMap::new();
/// map.insert("place", "delicious world");
///
/// assert_eq!(femstache::render(&tpl, map).unwrap(), "hello, delicious world! :3c");
/// ```
pub fn render(
    template: &Template,
    inputs: impl TemplateInputs + 'static,
) -> FemstacheResult<String> {
    let femstache = Femstache::new(TemplateSource::none(), EscapeMode::None);
    femstache.render_direct(template, inputs)
}