use super::ManifestValue;
use crate::ast::MacroDefinition;
use crate::localization::{self, keys};
use anyhow::{Context, Result};
use minijinja::{Environment, Error};
use serde::Serialize;
mod call;
mod invocation;
mod telemetry;
#[cfg(test)]
pub(crate) use call::call_macro_value;
use invocation::{make_macro_fn, validate_macro};
const MACRO_IMPORTS_GLOBAL: &str = "__netsuke_manifest_macro_imports";
pub(crate) fn parse_macro_name(signature: &str) -> Result<String> {
let trimmed = signature.trim();
if trimmed.is_empty() {
return Err(anyhow::anyhow!(
"{}",
localization::message(keys::MANIFEST_MACRO_SIGNATURE_MISSING_IDENTIFIER)
.with_arg("signature", signature)
));
}
let Some((name_segment, _rest)) = trimmed.split_once('(') else {
return Err(anyhow::anyhow!(
"{}",
localization::message(keys::MANIFEST_MACRO_SIGNATURE_MISSING_PARAMS)
.with_arg("signature", signature)
));
};
let identifier = name_segment.trim();
if identifier.is_empty() {
return Err(anyhow::anyhow!(
"{}",
localization::message(keys::MANIFEST_MACRO_SIGNATURE_MISSING_IDENTIFIER)
.with_arg("signature", signature)
));
}
Ok(identifier.to_owned())
}
pub(crate) fn register_macro(
env: &mut Environment<'static>,
macro_def: &MacroDefinition,
index: usize,
) -> Result<()> {
let name = parse_macro_name(¯o_def.signature)?;
let template_name = format!("__manifest_macro_{index}_{name}");
let template_source = format!(
"{{% macro {} %}}{}{{% endmacro %}}",
macro_def.signature, macro_def.body
);
env.add_template_owned(template_name.clone(), template_source)
.with_context(|| {
localization::message(keys::MANIFEST_MACRO_COMPILE_FAILED).with_arg("name", &name)
})?;
validate_macro(env, &template_name, &name)?;
register_macro_import(env, &template_name, &name);
env.add_function(name.clone(), make_macro_fn(template_name, name));
Ok(())
}
pub(crate) fn register_manifest_macros(
doc: &ManifestValue,
env: &mut Environment<'static>,
) -> Result<()> {
let Some(macros) = doc.get("macros").cloned() else {
return Ok(());
};
let defs: Vec<MacroDefinition> = serde_json::from_value(macros)
.context(localization::message(keys::MANIFEST_MACRO_SEQUENCE_INVALID))?;
for (idx, def) in defs.iter().enumerate() {
register_macro(env, def, idx).with_context(|| {
localization::message(keys::MANIFEST_MACRO_REGISTER_FAILED)
.with_arg("signature", &def.signature)
})?;
}
Ok(())
}
pub(crate) fn render_template(
env: &Environment,
template: &str,
context: &impl Serialize,
) -> Result<String, Error> {
let imports = macro_imports(env);
let has_macro_imports = imports.is_some();
telemetry::instrument_template_render(has_macro_imports, || {
imports.map_or_else(
|| env.render_str(template, context),
|import_block| env.render_str(&[import_block.as_str(), template].concat(), context),
)
})
}
fn register_macro_import(env: &mut Environment<'static>, template_name: &str, macro_name: &str) {
let existing = macro_imports(env).unwrap_or_default();
let import = format!("{{% from '{template_name}' import {macro_name} %}}");
env.add_global(MACRO_IMPORTS_GLOBAL, [existing, import].concat());
}
fn macro_imports(env: &Environment) -> Option<String> {
env.globals().find_map(|(name, value)| {
(name == MACRO_IMPORTS_GLOBAL)
.then(|| value.as_str().map(str::to_owned))
.flatten()
})
}