hornbeam 0.0.5

Hornbeam template engine (high-level crate for use in applications)
Documentation
//! Interpreted template mode. Suitable for development.
//!
//! Features:
//! - Supports hot reloading of templates and translations.
//! - Loads templates and translations from disk (not bundled with the application) and parses them
//!   at runtime.

use arc_swap::ArcSwap;
use hornbeam_interpreter::localisation::fluent::{
    FluentLocalisationError, FluentLocalisationSystem,
};
use hornbeam_interpreter::{InterpreterError, LoadedTemplates};
use std::convert::Infallible;
use std::path::{Path, PathBuf};

use std::sync::Arc;

pub type TemplateManager = Arc<ArcSwap<LoadedTemplates<FluentLocalisationSystem>>>;
pub type TemplateError = InterpreterError<FluentLocalisationError, Infallible>;

pub use hornbeam_interpreter::Params;

use crate::interpreted::hot_reload::start_hot_reloader;
pub use lazy_static::lazy_static;

#[cfg(feature = "hot_reload")]
pub mod hot_reload;

/// Makes a template manager.
/// The syntax within the macro resembles a static ref binding so that the effect of the macro is
/// more obvious...
#[macro_export]
macro_rules! make_template_manager {
    {
        static ref $name:ident = {
            default_locale: $locale:expr,
        };
    } => {
        $crate::lazy_static! {
            static ref $name: $crate::TemplateManager =
                $crate::new_template_manager($locale, env!("CARGO_PKG_NAME"));
        }
    }
}

/// INVOKED BY MACRO; don't use manually (no point).
pub fn new_template_manager(
    default_locale: &'static str,
    crate_name: &'static str,
) -> TemplateManager {
    fn is_valid_base_dir(path: &Path) -> bool {
        path.is_dir() && path.join("translations").is_dir() && path.join("templates").is_dir()
    }

    let base_dir = if let Ok(base_dir_env) = std::env::var("HORNBEAM_BASE") {
        let path = PathBuf::from(&base_dir_env);
        if is_valid_base_dir(&path) {
            path
        } else {
            panic!("Could not find templates at position of HORNBEAM_BASE environment variable ({base_dir_env:?})");
        }
    } else {
        let try_paths = [crate_name, "."];
        let mut successful = None;
        for path in try_paths {
            let path = PathBuf::from(path);
            if is_valid_base_dir(&path) {
                successful = Some(path);
                break;
            }
        }

        successful.unwrap_or_else(|| panic!("Could not find Hornbeam templates/translations: tried looking in {try_paths:?} and no HORNBEAM_BASE environment variable set!"))
    };
    let template_sys = match load_new_template_system(
        default_locale,
        &base_dir.join("templates"),
        &base_dir.join("translations"),
    ) {
        Ok(v) => v,
        Err(err) => {
            panic!("Failed to create Hornbeam environment! {err}");
        }
    };
    let templates = Arc::new(ArcSwap::new(Arc::new(template_sys)));

    #[cfg(feature = "hot_reload")]
    if is_hot_reload_enabled() {
        start_hot_reloader(
            default_locale.to_owned(),
            templates.clone(),
            base_dir.join("templates"),
            base_dir.join("translations"),
        );
    }

    templates
}

fn load_new_template_system(
    default_locale: &str,
    template_dir: &Path,
    translation_dir: &Path,
) -> Result<LoadedTemplates<FluentLocalisationSystem>, InterpreterError<Infallible, Infallible>> {
    let localisation_system = FluentLocalisationSystem::new(translation_dir, default_locale)
        .map_err(|err| {
            InterpreterError::TemplateFindError(format!(
                "Failed to create Fluent localisation system for Hornbeam environment: {err:?}"
            ))
        })?;
    let mut templates = LoadedTemplates::new(localisation_system);
    templates.load_templates_from_dir(template_dir)?;
    Ok(templates)
}

