mustache / spec

The Mustache spec.
MIT License
361 stars 71 forks source link

Specify substituted block scope resolution #129

Closed jgonggrijp closed 2 years ago

jgonggrijp commented 3 years ago

This is my first follow-up on #125. I planned this addition already in April, but the work was delayed until now due to other projects intervening.

The diff, which is very small, probably speaks for itself. I wrote this spec after completing my own implementation of template inheritance, and I didn't need to change my implementation in order to pass the new spec. If I understood correctly, at least Mustache.php by @bobthecow and Ocaml-Mustache by @gasche already implement this spec, too. To me this seems the most intuitive interpretation of scope in a substituted block, so I hope it will be fairly uncontroversial.

gasche commented 3 years ago

Personally I don't have an opinion in one direction or another; I made the choice that would match a test in the mustache.php testsuite. It's good that you have an intuition on which direction should be taken! Thanks for your work on the Mustache spec.

jgonggrijp commented 2 years ago

Bump!

jgonggrijp commented 2 years ago

@Danappelxx @spullara

bobthecow commented 2 years ago

Confirmed that this test passes with mustache.php.

:shipit: 🙂

Danappelxx commented 2 years ago

This does indeed seem like the most reasonable interpretation. Thank you for your continued contributions! Merging and tagging v1.2.2.

spullara commented 2 years ago

Verified that it works as specified on mustache.java.

agentgt commented 1 year ago

Guys I have a really hard time accepting the interpretation of block scoping. I know the ship has sailed on this but I think it is wrong.

I think that the block should be evaluated immediately and thus in the child's context.

If folks are interested I can provide numerous examples of how easy it is too screw it up. I can also show how very few templating languages do it the way mustache is doing (that is most evaluate eagerly).

Anyway prior to inheritance (and apparently it still will be the case) there was absolutely no way to pass a rendered text to something else as lambdas only accept unrendered text. I was hoping inheritance would do this. And in general it might provided you don't do weird things in your parent like nesting a block in a section (assuming the child passes its current context to the parent which again I'm fairly sure it does but not entirely).

In fact if the context was passed to lambdas (which is the case for some implementations and possibly the future with power lambdas) one could easily implement single inheritance with custom lambdas as the lambdas will accept unrendered text.

That is instead of:

{{<parent}}{{$block}}I say {{fruit}}.{{/block}}{{/parent}}

You could do:

{{#parent}}{{$block}}I say {{fruit}}.{{/block}}{{/parent}}

(in this case the lambda name is the template to load but you could use another block parameter like say {{$template}}).

Now the lambda author has to do all the work of replacing blocks and loading the parent but it is probably largely a simple search and replace.

Yes it is not standardized and you have to write a custom lambda but you could make it library and thus standardize that library... ie you could spec it there.

My point is passing around unrendered text that then gets expanded in mustache was a solved problem.

What we don't have is the ability to pass rendered (ie eagerly evaluated) text to then be used which is important for componentization. A component should not have to need to know what the model is of the child. The child adapts the model to what the component needs.

Anyway given that this apparently has reached consensus and given that if one is careful with parents the blocks will largely act like eager evaluation anyway I will still try to implement block scoping (provided the child is passing the context to the parent as its starting point) but I am concerned.

(apologies for the edits)

jgonggrijp commented 1 year ago

Just for completeness, let me refer to https://github.com/mustache/spec/pull/131#issuecomment-1272175384. Halfway in that comment, I illustrate how the present block scoping rules logically follow from a mental substitution of the parent template for the parent tag pair, overriding blocks as provided. I am aware that you (@agentgt) already read that, but with hindsight, it belonged here in the first place.

Adding to that comment, I should mention that this mental substitution is a natural extension of the semantics of old-fashioned partials, as described in the manpage. (Note that this same explanation already appeared in the original, outdated manpage, long before I touched it.)

With this justification in mind, let me address some details of your post:

  • A child knows more about what needs to be rendered than a parent.
  • Parent's are usually generic for like layout. They do not know as much about the exact content.

The child knows more about how the block should be filled, so in that sense you are right. However, the parent knows more about where the block will be placed. As we have seen, that might be inside a section. It might even be passed on as an argument to another parent (which in turn might also be nested inside a section, or nest the block in a section internally, or both).

  • In a nontrivial application name collision is much higher and thus unexpected results are more likely to happen. (for example {{name}}).

This is true regardless of whether context is evaluated in the child or in the parent.

  • It requires the child templates author to know much more than just the parameters of the parent template.

This is not really different from old-fashioned partials, or even templates in general. You need to know what data to pass to a template in order to render it. Keep in mind that partials, parents and blocks are just templates; they have exactly the same semantics as the "main" or "entry" template.

  • I do not know of many templating systems that do this so teaching it to others is painful

Mustache works with an implicit context stack. This is a cost we have to pay for the logic-free syntax. Other template languages do not even need to distinguish between "eager" and "lazy" evaluation, because they don't work with a context stack in the first place.

  • The notion of the "parents" context is confusing. Are we at the root while in the parent?

No (if you mean the root from the child);

Does the child pass its current context to the parent?

yes;

(neither the spec nor its test I think does not actually answer this... it just says parent context).

the spec does actually define these semantics here and here. The manpage also states that partials (and by extension, parents) "inherit the calling context".

If folks are interested I can provide numerous examples of how easy it is too screw it up.

You can provide as many examples as you wish, but they are not going to change the fact that, given the use of an implicit context stack, inheritance cannot work correctly unless the block is evaluated in the context of the parent.

Anyway prior to inheritance (and apparently it still will be the case) there was absolutely no way to pass a rendered text to something else as lambdas only accept unrendered text. I was hoping inheritance would do this.

I understand this is disappointing. Blocks were never intended as a general solution for passing information around; they are only meant as inline templates with which you can fill slots inside other templates. Power lambdas (#135) would probably be a better solution to what you are trying to achieve.

(...) provided you don't do weird things in your parent like nesting a block in a section (...).

I would like to emphasize that nesting a block in a section is a perfectly reasonable thing to do. For illustration, I might have a parent template for listing news headlines (frontpage.mustache):

{{#headlines}}
{{$headline-format}}
<div class="headline">
    <h2><a href="{{&url}}">{{title}}</a></h2>
    <p>{{synopsis}}</p>
</div>
{{/headline-format}}
<a href="#top">Back to top</a>
{{/headlines}}

In a child template, I might want to override the format to use more modern, semantic html:

{{<frontpage}}
    {{$headline-format}}
    <article class="headine">
        <heading><h2><a href="{{&url}}">{{title}}</a></h2></heading>
        <p>{{synopsis}}</p>
    </article>
    {{/headline-format}}
{{/frontpage}}

In fact if the context was passed to lambdas (which is the case for some implementations and possibly the future with power lambdas) one could easily implement single inheritance with custom lambdas as the lambdas will accept unrendered text.

That is instead of:

{{<parent}}{{$block}}I say {{fruit}}.{{/block}}{{/parent}}

You could do:

{{#parent}}{{$block}}I say {{fruit}}.{{/block}}{{/parent}}

(in this case the lambda name is the template to load but you could use another block parameter like say {{$template}}).

Here you are losing me. What is that second template supposed to do?

What we don't have is the ability to pass rendered (ie eagerly evaluated) text to then be used which is important for componentization.

What do you mean by "componentization" and "component"?

A component should not have to need to know what the model is of the child. The child adapts the model to what the component needs.

If you substitute "parent" for "component", then these sentences are already true under the current spec. But I suspect that is not what you meant.