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        |lua, this, ()| -> mlua::Result<mlua::Value> {
165            let ae: LuaAutoEscape = this.state().auto_escape().into();
166            lua.to_value(&ae)
167        },
168    );
169
170    // The current undefined behavior
171    methods.add_method(
172        "undefined_behavior",
173        |lua, this, ()| -> mlua::Result<mlua::Value> {
174            let ub: LuaUndefinedBehavior = this.state().undefined_behavior().into();
175            lua.to_value(&ub)
176        },
177    );
178
179    // The name of the current block
180    methods.add_method(
181        "current_block",
182        |_, this, ()| -> mlua::Result<Option<String>> {
183            Ok(this.state().current_block().map(|s| s.to_string()))
184        },
185    );
186
187    // Lookup a value by key in the current context
188    methods.add_method(
189        "lookup",
190        |lua, this, name: mlua::BorrowedStr| -> mlua::Result<mlua::MultiValue> {
191            // Since the context may contain dynamic objects, convert the returned value
192            // through the custom layer before returning.
193            Ok(this
194                .state()
195                .lookup(&name)
196                .and_then(|v| minijinja_to_lua(lua, &v))
197                .unwrap_or_default())
198        },
199    );
200
201    // Call the named macro with the provided args.
202    methods.add_method(
203        "call_macro",
204        |lua,
205         this,
206         (name, mut args): (mlua::BorrowedStr, mlua::MultiValue)|
207         -> mlua::Result<String> {
208            let args: Vec<JinjaValue> = lua_args_to_minijinja(lua, &mut args, true);
209
210            this.state()
211                .call_macro(&name, &args)
212                .map_err(mlua::Error::external)
213        },
214    );
215
216    // A list of exported variables
217    methods.add_method("exports", |_, this, ()| -> mlua::Result<Vec<String>> {
218        Ok(this
219            .state()
220            .exports()
221            .into_iter()
222            .map(|i| i.to_string())
223            .collect())
224    });
225
226    // A list of all known variables
227    methods.add_method(
228        "known_variables",
229        |_, this, ()| -> mlua::Result<Vec<String>> {
230            Ok(this
231                .state()
232                .known_variables()
233                .into_iter()
234                .map(|i| i.to_string())
235                .collect())
236        },
237    );
238
239    // Apply the named filter with the provided args
240    methods.add_method(
241        "apply_filter",
242        |lua,
243         this,
244         (filter, mut args): (mlua::BorrowedStr, mlua::MultiValue)|
245         -> mlua::Result<mlua::MultiValue> {
246            let args: Vec<JinjaValue> = lua_args_to_minijinja(lua, &mut args, true);
247
248            // Since the context may contain dynamic objects, convert the returned value
249            // through the custom layer before returning.
250            this.state()
251                .apply_filter(&filter, &args)
252                .map(|v| minijinja_to_lua(lua, &v).unwrap_or_default())
253                .map_err(mlua::Error::external)
254        },
255    );
256
257    // Perform the named test with the provided args
258    methods.add_method(
259        "perform_test",
260        |lua,
261         this,
262         (test, mut args): (mlua::BorrowedStr, mlua::MultiValue)|
263         -> mlua::Result<bool> {
264            let args: Vec<JinjaValue> = lua_args_to_minijinja(lua, &mut args, true);
265
266            this.state()
267                .perform_test(&test, &args)
268                .map_err(mlua::Error::external)
269        },
270    );
271
272    // Format a value to a string
273    methods.add_method(
274        "format",
275        |lua, this, val: mlua::Value| -> mlua::Result<String> {
276            let val = lua_to_minijinja(lua, &val).unwrap_or_default();
277
278            this.state().format(val).map_err(mlua::Error::external)
279        },
280    );
281
282    // A tuple of the current and remaining fuel usage
283    methods.add_method(
284        "fuel_levels",
285        |lua, this, ()| -> mlua::Result<mlua::Value> { lua.to_value(&this.state().fuel_levels()) },
286    );
287
288    // Get a temp value.
289    // See: https://docs.rs/minijinja/latest/minijinja/struct.State.html#method.get_temp
290    methods.add_method(
291        "get_temp",
292        |lua, this, name: mlua::BorrowedStr| -> mlua::Result<mlua::MultiValue> {
293            // Since the context may contain dynamic objects, convert the returned value
294            // through the custom layer before returning.
295            Ok(this
296                .state()
297                .get_temp(&name)
298                .and_then(|v| minijinja_to_lua(lua, &v))
299                .unwrap_or_default())
300        },
301    );
302
303    // Set a temp value and return the old value
304    methods.add_method(
305        "set_temp",
306        |lua,
307         this,
308         (name, val): (mlua::BorrowedStr, mlua::Value)|
309         -> mlua::Result<mlua::MultiValue> {
310            if let Some(val) = lua_to_minijinja(lua, &val) {
311                Ok(this
312                    .state()
313                    .set_temp(&name, val)
314                    .and_then(|v| minijinja_to_lua(lua, &v))
315                    .unwrap_or_default())
316            } else {
317                Err(mlua::Error::FromLuaConversionError {
318                    from: val.type_name(),
319                    to: "minijinja::Value".to_string(),
320                    message: None,
321                })
322            }
323        },
324    );
325
326    // Get a temp value or call `func` to add the value
327    methods.add_method(
328        "get_or_set_temp",
329        |lua,
330         this,
331         (name, func): (mlua::BorrowedStr, mlua::Function)|
332         -> mlua::Result<mlua::MultiValue> {
333            let val = match this.state().get_temp(&name) {
334                Some(v) => v,
335                None => {
336                    let val = func.call::<mlua::Value>(mlua::Value::Nil)?;
337
338                    if let Some(val) = lua_to_minijinja(lua, &val) {
339                        this.state().set_temp(&name, val.clone());
340                        val
341                    } else {
342                        return Err(mlua::Error::FromLuaConversionError {
343                            from: val.type_name(),
344                            to: "minijinja::Value".to_string(),
345                            message: None,
346                        });
347                    }
348                },
349            };
350
351            Ok(minijinja_to_lua(lua, &val).unwrap_or_default())
352        },
353    );
354}