Skip to main content

minijinja_lua/
state.rs

1// SPDX-License-Identifier: MIT
2
3use std::{
4    fmt,
5    ops::{Deref, DerefMut},
6};
7
8use minijinja::Value as JinjaValue;
9use mlua::LuaSerdeExt;
10
11use crate::convert::{
12    LuaAutoEscape,
13    LuaUndefinedBehavior,
14    lua_args_to_minijinja,
15    lua_to_minijinja,
16    minijinja_to_lua,
17};
18
19pub(crate) trait LuaState<'template, 'env> {
20    fn state(&self) -> &minijinja::State<'template, 'env>;
21}
22
23/// A [`mlua::UserData`] wrapper around a [`minijinja::State`]. This is passed to
24/// filters and other callbacks in the Jinja environment. It can only be
25/// initialized within an [`mlua::Lua::scope`] callback, as it is not `'static`
26#[derive(Debug)]
27pub struct LuaStateRef<'scope, 'template, 'env>(&'scope minijinja::State<'template, 'env>);
28
29impl<'scope, 'template, 'env> From<&'scope minijinja::State<'template, 'env>>
30    for LuaStateRef<'scope, 'template, 'env>
31{
32    fn from(value: &'scope minijinja::State<'template, 'env>) -> Self {
33        LuaStateRef(value)
34    }
35}
36
37impl<'scope, 'template, 'env> From<LuaStateRef<'scope, 'template, 'env>>
38    for &'scope minijinja::State<'template, 'env>
39{
40    fn from(value: LuaStateRef<'scope, 'template, 'env>) -> Self {
41        value.0
42    }
43}
44
45impl<'scope, 'template, 'env> Deref for LuaStateRef<'scope, 'template, 'env> {
46    type Target = minijinja::State<'template, 'env>;
47
48    fn deref(&self) -> &Self::Target {
49        self.0
50    }
51}
52
53impl<'scope, 'template, 'env> LuaState<'template, 'env> for LuaStateRef<'scope, 'template, 'env> {
54    fn state(&self) -> &minijinja::State<'template, 'env> {
55        self.0
56    }
57}
58
59impl<'scope, 'template, 'env> fmt::Display for LuaStateRef<'scope, 'template, 'env> {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(f, "State")
62    }
63}
64
65impl<'scope, 'template, 'env> mlua::UserData for LuaStateRef<'scope, 'template, 'env> {
66    fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
67        fields.add_meta_field("__name", "state");
68    }
69
70    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
71        add_common_methods(methods);
72    }
73}
74
75/// A [`mlua::UserData`] wrapper around a mutable [`minijinja::State`]. This is passed to
76/// the callback provided to [`LuaEnvironment.render_captured`](crate::environment::LuaEnvironment).
77/// It can only be initialized within an [`mlua::Lua::scope`] callback, as it is not `'static`
78#[derive(Debug)]
79pub struct LuaStateMut<'scope, 'template, 'env>(&'scope mut minijinja::State<'template, 'env>);
80
81impl<'scope, 'template, 'env> LuaStateMut<'scope, 'template, 'env> {
82    fn state_mut(&mut self) -> &mut minijinja::State<'template, 'env> {
83        self.0
84    }
85}
86
87impl<'scope, 'template, 'env> From<&'scope mut minijinja::State<'template, 'env>>
88    for LuaStateMut<'scope, 'template, 'env>
89{
90    fn from(value: &'scope mut minijinja::State<'template, 'env>) -> Self {
91        LuaStateMut(value)
92    }
93}
94
95impl<'scope, 'template, 'env> From<LuaStateMut<'scope, 'template, 'env>>
96    for &'scope mut minijinja::State<'template, 'env>
97{
98    fn from(value: LuaStateMut<'scope, 'template, 'env>) -> Self {
99        value.0
100    }
101}
102
103impl<'scope, 'template, 'env> Deref for LuaStateMut<'scope, 'template, 'env> {
104    type Target = minijinja::State<'template, 'env>;
105
106    fn deref(&self) -> &Self::Target {
107        self.0
108    }
109}
110
111impl<'scope, 'template, 'env> DerefMut for LuaStateMut<'scope, 'template, 'env> {
112    fn deref_mut(&mut self) -> &mut Self::Target {
113        self.0
114    }
115}
116
117impl<'scope, 'template, 'env> LuaState<'template, 'env> for LuaStateMut<'scope, 'template, 'env> {
118    fn state(&self) -> &minijinja::State<'template, 'env> {
119        self.0
120    }
121}
122
123impl<'scope, 'template, 'env> fmt::Display for LuaStateMut<'scope, 'template, 'env> {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(f, "State")
126    }
127}
128
129impl<'scope, 'template, 'env> mlua::UserData for LuaStateMut<'scope, 'template, 'env> {
130    fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
131        fields.add_meta_field("__name", "state");
132    }
133
134    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
135        add_common_methods(methods);
136
137        // Render the named block
138        methods.add_method_mut(
139            "render_block",
140            |_, this, block: mlua::BorrowedStr| -> mlua::Result<String> {
141                this.state_mut()
142                    .render_block(&block)
143                    .map_err(mlua::Error::external)
144            },
145        );
146    }
147}
148
149/// A helper to add methods shared between [`LuaStateRef`] and [`LuaStateMut`].
150fn add_common_methods<'template, 'env, S, M>(methods: &mut M)
151where
152    S: LuaState<'template, 'env>,
153    M: mlua::UserDataMethods<S>,
154    'env: 'template,
155{
156    // The name of the current template
157    methods.add_method("name", |_, this, ()| -> mlua::Result<String> {
158        Ok(this.state().name().to_string())
159    });
160
161    // The current auto escape flag
162    methods.add_method(
163        "auto_escape",
164        |_, this, ()| -> mlua::Result<LuaAutoEscape> { Ok(this.state().auto_escape().into()) },
165    );
166
167    // The current undefined behavior
168    methods.add_method(
169        "undefined_behavior",
170        |_, this, ()| -> mlua::Result<LuaUndefinedBehavior> {
171            Ok(this.state().undefined_behavior().into())
172        },
173    );
174
175    // The name of the current block
176    methods.add_method(
177        "current_block",
178        |_, this, ()| -> mlua::Result<Option<String>> {
179            Ok(this.state().current_block().map(|s| s.to_string()))
180        },
181    );
182
183    // Lookup a value by key in the current context
184    methods.add_method(
185        "lookup",
186        |lua, this, name: mlua::BorrowedStr| -> mlua::Result<mlua::MultiValue> {
187            // Since the context may contain dynamic objects, convert the returned value
188            // through the custom layer before returning.
189            Ok(this
190                .state()
191                .lookup(&name)
192                .and_then(|v| minijinja_to_lua(lua, &v))
193                .unwrap_or_default())
194        },
195    );
196
197    // Call the named macro with the provided args.
198    methods.add_method(
199        "call_macro",
200        |lua,
201         this,
202         (name, mut args): (mlua::BorrowedStr, mlua::MultiValue)|
203         -> mlua::Result<String> {
204            let args: Vec<JinjaValue> = lua_args_to_minijinja(lua, &mut args, true);
205
206            this.state()
207                .call_macro(&name, &args)
208                .map_err(mlua::Error::external)
209        },
210    );
211
212    // A list of exported variables
213    methods.add_method("exports", |_, this, ()| -> mlua::Result<Vec<String>> {
214        Ok(this
215            .state()
216            .exports()
217            .into_iter()
218            .map(|i| i.to_string())
219            .collect())
220    });
221
222    // A list of all known variables
223    methods.add_method(
224        "known_variables",
225        |_, this, ()| -> mlua::Result<Vec<String>> {
226            Ok(this
227                .state()
228                .known_variables()
229                .into_iter()
230                .map(|i| i.to_string())
231                .collect())
232        },
233    );
234
235    // Apply the named filter with the provided args
236    methods.add_method(
237        "apply_filter",
238        |lua,
239         this,
240         (filter, mut args): (mlua::BorrowedStr, mlua::MultiValue)|
241         -> mlua::Result<mlua::MultiValue> {
242            let args: Vec<JinjaValue> = lua_args_to_minijinja(lua, &mut args, true);
243
244            // Since the context may contain dynamic objects, convert the returned value
245            // through the custom layer before returning.
246            this.state()
247                .apply_filter(&filter, &args)
248                .map(|v| minijinja_to_lua(lua, &v).unwrap_or_default())
249                .map_err(mlua::Error::external)
250        },
251    );
252
253    // Perform the named test with the provided args
254    methods.add_method(
255        "perform_test",
256        |lua,
257         this,
258         (test, mut args): (mlua::BorrowedStr, mlua::MultiValue)|
259         -> mlua::Result<bool> {
260            let args: Vec<JinjaValue> = lua_args_to_minijinja(lua, &mut args, true);
261
262            this.state()
263                .perform_test(&test, &args)
264                .map_err(mlua::Error::external)
265        },
266    );
267
268    // Format a value to a string
269    methods.add_method(
270        "format",
271        |lua, this, val: mlua::Value| -> mlua::Result<String> {
272            let val = lua_to_minijinja(lua, &val).unwrap_or_default();
273
274            this.state().format(val).map_err(mlua::Error::external)
275        },
276    );
277
278    // A tuple of the current and remaining fuel usage
279    methods.add_method(
280        "fuel_levels",
281        |lua, this, ()| -> mlua::Result<mlua::Value> { lua.to_value(&this.state().fuel_levels()) },
282    );
283
284    // Get a temp value.
285    // See: https://docs.rs/minijinja/latest/minijinja/struct.State.html#method.get_temp
286    methods.add_method(
287        "get_temp",
288        |lua, this, name: mlua::BorrowedStr| -> mlua::Result<mlua::MultiValue> {
289            // Since the context may contain dynamic objects, convert the returned value
290            // through the custom layer before returning.
291            Ok(this
292                .state()
293                .get_temp(&name)
294                .and_then(|v| minijinja_to_lua(lua, &v))
295                .unwrap_or_default())
296        },
297    );
298
299    // Set a temp value and return the old value
300    methods.add_method(
301        "set_temp",
302        |lua,
303         this,
304         (name, val): (mlua::BorrowedStr, mlua::Value)|
305         -> mlua::Result<mlua::MultiValue> {
306            if let Some(val) = lua_to_minijinja(lua, &val) {
307                Ok(this
308                    .state()
309                    .set_temp(&name, val)
310                    .and_then(|v| minijinja_to_lua(lua, &v))
311                    .unwrap_or_default())
312            } else {
313                Err(mlua::Error::FromLuaConversionError {
314                    from: val.type_name(),
315                    to: "minijinja::Value".to_string(),
316                    message: None,
317                })
318            }
319        },
320    );
321
322    // Get a temp value or call `func` to add the value
323    methods.add_method(
324        "get_or_set_temp",
325        |lua,
326         this,
327         (name, func): (mlua::BorrowedStr, mlua::Function)|
328         -> mlua::Result<mlua::MultiValue> {
329            let val = match this.state().get_temp(&name) {
330                Some(v) => v,
331                None => {
332                    let val = func.call::<mlua::Value>(mlua::Value::Nil)?;
333
334                    if let Some(val) = lua_to_minijinja(lua, &val) {
335                        this.state().set_temp(&name, val.clone());
336                        val
337                    } else {
338                        return Err(mlua::Error::FromLuaConversionError {
339                            from: val.type_name(),
340                            to: "minijinja::Value".to_string(),
341                            message: None,
342                        });
343                    }
344                },
345            };
346
347            Ok(minijinja_to_lua(lua, &val).unwrap_or_default())
348        },
349    );
350}