Fully escaping a multiline erb segment - ruby-on-rails

I'm trying to create a simple partial that allows me to display code blocks without going through weird contortions in my code.
So I did this in the partial:
<% lang ||= "" %>
<% language = "lang='#{lang}'" %>
<div class="codebox">
<% if title %>
<h3><%= title %></h3>
<% end %>
<pre <%= language %>><%=text.unindent%></pre>
</div>
And this in lib for unindenting strings (thanks to a very nice SO suggestion):
class String
def unindent; gsub(/^#{scan(/^\s+/).min}/, "") end
end
Then, I can just do this, and get a very pretty little code box:
<%= render partial: 'pre', locals: { title: "example.html", lang: 'html', text: "
<div class='cl' style='text-align:center'>
<div class='collapse-group'>
<!-- Title, always viewable -->
<a class='bundle' href='#'>'Click here to expand'</a>
<div class='collapse'>
<!-- The content to be hidden or shown -->
</div>
</div>
</div>
"} %>
This turns into this:
Works like a charm, unless I put in a bunch of erb, in which case it goes nuts and starts erroring all over the place. Ex of an error-producer (contents not super relevant. I made sure that all quotes within are double, while the "string" is in single quotes):
<%= render partial: 'pre', locals: { title: "example.html", lang: 'html', text: '
<% sub ||= "" %>
<% term ||= "(expand)" %>
<% style ||= "" %>
<div class="cl" style="text-align:center">
<div class="collapse-group">
<<%=tag%> class="squeeze" style=<%="#{style}"%>>
<%=title%>
<% if sub != "" %>
<small><%= sub %></small>
<% end %>
<a class="bundle" href="#"><%= term %></a>
</<%=tag%>>
<div class="collapse">
' } %>
Any way I can just tell html that what I'm putting inside those quotes is 100% literal characters? I've tried individually escaping ">"s and ">"s and "%"s and all that, and it's a messy (and ineffectual) path I'm hoping not to go down.
EX of what I want the above to look like:

I think a nice approach in this would be to use #capture, for example in a helper (not tested, just a hint of what to do) :
def code_block( title = nil, lang = nil, &block )
output = capture( &block ) # this is the answer to all your problems
output = output.unindent # optional, escape it as you want, too
# rendering a partial is still possible,
# but i'd recommend using an absolute path :
render partial: 'my_html_bits/code_block',
locals: {title: title, lang: lang, text: output }
end
then you could do :
<%= code_block( 'example.html', 'html' ) do %>
<%# whatever code here will be captured %>
<p>Even plain old html.</p>
<% end %>
As a side note :
your #unindent method more or less mimicks an existing ActiveSupport monkey-patch on String, #strip_heredoc
in those cases, using #content_tag can also save you a lot of hassle, i.e :
<<%=tag%> class="squeeze" style=<%="#{style}"%>>
# more code...
could become :
<%= content_tag tag, class: 'squeeze', style: style do %>
# more code...

Related

How do I override ActiveSupport::SafeBuffer when I yield a hash provided in a view

My profile.html.erb provides a hash upstream to the layout.html.erb to be included in the metadata
<% provide(:twitter, { %>
<% card: 'summary', %>
<% site: '#twitter', %>
<% title: 'Lorem ipsum', %>
<% description: 'Non sequitar', %>
<% image: gravatar_for(#user, { size: 150 } )%>
<% }) %>
The layout.html.erb then is supposed to iterate over this and create the meta tags:
<% yield(:twitter).each do |key, value|%>
<meta name="twitter:#{key}" content="#{value}" />
<% end %>
However, it gives me the error undefined method 'each' for #<ActiveSupport::SafeBuffer:0x007f8d616e5870>, and after inspecting yield(:twitter) with byebug I see that I yield an instance of ActiveSupport::SafeBuffer:
(byebug) hash = yield(:twitter)
"{:card=>"summary", :site=>"#twitter", :title=>"Lorem ipsum", :description=>"Non sequitar", :image=>"https://secure.gravatar.com/avatar/a937016807c66d89d0a62d7a1b235533?s=150"}"
(byebug) hash.class
ActiveSupport::SafeBuffer
I could eval the string into a hash, but ActiveSupport::SafeBuffer encodes it html safe, and there's no method to get the original back out. Is there a smarter way to do this, or do I have to hack around it with a helper converting the string back into a hash, and then eval()ing it into a has?
Since provide expects either a string or a block then it might be better to create the meta tags in a helper and pass it as string argument.
module TwitterHelper
def twitter_meta_tags(hash)
hash.each_with_object([]) do |(key, value), array|
a.push( tag(:meta, { name: "twitter:#{k}", content: v }, false, false ) )
end.join("\n").html_safe
# not sure if that last html_safe is really needed.
end
end
Downstream view:
<%
provide(:twitter, twitter_meta_tags({
foo: "bar"
}));
%>
Layout:
<%= yield(:twitter) %>
Using a partial
Another option is to use a partial and pass a block instead of a string to provide:
<% # app/views/twitter/_meta_tags.html.erb %>
<% hash.each do |key, value|%>
<meta name="twitter:#{key}" content="#{value}" />
<% end %>
Downstream view:
<% provide(:twitter) do %>
<%= render partial: "twitter/meta_tags", hash: {
card: 'summary',
site: '#twitter',
title: 'Lorem ipsum',
description: 'Non sequitar',
image: gravatar_for(#user, { size: 150 } )
} %>
<% end %>
Layout:
<%= yield(:twitter) %>
I fixed it with the following:
<% twitter_tags = String.new yield(:twitter)%>
<% twitter_tags = eval ( CGI.unescapeHTML( twitter_tags ) ) %>
<% twitter_tags.each do |key, value|%>
<%= tag(:meta, name: "twitter:#{key}", content: (value) )%>
<% end %>
IMHO this is an extremely ugly way of hacking around the problem. I'll write a helper to take away most of this from the view, however, if you have a more elegant solution, please share it and I'll accept your answer instead of mine.
By the default, all the provided values are instances of ActiveSupport::SafeBuffer, this converts it into a string, and evaluates it into a hash again.

How to create a HTML template in Rails?

I have a long code. For example:
<% Post.limit(10).offset(10).order(id: :desc).each do |post| %>
######## FROM HERE ###########
<% unless post.image.blank? %>
<% img = post.image.split(' ') %>
<% if post.post_type == 1 %>
<div class="post_type1">
<h1>
<a href="post/<%= post.slug %>">
<%= post.title %>
</a>
</h1>
</div>
<% end %>
<% end %>
####### TO HERE #########
<% end %>
So, I want to make this part(FROM HERE <-> TO HERE part) like a template. So I wrote first line, later render or something else, I do not know and the last line for displaying all the page. Can you help me with it?
Just put it in a file (lets call it _post.html.erb) and include it like:
<% Post.limit(10).offset(10).order(id: :desc).each do |post| %>
<%= render partial: 'post', locals: { post: post } %>
<% end %>
Note the use of locals: { post: post } that passes the variable post in your loop to your partial. Another thing to note is that your file name starts with an underscore (_) which is left out when you render the partial.
Read more about partials in Rails guide.

Create an HTML element with Ruby

I would like some pointers on how to rewrite the post to have links to hashtags. I need to have link_to( ..........) inside the post after I rewrite it and I can't really figure out how to do that in the backend application.
The current method I am trying to use is:
def twitify(tweet = '')
tweet.gsub!(/(?:\s|^)(?:#(?!(?:\d+|\w+?_|_\w+?)(?:\s|$)))(\w+)(?=\s|$)/) do |tag|
link_to("##{tag}", '#')
end
end
However this only gives me:
jdawiodwiajdaw #mergiCaTeCrap (which is not a link)
This is the HTML:
<li id="tweet-<%= tweet.id %>">
<%= link_to gravatar_for(tweet.user, size: 50), tweet.user %>
<span class="user"><%= link_to tweet.user.name, tweet.user %></span>
<span class="content">
<%= twitify(tweet.content) %>
<%= image_tag tweet.picture.url if tweet.picture? %>
</span>
<span class="timestamp">
Posted <%= time_ago_in_words(tweet.created_at) %> ago.
<% if current_user?(tweet.user) %>
<%= link_to "delete", tweet, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
</span>
</li>
I would like it to not show me the href thing, how would I do that?
This is probably because Rails is escaping the html tags due to security reasons, in this case you just have to call the html_safe on the link_to like so:
def twitify(tweet = '')
tweet.gsub(/(?:\s|^)(?:#(?!(?:\d+|\w+?_|_\w+?)(?:\s|$)))(\w+)(?=\s|$)/) do |tag|
" " + link_to("#{tag.strip}", '#')
end.html_safe
end
You should be good to go!
Okay first of all there is nothing wrong with the way you are going about it.
def twitify(tweet = '')
tweet.gsub(/(?:\s|^)(?:#(?!(?:\d+|\w+?_|_\w+?)(?:\s|$)))(\w+)(?=\s|$)/) do |tag|
" " + link_to("#{tag.strip}", '#')
end
end
All I did was avoid modifying the original tweet (used gsub instead of the bang method gsub!), added a space before the #hash_tag,removed the extra # and space from the link.
As you can see
tweet = "This is a #tweet and it can make #hash_tags into #links"
twitify(tweet)
#=> "This is a #tweet and it can make #hash_tags into #links"
#your implementation
original_twitify(tweet)
#=> "This is a # #tweet and it can make # #hash_tags into # #links"
So I don't think there is really an issue here you pretty much had everything correct.

Can one use conditions and loops on a single line in Ruby?

How would one go about turning the following code into the latter?
<div id="faqs">
<% if #faqs.length > 0 %>
<% #faqs.each do |faq| %>
<div class="faq">
<strong>Q:</strong> <%= faq.question %>
<br />
<strong>A:</strong> <%= faq.answer %>
</div>
<% end %>
<% else %>
<p>No FAQs to display.</p>
<% end %>
</div>
<div id="faqs">
<% #faqs.empty? ? content_tag(:p, "No FAQs to display.") : #faqs.each do |faq| %>
<div class="faq">
<strong>Q:</strong> <%= faq.question %>
<br />
<strong>A:</strong> <%= faq.answer %>
</div>
<% end %>
</div>
I'm curious as to whether I can get the latter code to work. The only element of it that is failing at the moment is that the content_tag() is not displaying - this is due to the fact that I'm not using printable ruby tags (<%= # %>) but using them will dump out the FAQ objects underneath the content.
I considered the use of puts() to print the content_tag() while inside the ruby tags but that didn't work.
I've tried to search for this issue but haven't yielded anything useful.
Is this achievable and if so, does it have any benefits other than being prettier?
One way to make the later code to work if you can put the body of the loop in a helper function and return the out put of content_tag from that. The line in view file might be somewhat like this.
<%= #faqs.empty? ? content_tag(:p, "No FAQs to display.") : printList(#faqs) %>
and your printList function will return the output of nested content_tags. You can make a generic list printing function which can be used for any list.
Something so obvious but still shared.
This should work (for clarity, I moved FAQ tag generation in separate helper method):
<div id="faqs">
<%= raw (#faqs.empty? ? content_tag(:p, "No FAQs to display.") : #faqs.map { |faq| faq_div(faq) }.join) %>
</div>
or, perhaps more clean:
<div id="faqs">
<%= content_tag(:p, "No FAQs to display.") if #faqs.empty? %>
<%= raw #faqs.map { |faq| faq_div(faq) }.join %>
</div>
meanwhile, in helpers:
def faq_div(faq)
'<div class="faq"><strong>Q:</strong> %s<br /><strong>A:</strong> %s</div>' % [faq.question, faq.answer]
end
This should work:
<% if #faqs.each do |faq| %>
<div class="faq">
<strong>Q:</strong> <%= faq.question %>
<br />
<strong>A:</strong> <%= faq.answer %>
</div>
<% end.empty? %>
<p>No FAQs to display.</p>
<% end %>

Rails - Outputting a list of items with a comma after each record excluding the last

I'm doing the following right now in one of my user mailer views:
<% #participants.each do |participant| %>
<%=participant.user.full_name%>
<% end %>
I want a comma after every record except for the last, I suppose I could add an if block to see if the current record is the last, but that seems like a lot of code. Does rails have a better way to output a comma after every item excluding the last.
Good: XXXXX, XXXXXX, XXXXX
Bad: XXX,XXX,XXXX,
Thanks
You could do something like
#participants.map{|p| p.user.full_name}.join(",")
You also may want to look into the to_sentence method that Rails adds to the Array class; it lets you do stuff like output "xxx, yyy, and zzz" automatically.
["Apple", "Orange", "Pie"].join(", ") => "Apple, Orange, Pie"
in more complex job:
<% #participants.each do |participant| %>
<%= participant.user.full_name %>: <%= participiant.comment %><%= "," unless participiant == #participiants.last %>
<% end %>
If your actual case is more complicated (would benefit from partials), you can vastly simplify it by replacing your loop with a partial collection:
<%= render :partial => "name", :collection => #participants, :spacer_template => "comma" %>
<% #example.each_with_index do|example,index|%>
<%=example.name%>
<%= "," unless #example.size==index+1 %>
<%end%>
You can also do it with css
.list-csv {
display: inline;
list-style: none;
}
.list-csv li {
display: inline;
}
.list-csv li:after {
content: ", ";
}
.list-csv li:last-child:after {
content: "";
}
<ul class="list-csv">
<li>one</li>
<li>two</li>
<li>three</li>
</ul>

Resources