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::PathBuf, sync::OnceLock};

use femstache_eval::{TemplateInputs, Value};
use femstache_loader::TemplateSource;
use femstache_parser::Template;

use crate::{error::FemstacheResult, femstache::Femstache, output::EscapeMode};

const TEMPLATES: &[(&str, &str)] = &[("meow.fem.html", "{{meow}} :3")];

static FEMSTACHE: OnceLock<Femstache> = OnceLock::new();

fn render(input: impl TemplateInputs + 'static, template: &Template) -> FemstacheResult<String> {
    let femstache = FEMSTACHE.get_or_init(|| {
        let mut templates = BTreeMap::new();
        for &(name, source) in TEMPLATES {
            templates.insert(PathBuf::from(name), source.to_owned());
        }
        Femstache::new(TemplateSource::map(templates), EscapeMode::Html)
    });
    Ok(femstache.render_direct(template, input).unwrap())
}

macro_rules! template_test {
    ($name:ident, $input:expr, $tpl:literal, $expected:literal) => {
        #[test]
        fn $name() {
            let tpl = Template::parse($tpl).unwrap();
            let output = render($input, &tpl).unwrap();
            let mut output_words = output.split_whitespace();
            let mut expected_words = $expected.split_whitespace();
            loop {
                match (output_words.next(), expected_words.next()) {
                    (Some(a), Some(b)) => {
                        if a != b {
                            panic!("{output:?} does not match {:?}", $expected);
                        }
                    }
                    (None, None) => {
                        break;
                    }
                    (_, _) => {
                        panic!("{output:?} does not match {:?}", $expected);
                    }
                }
            }
        }
    };
}

struct Input;
impl TemplateInputs for Input {
    fn get(&self, key: &str) -> Option<Value> {
        match key {
            "meow" => Some(Value::string("gay")),
            "meows" => Some(Value::List(vec![
                Value::string("meow"),
                Value::string("nyan"),
                Value::string("miauw"),
                Value::string("nyaa"),
            ])),
            "maybe_meow" => Some(Value::Option(Some(Box::new(Value::string("meow :3"))))),
            "cuties" => {
                const CUTIES: [(&str, &str, &str); 3] = [
                    ("bitey kitty", "nhac nhac chomp", "you"),
                    ("sylveon", "meow~", "estrogen"),
                    ("shadow the hedgehog", "nice ****!", "testosterone"),
                ];
                Some(Value::List(
                    CUTIES
                        .iter()
                        .map(|&(name, sound, favorite_food)| {
                            let mut fields = BTreeMap::new();
                            fields.insert("name".to_owned(), Value::string(name));
                            fields.insert("sound".to_owned(), Value::string(sound));
                            fields.insert("favorite_food".to_owned(), Value::string(favorite_food));
                            Value::Struct(fields)
                        })
                        .collect(),
                ))
            }
            _ => None,
        }
    }
}

fn make_input() -> Input {
    Input
}

template_test!(empty, make_input(), "", "");

template_test!(literal_meow, make_input(), "meow", "meow");

template_test!(stache_meow, make_input(), "{{meow}}", "gay");

template_test!(
    if_statement_var,
    make_input(),
    r#"{% if meow != "" %}woa{% end %}{% if meows != [] %} x3{% end %}"#,
    "woa x3"
);

template_test!(
    if_statement_true,
    make_input(),
    "{% if true %}woa{% end %}",
    "woa"
);

template_test!(
    if_statement_false,
    make_input(),
    "{% if false %}woa{% end %}",
    ""
);

template_test!(
    if_statement_eq,
    make_input(),
    r#"{% if true == true %}woa{% end %}"#,
    "woa"
);

template_test!(
    if_statement_neq,
    make_input(),
    r#"{% if "a" != "b" %}woa{% end %}"#,
    "woa"
);

template_test!(
    if_let_statement,
    make_input(),
    r#"{% if let Some(meow) = maybe_meow %}{{ meow }}{% end %}"#,
    "meow :3"
);

template_test!(
    if_statement_else_1,
    make_input(),
    r#"{% if true %}woa{% else %}wao{% end %}"#,
    "woa"
);

template_test!(
    if_statement_else_2,
    make_input(),
    r#"{% if false %}woa{% else %}wao{% end %}"#,
    "wao"
);

template_test!(
    if_statement_else_if_1,
    make_input(),
    r#"{% if true %}meow{% else if true %}bark{% else %}honk{% end %}"#,
    "meow"
);

