femstache 0.1.8

Embrace the feminine mustache to unlock your templating dreams! Or something, I don't know. :3
Documentation
//! # Femstache
//!
//! Femstache is a mustache-like templating library with its own extensions.
//!
//! ## Syntax
//!
//! Templates are composed of text interspersed with three kinds of control sequences:
//!
//!   - `{{ ... }}`: interpolation
//!   - `{% ... %}`: control
//!   - `{# ... #}`: metadata (only at the top of a document)
//!
//! There are a few kinds of control structures:
//!
//!   - `{% if COND %} ... {% end %}`
//!   - `{% for item in LIST %} ... {% end %}`
//!   - `{% for item in dict DICT %} ... {% end %}`
//!
//! There are also variable assignments, these are of the form `{% let VAR = EXPR %}`.
//!
//! Template inheritance is also supported, using `{# inherit "PATH_TO_PARENT" #}` and
//! `{% block BLOCKNAME %}{% end %}`.
//!
//! Expressions have the typical syntax of most programming languages, there are
//! a few binary operators:
//!
//!   - Equals: `x == y`
//!   - Not equals: `x != y`
//!   - String concat: `x ++ y`
//!   - Member access: `x.y`
//!   - Dict access: `x[y]`
//!   - Function calls: `x(y)`
//!
//! ## Types
//!
//! There are several available types:
//!
//!   - boolean values: `bool`
//!   - string values: `string` (these can be raw strings, which are not escaped)
//!   - optional values: `option T`
//!   - lists: `list T`
//!   - dicts: `dict T` (this is from strings to `T`)
//!   - structs: `struct(field1: ty1, field2: ty2, ...)`
//!   - functions: `fun A -> R`
//!
//! These types are truthy:
//!
//!   - `true`
//!   - non-empty string
//!   - non-empty dict
//!   - non-empty list
//!   - non-empty struct
//!   - non-empty option
//!   - function
//!
//! ## Typechecking
//!
//! Typechecking is handled using Hindley-Milner and supports extensible records.
//!
//! ## Example
//!
//! ```rust
//! use std::{collections::BTreeMap, path::PathBuf};
//! use femstache::{Femstache, TemplateSource, EscapeMode, Value};
//!
//! let mut files = BTreeMap::new();
//!
//! files.insert(PathBuf::from("hello.fem.html"), "{% for place in places %}hello {{ place }}! :3\n{% end %}".to_owned());
//!
//! let mut femstache = Femstache::new(TemplateSource::map(files), EscapeMode::Html);
//!
//! let mut inputs = BTreeMap::new();
//! inputs.insert("places".to_owned(), Value::List(vec![
//!     Value::string("world"),
//!     Value::string("mom"),
//!     Value::string("puniqt"),
//! ]));
//!
//! let output = femstache.render("hello.fem.html", &inputs).unwrap();
//!
//! assert_eq!(output, "hello world! :3\nhello mom! :3\nhello puniqt! :3\n");
//! ```

use std::collections::BTreeMap;

use femstache_eval::{EvalError, StringValue, TemplateEnv, TemplateInput, Value};
use femstache_loader::{TemplateLoader, TemplateSource};
use femstache_parser::Template;
use femstache_types::{Ty, TyContext, TyError, TyStore};

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

fn make_builtins(store: &mut TyStore) -> Vec<(String, Value)> {
    let mut builtins = Vec::new();
    let arg_ty_id = store.intern(Ty::String);
    let ret_ty_id = store.intern(Ty::String);
    builtins.push((
        "raw".to_owned(),
        Value::builtin(arg_ty_id, ret_ty_id, |v| {
            let Some(s) = v.into_string() else {
                return Err(EvalError::InvalidType);
            };
            Ok(Value::String(match s {
                StringValue::Regular(s) => StringValue::Raw(s),
                StringValue::Raw(s) => StringValue::Raw(s),
            }))
        }),
    ));
    builtins
}

/// 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 }
    }

    /// Render the template at `path` in the store, with `inputs` as the inputs
    pub fn render(&self, path: &str, inputs: &impl TemplateInput) -> FemstacheResult<String> {
        let template = self.loader.load(path)?;
        self.render_direct(&template, inputs)
    }

    pub fn render_direct(
        &self,
        template: &Template,
        inputs: &impl TemplateInput,
    ) -> FemstacheResult<String> {
        let mut ctx = TyContext::new(self.loader.clone());
        let builtins = make_builtins(ctx.store_mut());
        for (name, value) in &builtins {
            let Some(builtin) = value.as_builtin() else {
                panic!("only builtins are supported as builtins");
            };
            let fun_ty_id = ctx.store_mut().intern(Ty::Fun(builtin.arg, builtin.ret));
            ctx.add_builtin(name, fun_ty_id);
        }
        ctx.infer_document(template.document())?;
        for (name, ty_id) in ctx.inputs() {
            // TODO: handle optional
            let Some(value) = inputs.get(name) else {
                return Err(FemstacheError::TypeError(TyError::VarNotFound(
                    name.to_owned(),
                )));
            };
            if !value.conforms_to(ctx.store(), *ty_id) {
                return Err(FemstacheError::TypeError(TyError::VarInvalidType(
                    name.to_owned(),
                )));
            }
        }
        let mut env = TemplateEnv::new(inputs);
        for (name, value) in &builtins {
            env.set(name, value.clone());
        }
        let mut renderer = TemplateRenderer::new(env, BTreeMap::new(), Some(self.loader.clone()));
        let mut output = Output::string(self.escape);
        renderer.render_document(template.document(), &mut output)?;
        Ok(output.into_inner())
    }
}

/// 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 TemplateInput) -> FemstacheResult<String> {
    let femstache = Femstache::new(TemplateSource::none(), EscapeMode::None);
    femstache.render_direct(template, inputs)
}