Keats / tera

A template engine for Rust based on Jinja2/Django
http://keats.github.io/tera/
MIT License
3.53k stars 282 forks source link

Allow macro definition in renderable template #822

Closed sdedovic closed 1 year ago

sdedovic commented 1 year ago

This change refactors the MacroDefinition rendering to no-op so that a template may contain a macro definition and still render. The previous behaviour was to error.

This means I may define a macro in a template and use it with the self namespace, as the test shows. It's useful for me to be able to extract some common behaviour in my templates out without having multiple files.

Related issues:

sdedovic commented 1 year ago

Just now seeing the discussion here: https://github.com/Keats/tera/issues/370#issuecomment-455483920

My opinion is that forcing separation of the macro can make things less ergonomic. Additionally, it was be trivial to enforce macros only at the top level.

In my case, I have a template for rending a recipe and a macro that renders the list of ingredients. I want to call the macro multiple times, because my recipe has a few different sections (e.g. dough, topping, filling). This template uses some shared CSS and only makes sense in my recipe template. I don't want to create a set of files, one for templates, and one for macros, just to achieve this.

sdedovic commented 1 year ago

I have updated the grammer, parser, and docs to force macros at the top-level only. This should remove any concern of a macro in a for, if, etc.

sdedovic commented 1 year ago

Result of running cargo test -- --format=terse


running 423 tests
........................................................................................ 88/423
........................................................................................ 176/423
........................................................................................ 264/423
........................................................................................ 352/423
.......................................................................
test result: ok. 423 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.07s

     Running tests/render_fails.rs (target/debug/deps/render_fails-1e0a12e06effa9e9)

running 11 tests
...........
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s

   Doc-tests tera

running 4 tests
....
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.06s```
sdedovic commented 1 year ago

That looks ok. I'm not sure what behaviour I want for Tera v2 concerning that.

I'll leave my reply here and also in the #637 thread:

I believe macro definition should only be done:

See the next section to understand why.


With jinja2 templates you can do the following:

{% if some_val %}
  {% macro foo() %}42{% endmacro %}
{%  endif %}
{{ foo() }}

The macro may or may not be defined during rendering based on the truthiness of some_val. This is because Jinja takes the template AST and converts it into a Python module (using string concatenation) and the "rendering" is more or less evaling the Python at runtime.

This can't work easily with Rust, for obvious reasons. (Maybe if you turn the Tera template AST into rust code, compile it, and load it dynamically...) anyway,

This becomes a point of divergence for Tera macros. The current code will load all the macro definitions into one table as a first pass during rendering. Then they are looked up from this table when traversing the AST, and thus we cannot have dynamic, conditional macro definitions. This can be fixed by defining macros during AST traversal with Context in scope, or by extending the multi-pass approach to optimize the AST and generate macro definitions prior to rendering.

In any case, I personally believe this is likely an anti-pattern. Rarely is there a valid use case to define globals dynamically, and even less so when these act like pure functions, or "template snippets" as I see it.

sdedovic commented 1 year ago

Hey @Keats I think the CI is failing due to some kind of configuration issues. Haven't looked into it. Is there a way to re-trigger a build or should I look to fix the problem?

Keats commented 1 year ago

I think it was just a spurious issue, i've restarted the jobs

sdedovic commented 1 year ago

Rebased on top of #825 to fix failing builds