template_test!(
    if_statement_else_if_2,
    make_input(),
    r#"{% if false %}meow{% else if true %}bark{% else %}honk{% end %}"#,
    "bark"
);

template_test!(
    if_statement_else_if_3,
    make_input(),
    r#"{% if false %}meow{% else if false %}bark{% else %}honk{% end %}"#,
    "honk"
);

template_test!(
    if_statement_else_if_let_1,
    make_input(),
    r#"{% if false %}woof{% else if let Some(meow) = maybe_meow %}{{ meow }}{% end %}"#,
    "meow :3"
);

template_test!(
    for_statement_var_1,
    make_input(),
    r#"{% for item in meows %}woa{% end %}"#,
    "woawoawoawoa"
);

template_test!(
    for_statement_var_2,
    make_input(),
    r#"{% for item in meows %}woa{{item}}{% end %}"#,
    "woameowwoanyanwoamiauwwoanyaa"
);

template_test!(
    for_statement_literal,
    make_input(),
    r#"hi{% for item in ["uwu", "owo", "awa"] %} {{item}}{% end %}"#,
    "hi uwu owo awa"
);

template_test!(
    for_range_1,
    make_input(),
    r#"{% for i in range 0..3 %}meow{% end %}"#,
    "meowmeowmeow"
);

template_test!(
    let_1,
    make_input(),
    r#"{% let x3 = "x3" %}*nuzzles u* {{ x3 }}"#,
    "*nuzzles u* x3"
);

template_test!(
    let_2,
    make_input(),
    r#"{% let nyan = meow %}*nuzzles u* {{ nyan }}"#,
    "*nuzzles u* gay"
);

template_test!(
    stuff,
    make_input(),
    r#"hewo! {% let nyan = meow %}{% let nya = None %}{% if nyan != "" %}you are {{ meow }}{% end %}{% if let Some(x) = nya %}oh noes!{% end %} {% for meow in meows %}{{ meow }} {% end %}{{ meow }}{% if false %}nope{% end %}{% if nya != None %}nope{% end %}{% if true %} yuh{% end %}"#,
    "hewo! you are gay meow nyan miauw nyaa gay yuh"
);

template_test!(string_1, make_input(), r#"{{ "meow!" }}"#, "meow!");

template_test!(
    string_2,
    make_input(),
    r#"{{ "meow!" ++ " :3" }}"#,
    "meow! :3"
);

template_test!(
    string_3,
    make_input(),
    r#"{{ "meow!" ++ " " ++ meow }}"#,
    "meow! gay"
);

template_test!(
    string_4,
    make_input(),
    r#"{{ ("me" ++ "ow!") ++ (":3" ++ "c") }}"#,
    "meow!:3c"
);

template_test!(show_1, make_input(), r#"{{ show_int(23) }}"#, "23");

template_test!(show_2, make_input(), r#"{{ show_bool(true) }}"#, "true");

template_test!(show_3, make_input(), r#"{{ show_bool(false) }}"#, "false");

template_test!(
    struct_1,
    make_input(),
    r#"<ul>{% for cutie in cuties %}<li><strong>{{ cutie.name }}</strong>: {{ cutie.sound }} i like to eat {{ cutie.favorite_food }}</li>{% end %}</ul>"#,
    "<ul><li><strong>bitey kitty</strong>: nhac nhac chomp i like to eat you</li><li><strong>sylveon</strong>: meow~ i like to eat estrogen</li><li><strong>shadow the hedgehog</strong>: nice ****! i like to eat testosterone</li></ul>"
);

template_test!(
    include,
    make_input(),
    r#"{% include "meow.fem.html" %}"#,
    "gay :3"
);

template_test!(
    fun_1,
    make_input(),
    r#"{% let nya = fun (x) -> "meow " ++ x ++ " :3" %}{{ nya("you") }}"#,
    "meow you :3"
);

template_test!(
    fun_2,
    make_input(),
    r#"{% let x = "you" %}{% let nya = fun (y) -> "meow " ++ x ++ " " ++ y %}{% let x = "lmao" %}{{ nya(":3") }}"#,
    "meow you :3"
);

template_test!(
    macro_1,
    make_input(),
    r#"
    {% macro noise({ animal }) %}
        {% if animal == "cat" %}
            meow!
        {% else if animal == "dog" %}
            woof!
        {% end %}
    {% end %}

    {% render noise({ animal: "cat" }) %}
    {% render noise({ animal: "dog" }) %}
    "#,
    "meow! woof!"
);