use std::ops::Deref;
pub mod error;
pub use minijinja::context;
use minijinja::Environment;
use minijinja::Value;
use std::sync::LazyLock;
use std::sync::Mutex;
pub static ENVIRO: LazyLock<Mutex<Environment>> = LazyLock::new(|| Mutex::new(Environment::new()));
#[derive(Debug, Clone)]
pub struct Templates {
pub entry: Entry,
pub output: Entry,
pub rest: Vec<Entry>,
}
impl Templates {
pub fn new(entry: Index, output: Entry, rest: Rest) -> Self {
Self {
entry: entry.0,
output,
rest: rest.0,
}
}
}
pub struct Index(Entry);
impl Index {
pub fn new(name: &'static str, template: &'static str) -> Self {
Self(Entry::new(name, template))
}
}
impl Deref for Index {
type Target = Entry;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct Rest(Vec<Entry>);
impl Rest {
pub fn new(vals: Vec<Entry>) -> Self {
Self(vals)
}
pub fn push(&mut self, entry: Entry) {
self.0.push(entry);
}
}
impl Deref for Rest {
type Target = Vec<Entry>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct Entry {
pub name: String,
pub template: String,
}
impl Entry {
pub fn new(name: &'static str, template: &'static str) -> Self {
Self {
name: name.to_string(),
template: template.to_string(),
}
}
}
impl IntoIterator for Templates {
type Item = (String, String);
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
let mut v = Vec::new();
v.push((self.entry.name, self.entry.template));
v.push((self.output.name, self.output.template));
for e in self.rest {
v.push((e.name, e.template));
}
v.into_iter()
}
}
pub fn customize<T>(
templates: T,
tracker: fn(String, String) -> String,
) -> Result<(), error::RenderError>
where
T: IntoIterator<Item = (String, String)> + std::fmt::Debug,
{
let mut env = ENVIRO.lock().unwrap();
for (name, template) in templates.into_iter() {
env.add_template_owned(name, template)?;
}
env.add_filter("on", tracker);
Ok(())
}
pub fn render(
target: &str,
ctx: Value,
) -> Result<String, error::RenderError> {
let env = ENVIRO.lock().unwrap();
let tmpl = env.get_template(target)?;
let rendered = tmpl.render(&ctx).map_err(|e| {
let mut err = &e as &dyn std::error::Error;
while let Some(next_err) = err.source() {
err = next_err;
}
error::RenderError::from(e)
})?;
Ok(rendered)
}
#[macro_export]
macro_rules! prelude_bindgen {
(
$guest: ident,
$component: ident,
$pagecontext: ident,
$context:ident,
$state:ident
) => {
use $crate::prelude::*;
type ListenerMap = std::collections::HashMap<String, String>;
lazy_static! {
static ref LISTENERS_MAP: std::sync::Mutex<ListenerMap> = std::sync::Mutex::new(std::collections::HashMap::new());
static ref $state: std::sync::Mutex<Option<$pagecontext>> = std::sync::Mutex::new(None);
}
pub static DEFAULT_TEMPLATES: std::sync::OnceLock<Templates> = std::sync::OnceLock::new();
mod wurbo_tracker {
pub fn track(elem_id: String, ty: String) -> String {
let mut listeners = super::LISTENERS_MAP.lock().unwrap();
if elem_id.is_empty() {
eprintln!("WARNING: elem_id is empty for listener type {ty}");
} else {
listeners.insert(format!("#{elem_id}"), ty);
}
elem_id
}
}
impl $guest for $component {
fn customize(templates: Vec<(String, String)>) -> Result<(), String> {
$crate::jinja::customize(templates, wurbo_tracker::track).map_err(|e| e.to_string())
}
fn render(context: $context) -> Result<String, String> {
let templates = DEFAULT_TEMPLATES.get_or_init(|| {
let tmpls = get_templates();
let env = $crate::jinja::ENVIRO.lock().unwrap();
if let Err(_) = env
.get_template(&tmpls.entry.name)
.and(env.get_template(&tmpls.output.name))
{
drop(env);
$crate::jinja::customize(tmpls.clone(), wurbo_tracker::track)
.expect("Should be able to load Default templates");
};
tmpls
});
let page_context = $pagecontext::from(&context);
let mut last_state = $state.lock().unwrap();
let target = if let Some(ref target) = page_context.target {
target
} else if last_state.is_none() {
&templates.entry.name
} else {
&templates.output.name
};
*last_state = Some(page_context.clone());
let ctx = Value::from_object(page_context.clone());
Ok(wurbo::jinja::render(&target, ctx)?)
}
fn activate(selectors: Option<Vec<String>>) {
let listeners = LISTENERS_MAP.lock().unwrap();
for (selector, ty) in listeners.iter() {
if let Some(selectors) = &selectors {
if !selectors.contains(&selector.to_string()) {
continue;
}
}
let deets = wurbo_in::ListenDetails {
selector: selector.to_string(),
ty: ty.to_string(),
};
wurbo_in::addeventlistener(&deets);
}
}
}
};
}
#[cfg(test)]
mod jinja_unit_tests {
#[test]
fn smoke() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
}