#![warn(missing_docs)]
pub mod _template_book;
pub mod error;
pub mod syntax;
mod env;
mod parse;
mod resolve;
mod scan;
mod transpile;
mod value;
use std::{cell::RefCell, marker::PhantomData, rc::Rc};
use somni_expr::{Context, ExpressionVisitor, TypeSet};
use somni_parser::{Location, ast::Item};
pub use env::Env;
pub use error::TemplateError;
pub use somni_expr::{SomniIterator, SomniStruct, TypedValue};
pub use syntax::{BlockStyle, Syntax, resolve_syntax, split_frontmatter};
pub use value::{IntoValue, Iter, TemplateTypes};
use transpile::{EMIT_FN, EMIT_LIT_FN, RENDER_FN, Transpiled};
struct Literals {
template: String,
spans: Vec<Location>,
}
#[derive(Clone, Debug)]
pub struct Template {
template: String,
transpiled: Transpiled,
}
impl Template {
pub fn compile(source: &str, syntax: &Syntax) -> Result<Template, TemplateError> {
Self::compile_with(source, syntax, &mut |path| {
Err(format!(
"include `{path}` requires Template::compile_with with a loader"
))
})
}
pub fn compile_with(
source: &str,
syntax: &Syntax,
loader: &mut dyn FnMut(&str) -> Result<String, String>,
) -> Result<Template, TemplateError> {
let (syntax, _body, body_offset) = resolve_syntax(source, syntax)?;
let mut nodes = parse::parse(source, &syntax, body_offset)?;
let mut arena = source.to_string();
let modules = resolve::resolve_includes(&mut arena, &mut nodes, &syntax, loader)
.map_err(|e| e.with_diagnostic_source(arena.clone()))?;
let transpiled = transpile::transpile(&arena, &nodes, &modules);
if let Err(err) =
somni_parser::parser::parse::<<TemplateTypes as TypeSet>::Parser>(&transpiled.source)
{
let location = transpiled
.map_location(err.location)
.unwrap_or(Location {
start: body_offset,
end: body_offset,
});
return Err(TemplateError::new(
format!("invalid expression: {}", err.error),
location,
)
.with_diagnostic_source(arena));
}
Ok(Template {
template: arena,
transpiled,
})
}
pub fn generated_program(&self) -> &str {
&self.transpiled.source
}
pub fn render(&self, env: Env) -> Result<String, TemplateError> {
let (names, env) = env.into_parts();
let buffer = Rc::new(RefCell::new(String::new()));
let literals = Rc::new(Literals {
template: self.template.clone(),
spans: self.transpiled.literals.clone(),
});
let mut ctx = Context::<TemplateTypes>::parse_with_types(&self.transpiled.source)
.expect("generated program is validated during compile");
{
let buffer = buffer.clone();
ctx.add_function(EMIT_FN, move |s: &str| {
buffer.borrow_mut().push_str(s);
});
}
{
let buffer = buffer.clone();
let literals = literals.clone();
ctx.add_function(EMIT_LIT_FN, move |index: u64| {
if let Some(span) = literals.spans.get(index as usize) {
buffer
.borrow_mut()
.push_str(&literals.template[span.start..span.end]);
}
});
}
env.apply(&mut ctx, &names);
let program = somni_parser::parser::parse::<<TemplateTypes as TypeSet>::Parser>(
&self.transpiled.source,
)
.expect("generated program is validated during compile");
let render_fn = program
.items
.iter()
.find_map(|item| match item {
Item::Function(f) if f.name.source(&self.transpiled.source) == RENDER_FN => Some(f),
_ => None,
})
.expect("generated program always defines the render function");
let result = {
let mut visitor = ExpressionVisitor::<Context<'_, TemplateTypes>, TemplateTypes> {
context: &mut ctx,
source: &self.transpiled.source,
_marker: PhantomData,
};
visitor.visit_function(render_fn, &[])
};
result.map_err(|err| {
let location = self
.transpiled
.map_location(err.location)
.unwrap_or(Location { start: 0, end: 0 });
TemplateError::new(err.message, location)
.with_diagnostic_source(self.template.clone())
})?;
drop(ctx);
let output = Rc::try_unwrap(buffer)
.map(RefCell::into_inner)
.unwrap_or_else(|rc| rc.borrow().clone());
Ok(output)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compile_rejects_include_without_loader() {
let err = Template::compile(r#"{% include "x.tmpl" %}"#, &Syntax::brackets()).unwrap_err();
assert!(
err.message.contains("compile_with") || err.message.contains("loader"),
"unexpected: {}",
err.message
);
}
}