1use std::any::Any;
2use std::io::Write;
3use std::ops::Deref;
4use std::ops::DerefMut;
5use std::sync::Arc;
6
7use logos::Logos;
8use parking_lot::RwLock;
9use rhai::Dynamic;
10use rhai::EvalAltResult;
11use rhai::Scope;
12use tracing::error;
13use tracing::trace;
14
15#[derive(Debug, PartialEq, Clone, Copy, Default, thiserror::Error)]
16#[error("parse error")]
17pub struct ParseError;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Logos)]
20#[logos(
21 skip r"[^<]+",
22 error = ParseError,
23)]
24enum Tag {
25 #[token("<%")]
27 Control,
28
29 #[token("<%=")]
31 Output,
32}
33
34#[derive(Debug, Logos)]
35#[logos(skip r"[^%]+",)]
36enum Closing {
37 #[token("%>")]
38 Match,
39}
40
41pub trait Writer: Write + Send + Sync + 'static {}
43impl<T> Writer for T where T: Write + Send + Sync + 'static {}
44
45pub trait State: Clone + Send + Sync + 'static {}
47impl<T> State for T where T: Clone + Send + Sync + 'static {}
48
49pub struct TemplateWriter<W: Write, S> {
51 writer: Arc<RwLock<W>>,
52 pub state: S,
53}
54
55impl<W, S> Clone for TemplateWriter<W, S>
56where
57 W: Write,
58 S: Clone,
59{
60 fn clone(&self) -> Self {
61 Self {
62 writer: self.writer.clone(),
63 state: self.state.clone(),
64 }
65 }
66}
67
68impl<W, S> Write for TemplateWriter<W, S>
69where
70 W: Write,
71{
72 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
73 self.writer.write().write(buf)
74 }
75
76 fn flush(&mut self) -> std::io::Result<()> {
77 self.writer.write().flush()
78 }
79
80 fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
81 self.writer.write().write_vectored(bufs)
82 }
83
84 fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
85 self.writer.write().write_all(buf)
86 }
87
88 fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> {
89 self.writer.write().write_fmt(fmt)
90 }
91}
92
93impl<W, S> TemplateWriter<W, S>
94where
95 W: Write,
96{
97 pub fn new(w: W, state: S) -> Self {
99 Self {
100 writer: Arc::new(RwLock::new(w)),
101 state,
102 }
103 }
104
105 pub fn write_str(&mut self, data: &str) -> Result<(), Box<EvalAltResult>> {
106 if data.is_empty() {
107 return Ok(());
108 }
109 self.write_all(data.as_bytes())
110 .map_err(|e| Box::new(EvalAltResult::from(e.to_string())))
111 }
112
113 pub fn write_char(&mut self, data: char) -> Result<(), Box<EvalAltResult>> {
114 let mut b = [0; 2];
115 let len = data.encode_utf8(&mut b).len();
116 self.write_all(&b[0..len])
117 .map_err(|e| Box::new(EvalAltResult::from(e.to_string())))
118 }
119
120 pub fn write_dynamic(&mut self, d: Dynamic) -> Result<(), Box<EvalAltResult>> {
121 self.write_all(d.to_string().as_bytes())
122 .map_err(|e| Box::new(EvalAltResult::from(e.to_string())))
123 }
124}
125
126pub struct Engine {
128 engine: rhai::Engine,
129}
130
131impl Deref for Engine {
132 type Target = rhai::Engine;
133
134 fn deref(&self) -> &Self::Target {
135 &self.engine
136 }
137}
138
139impl DerefMut for Engine {
140 fn deref_mut(&mut self) -> &mut Self::Target {
141 &mut self.engine
142 }
143}
144
145impl Engine {
146 pub fn new<W: Writer, S: State>() -> Self {
148 let mut engine = rhai::Engine::new();
149
150 engine.register_type::<TemplateWriter<W, S>>();
151 engine.register_fn("write", TemplateWriter::<W, S>::write_str);
152 engine.register_fn("write", TemplateWriter::<W, S>::write_char);
153 engine.register_fn("write", TemplateWriter::<W, S>::write_dynamic);
154
155 Self { engine }
156 }
157
158 pub fn compile(&self, input: &str) -> Result<Template<&rhai::Engine>, CompileError> {
160 Ok(Template {
161 ast: self._compile(input)?,
162 evaluator: &self.engine,
163 })
164 }
165
166 pub fn compile_mut(
167 &mut self,
168 input: &str,
169 ) -> Result<Template<&mut rhai::Engine>, CompileError> {
170 Ok(Template {
171 ast: self._compile(input)?,
172 evaluator: &mut self.engine,
173 })
174 }
175
176 pub fn _compile(&self, input: &str) -> Result<rhai::AST, CompileError> {
177 let mut lex = Tag::lexer(input);
178
179 let mut program: String = String::new();
180
181 let mut last_tag = None;
182 let mut last = 0;
183
184 while let Some(tag) = lex.next() {
185 let tag = tag?;
186
187 let before = &lex.source()[last..lex.span().start];
188 last = lex.span().end;
189
190 rhai_enquote(&mut program, before, matches!(last_tag, Some(Tag::Control)));
191
192 let mut closing = lex.morph::<Closing>();
193
194 let _tok = closing.next();
195 if !matches!(Some(Closing::Match), _tok) {
196 return Err(CompileError::UnclosedTag);
197 }
198
199 let content = &closing.source()[last..closing.span().start];
200 last = closing.span().end;
201
202 match tag {
203 Tag::Control => {
204 trace!("CONTROL");
205 program.push_str(content);
206 }
207 Tag::Output => {
208 trace!("OUTPUT: {content:?}");
209 program.push_str("__tpl_writer.write(");
210 program.push_str(content);
211 program.push_str(");\n");
212 }
213 }
214
215 last_tag = Some(tag);
216
217 lex = closing.morph();
218 }
219 trace!("DONE");
220
221 let tail = &lex.source()[last..];
222
223 rhai_enquote(&mut program, tail, matches!(last_tag, Some(Tag::Control)));
224
225 trace!("program: {program}");
226
227 Ok(self.engine.compile(program)?)
228 }
229}
230
231fn rhai_enquote(program: &mut String, text: &str, strip_newline: bool) {
232 if !text.is_empty() {
233 trace!("enquoting: {text:?}");
234 if text == "\n" {
235 program.push_str("__tpl_writer.write('\\n');\n");
236 } else {
237 if !strip_newline && text.starts_with('\n') {
238 program.push_str("__tpl_writer.write('\\n');\n");
239 }
240
241 program.push_str(r#"__tpl_writer.write("#);
242 program.push_str(&enquote::enquote('`', text));
243 program.push_str(");\n");
244 }
245 }
246}
247
248pub struct Template<E> {
250 ast: rhai::AST,
251 evaluator: E,
252}
253
254impl<E> Template<E>
255where
256 E: TemplateEvaluator,
257{
258 pub fn render<W: Writer, S: State>(
260 &self,
261 w: W,
262 state: S,
263 ) -> Result<TemplateWriter<W, S>, Box<rhai::EvalAltResult>> {
264 let mut scope = Scope::new();
265 let mut w = TemplateWriter::new(w, state);
266 scope.push("__tpl_writer", w.clone());
267
268 self.evaluator.eval(&mut scope, &self.ast)?;
269
270 w.flush()
271 .map_err(|e| Box::new(EvalAltResult::from(e.to_string())))?;
272
273 Ok(w)
274 }
275
276 pub fn evaluator(&self) -> &E {
277 &self.evaluator
278 }
279
280 pub fn evaluator_mut(&mut self) -> &mut E {
281 &mut self.evaluator
282 }
283}
284
285pub trait TemplateEvaluator {
286 fn eval<T: Any + Clone + Send + Sync>(
287 &self,
288 scope: &mut Scope,
289 ast: &rhai::AST,
290 ) -> Result<T, Box<EvalAltResult>>;
291}
292
293impl<'a> TemplateEvaluator for &'a rhai::Engine {
294 fn eval<T: Any + Clone + Send + Sync>(
295 &self,
296 scope: &mut Scope,
297 ast: &rhai::AST,
298 ) -> Result<T, Box<EvalAltResult>> {
299 self.eval_ast_with_scope(scope, ast)
300 }
301}
302
303impl<'a> TemplateEvaluator for &'a mut rhai::Engine {
304 fn eval<T: Any + Clone + Send + Sync>(
305 &self,
306 scope: &mut Scope,
307 ast: &rhai::AST,
308 ) -> Result<T, Box<EvalAltResult>> {
309 self.eval_ast_with_scope(scope, ast)
310 }
311}
312
313#[derive(Debug, thiserror::Error)]
314pub enum CompileError {
315 #[error(transparent)]
316 Parse(#[from] ParseError),
317 #[error(transparent)]
318 Rhai(#[from] Box<rhai::EvalAltResult>),
319 #[error(transparent)]
320 RhaiParse(#[from] rhai::ParseError),
321 #[error("unclosed tag")]
322 UnclosedTag,
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_basic() {
331 let tmpdir = tempfile::tempdir().unwrap();
332 let filepath = tmpdir.path().join("output");
333
334 let f = std::fs::OpenOptions::new()
335 .create(true)
336 .write(true)
337 .open(&filepath)
338 .unwrap();
339
340 let engine = Engine::new::<std::fs::File, ()>();
341
342 let input = r#"<%
343let a = [42, 123, 999, 0, true, "hello", "world!", 987.6543];
344
345// Loop through the array
346for (item, count) in a { %>
347Item #<%= count + 1 %> = <%= item %>
348<% } %>
349
350tail"#;
351
352 let tpl = engine.compile(input).unwrap();
353 tpl.render(f, ()).unwrap();
354
355 let output = std::fs::read_to_string(filepath).unwrap();
356
357 assert_eq!(
358 output,
359 "Item #1 = 42
360Item #2 = 123
361Item #3 = 999
362Item #4 = 0
363Item #5 = true
364Item #6 = hello
365Item #7 = world!
366Item #8 = 987.6543
367
368tail",
369 );
370 }
371
372 #[test]
373 fn test_extend() {
374 let tmpdir = tempfile::tempdir().unwrap();
375 let filepath = tmpdir.path().join("output");
376
377 let f = std::fs::OpenOptions::new()
378 .create(true)
379 .write(true)
380 .open(&filepath)
381 .unwrap();
382
383 let mut engine = Engine::new::<std::fs::File, ()>();
384
385 engine.register_fn(
386 "write",
387 |tw: &mut TemplateWriter<std::fs::File, ()>,
388 d: Dynamic|
389 -> Result<(), Box<EvalAltResult>> {
390 tw.write_all(format!("overloaded: {d}").as_bytes())
391 .map_err(|e| Box::new(EvalAltResult::from(e.to_string())))?;
392 Ok(())
393 },
394 );
395
396 let tpl = engine.compile("<%= 123 %>").unwrap();
397
398 tpl.render(f, ()).unwrap();
399
400 let output = std::fs::read_to_string(filepath).unwrap();
401
402 assert_eq!(output, "overloaded: 123");
403 }
404}