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

Broken Capturing (I guess) #10

Closed
AlexWayfer opened this issue Oct 3, 2017 · 21 comments
Closed

Broken Capturing (I guess) #10

AlexWayfer opened this issue Oct 3, 2017 · 21 comments

Comments

@AlexWayfer
Copy link

<%|= foo do %>
  <p>Hello!</p>
<%| end %>

(From README)

It works. But these code blocks don't work:

<%| foo do %> <!-- without output, from `README` -->
  <p>Hello!</p>
<%| end %>
<!-- syntax error, unexpected ')' -->

<%|= foo block: proc do %> <!-- with output, but block from variable -->
  <p>Hello!</p>
<%| end %>
<!-- ArgumentError - tried to create Proc object without a block -->

<%| block = proc do %> <!-- just assign variable -->
  <p>Hello!</p>
<%| end %>
<!-- syntax error, unexpected ')' -->

So, <%| tag exists in README, but I can't use it.

Thanks.

@jeremyevans
Copy link
Owner

<%| is only used for closing <%|= and <%|== tags, signalling the end of the capture. I'll reword the README to make this more clear.

@AlexWayfer
Copy link
Author

OK, thank you.

Note about arguments: it works with parentheses:

<%|= foo block: (proc do %>
  <p>Hello!</p>
<%| end) %>

@AlexWayfer
Copy link
Author

