mitsuhiko / minijinja

MiniJinja is a powerful but minimal dependency template engine for Rust compatible with Jinja/Jinja2
https://docs.rs/minijinja/
Apache License 2.0
1.65k stars 100 forks source link

Custom callable as filter #415

Closed jplatte closed 8 months ago

jplatte commented 8 months ago

Is it possible to pass custom objects to a filter the way it is possible to do for functions by creating a custom callable (impl Object) and instantiating it as required, then passing it via context?

My motivation is that I want to create a markdown filter that can do syntax highlighting, but the highlighting theme is not known up-front / could even change from one render call to another. For now I'll try to work around this by serializing the theme name into the context and looking it up via State in the filter.

mitsuhiko commented 8 months ago

Short answer is no, but longer answer is that you can accomplish the same thing via a closure. Can you show to me how you would implement this with impl Object and I can probably show you how to make it work with what is possible with filters today.

jplatte commented 8 months ago

The closure approach would mean I have to re-add the filter before rendering the template whenever I want the data captured in the closure to change, right? If so, I'll just keep using my current workaround of grabbing the relevant data from the context within the filter, because I don't have mutable access to the minijinja env when rendering.

Here is the current, working filter (AFAIK, haven't tested in depth): https://github.com/jplatte/hinoki/commit/e26892d631f89dce7d0e59a857aab0808dcfdd69

Assuming that this isn't possible w/o mutating the environment, would that be a feature you could see being added down the line (potentially with somebody else, i.e. me, putting in the main work), or is this a wontfix?

mitsuhiko commented 8 months ago

So generally speaking putting configuration into the context is actually encouraged. Not with hidden names though. For instance the minijinja-contrib filters use this to determine the default time format (DATE_FORMAT) or the timezone (TIMEZONE) (see https://docs.rs/minijinja-contrib/latest/minijinja_contrib/filters/fn.dateformat.html). So in your case I would actually do that but instead of using __hinoki_internal__syntax_highlight_theme just literally call it HINOKI_SYNTAX_HIGHLIGHT_THEME if that's what you have in mind. This also lets a template author temporarily rebind it.

If you want to not make this reconfigurable you can use $ as a prefix.

Generally both State and Environment are intentionally immutable from the perspective of the template. This is done for multiple reasons but the most important one is that the lifetime of these objects is not well defined. For instance sometimes the state is re-created, sometimes it's shared. If you want to store stuff there yourself you would most likely not have the best of times.

mitsuhiko commented 8 months ago

I added documentation for this here: https://github.com/mitsuhiko/minijinja/commit/8372c6fcdbfaf252508a5d1eb481cdae8be772f6

I'm going to close this issue as a custom callable would not actually solve this issue as far as I can tell.

jplatte commented 8 months ago

I'm going to close this issue as a custom callable would not actually solve this issue as far as I can tell.

Why not? I'm successfully using custom function callables for passing (in some cases non-serializable) things into my templates. For example this get_file function,

mitsuhiko commented 8 months ago

Maybe I'm still missing something here. A custom callable at most can be a closure + a function. From what I understand about your issue a closure cannot solve it either. Note you can always just make a MyCallable trait that works like you do, and you have a helper method that makes a boxed function. That's in fact how this system works internally already.

https://github.com/mitsuhiko/minijinja/blob/8372c6fcdbfaf252508a5d1eb481cdae8be772f6/minijinja/src/functions.rs#L210-L225

jplatte commented 8 months ago

Maybe my issue description was unclear. What I'm looking for is a way to pass filters¹ via context! / minijinja::Value, like I can do for function-like callables.

AFAICT, the only current way to pass filters to templates is by mutating the minijinja::Environment via add_filter.

¹ whether as a user-defined type that implements a trait similar to Object or a closure, I don't care much

mitsuhiko commented 8 months ago

Ah. That's a different question. Basically you want to provide per-invocation filters. You can clone the entire environment and add filters to it. While not super cheap, it might be quick enough for your problem as the environment uses a lot of reference counting internally.

jplatte commented 8 months ago

Well, the only reason they're per-invocation is that I pass custom data into them per invocation 😄

Thanks for the hint about cloning the environment, I will keep it in mind if I ever need to pass non-serializable things! For now I'll think about whether it makes sense to support template code overwriting the syntax theme and update the context binding name based on that.