#[cfg(feature = "hot_reload")]
pub fn is_hot_reload_enabled() -> bool {
    #[cfg(debug_assertions)]
    const DEFAULT_HOT_RELOAD: bool = true;
    #[cfg(not(debug_assertions))]
    const DEFAULT_HOT_RELOAD: bool = false;

    std::env::var("HORNBEAM_HOT")
        .map(|env_var| {
            if let Ok(i) = env_var.parse::<u32>() {
                return i != 0;
            }
            if let Ok(b) = env_var.parse::<bool>() {
                return b;
            }
            if env_var.eq_ignore_ascii_case("no") {
                return false;
            }
            if env_var.eq_ignore_ascii_case("off") {
                return false;
            }
            eprintln!("Not sure how to interpret HORNBEAM_HOT={env_var:?}, assuming yes.");
            true
        })
        .unwrap_or(DEFAULT_HOT_RELOAD)
}

#[cfg(not(feature = "hot_reload"))]
pub const fn is_hot_reload_enabled() -> bool {
    false
}

/// On startup, must be called with the identifier of the manager.
/// Note that the manager uses lazy_static! and thus must be initialised without any potential
/// for thread races!
#[macro_export]
macro_rules! initialise_template_manager {
    ($name:ident) => {
        // Ensure the lazy static is loaded, by trying to deref it.
        let _ = $name.load();
    };
}

// #[macro_export]
// macro_rules! render_template_string {
//     ($manager:ident, $template_name:ident, $locale:expr $(, $k:ident : $v:expr)*) => {
//         // TODO LOCALE
//         $manager.prepare()
//     }
// }
//
// #[macro_export]
// macro_rules! interpreter_params {
//     ($(, $k:ident : $v:expr)*) => {
//         $crate::hornbeam::Params::new()
//             $(.set(stringify!($k), $v))*
//     }
// }

#[macro_export]
macro_rules! render_template_string {
    ($manager:ident, $template_name:ident, $locale:expr) => {
        $crate::render_template_string!($manager, $template_name, $locale, {})
    };
    ($manager:ident, $template_name:ident, $locale:expr, { $($params:tt)* }) => {
        // TODO LOCALE
        {
            let manager = $manager.load();
            let prepared = manager.prepare(stringify!($template_name), None, $crate::interpreter_params!{ $($params)* }, $locale);
            let finished_string = prepared.render_to_string();
            if $crate::is_hot_reload_enabled() {
                // Hacky, but inject the auto-hot-reloader into the page :P
                finished_string.map(|string| string.replace("<head>", r#"<head><script type="text/javascript">
async function __hot_reload() {
  let response = await fetch(`${window.location.origin.replace(/:[0-9]+/, "")}:7015/wait_hot_reload`);
  if (response.status == 502) {
    window.setTimeout(__hot_reload, 100);
  } else if (response.status != 200) {
    console.log("hot reload fail: ", response.statusText);
    window.setTimeout(__hot_reload, 1000);
  } else {
    window.location.reload();
  }
}
window.setTimeout(__hot_reload, 100);
</script>"#))
            } else {
                finished_string
            }
        }
    };
}

#[macro_export]
macro_rules! interpreter_params {
    { $($all:tt)* } => {
        {
            let mut params = $crate::Params::default();

            $crate::interpreter_setters!{ params; $($all)* }

            params
        }
    };
}

#[macro_export]
macro_rules! interpreter_setters {
    { $params:ident; } => {};
    { $params:ident; $k:ident : $v:expr } => {
        $params = $params.set(stringify!($k), $v);
    };
    { $params:ident; $k:ident } => {
        $params = $params.set(stringify!($k), $k);
    };
    { $params:ident; $k:ident : $v:expr, $($tail:tt)* } => {
        $params = $params.set(stringify!($k), $v);
        $crate::interpreter_setters!{ $params; $($tail)* }
    };
    { $params:ident; $k:ident, $($tail:tt)* } => {
        $params = $params.set(stringify!($k), $k);
        $crate::interpreter_setters!{ $params; $($tail)* }
    };
}