Oh, problems in cycles :(

<%
  def foo
    [{ name: 'Alex' }, { name: 'Ivan' }, { name: 'Boris' }]
      .reduce('') do |_mem, obj|
        p yield(obj)
      end
  end
%>

<%|= foo do |obj| %>
  <h1>Hello, <%= obj[:name] %></h1>
<%| end %>

Output:

pry: main > eval(Erubi::CaptureEndEngine.new(File.read('.test.erb')).src)
"\n  <h1>Hello, Alex</h1>\n"
"\n  <h1>Hello, Alex</h1>\n\n  <h1>Hello, Ivan</h1>\n"
"\n  <h1>Hello, Alex</h1>\n\n  <h1>Hello, Ivan</h1>\n\n  <h1>Hello, Boris</h1>\n"
=> "\n\n  &lt;h1&gt;Hello, Alex&lt;/h1&gt;\n\n  &lt;h1&gt;Hello, Ivan&lt;/h1&gt;\n\n  &lt;h1&gt;Hello, Boris&lt;/h1&gt;\n\n"

Why does the concatenation occur with the previous result?

@jeremyevans
Copy link
Owner

Probably because it was never tested with a loop. I think it's safe to say that is not the expected behavior. I'll look into this today and see if I can fix it.

@jeremyevans jeremyevans reopened this Oct 4, 2017
@AlexWayfer
Copy link
Author

Thanks, I'll wait for updates!

@jeremyevans
Copy link
Owner

I misread the previous code. I thought the the "Hello, Alex" result was injected into the template 3 times, and the "Hello, Ivan" was injected into the template 2 times. Now I can see each is only injected a single time, which is what I would expect.

The reason that yield(obj) returns a concatenated result is that the return value of the block is the temporary buffer created by erubi, since that is the last expression in the block. If you change the template code to:

<%|= foo do |obj| %>
  <h1>Hello, <%= obj[:name] %></h1>
<%| obj; end %>

Then yield(obj) will return the object itself. However, that will probably not give you the result you want in terms of template output.

In general when you are using the capture_end support, any methods you want to be callable from the template using <%|= or <%|== should be returning a single string, and will probably need to be aware of the template buffer variable.

In your case, if you are just trying to output that heading for each name, you probably don't want to use the capture support, you should probably just use regular <% tags:

<% foo do |obj| %>
  <h1>Hello, <%= obj[:name] %></h1>
<% end %>

I did notice a fairly large issue with the capture_end support, which is that the escaping flag is inverted. That will definitely need to be fixed, I'll work on that soon.

@AlexWayfer
Copy link
Author

In your case, if you are just trying to output that heading for each name, you probably don't want to use the capture support, you should probably just use regular <% tags:

No, there is no output with this tag :(

And <%= gives error:

SyntaxError: (eval):12: syntax error, unexpected ')'
eeze; _buf << ( foo do |obj| ).to_s; _buf << '
                              ^
(eval):15: syntax error, unexpected end-of-input, expecting ')'
from (pry):12:in `eval'

Real case:

# common_list.erb

<ul>
  <% items.each do |item| %>
    <li>
      <header>
        <%= yield(item) %>
      </header>
      <!-- content -->
    </li>
  <% end %>
</ul>
# articles_list.erb

<!-- there is I tried to use capture end or block-as-variable (works the same in cycles) -->
<%= render 'common_list' do |article| %>
  <h1><%= article.title %><h1>
<% end %>
# pages_list.erb

<%= render 'common_list' do |article| %>
  <h1>
    <%= page.title %>
    <small><%= page.author %></small>
  <h1>
<% end %>

I did notice a fairly large issue with the capture_end support, which is that the escaping flag is inverted. That will definitely need to be fixed, I'll work on that soon.

Yes, I noticed this. I apologize for not writing about this.

Another strange thing: Erubi::CaptureEndEngine, but require 'erubi/capture_end' (not capture_end_engine).

@jeremyevans
Copy link
Owner

There should be output with <% tags. Example:

template = <<END
<% foo do |obj| %>
  <h1>Hello, <%= obj[:name] %></h1>
<% end %>
END
def foo
  [{ name: 'Alex' }, { name: 'Ivan' }, { name: 'Boris' }]
    .reduce('') do |_mem, obj|
      yield(obj)
    end
end
eval Erubi::CaptureEndEngine.new(template).src
=> "  <h1>Hello, Alex</h1>\n  <h1>Hello, Ivan</h1>\n  <h1>Hello, Boris</h1>\n"

<%= giving an error is expected, as that results in invalid ruby syntax.

The reason behind the <%|= tags are to allow you to define methods that take blocks and return strings, and include the resulting string output in the template, making sure the template output variable is temporarily changed so that outputting to the template inside the loop doesn't effect the template output outside the loop.

I'm not sure exactly what you want as there is no definition for the render method. However, I think the issue you are having is that the the temporary buffer is not cleared automatically on every yield (as the template code doesn't even know you are yielding), and your block returns the output of the current template buffer (building up cumulatively in each loop). I'm fairly sure that doing that automatically clearing the template buffer at the closing %> for a <%|= would fix this particular case but break other cases. In any case, you can specifically clear the template buffer at the start of the loop manually, which should fix your issue. Here's some example code (note how the render method needs to know the template buffer variable):

@common_list = <<END
<ul>
  <% @items.each do |item| %>
    <li>
      <header>
        <%= yield(item) %>
      </header>
      <!-- content -->
    </li>
  <% end %>
</ul>
END
@articles_list = <<END
<%|= render 'common_list' do |article| %>
  <h1><%= article[:name] %><h1>
<%| end %>
END

@items = [{ name: 'Alex' }, { name: 'Ivan' }, { name: 'Boris' }]
def render(template)
  render_partial(template) do |item|
    @a = String.new
    yield item
  end
end
def render_partial(template)
  eval Erubi::CaptureEndEngine.new(instance_variable_get("@#{template}")).src
end
eval Erubi::CaptureEndEngine.new(@articles_list, bufvar: '@a').src

which outputs:

<ul>
    <li>
      <header>

  <h1>Alex<h1>

      </header>
      <!-- content -->
    </li>
    <li>
      <header>

  <h1>Ivan<h1>

      </header>
      <!-- content -->
    </li>
    <li>
      <header>

  <h1>Boris<h1>

      </header>
      <!-- content -->
    </li>
</ul>

Part of the reason you don't have to manually do this in Rails is that render takes care of it for you, not the template engine. All of the capture logic in Rails is inside the render methods, it's not inside the template engine itself. Erubi's capture_end support brings part of the logic inside the template engine, but it can't generically handle all cases.

@AlexWayfer
Copy link
Author

AlexWayfer commented Oct 5, 2017

Thanks for the detailed explanation!

I'm not sure exactly what you want as there is no definition for the render method.

Work with Tilt: onetwothreefour.

Here's some example code (note how the render method needs to know the template buffer variable):

There is working code in my case (with Tilt): (<%|== because of old version)

require 'erubi'
require 'erubi/capture_end'

@common_list = <<END
<ul>
  <% @items.each do |item| %>
    <li>
      <header>
        <%= yield(item) %>
      </header>
      <!-- content -->
    </li>
  <% end %>
</ul>
END

@articles_list = <<END
<%|== render 'common_list' do |article| %>
  <% @header = '' %>
  <h1><%= article[:name] %><h1>
<%| end %>
END

@items = [{ name: 'Alex' }, { name: 'Ivan' }, { name: 'Boris' }]

def render(template)
  render_partial(template) do |item|
    yield item
  end
end
def render_partial(template)
  eval Erubi::CaptureEndEngine.new(instance_variable_get("@#{template}")).src
end

puts eval Erubi::CaptureEndEngine.new(@articles_list, bufvar: '@header').src

The same example with block in variable:

require 'erubi'
require 'erubi/capture_end'

@common_list = <<END
<ul>
  <% @items.each do |item| %>
    <li>
      <header>
        <%= header.call(item) %>
      </header>
      <!-- content -->
    </li>
  <% end %>
</ul>
END

@articles_list = <<END
<%|== render 'common_list', header: (proc do |article| %>
  <% @header = '' %>
  <h1><%= article[:name] %><h1>
<%| end) %>
END

@items = [{ name: 'Alex' }, { name: 'Ivan' }, { name: 'Boris' }]

def render(template, header: nil)
  render_partial(template, header: header) do |item|
    yield item
  end
end
def render_partial(template, header: nil)
  eval Erubi::CaptureEndEngine.new(instance_variable_get("@#{template}")).src
end

puts eval Erubi::CaptureEndEngine.new(@articles_list, bufvar: '@header').src

Thanks again.

@AlexWayfer
Copy link
Author

But what if I need to use two or more blocks inside template?

require 'erubi'
require 'erubi/capture_end'

@common_list = <<END
<ul>
  <% @items.each do |item| %>
    <li>
      <header>
        <%= header.call(item) %>
      </header>
      <section>
        <%= section.call(item) %>
      </section>
      <!-- content -->
    </li>
  <% end %>
</ul>
END

@articles_list = <<END
<%|=
  render 'common_list',
    header: (proc do |article| %>
      <% @header = '' %>
      <h1><%= article[:name] %><h1>
    <% end),
    section: (proc do |article| %>
      <% @section = '' %>
      <%= article[:age] %>
<%| end) %>
END

@items = [
  { name: 'Alex', age: 24 },
  { name: 'Ivan', age: 22 },
  { name: 'Boris', age: 26 }
]

def render(template, header: nil, section: nil)
  render_partial(template, header: header, section: section) do |item|
    yield item
  end
end
def render_partial(template, header: nil, section: nil)
  eval Erubi::CaptureEndEngine.new(instance_variable_get("@#{template}")).src
end

puts eval Erubi::CaptureEndEngine.new(@articles_list, bufvar: '@header').src

bufvar is single, and it seems that I'm not able to reset them separately.

@jeremyevans
Copy link
Owner

The capture_end support uses a stack of bufvars, so there isn't just a single one. There are tests for nested use of <%|= and <%| tags, so this is something that should work. If you think there is a problem, can you please submit a failing test?

@AlexWayfer
Copy link
Author

Sorry, my bad. It works without complications. But there is probably another strange bug, with condition inside captured block. I'll try to reproduce it in minimal example.

@AlexWayfer
Copy link
Author

AlexWayfer commented Feb 13, 2020

Let's take your example from here: #10 (comment)

And just add:

  <% if article[:name] == 'Alex' %>
    That's Alex!
  <% end %>

The result code:

require 'erubi'
require 'erubi/capture_end'

@common_list = <<END
<ul>
  <% @items.each do |item| %>
    <li>
      <header>
        <%= yield(item) %>
      </header>
      <!-- content -->
    </li>
  <% end %>
</ul>
END
@articles_list = <<END
<%|= render 'common_list' do |article| %>
  <h1><%= article[:name] %><h1>
  <% if article[:name] == 'Alex' %>
    That's Alex!
  <% end %>
<%| end %>
END

@items = [{ name: 'Alex' }, { name: 'Ivan' }, { name: 'Boris' }]

def render(template)
  render_partial(template) do |item|
    @a = String.new
    yield item
  end
end

def render_partial(template)
  eval Erubi::CaptureEndEngine.new(instance_variable_get("@#{template}")).src
end

puts eval Erubi::CaptureEndEngine.new(@articles_list, bufvar: '@a').src

The output:

<ul>
    <li>
      <header>
        
  <h1>Alex<h1>
    That's Alex!

      </header>
      <!-- content -->
    </li>
    <li>
      <header>
        
      </header>
      <!-- content -->
    </li>
    <li>
      <header>
        
      </header>
      <!-- content -->
    </li>
</ul>

There are no names when condition is falsey…

You can turn condition into != 'Alex' and the first name will be absent.

And all the content before falsey condition will disappear.

@AlexWayfer
Copy link
Author

For now I can't reproduce it in Erubi tests. 😔

@AlexWayfer
Copy link
Author

Work-around and related reason by @WorstOfAny: it's because of nil as result of block, adding something after condition helps.

@jeremyevans
Copy link
Owner

@AlexWayfer Maybe you want the :yield_returns_buffer option?

@AlexWayfer
Copy link
Author

AlexWayfer commented Feb 13, 2020

@AlexWayfer Maybe you want the :yield_returns_buffer option?

Yes, it helps. Thank you. Probably it should be true by default.

@jeremyevans
Copy link
Owner

I'm definitely considering true by default for Erubi 2.

@AlexWayfer
Copy link
Author

I'm definitely considering true by default for Erubi 2.

Oh, thank you. Is there any plans for Erubi 2?

@jeremyevans
Copy link
Owner

No current planned release date. Few people are using capture_end, and bumping the major just for changes to capture_end is not something I plan to do. If I have to bump the major for something else, then I'll probably include changing the default in capture_end.

@AlexWayfer
Copy link
Author

So, it can take years before major update… it's sadly. Thanks.

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