cleaver 0.0.1

Template engine
Documentation
use crate::Component;
use regex::Regex;
use serde_json::json;
use std::collections::HashMap;

#[derive(Default)]
pub struct CleaverEngine {}

impl CleaverEngine {
    pub fn new() -> Self {
        CleaverEngine {}
    }

    pub fn render_raw(&self, template: &str, data: serde_json::Value) -> String {
        let print_regex = Regex::new(r"\{\{(.*?)\}\}").unwrap();

        let template = print_regex.replace_all(template, |caps: &regex::Captures| {
            let key = caps.get(1).unwrap().as_str().trim();

            if key.contains('.') {
                let parts = key.split('.');

                let mut value: &serde_json::Value = &data;

                for (index, part) in parts.enumerate() {
                    if index == 0 {
                        value = data.get(part).unwrap();
                    } else {
                        value = value.get(part).unwrap();
                    }
                }

                let value = value.as_str().unwrap();

                return value;
            }

            let value = data.get(key).unwrap().as_str().unwrap();

            value
        });

        let component_re = Regex::new(r"<c:(\w+)\s+(.*?)\s*/>").unwrap();

        let template = component_re.replace_all(&template, |caps: &regex::Captures| {
            let name = caps.get(1).unwrap().as_str().trim();
            let attributes = caps.get(2).unwrap().as_str().trim();

            let mut attributes = attributes
                .split(' ')
                .map(|attribute| {
                    let mut parts = attribute.split('=');

                    let key = parts.next().unwrap().trim();
                    let value = parts.next().unwrap().trim();
                    let value = &value[1..value.len() - 1];

                    (key, value)
                })
                .collect::<HashMap<&str, &str>>();

            attributes.insert("type", name);

            let value = json!(attributes);

            let component: Box<dyn Component> = serde_json::from_value(value.clone()).unwrap();

            self.render_raw(component.render(), value)
        });

        let component_re = Regex::new(r"<c:(\w+)\s+(.*?)\s*>(.*?)</c:(\w+)>").unwrap();

        let template = component_re.replace_all(&template, |caps: &regex::Captures| {
            let name = caps.get(1).unwrap().as_str().trim();
            let attributes = caps.get(2).unwrap().as_str().trim();
            let slot = caps.get(3).unwrap().as_str().trim();

            let mut attributes = attributes
                .split(' ')
                .map(|attribute| {
                    let mut parts = attribute.split('=');

                    let key = parts.next().unwrap().trim();
                    let value = parts.next().unwrap().trim();
                    let value = &value[1..value.len() - 1];

                    (key, value)
                })
                .collect::<HashMap<&str, &str>>();

            attributes.insert("type", name);
            attributes.insert("slot", slot);

            let value = json!(attributes);

            let component: Box<dyn Component> = serde_json::from_value(value.clone()).unwrap();

            self.render_raw(component.render(), value)
        });

        template.to_string()
    }

    pub fn render(&self, name: &str, data: serde_json::Value) -> String {
        let input = std::fs::read_to_string(format!("templates/{}.cleaver.html", name))
            .expect("Something went wrong reading the file");

        self.render_raw(&input, data)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn simple() {
        let engine = CleaverEngine::new();

        let result = engine.render_raw(
            "Hello, {{ message }}",
            json!({
                "message": "world!"
            }),
        );

        assert_eq!(result, "Hello, world!");
    }

    #[test]
    fn template_file() {
        let engine = CleaverEngine::new();

        let result = engine.render(
            "template01",
            json!({
                "message": "world!"
            }),
        );

        assert_eq!(result, "Hello, world!");
    }

    #[test]
    fn component() {
        let engine = CleaverEngine::new();

        #[derive(serde::Serialize, serde::Deserialize)]
        pub struct Alert {
            level: String,
            message: String,
        }

        #[typetag::serde]
        impl Component for Alert {
            fn render(&self) -> &'static str {
                r#"<div class="alert alert-{{ level }}">{{ message }}</div>"#
            }
        }

        let result = engine.render_raw(r#"<c:Alert level="red" message="hi" />"#, json!({}));

        assert_eq!(result, r#"<div class="alert alert-red">hi</div>"#);

        let result2 =
            engine.render_raw(r#"<c:Alert level="red" message="hi"></c:Alert>"#, json!({}));

        assert_eq!(result2, r#"<div class="alert alert-red">hi</div>"#);
    }

    #[test]
    fn component_with_slot() {
        let engine = CleaverEngine::new();

        #[derive(serde::Serialize, serde::Deserialize)]
        pub struct AlertWithSlot {
            level: String,
        }

        #[typetag::serde]
        impl Component for AlertWithSlot {
            fn render(&self) -> &'static str {
                r#"<div class="alert alert-{{ level }}">{{ slot }}</div>"#
            }
        }

        let result = engine.render_raw(
            r#"<c:AlertWithSlot level="red">hello</c:AlertWithSlot>"#,
            json!({}),
        );

        assert_eq!(result, r#"<div class="alert alert-red">hello</div>"#);
    }
}