Skip to main content

minijinja_lua/
environment.rs

1// SPDX-License-Identifier: MIT
2
3use std::{borrow::Cow, fmt, ops::Deref};
4
5use minijinja::{
6    Environment,
7    Error as JinjaError,
8    ErrorKind as JinjaErrorKind,
9    State,
10    args,
11    value::{Rest as JinjaRest, Value as JinjaValue},
12};
13use mlua::LuaSerdeExt;
14use rs_mod_lua_core::guard::bind_lua;
15
16use crate::convert::{
17    LuaAutoEscape,
18    LuaFunctionObject,
19    LuaSyntaxConfig,
20    LuaTableObject,
21    LuaUndefinedBehavior,
22    lua_to_minijinja,
23    minijinja_to_lua,
24};
25
26/// A wrapper around a [`minijinja::Environment`]. This wrapper can be serialized into
27/// an [`mlua::UserData`] object for use within mlua::Lua.
28#[derive(mlua::UserData, Debug)]
29pub struct LuaEnvironment(Environment<'static>);
30
31impl From<Environment<'static>> for LuaEnvironment {
32    fn from(value: Environment<'static>) -> Self {
33        LuaEnvironment(value)
34    }
35}
36
37impl From<LuaEnvironment> for Environment<'static> {
38    fn from(value: LuaEnvironment) -> Self {
39        value.0
40    }
41}
42
43impl Deref for LuaEnvironment {
44    type Target = Environment<'static>;
45
46    fn deref(&self) -> &Self::Target {
47        &self.0
48    }
49}
50
51impl fmt::Display for LuaEnvironment {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "Environment")
54    }
55}
56
57#[mlua::userdata_impl]
58impl LuaEnvironment {
59    /// Get a new environment
60    #[lua(name = "new", infallible)]
61    pub(crate) fn lua_new() -> Self {
62        let mut env = Environment::new();
63
64        #[cfg(feature = "minijinja-contrib")]
65        minijinja_contrib::add_to_environment(&mut env);
66
67        #[cfg(feature = "json")]
68        crate::contrib::json::add_to_environment(&mut env);
69
70        #[cfg(feature = "datetime")]
71        crate::contrib::datetime::add_to_environment(&mut env);
72
73        env.into()
74    }
75
76    /// Get a new empty environment
77    #[lua(name = "empty", infallible)]
78    pub(crate) fn lua_empty() -> Self {
79        Environment::empty().into()
80    }
81
82    #[lua(name = "keep_trailing_newline", getter, infallible)]
83    pub(crate) fn lua_keep_trailing_newline(&self) -> bool {
84        self.0.keep_trailing_newline()
85    }
86
87    #[lua(name = "keep_trailing_newline", setter, infallible)]
88    pub(crate) fn lua_set_keep_trailing_newline(&mut self, val: bool) {
89        self.0.set_keep_trailing_newline(val)
90    }
91
92    #[lua(name = "trim_blocks", getter, infallible)]
93    pub(crate) fn lua_trim_blocks(&self) -> bool {
94        self.0.trim_blocks()
95    }
96
97    #[lua(name = "trim_blocks", setter, infallible)]
98    pub(crate) fn lua_set_trim_blocks(&mut self, val: bool) {
99        self.0.set_trim_blocks(val)
100    }
101
102    #[lua(name = "lstrip_blocks", getter, infallible)]
103    pub(crate) fn lua_lstrip_blocks(&self) -> bool {
104        self.0.lstrip_blocks()
105    }
106
107    #[lua(name = "lstrip_blocks", setter, infallible)]
108    pub(crate) fn lua_set_lstrip_blocks(&mut self, val: bool) {
109        self.0.set_lstrip_blocks(val)
110    }
111
112    #[lua(name = "debug", getter, infallible)]
113    pub(crate) fn lua_debug(&self) -> bool {
114        self.0.debug()
115    }
116
117    #[lua(name = "debug", setter, infallible)]
118    pub(crate) fn lua_set_debug(&mut self, val: bool) {
119        self.0.set_debug(val)
120    }
121
122    #[lua(name = "fuel", getter, infallible)]
123    pub(crate) fn lua_fuel(&self) -> Option<u64> {
124        self.0.fuel()
125    }
126
127    #[lua(name = "fuel", setter, infallible)]
128    pub(crate) fn lua_set_fuel(&mut self, val: Option<u64>) {
129        self.0.set_fuel(val)
130    }
131
132    #[lua(name = "recursion_limit", getter, infallible)]
133    pub(crate) fn lua_recursion_limit(&self) -> usize {
134        self.0.recursion_limit()
135    }
136
137    #[lua(name = "recursion_limit", setter, infallible)]
138    pub(crate) fn lua_set_recursion_limit(&mut self, val: usize) {
139        self.0.set_recursion_limit(val)
140    }
141
142    #[lua(name = "undefined_behavior", getter, infallible)]
143    pub(crate) fn lua_undefined_behavior(&self) -> LuaUndefinedBehavior {
144        self.0.undefined_behavior().into()
145    }
146
147    #[lua(name = "undefined_behavior", setter, infallible)]
148    pub(crate) fn lua_set_undefined_behavior(&mut self, val: LuaUndefinedBehavior) {
149        self.0.set_undefined_behavior(val.into());
150    }
151
152    #[lua(name = "add_template", infallible)]
153    pub(crate) fn lua_add_template(
154        &mut self,
155        lua: &mlua::Lua,
156        name: String,
157        source: String,
158    ) -> mlua::Result<()> {
159        bind_lua(lua, || {
160            self.0
161                .add_template_owned(name, source)
162                .map_err(mlua::Error::external)
163        })
164    }
165
166    #[lua(name = "remove_template", infallible)]
167    pub(crate) fn lua_remove_template(&mut self, lua: &mlua::Lua, name: &str) {
168        bind_lua(lua, || self.0.remove_template(name))
169    }
170
171    #[lua(name = "clear_templates", infallible)]
172    pub(crate) fn lua_clear_templates(&mut self, lua: &mlua::Lua) {
173        bind_lua(lua, || self.0.clear_templates())
174    }
175
176    #[lua(name = "undeclared_variables")]
177    pub(crate) fn lua_undeclared_variables(
178        &mut self,
179        lua: &mlua::Lua,
180        name: &str,
181        nested: Option<bool>,
182    ) -> mlua::Result<mlua::Value> {
183        bind_lua(lua, || {
184            let nested = nested.unwrap_or(false);
185
186            let vars = self
187                .0
188                .get_template(name)
189                .map_err(mlua::Error::external)?
190                .undeclared_variables(nested);
191
192            lua.to_value(&vars)
193        })
194    }
195
196    #[lua(name = "set_loader")]
197    pub(crate) fn lua_set_loader(
198        &mut self,
199        lua: &mlua::Lua,
200        callback: mlua::Function,
201    ) -> mlua::Result<()> {
202        let func = LuaFunctionObject::from_value(lua, &callback)?;
203
204        self.0.set_loader(move |name| {
205            func.with_func::<Option<mlua::LuaString>>(args!(name), None)
206                .map(|v| v.and_then(|v| v.as_str().map(|s| s.to_string())))
207        });
208
209        Ok(())
210    }
211
212    #[lua(name = "set_path_join_callback")]
213    pub(crate) fn lua_set_path_join_callback(
214        &mut self,
215        lua: &mlua::Lua,
216        callback: mlua::Function,
217    ) -> mlua::Result<()> {
218        let func = LuaFunctionObject::from_value(lua, &callback)?;
219
220        self.0.set_path_join_callback(move |name, parent| {
221            func.with_func::<String>(args!(name, parent), None)
222                .ok()
223                .flatten()
224                .and_then(|v| v.as_str().map(|s| Cow::Owned(s.to_string())))
225                .unwrap_or(Cow::Borrowed(name))
226        });
227
228        Ok(())
229    }
230
231    #[lua(name = "set_unknown_method_callback")]
232    pub(crate) fn lua_set_unknown_method_callback(
233        &mut self,
234        lua: &mlua::Lua,
235        callback: mlua::Function,
236    ) -> mlua::Result<()> {
237        let mut func = LuaFunctionObject::from_value(lua, &callback)?;
238        func.set_pass_state(true);
239
240        self.0
241            .set_unknown_method_callback(move |state, value, method, args| {
242                func.with_func::<mlua::MultiValue>(args!(value, method, ..args), Some(state))
243                    .map(|v| v.unwrap_or_default())
244            });
245
246        Ok(())
247    }
248
249    #[cfg(feature = "minijinja-contrib")]
250    #[lua(name = "set_pycompat", infallible)]
251    pub(crate) fn lua_set_pycompat(&mut self, enable: Option<bool>) {
252        match enable {
253            Some(true) | None => self
254                .0
255                .set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback),
256            Some(false) => self.0.set_unknown_method_callback(|_, _, _, _| {
257                Err(JinjaError::from(JinjaErrorKind::UnknownMethod))
258            }),
259        }
260    }
261
262    #[lua(name = "set_auto_escape_callback")]
263    pub(crate) fn lua_set_auto_escape_callback(
264        &mut self,
265        lua: &mlua::Lua,
266        callback: mlua::Function,
267    ) -> mlua::Result<()> {
268        let func = LuaFunctionObject::from_value(lua, &callback)?;
269
270        self.0
271            .set_auto_escape_callback(move |name| -> minijinja::AutoEscape {
272                func.with_func_ser::<LuaAutoEscape>(args!(name), None)
273                    .unwrap_or_default()
274                    .into()
275            });
276
277        Ok(())
278    }
279
280    #[lua(name = "set_formatter")]
281    pub(crate) fn lua_set_formatter(
282        &mut self,
283        lua: &mlua::Lua,
284        callback: mlua::Function,
285    ) -> mlua::Result<()> {
286        let mut func = LuaFunctionObject::from_value(lua, &callback)?;
287        func.set_pass_state(true);
288
289        self.0.set_formatter(move |out, state, value| {
290            func.with_func::<Option<String>>(args!(value), Some(state))
291                .ok()
292                .flatten()
293                .map(|val| {
294                    let s = val.as_str().ok_or_else(|| {
295                        JinjaError::new(
296                            JinjaErrorKind::WriteFailure,
297                            "formatter must return a string",
298                        )
299                    })?;
300                    out.write_str(s).map_err(|err| {
301                        JinjaError::new(JinjaErrorKind::WriteFailure, err.to_string())
302                    })
303                })
304                .unwrap_or(Ok(()))
305        });
306
307        Ok(())
308    }
309
310    #[lua(name = "set_syntax")]
311    pub(crate) fn lua_set_syntax(&mut self, syntax: LuaSyntaxConfig) -> mlua::Result<()> {
312        self.0.set_syntax(syntax.into());
313
314        Ok(())
315    }
316
317    #[lua(name = "render_template")]
318    pub(crate) fn lua_render_template(
319        &mut self,
320        lua: &mlua::Lua,
321        name: &str,
322        ctx: Option<mlua::Table>,
323    ) -> mlua::Result<String> {
324        let ctx: Option<JinjaValue> = ctx
325            .and_then(|t| LuaTableObject::from_value(lua, &t).ok())
326            .map(|obj| obj.into());
327
328        bind_lua(lua, || {
329            self.0
330                .get_template(name)
331                .map_err(mlua::Error::external)?
332                .render(ctx)
333                .map_err(mlua::Error::external)
334        })
335    }
336
337    #[lua(name = "render_str")]
338    pub(crate) fn lua_render_str(
339        &self,
340        lua: &mlua::Lua,
341        source: &str,
342        ctx: Option<mlua::Table>,
343        name: Option<String>,
344    ) -> mlua::Result<String> {
345        let ctx: Option<JinjaValue> = ctx
346            .and_then(|t| LuaTableObject::from_value(lua, &t).ok())
347            .map(|obj| obj.into());
348
349        let name = name.unwrap_or("<string>".to_string());
350
351        bind_lua(lua, || {
352            self.0
353                .render_named_str(&name, source, ctx)
354                .map_err(mlua::Error::external)
355        })
356    }
357
358    #[lua(name = "render_captured")]
359    pub(crate) fn lua_render_captured(
360        &mut self,
361        lua: &mlua::Lua,
362        name: &str,
363        ctx: Option<mlua::Table>,
364        callback: mlua::Function,
365    ) -> mlua::Result<mlua::MultiValue> {
366        let mut func = LuaFunctionObject::from_value(lua, &callback)?;
367        func.set_pass_state(true);
368
369        let ctx: Option<JinjaValue> = ctx
370            .and_then(|t| LuaTableObject::from_value(lua, &t).ok())
371            .map(|obj| obj.into());
372
373        bind_lua(lua, || {
374            let mut captured = self
375                .0
376                .get_template(name)
377                .map_err(mlua::Error::external)?
378                .render_captured(ctx)
379                .map_err(mlua::Error::external)?;
380
381            let mut mv = captured
382                .with_state_mut(|state| func.with_func_mut::<mlua::MultiValue>(&[], Some(state)))
383                .map_err(mlua::Error::external)?
384                .and_then(|v| minijinja_to_lua(lua, &v))
385                .unwrap_or_default();
386
387            let rendered = captured.into_output();
388
389            mv.push_front(mlua::Value::String(lua.create_string(rendered)?));
390
391            Ok(mv)
392        })
393    }
394
395    #[lua(name = "eval")]
396    pub(crate) fn lua_eval(
397        &self,
398        lua: &mlua::Lua,
399        source: &str,
400        ctx: Option<mlua::Table>,
401    ) -> mlua::Result<mlua::MultiValue> {
402        let ctx: Option<JinjaValue> = ctx
403            .and_then(|t| LuaTableObject::from_value(lua, &t).ok())
404            .map(|obj| obj.into());
405
406        bind_lua(lua, || {
407            let expr = self
408                .0
409                .compile_expression(source)
410                .map_err(mlua::Error::external)?
411                .eval(ctx)
412                .map_err(mlua::Error::external)?;
413
414            minijinja_to_lua(lua, &expr).ok_or_else(|| {
415                mlua::Error::DeserializeError("could not convert output to lua".to_string())
416            })
417        })
418    }
419
420    #[lua(name = "add_filter")]
421    pub(crate) fn lua_add_filter(
422        &mut self,
423        lua: &mlua::Lua,
424        name: String,
425        filter: mlua::Function,
426        pass_state: Option<bool>,
427    ) -> mlua::Result<()> {
428        let mut func = LuaFunctionObject::from_value(lua, &filter)?;
429        func.set_pass_state(pass_state.unwrap_or(true));
430
431        self.0
432            .add_filter(name, move |state: &State, args: JinjaRest<JinjaValue>| {
433                func.with_func::<mlua::MultiValue>(&args, Some(state))
434            });
435
436        Ok(())
437    }
438
439    #[lua(name = "remove_filter", infallible)]
440    pub(crate) fn lua_remove_filter(&mut self, name: String) {
441        self.0.remove_filter(&name)
442    }
443
444    #[lua(name = "add_test")]
445    pub(crate) fn lua_add_test(
446        &mut self,
447        lua: &mlua::Lua,
448        name: String,
449        test: mlua::Function,
450        pass_state: Option<bool>,
451    ) -> mlua::Result<()> {
452        let mut func = LuaFunctionObject::from_value(lua, &test)?;
453        func.set_pass_state(pass_state.unwrap_or(true));
454
455        self.0
456            .add_test(name, move |state: &State, args: JinjaRest<JinjaValue>| {
457                func.with_func::<bool>(&args, Some(state))
458            });
459
460        Ok(())
461    }
462
463    #[lua(name = "remove_test", infallible)]
464    pub(crate) fn lua_remove_test(&mut self, name: String) {
465        self.0.remove_test(&name)
466    }
467
468    #[lua(name = "add_global")]
469    pub(crate) fn add_global(
470        &mut self,
471        lua: &mlua::Lua,
472        name: String,
473        val: mlua::Value,
474        pass_state: Option<bool>,
475    ) -> mlua::Result<()> {
476        match val {
477            mlua::Value::Function(f) => {
478                let mut func = LuaFunctionObject::from_value(lua, &f)?;
479                func.set_pass_state(pass_state.unwrap_or(true));
480
481                self.0
482                    .add_function(name, move |state: &State, args: JinjaRest<JinjaValue>| {
483                        func.with_func::<mlua::MultiValue>(&args, Some(state))
484                    })
485            },
486            _ => self.0.add_global(name, lua_to_minijinja(lua, &val)),
487        };
488
489        Ok(())
490    }
491
492    #[lua(name = "remove_global", infallible)]
493    pub(crate) fn lua_remove_global(&mut self, name: &str) {
494        self.0.remove_global(name)
495    }
496
497    #[lua(name = "globals")]
498    pub(crate) fn lua_globals(&self, lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
499        let table = lua.create_table()?;
500
501        for (name, value) in self.0.globals() {
502            minijinja_to_lua(lua, &value)
503                .and_then(|mut v| table.set(name, v.pop_front().unwrap_or_default()).ok());
504        }
505
506        Ok(table)
507    }
508}