Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: extend macros to support using a block given by the calee (in addition to arguments) #282

Open
cipriancraciun opened this issue Dec 3, 2024 · 1 comment

Comments

@cipriancraciun
Copy link
Contributor

Proposal summary

In the current Jinja implementation (see https://jinja.palletsprojects.com/en/stable/templates/#call), the macro has a feature in which the caller of a macro can transmit a block to the macro to be included in its body. (All this in addition to plain macro arguments.)

Here is an example from the Jinja documentation:

{% macro render_dialog(title, class='dialog') -%}
    <div class="{{ class }}">
        <h2>{{ title }}</h2>
        <div class="contents">
            {{ caller() }}
        </div>
    </div>
{%- endmacro %}

{% call render_dialog('Hello World') %}
    This is a simple dialog rendered by using a macro and
    a call block.
{% endcall %}

Here, the {{ caller() }} used inside the macro body, expands to the block given by the caller between the {% call ... %} and {% endcall %}.

(I'll note below the compatibility issue with Rinja's usage of {% call ... %}.

(See at the end for the proposed Rinja syntax.)

Motivation

When developing larger HTML-based applications, one often finds the need to define widgets or reusable components, like for example input widgets, dialog boxes, panes and other containers, and at the moment with Rinja there are only these options to implement everything:

  • macro -- the best choice, as it allows arguments, and many can be defined in the same file that is then imported; however, it doesn't currently support a way to send a block of template to be included at its control;
  • include -- not only doesn't it allow sending them a block of template, it also doesn't support other arguments;
  • sub-templates -- as with include, it doesn't allow sending a block of template, and it involves touching the Rust code;

Compatibility issues with current Rinja syntax

Jinja has two ways to invoke a macro:

  • {{ some_macro(...) }} (i.e. as a function),
  • and {% call some_macro(...) %} {# block to be given to macro as caller() #} {% endcall %} (i.e. as a block / control structure)

And unfortunately Askama / Rinja has chosen the {% call some_macro(...) %} to mean the first variant from Jinja. (I understand that Jinja, being implemented in Python has the luxury to detect at run-time what does a function call like {{ something(...) }} mean, and to detect if it's a macro, or something in the context. Meanwhile Askama / Rinja needs to know at compile time what a function might be.)

However, Jinja2 has an additional feature, allowing the call site of a macro to provide a way for the macro to pass back arguments (i.e. use {% call(caller_argument) some_macro(macro_argument %} and then the macro can call the caller's block with {{ caller("something") }}).

Thus, we can salvage the situation by using in Rinja {% call() some_macro(...) %} to imply that this is a call to a macro which also passes a block, thus the parser should expect a matching {% endcall %}.


Proposed Rinja syntax (with no caller arguments)

Add a new variant of invoking a macro that allows the caller to pass a block of template to the macro:

{% macro some_macro() %}
   ... {% call caller() %} ...
{% endmacro %}

{% call() some_macro() %}
   ...
{% endcall %}

(There should be allowed any number of whitespaces between {% call ( ) some_macro ( ... ) %} and {% call caller ( ) %}.

The macro can now try to include the caller's block with {% call caller() %}, similar to how we include the original block on overriding with {% call super() %}.

A macro can only use {% call caller() %} if it was called with the extended syntax, thus the developer must make sure each macro is properly used with the proper call syntax. (The current Jinja implementation behaves this way, i.e. one can't call {{ caller() }} unless the macro was invoked with a block.)

Proposed Rinja syntax (with caller arguments)

To be even more helpful, the macro could send back some arguments back to the caller, thus the extended syntax would be:

{% macro some_macro() %}
Before in macro
{% call caller(1, 2) %}
After in macro
{% endcall %}

{% call(arg1, arg2) some_macro() %}
Use {{ arg1 }} and {{ arg2 }} in callee
{% endcall %}

Which would expand to:

Before in macro
Use 1 and 2 in callee
After in macro

Question: Can the macro call back the caller a with block of its own?

Here is a contrived example that I think describes best the question:

{% macro some_macro() %}
    In some_macro() before
        {% call(arg3, arg4) caller(1, 2) %}
            In some_macro caller() block calledback with arg3={{ arg3 }} and arg4={{ arg4 }}
        {% endcall %}
    In some_macro() after
{% endmacro %}

{% call(arg1, arg2) some_macro() %}
    In call() block before
        call() block called with arg1={{ arg1 }} arg2={{ arg2 }}
        {% call caller(3, 4) %}
    In call() block after
{% endcall %}

That would render to (I've changed the indentation to present the proper nesting):

In some_macro() before
    In call() block before
        call() block called with arg1=1 arg2=2
            In some_macro caller() block calledback with arg3=3 arg4=4
    In call() block after
In some_macro() after

I.e. here the caller and the macro behave in a co-recursive manner, namely:

  • the caller calls the macro, and provides a block to be called back by the macro;
  • the macro calls back to the callee (via caller()), but
  • at the same time the macro provides back to the callee another block that the callee can use in its own block;
  • (and so on, for any finite number of steps that the caller and macro match;)

Apparently the current Jinja implementation does allow for this example (just adjust the {% call caller(3, 4) %} into {{ caller(3, 4) }} to adjust for the syntax difference).

However, Jinja doesn't allow more deeply nested co-recursive callbacks. (Not that I believe it's not theoretically possible, but I believe because the way Jinja implements the caller() callback.)

@kakalos12
Copy link

Really love to see this feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants