stencilproject / Stencil

Stencil is a simple and powerful template language for Swift.
https://stencil.fuller.li
BSD 2-Clause "Simplified" License
2.34k stars 223 forks source link

Using blocks more than once? #158

Closed NocturnalSolutions closed 2 years ago

NocturnalSolutions commented 6 years ago

layout.stencil:

<!doctype html>
<html>
  <head>
    <title>My Music Collection: {% block pageTitle %}{% endblock %}</title>
  </head>
  <body>
    <h1>My Music Collection</h1>
    <h2>{% block pageTitle %}{% endblock %}</h2>
    {% block pageContent %}{% endblock %}
  </body>
</html>

hello.stencil:

{% extends "layout.stencil" %}

{% block pageTitle %}Hello!{% endblock %}

{% block pageContent %}
<p>
  Hello, {{ name|default:"World" }}!
</p>
{% endblock %}

So my ideal result would be:

<!doctype html>
<html>
  <head>
    <title>My Music Collection: Hello!</title>
  </head>
  <body>
    <h1>My Music Collection</h1>
    <h2>Hello!</h2>
    <p>
      Hello, World!
    </p>
  </body>
</html>

I think this is a natural way to have a "subtitle" that appears both in the page content and in the <title> tag, but it doesn't seem to work - the second pageTitle block isn't filled in. So the result looks like:

<h1>My Music Collection</h1>
<h2></h2>

Not sure if this counts as a bug. If not, please consider it a feature request.

ilyapuchka commented 6 years ago

@NocturnalSolutions I have a solution for that but I'm not sure it's a right way to use blocks. In your example imagine that you will change {% block pageTitle %}Hello!{% endblock %} to {% block pageTitle %}{{ block.super }}Hello!{% endblock %}. This should render the content of this block from layout.stencil, but there are two of them and they may be different, what we should pick?

If I understand correctly in your case you are using blocks as a placeholders for something that is defined in extended templates. But I think blocks were originally designed to support another use case - extending their content in extended templates.

@kylef what do you think about that? Here is the change that will fix this issue. Here I'm pushing back already rendered node from extended template so that it can be used again by base template. All tests are passing but maybe we are missing some edge case where it can lead to infinite recursion.

diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift
index b9bf87a..4f37022 100644
--- a/Sources/Inheritence.swift
+++ b/Sources/Inheritence.swift
@@ -129,6 +129,7 @@ class BlockNode : NodeType {

   func render(_ context: Context) throws -> String {
     if let blockContext = context[BlockContext.contextKey] as? BlockContext, let node = blockContext.pop(name) {
+      defer { blockContext.push(node, forKey: name) }
       let newContext: [String: Any] = [
         BlockContext.contextKey: blockContext,
         "block": ["super": try self.render(context)]
ilyapuchka commented 6 years ago

Actually tests do fail with this change, so I'm even less sure about it.

ilyapuchka commented 6 years ago

Jinja2 solves this this way:

If you want to print a block multiple times, you can, however, use the special self variable and call the block with that name:

<title>{% block title %}{% endblock %}</title>
<h1>{{ self.title() }}</h1>
{% block body %}{% endblock %}

We can do something similar, i.e. by storing all blocks in blocks context variable and make them resolvable. @kylef what do you think?

NocturnalSolutions commented 6 years ago

Requiring new syntax just for printing a block a second time, while better than nothing, still strikes me as odd, though. Why can't I just use the same syntax in both places? Am I really that odd for wanting/expecting this?

Aside from the example I give in the OP of repeating a title both in <title> and in a page header, I can think of another example where you might want to use a block twice on a page; you have a pager ("< Prev 1 2 3 4 Next >") that you want to display both at the top and bottom of a very large page.

Yes, I can work around this by using variables, but there are cases where that would clearly break the concept of code/design separation I'm supposed to gain by using a template engine.

Perhaps there are other workarounds I could use that I haven't discovered yet, but what I'm hoping for in the OP seems to me to be the simplest solution from the perspective of the template author.

ilyapuchka commented 6 years ago

@NocturnalSolutions I tried to explain in my previous comment why the same syntax can't be used in the way you want it, it creates ambiguity in inheritance. The syntax that is used to define a block can't be the same as syntax used to call this block later, the same way as syntax for defining and calling functions are different in programming languages. I didn't look at it close enough yet, just came across it going through Jinja2 docs, but it seems that using separate syntax is a good solution for this problem. If @kylef will agree I'll be glad to try to implement this in one or another way.

svanimpe commented 6 years ago

@NocturnalSolutions Doesn't a variable make more sense here?

<!doctype html>
<html>
  <head>
    <title>My Music Collection: {{ pageTitle }}</title>
  </head>
  <body>
    <h1>My Music Collection</h1>
    <h2>{{ pageTitle }}</h2>
    {% block pageContent %}{% endblock %}
  </body>
</html>

The only difference is that you specify the value in your rendering context, not on the page itself. I do something like that in https://github.com/svanimpe/swift-blog.

NocturnalSolutions commented 6 years ago

Sure, that works in that case. But what about the more complex example I put in my OP of a pager you want to appear at both the top and bottom of a page?

I suppose as a workaround you could render a template to a string that you then use as a variable. I still think just being able to reuse blocks would be a more pleasant solution, though.

svanimpe commented 6 years ago

In your OP, the content of block pageTitle is just some text, which is why I suggested a variable. If you want an entire block of HTML, you could try an include tag? I think that's the only way you can include the same snippet multiple times.

I view blocks as something similar to method overrides with subclassing. That is: you define a default in the parent, and optionally override it in a child. So not simply reusable blocks. I think that's what the include tag is for.