minijinja/vm/state.rs
1use std::borrow::Cow;
2use std::collections::{BTreeMap, BTreeSet};
3use std::fmt;
4use std::sync::{Arc, Mutex};
5
6use crate::compiler::instructions::Instructions;
7use crate::environment::Environment;
8use crate::error::{Error, ErrorKind};
9use crate::output::Output;
10use crate::template::Template;
11use crate::utils::{AutoEscape, UndefinedBehavior};
12use crate::value::{ArgType, Object, Value};
13use crate::vm::context::Context;
14
15#[cfg(feature = "fuel")]
16use crate::vm::fuel::FuelTracker;
17
18/// When macros are used, the state carries an `id` counter. Whenever a state is
19/// created, the counter is incremented. This exists because macros can keep a reference
20/// to instructions from another state by index. Without this counter it would
21/// be possible for a macro to be called with a different state (different id)
22/// which mean we likely panic.
23#[cfg(feature = "macros")]
24static STATE_ID: std::sync::atomic::AtomicIsize = std::sync::atomic::AtomicIsize::new(0);
25
26/// Provides access to the current execution state of the engine.
27///
28/// A read only reference is passed to filter functions and similar objects to
29/// allow limited interfacing with the engine. The state is useful to look up
30/// information about the engine in filter, test or global functions. It not
31/// only provides access to the template environment but also the context
32/// variables of the engine, the current auto escaping behavior as well as the
33/// auto escape flag.
34///
35/// In some testing scenarios or more advanced use cases you might need to get
36/// a [`State`]. The state is managed as part of the template execution but the
37/// initial state can be retrieved via [`Template::new_state`](crate::Template::new_state).
38/// The most common way to get hold of the state however is via functions of filters.
39///
40/// **Notes on lifetimes:** the state object exposes some of the internal
41/// lifetimes through the type. You should always elide these lifetimes
42/// as there might be lifetimes added or removed between releases.
43pub struct State<'template, 'env> {
44 pub(crate) ctx: Context<'env>,
45 pub(crate) current_block: Option<&'env str>,
46 pub(crate) auto_escape: AutoEscape,
47 pub(crate) instructions: &'template Instructions<'env>,
48 pub(crate) temps: Arc<Mutex<BTreeMap<Box<str>, Value>>>,
49 pub(crate) blocks: BTreeMap<&'env str, BlockStack<'template, 'env>>,
50 #[allow(unused)]
51 pub(crate) loaded_templates: BTreeSet<&'env str>,
52 #[cfg(feature = "macros")]
53 pub(crate) id: isize,
54 #[cfg(feature = "macros")]
55 pub(crate) macros: std::sync::Arc<Vec<(&'template Instructions<'env>, u32)>>,
56 #[cfg(feature = "macros")]
57 pub(crate) closure_tracker: std::sync::Arc<crate::vm::closure_object::ClosureTracker>,
58 #[cfg(feature = "fuel")]
59 pub(crate) fuel_tracker: Option<std::sync::Arc<FuelTracker>>,
60}
61
62impl fmt::Debug for State<'_, '_> {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 let mut ds = f.debug_struct("State");
65 ds.field("name", &self.instructions.name());
66 ds.field("current_block", &self.current_block);
67 ds.field("auto_escape", &self.auto_escape);
68 ds.field("ctx", &self.ctx);
69 ds.field("env", &self.env());
70 ds.finish()
71 }
72}
73
74impl<'template, 'env> State<'template, 'env> {
75 /// Creates a new state.
76 pub(crate) fn new(
77 ctx: Context<'env>,
78 auto_escape: AutoEscape,
79 instructions: &'template Instructions<'env>,
80 blocks: BTreeMap<&'env str, BlockStack<'template, 'env>>,
81 ) -> State<'template, 'env> {
82 State {
83 #[cfg(feature = "macros")]
84 id: STATE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
85 current_block: None,
86 auto_escape,
87 instructions,
88 blocks,
89 temps: Default::default(),
90 loaded_templates: BTreeSet::new(),
91 #[cfg(feature = "macros")]
92 macros: Default::default(),
93 #[cfg(feature = "macros")]
94 closure_tracker: Default::default(),
95 #[cfg(feature = "fuel")]
96 fuel_tracker: ctx.env().fuel().map(FuelTracker::new),
97 ctx,
98 }
99 }
100
101 /// Creates an empty state for an environment.
102 pub(crate) fn new_for_env(env: &'env Environment) -> State<'env, 'env> {
103 State::new(
104 Context::new(env),
105 AutoEscape::None,
106 &crate::compiler::instructions::EMPTY_INSTRUCTIONS,
107 BTreeMap::new(),
108 )
109 }
110
111 /// Returns a reference to the current environment.
112 #[inline(always)]
113 pub fn env(&self) -> &'env Environment<'env> {
114 self.ctx.env()
115 }
116
117 /// Returns the name of the current template.
118 pub fn name(&self) -> &str {
119 self.instructions.name()
120 }
121
122 /// Returns the current value of the auto escape flag.
123 #[inline(always)]
124 pub fn auto_escape(&self) -> AutoEscape {
125 self.auto_escape
126 }
127
128 /// Returns the current undefined behavior.
129 #[inline(always)]
130 pub fn undefined_behavior(&self) -> UndefinedBehavior {
131 self.env().undefined_behavior()
132 }
133
134 /// Returns the name of the innermost block.
135 #[inline(always)]
136 pub fn current_block(&self) -> Option<&str> {
137 self.current_block
138 }
139
140 /// Looks up a variable by name in the context.
141 ///
142 /// # Note on Closures
143 ///
144 /// Macros and call blocks analyze which variables are referenced and
145 /// create closures for them. This means that unless a variable is defined
146 /// as a [global](Environment::add_global) in the environment, was passed in the
147 /// initial render context, or was referenced by a macro, this method won't be
148 /// able to find it.
149 #[inline(always)]
150 pub fn lookup(&self, name: &str) -> Option<Value> {
151 self.ctx.load(name)
152 }
153
154 /// Looks up a global macro and calls it.
155 ///
156 /// This looks up a value as [`lookup`](Self::lookup) does and calls it
157 /// with the passed args.
158 #[cfg(feature = "macros")]
159 #[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
160 pub fn call_macro(&self, name: &str, args: &[Value]) -> Result<String, Error> {
161 let f = ok!(self.lookup(name).ok_or_else(|| Error::new(
162 crate::error::ErrorKind::UnknownFunction,
163 "macro not found"
164 )));
165 f.call(self, args).map(Into::into)
166 }
167
168 /// Renders a block with the given name into a string.
169 ///
170 /// This method works like [`Template::render`](crate::Template::render) but
171 /// it only renders a specific block in the template. The first argument is
172 /// the name of the block.
173 ///
174 /// This renders only the block `hi` in the template:
175 ///
176 /// ```
177 /// # use minijinja::{Environment, context};
178 /// # fn test() -> Result<(), minijinja::Error> {
179 /// # let mut env = Environment::new();
180 /// # env.add_template("hello", "{% block hi %}Hello {{ name }}!{% endblock %}")?;
181 /// let tmpl = env.get_template("hello")?;
182 /// let rv = tmpl
183 /// .eval_to_state(context!(name => "John"))?
184 /// .render_block("hi")?;
185 /// println!("{}", rv);
186 /// # Ok(()) }
187 /// ```
188 ///
189 /// Note that rendering a block is a stateful operation. If an error
190 /// is returned the module has to be re-created as the internal state
191 /// can end up corrupted. This also means you can only render blocks
192 /// if you have a mutable reference to the state which is not possible
193 /// from within filters or similar.
194 #[cfg(feature = "multi_template")]
195 #[cfg_attr(docsrs, doc(cfg(feature = "multi_template")))]
196 pub fn render_block(&mut self, block: &str) -> Result<String, Error> {
197 let mut buf = String::new();
198 crate::vm::Vm::new(self.env())
199 .call_block(block, self, &mut Output::new(&mut buf))
200 .map(|_| buf)
201 }
202
203 /// Renders a block with the given name into an [`io::Write`](std::io::Write).
204 ///
205 /// For details see [`render_block`](Self::render_block).
206 #[cfg(feature = "multi_template")]
207 #[cfg_attr(docsrs, doc(cfg(feature = "multi_template")))]
208 pub fn render_block_to_write<W>(&mut self, block: &str, w: W) -> Result<(), Error>
209 where
210 W: std::io::Write,
211 {
212 let mut wrapper = crate::output::WriteWrapper { w, err: None };
213 crate::vm::Vm::new(self.env())
214 .call_block(block, self, &mut Output::new(&mut wrapper))
215 .map(|_| ())
216 .map_err(|err| wrapper.take_err(err))
217 }
218
219 /// Returns a list of the names of all exports (top-level variables).
220 pub fn exports(&self) -> Vec<&str> {
221 self.ctx.exports().keys().copied().collect()
222 }
223
224 /// Returns a list of all known variables.
225 ///
226 /// This list contains all variables that are currently known to the state.
227 /// To retrieve the values you can use [`lookup`](Self::lookup). This will
228 /// include all the globals of the environment. Note that if the context
229 /// has been initialized with an object that lies about variables (eg: it
230 /// does not correctly implement enumeration), the returned list might not
231 /// be complete.
232 pub fn known_variables(&self) -> Vec<Cow<'_, str>> {
233 Vec::from_iter(self.ctx.known_variables(true))
234 }
235
236 /// Fetches a template by name with path joining.
237 ///
238 /// This works like [`Environment::get_template`] with the difference that the lookup
239 /// undergoes path joining. If the environment has a configured path joining callback,
240 /// it will be invoked with the name of the current template as parent template.
241 ///
242 /// For more information see [`Environment::set_path_join_callback`].
243 pub fn get_template(&self, name: &str) -> Result<Template<'env, 'env>, Error> {
244 self.env()
245 .get_template(&self.env().join_template_path(name, self.name()))
246 }
247
248 /// Invokes a filter with some arguments.
249 ///
250 /// ```
251 /// # use minijinja::Environment;
252 /// # let mut env = Environment::new();
253 /// # env.add_filter("upper", |x: &str| x.to_uppercase());
254 /// # let tmpl = env.template_from_str("").unwrap();
255 /// # let state = tmpl.new_state();
256 /// let rv = state.apply_filter("upper", &["hello world".into()]).unwrap();
257 /// assert_eq!(rv.as_str(), Some("HELLO WORLD"));
258 /// ```
259 pub fn apply_filter(&self, filter: &str, args: &[Value]) -> Result<Value, Error> {
260 match self.env().get_filter(filter) {
261 Some(filter) => filter.call(self, args),
262 None => Err(Error::from(ErrorKind::UnknownFilter)),
263 }
264 }
265
266 /// Invokes a test function on a value.
267 ///
268 /// ```
269 /// # use minijinja::Environment;
270 /// # let mut env = Environment::new();
271 /// # env.add_test("even", |x: i32| x % 2 == 0);
272 /// # let tmpl = env.template_from_str("").unwrap();
273 /// # let state = tmpl.new_state();
274 /// let rv = state.perform_test("even", &[42i32.into()]).unwrap();
275 /// assert!(rv);
276 /// ```
277 pub fn perform_test(&self, test: &str, args: &[Value]) -> Result<bool, Error> {
278 match self.env().get_test(test) {
279 Some(test) => test.call(self, args).map(|x| x.is_true()),
280 None => Err(Error::from(ErrorKind::UnknownTest)),
281 }
282 }
283
284 /// Formats a value to a string using the formatter on the environment.
285 ///
286 /// ```
287 /// # use minijinja::{value::Value, Environment};
288 /// # let mut env = Environment::new();
289 /// # let tmpl = env.template_from_str("").unwrap();
290 /// # let state = tmpl.new_state();
291 /// let rv = state.format(Value::from(42)).unwrap();
292 /// assert_eq!(rv, "42");
293 /// ```
294 pub fn format(&self, value: Value) -> Result<String, Error> {
295 let mut rv = String::new();
296 let mut out = Output::new(&mut rv);
297 self.env().format(&value, self, &mut out).map(|_| rv)
298 }
299
300 /// Returns the fuel levels.
301 ///
302 /// When the fuel feature is enabled, during evaluation the template will keep
303 /// track of how much fuel it has consumed. If the fuel tracker is turned on
304 /// the returned value will be `Some((consumed, remaining))`. If fuel tracking
305 /// is not enabled, `None` is returned instead.
306 #[cfg(feature = "fuel")]
307 #[cfg_attr(docsrs, doc(cfg(feature = "fuel")))]
308 pub fn fuel_levels(&self) -> Option<(u64, u64)> {
309 self.fuel_tracker
310 .as_ref()
311 .map(|x| (x.consumed(), x.remaining()))
312 }
313
314 /// Looks up a temp and returns it.
315 ///
316 /// Temps are similar to context values but the engine never looks them up
317 /// on their own and they are not scoped. The lifetime of temps is limited
318 /// to the rendering process of a template. Temps are useful so that
319 /// filters and other things can temporary stash away state without having
320 /// to resort to thread locals which are hard to manage. Unlike context
321 /// variables, temps can also be modified during evaluation by filters and
322 /// functions.
323 ///
324 /// Temps are values but if you want to hold complex state you can store a
325 /// custom object there.
326 ///
327 /// # Example
328 ///
329 /// ```
330 /// use minijinja::{Value, State};
331 ///
332 /// fn inc(state: &State) -> Value {
333 /// let old = state
334 /// .get_temp("my_counter")
335 /// .unwrap_or_else(|| Value::from(0i64));
336 /// let new = Value::from(i64::try_from(old).unwrap() + 1);
337 /// state.set_temp("my_counter", new.clone());
338 /// new
339 /// }
340 /// ```
341 pub fn get_temp(&self, name: &str) -> Option<Value> {
342 self.temps.lock().unwrap().get(name).cloned()
343 }
344
345 /// Inserts a temp and returns the old temp.
346 ///
347 /// For more information see [`get_temp`](Self::get_temp).
348 pub fn set_temp(&self, name: &str, value: Value) -> Option<Value> {
349 self.temps
350 .lock()
351 .unwrap()
352 .insert(name.to_owned().into(), value)
353 }
354
355 /// Shortcut for registering an object as a temp.
356 ///
357 /// If the value is already there, it's returned as object, if it's
358 /// not there yet, the function is invoked to create it.
359 ///
360 /// # Example
361 ///
362 /// ```
363 /// use std::sync::atomic::{AtomicUsize, Ordering};
364 /// use minijinja::{Value, State};
365 /// use minijinja::value::Object;
366 ///
367 /// #[derive(Debug, Default)]
368 /// struct MyObject(AtomicUsize);
369 ///
370 /// impl Object for MyObject {}
371 ///
372 /// fn inc(state: &State) -> Value {
373 /// let obj = state.get_or_set_temp_object("my_counter", MyObject::default);
374 /// let old = obj.0.fetch_add(1, Ordering::AcqRel);
375 /// Value::from(old + 1)
376 /// }
377 /// ```
378 ///
379 /// # Panics
380 ///
381 /// This will panick if the value registered under that name is not
382 /// the object expected.
383 pub fn get_or_set_temp_object<O, F>(&self, name: &str, f: F) -> Arc<O>
384 where
385 O: Object + 'static,
386 F: FnOnce() -> O,
387 {
388 self.get_temp(name)
389 .unwrap_or_else(|| {
390 let rv = Value::from_object(f());
391 self.set_temp(name, rv.clone());
392 rv
393 })
394 .downcast_object()
395 .expect("downcast unexpectedly failed. Name conflict?")
396 }
397
398 #[cfg(feature = "debug")]
399 pub(crate) fn make_debug_info(
400 &self,
401 pc: u32,
402 instructions: &Instructions<'_>,
403 ) -> crate::debug::DebugInfo {
404 crate::debug::DebugInfo {
405 template_source: Some(instructions.source().to_string()),
406 referenced_locals: instructions
407 .get_referenced_names(pc)
408 .into_iter()
409 .filter_map(|n| Some((n.to_string(), some!(self.lookup(n)))))
410 .collect(),
411 }
412 }
413}
414
415impl<'a> ArgType<'a> for &State<'_, '_> {
416 type Output = &'a State<'a, 'a>;
417
418 fn from_value(_value: Option<&'a Value>) -> Result<Self::Output, Error> {
419 Err(Error::new(
420 ErrorKind::InvalidOperation,
421 "cannot use state type in this position",
422 ))
423 }
424
425 fn from_state_and_value(
426 state: Option<&'a State>,
427 _value: Option<&'a Value>,
428 ) -> Result<(Self::Output, usize), Error> {
429 match state {
430 None => Err(Error::new(ErrorKind::InvalidOperation, "state unavailable")),
431 Some(state) => Ok((state, 0)),
432 }
433 }
434}
435
436/// Tracks a block and it's parents for super.
437#[derive(Default)]
438pub(crate) struct BlockStack<'template, 'env> {
439 instructions: Vec<&'template Instructions<'env>>,
440 depth: usize,
441}
442
443impl<'template, 'env> BlockStack<'template, 'env> {
444 pub fn new(instructions: &'template Instructions<'env>) -> BlockStack<'template, 'env> {
445 BlockStack {
446 instructions: vec![instructions],
447 depth: 0,
448 }
449 }
450
451 pub fn instructions(&self) -> &'template Instructions<'env> {
452 self.instructions.get(self.depth).copied().unwrap()
453 }
454
455 pub fn push(&mut self) -> bool {
456 if self.depth + 1 < self.instructions.len() {
457 self.depth += 1;
458 true
459 } else {
460 false
461 }
462 }
463
464 #[track_caller]
465 pub fn pop(&mut self) {
466 self.depth = self.depth.checked_sub(1).unwrap()
467 }
468
469 #[cfg(feature = "multi_template")]
470 pub fn append_instructions(&mut self, instructions: &'template Instructions<'env>) {
471 self.instructions.push(instructions);
472 }
473}