Skip to content

Commit

Permalink
Expose the name of the currently called item (#150)
Browse files Browse the repository at this point in the history
  • Loading branch information
SergioBenitez committed Nov 19, 2022
1 parent 90bdb45 commit 3b4d992
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 7 deletions.
30 changes: 23 additions & 7 deletions minijinja/src/vm/mod.rs
Expand Up @@ -83,8 +83,9 @@ impl<'env> Vm<'env> {
env: self.env,
ctx: Context::new(Frame::new(root)),
current_block: None,
instructions,
current_call: None,
auto_escape,
instructions,
blocks: prepare_blocks(blocks),
loaded_templates: BTreeSet::new(),
#[cfg(feature = "macros")]
Expand Down Expand Up @@ -115,8 +116,9 @@ impl<'env> Vm<'env> {
env: self.env,
ctx,
current_block: None,
instructions,
current_call: None,
auto_escape: state.auto_escape(),
instructions,
blocks: BTreeMap::default(),
loaded_templates: BTreeSet::new(),
#[cfg(feature = "macros")]
Expand Down Expand Up @@ -421,6 +423,7 @@ impl<'env> Vm<'env> {
stack.push(out.end_capture(state.auto_escape));
}
Instruction::ApplyFilter(name, arg_count, local_id) => {
state.current_call = Some(name);
let filter =
ctx_ok!(get_or_lookup_local(&mut loaded_filters, *local_id, || {
state.env.get_filter(name)
Expand All @@ -435,8 +438,10 @@ impl<'env> Vm<'env> {
a = ctx_ok!(filter.apply_to(state, args));
stack.drop_top(*arg_count);
stack.push(a);
state.current_call = Some(name);
}
Instruction::PerformTest(name, arg_count, local_id) => {
state.current_call = Some(name);
let test = ctx_ok!(get_or_lookup_local(&mut loaded_tests, *local_id, || {
state.env.get_test(name)
})
Expand All @@ -447,10 +452,13 @@ impl<'env> Vm<'env> {
let rv = ctx_ok!(test.perform(state, args));
stack.drop_top(*arg_count);
stack.push(Value::from(rv));
state.current_call = None;
}
Instruction::CallFunction(function_name, arg_count) => {
Instruction::CallFunction(name, arg_count) => {
state.current_call = Some(name);

// super is a special function reserved for super-ing into blocks.
if *function_name == "super" {
if *name == "super" {
if *arg_count != 0 {
bail!(Error::new(
ErrorKind::InvalidOperation,
Expand All @@ -459,7 +467,7 @@ impl<'env> Vm<'env> {
}
stack.push(ctx_ok!(self.perform_super(state, out, true)));
// loop is a special name which when called recurses the current loop.
} else if *function_name == "loop" {
} else if *name == "loop" {
if *arg_count != 1 {
bail!(Error::new(
ErrorKind::InvalidOperation,
Expand All @@ -468,23 +476,27 @@ impl<'env> Vm<'env> {
}
// leave the one argument on the stack for the recursion
recurse_loop!(true);
} else if let Some(func) = state.ctx.load(self.env, function_name) {
} else if let Some(func) = state.ctx.load(self.env, name) {
let args = stack.slice_top(*arg_count);
a = ctx_ok!(func.call(state, args));
stack.drop_top(*arg_count);
stack.push(a);
} else {
bail!(Error::new(
ErrorKind::UnknownFunction,
format!("{} is unknown", function_name),
format!("{} is unknown", name),
));
}

state.current_call = None;
}
Instruction::CallMethod(name, arg_count) => {
state.current_call = Some(name);
let args = stack.slice_top(*arg_count);
a = ctx_ok!(args[0].call_method(state, name, &args[1..]));
stack.drop_top(*arg_count);
stack.push(a);
state.current_call = None;
}
Instruction::CallObject(arg_count) => {
let args = stack.slice_top(*arg_count);
Expand All @@ -499,9 +511,13 @@ impl<'env> Vm<'env> {
stack.pop();
}
Instruction::FastSuper => {
// Note that we don't store 'current_call' here since it
// would only be visible (and unused) internally.
ctx_ok!(self.perform_super(state, out, false));
}
Instruction::FastRecurse => {
// Note that we don't store 'current_call' here since it
// would only be visible (and unused) internally.
recurse_loop!(false);
}
// Explanation on the behavior of `LoadBlocks` and `RenderParent`.
Expand Down
9 changes: 9 additions & 0 deletions minijinja/src/vm/state.rs
Expand Up @@ -24,6 +24,7 @@ pub struct State<'vm, 'env> {
pub(crate) env: &'env Environment<'env>,
pub(crate) ctx: Context<'env>,
pub(crate) current_block: Option<&'env str>,
pub(crate) current_call: Option<&'env str>,
pub(crate) auto_escape: AutoEscape,
pub(crate) instructions: &'vm Instructions<'env>,
pub(crate) blocks: BTreeMap<&'env str, BlockStack<'vm, 'env>>,
Expand All @@ -38,6 +39,7 @@ impl<'vm, 'env> fmt::Debug for State<'vm, 'env> {
let mut ds = f.debug_struct("State");
ds.field("name", &self.instructions.name());
ds.field("current_block", &self.current_block);
ds.field("current_call", &self.current_call);
ds.field("auto_escape", &self.auto_escape);
ds.field("ctx", &self.ctx);
ds.field("env", &self.env);
Expand Down Expand Up @@ -66,6 +68,12 @@ impl<'vm, 'env> State<'vm, 'env> {
self.current_block
}

/// Returns the name of the item (filter, function, test, method) currently
/// being called.
pub fn current_call(&self) -> Option<&str> {
self.current_call
}

/// Looks up a variable by name in the context.
pub fn lookup(&self, name: &str) -> Option<Value> {
self.ctx.load(self.env(), name)
Expand All @@ -82,6 +90,7 @@ impl<'vm, 'env> State<'vm, 'env> {
blocks: BTreeMap::new(),
loaded_templates: BTreeSet::new(),
macros: Default::default(),
current_call: None,
})
}

Expand Down
3 changes: 3 additions & 0 deletions minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
Expand Up @@ -8,6 +8,9 @@ input_file: minijinja/tests/inputs/debug.txt
State {
name: "debug.txt",
current_block: None,
current_call: Some(
"debug",
),
auto_escape: None,
ctx: {
"x": 0,
Expand Down
101 changes: 101 additions & 0 deletions minijinja/tests/test_templates.rs
Expand Up @@ -146,3 +146,104 @@ fn test_loop_changed() {
);
assert_eq!(rv, "12345");
}

#[test]
fn test_current_call_state() {
use minijinja::value::{Object, Value};
use std::fmt;

#[derive(Debug)]
struct MethodAndFunc;

impl fmt::Display for MethodAndFunc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}

impl Object for MethodAndFunc {
fn call_method(&self, state: &State, name: &str, args: &[Value]) -> Result<Value, Error> {
assert_eq!(name, state.current_call().unwrap());
let args = args
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(", ");

Ok(format!("{}({args})", state.current_call().unwrap()).into())
}

fn call(&self, state: &State, args: &[Value]) -> Result<Value, Error> {
let args = args
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(", ");

Ok(format!("{}({args})", state.current_call().unwrap()).into())
}
}

fn current_call(state: &State, value: Option<&str>) -> String {
format!("{}({})", state.current_call().unwrap(), value.unwrap_or(""))
}

fn check_test(state: &State, value: &str) -> bool {
state.current_call() == Some(value)
}

let mut env = Environment::new();
env.add_function("fn_call_a", current_call);
env.add_function("fn_call_b", current_call);
env.add_filter("filter_call", current_call);
env.add_test("my_test", check_test);
env.add_test("another_test", check_test);
env.add_global("object", Value::from_object(MethodAndFunc));

env.add_template(
"test",
r#"
{{ fn_call_a() }}
{{ "foo" | filter_call }}
{{ fn_call_a() | filter_call }}
{{ fn_call_b() | filter_call }}
{{ fn_call_a(fn_call_b()) }}
{{ fn_call_a(fn_call_b()) | filter_call }}
{{ "my_test" is my_test }}
{{ "another_test" is my_test }}
{{ "another_test" is another_test }}
{{ object.foo() }}
{{ object.bar() }}
{{ object.foo(object.bar(object.baz())) }}
{{ object(object.bar()) }}
{{ object.baz(object()) }}
"#,
)
.unwrap();

let tmpl = env.get_template("test").unwrap();
let rv = tmpl.render(context!()).unwrap();
assert_eq!(
rv,
r#"
fn_call_a()
filter_call(foo)
filter_call(fn_call_a())
filter_call(fn_call_b())
fn_call_a(fn_call_b())
filter_call(fn_call_a(fn_call_b()))
true
false
true
foo()
bar()
foo(bar(baz()))
object(bar())
baz(object())
"#
);
}

0 comments on commit 3b4d992

Please sign in to comment.