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: ®ex::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: ®ex::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: ®ex::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>"#);
}
}