rhai_tpl/
lib.rs

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    /// `<% control %>` tag
26    #[token("<%")]
27    Control,
28
29    /// `<%= output %>` tag
30    #[token("<%=")]
31    Output,
32}
33
34#[derive(Debug, Logos)]
35#[logos(skip r"[^%]+",)]
36enum Closing {
37    #[token("%>")]
38    Match,
39}
40
41/// Bundled for acceptable template writers
42pub trait Writer: Write + Send + Sync + 'static {}
43impl<T> Writer for T where T: Write + Send + Sync + 'static {}
44
45/// Bundled for acceptable template states
46pub trait State: Clone + Send + Sync + 'static {}
47impl<T> State for T where T: Clone + Send + Sync + 'static {}
48
49/// Used for keeping state and writing the template to the provided writer
50pub 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    /// Creates a new TemplateWriter with the given state
98    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
126/// Template engine, manages [`rhai::Engine`]
127pub 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    /// Creates a new `Engine`
147    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    /// Compiles a template from a string representation
159    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
248/// Reusable compiled template
249pub struct Template<E> {
250    ast: rhai::AST,
251    evaluator: E,
252}
253
254impl<E> Template<E>
255where
256    E: TemplateEvaluator,
257{
258    /// Render teh template w/ the provided writer and state
259    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}