1#![warn(missing_docs)]
31
32pub mod _template_book;
33pub mod error;
34pub mod syntax;
35
36mod env;
37mod parse;
38mod resolve;
39mod scan;
40mod transpile;
41mod value;
42
43use std::{cell::RefCell, marker::PhantomData, rc::Rc};
44
45use somni_expr::{Context, ExpressionVisitor, TypeSet};
46use somni_parser::{Location, ast::Item};
47
48pub use env::Env;
49pub use error::TemplateError;
50pub use somni_expr::{SomniIterator, SomniStruct, TypedValue};
51pub use syntax::{BlockStyle, Syntax, resolve_syntax, split_frontmatter};
52pub use value::{IntoValue, Iter, TemplateTypes};
53
54use transpile::{EMIT_FN, EMIT_LIT_FN, RENDER_FN, Transpiled};
55
56struct Literals {
58 template: String,
59 spans: Vec<Location>,
60}
61
62#[derive(Clone, Debug)]
67pub struct Template {
68 template: String,
69 transpiled: Transpiled,
70}
71
72impl Template {
73 pub fn compile(source: &str, syntax: &Syntax) -> Result<Template, TemplateError> {
84 Self::compile_with(source, syntax, &mut |path| {
85 Err(format!(
86 "include `{path}` requires Template::compile_with with a loader"
87 ))
88 })
89 }
90
91 pub fn compile_with(
100 source: &str,
101 syntax: &Syntax,
102 loader: &mut dyn FnMut(&str) -> Result<String, String>,
103 ) -> Result<Template, TemplateError> {
104 let (syntax, _body, body_offset) = resolve_syntax(source, syntax)?;
105 let mut nodes = parse::parse(source, &syntax, body_offset)?;
108
109 let mut arena = source.to_string();
110 let modules = resolve::resolve_includes(&mut arena, &mut nodes, &syntax, loader)
111 .map_err(|e| e.with_diagnostic_source(arena.clone()))?;
112 let transpiled = transpile::transpile(&arena, &nodes, &modules);
113
114 if let Err(err) =
117 somni_parser::parser::parse::<<TemplateTypes as TypeSet>::Parser>(&transpiled.source)
118 {
119 let location = transpiled
120 .map_location(err.location)
121 .unwrap_or(Location {
122 start: body_offset,
123 end: body_offset,
124 });
125 return Err(TemplateError::new(
126 format!("invalid expression: {}", err.error),
127 location,
128 )
129 .with_diagnostic_source(arena));
130 }
131
132 Ok(Template {
133 template: arena,
134 transpiled,
135 })
136 }
137
138 pub fn generated_program(&self) -> &str {
143 &self.transpiled.source
144 }
145
146 pub fn render(&self, env: Env) -> Result<String, TemplateError> {
150 let (names, env) = env.into_parts();
152
153 let buffer = Rc::new(RefCell::new(String::new()));
154 let literals = Rc::new(Literals {
155 template: self.template.clone(),
156 spans: self.transpiled.literals.clone(),
157 });
158
159 let mut ctx = Context::<TemplateTypes>::parse_with_types(&self.transpiled.source)
160 .expect("generated program is validated during compile");
161
162 {
163 let buffer = buffer.clone();
164 ctx.add_function(EMIT_FN, move |s: &str| {
165 buffer.borrow_mut().push_str(s);
166 });
167 }
168 {
169 let buffer = buffer.clone();
170 let literals = literals.clone();
171 ctx.add_function(EMIT_LIT_FN, move |index: u64| {
172 if let Some(span) = literals.spans.get(index as usize) {
173 buffer
174 .borrow_mut()
175 .push_str(&literals.template[span.start..span.end]);
176 }
177 });
178 }
179
180 env.apply(&mut ctx, &names);
181
182 let program = somni_parser::parser::parse::<<TemplateTypes as TypeSet>::Parser>(
183 &self.transpiled.source,
184 )
185 .expect("generated program is validated during compile");
186 let render_fn = program
187 .items
188 .iter()
189 .find_map(|item| match item {
190 Item::Function(f) if f.name.source(&self.transpiled.source) == RENDER_FN => Some(f),
191 _ => None,
192 })
193 .expect("generated program always defines the render function");
194
195 let result = {
196 let mut visitor = ExpressionVisitor::<Context<'_, TemplateTypes>, TemplateTypes> {
197 context: &mut ctx,
198 source: &self.transpiled.source,
199 _marker: PhantomData,
200 };
201 visitor.visit_function(render_fn, &[])
202 };
203
204 result.map_err(|err| {
205 let location = self
206 .transpiled
207 .map_location(err.location)
208 .unwrap_or(Location { start: 0, end: 0 });
209 TemplateError::new(err.message, location)
210 .with_diagnostic_source(self.template.clone())
211 })?;
212
213 drop(ctx);
214
215 let output = Rc::try_unwrap(buffer)
216 .map(RefCell::into_inner)
217 .unwrap_or_else(|rc| rc.borrow().clone());
218 Ok(output)
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn compile_rejects_include_without_loader() {
228 let err = Template::compile(r#"{% include "x.tmpl" %}"#, &Syntax::brackets()).unwrap_err();
229 assert!(
230 err.message.contains("compile_with") || err.message.contains("loader"),
231 "unexpected: {}",
232 err.message
233 );
234 }
235}