Ruby: How to use generate HTML with dynamic values? - ruby

I am writing a Ruby script that will generate a large flat HTML menu for my website, I could generate this menu on the fly each time a page loads, but I think doing so is a waste of resources, especially as this will almost never need to change.
I want to effectively do the following (in semi-sudocode):
part_of_my_menu = eval %{
<script type="text/javascript">
var mapper = new Array();
<% parent_categories.each_with_index do |parent_category,i| -%>
mapper["#{parent_category.name}"] = <%= i -%>;
<% end -%>
</script>
}
and then be able to write the part_of_my_menu string variable to a HTML file (this I can do).
I know this is not how eval works in Ruby but does anyone know how to achieve this same "wrapper" functionality?
(fyi - the code I want to wrap with my "eval" function is much longer than this, I've only posted a very small snippet to illustrate what I am trying to achieve)
Thanks!

ERB is part of the standard library so you could do things like this:
tmpl = %q{<script type="text/javascript">...</script>}
erb = ERB.new(tmpl)
parent_categories = [ ... ]
part_of_my_menu = erb.result
The ERB documentation contains some good examples of how to use it.
You don't need a hand rolled eval construction, you can use standard existing libraries and your existing knowledge.

You might be interested in the dom gem that I have developed. You can generate HTML strings like this:
require "dom"
["foo".dom(:span, class: "bold"), "bar"].dom(:div).dom(:body).dom(:html)
# => "<html><body><div><span class=\"bold\">foo</span>bar</div></body></html>"

Related

Unescaping HAML in an Attribute Hash

I have a problem similar to some I've found on stackoverflow, but not quite the same. I'd like to avoid the solution to the following question:
https://stackoverflow.com/a/10407782/996587
Basically, would like the following HTML output:
<div class='myclass' extraattr='UNESCAPED <>& CONTENT'>
Content...
</div>
The HAML I'm using looks like this:
.myclass{ "extraattr" => "UNESCAPED <>& CONTENT" }
Content...
I can't quite figure out how to get the content to output the way I want. Tried applying .html_safe to the end of the string, but got the following error:
undefined method `html_safe' for "UNESCAPED <>& CONTENT":String
Later realized that for this particular application I'm not using Rails, just ruby + HAML. (I inherited this project and I'm just starting to learn HAML, ruby, and Rails anyways)
And again, for those of you too lazy to click links and didn't read the solution I referred to, I'd prefer not to configure HAML to not escape attrs for the entire file, just for this one attribute.
Thanks!
UPDATE
I just found the :plain filter, and was able to get what I wanted using that. However, if there's a trick I don't know about so I don't have to write all the HTML, I'd appreciate it. My "fix":
:plain
<div class='myclass' extraattr='UNESCAPED <>& CONTENT'>
Content...
</div>
There isn’t (currently) any way to turn off escaping for an individual attribute in Haml outside of Rails, it’s all or nothing using the :escape_attrs option. Depending on what you want, it might be worth looking at the :once option.
When Haml is used in Rails, it replaces the html escaping methods with some that respect the html_safe value that ActiveSupport adds (see lib/haml/helpers/xss_mods.rb).
It is possible to use these methods outside of Rails if you want. You will need to add html_safe and html_safe? methods to the String class in order for this to work (be careful here, this example is only a “poor man’s” version of the full XSS protection that Rails provides, it won’t really protect you from much but it will allow selective escaping of attributes).
Add the following somewhere after requiring Haml (it might be best in its own file that gets required):
class String
def html_safe?
defined?(#html_safe) && #html_safe
end
def html_safe
#html_safe = true
self
end
end
require 'haml/helpers/xss_mods'
module Haml::Helpers
include Haml::Helpers::XssMods
end
Now you can use html_safe on your strings, and Haml won’t escape them:
.myclass{ "extraattr" => "UNESCAPED <>& CONTENT".html_safe,
"otherextraattr" => "ESCAPED <>& CONTENT"}
Content...
Output:
<div class='myclass' extraattr='UNESCAPED <>& CONTENT' otherextraattr='ESCAPED <>& CONTENT'>
Content...
</div>
Have you tried using a \ to escape the characters.
{ :myattr => '\<\>\&' }

erb template and new line/spacing problems

I have a erb template that I am using to generate source code, but the problem is I want the erb template to look readable, but at the same time I want the generated output to look good also.
So I have this snippet:
<% model.attributes.each do |attribute| %>
private <%= model.get_instance(attribute) %> <%= ... %>;<% end%>
So my code output isn't coming out as I would like, I want it like:
private type name;
private type name2;
private type name3;
But it is coming out with an extra space between each line.
How can I fix this?
The other challenge like I mentioned is that I don't want my erb to look unreadable (if possible).
I had had the same issues.. trying to get the output source code well formated without compromising the readability of the template was a nightmare. I've opted for using a code formatter chained in the code generation process and just concentrate in my template file. Good luck

i18n markdown files in Rails 3 views

I am currently working through Michael Hartl's Rails Tutorial while experimenting with some other things not covered in the book. After completing Chapter 5, where the static pages are created, I decided change the view code to HAML, internationalize the pages, and put the static content into separate (non-partial) Markdown files, using the RDiscount gem to render them. For example:
app/views/static_pages/about.html.haml
- provide(:title, t('.about_us'))
:markdown
#{render file: "static_pages/about.#{params[:locale]}.md"}
Under the static_pages directory, I have Markdown files like about.en.md, about.it.md, about.ja.md etc, so interpolating in the :locale parameter is what determines which language Markdown file gets rendered.
My questions are:
The static_pages directory is a bit crowded with Markdown files, so are there any sensible default/best practice locations (perhaps outside of the app directory) to keep these Markdown files, where they could be presumably be edited by people who don't need to know about the inner workings of the app?
What better ways are there to implement rendering multi-lingual Markdown files in views? My use of :locale and the double string-interpolation seems inelegant.
Is there a way to change this code so that I can pass Ruby variables into the Markdown file? I know I can, for example, use a #{language} variable in the Markdown by just changing about.en.md into a HAML partial (_about.en.html.haml) and change the code to look something like:
app/views/static_pages/about.html.haml
- provide(:title, t('.about_us'))
:markdown
#{render "about.#{params[:locale]}", language: 'Markdown!'}
But, is there a way to do this without changing the Markdown file into another type of file? If such a way exists, is it recommended/feasible?
After having a look at this StackOverflow answer, it seemed that the best location for i18n Markdown files would be their own action name directories under the config/locales directory, and that there was a good opportunity to refactor the render code on all views for the StaticPagesController. So, using about.html.haml as an example below, the call to render in the home, help, about, and contact views has been changed to the exact same code:
app/views/static_pages/about.html.haml
- provide(:title, t('.about_us'))
:markdown
#{render file: localized_page_for(action_name, params[:locale])}
The localized_page_for method is defined in the StaticPagesHelper:
app/helpers/static_pages_helper.rb
module StaticPagesHelper
def localized_page_for(action, locale)
"#{Rails.root}/config/locales/#{action}/#{action}.#{locale.to_s}.md"
end
end
So, now all the Markdown files have been taken out of the app/views/static_pages directory and are called from their respective logical directories (eg. config/locales/about/about.en.md etc) using ActionController's action_name attribute and the locale, making for less clutter.
As for question 2 above, string-interpolation seems to be common enough for this kind of problem, so I'll consider it "elegant" enough as well.
As for question 3 above, after exhaustive searching, I haven't found a way anyone has passed in variables in to a pure Markdown file, and the documentation doesn't seem to say anything about supporting them, so I'm going to conclude that it's not possible. If passing Ruby variables in to Markdown is absolutely necessary, the file will need to be run through another interpreter, kind of like is described in this StackOverflow answer.
Update:
After running security scanner Brakeman against the app, it came up with a potential Dynamic Render Path security warning (albeit a weak one) due to dynamically passing in params[:locale] to the render call instead of passing it a static string. So, I moved the call to the localized_page method out of the views, moved the method itself out of the StaticPagesHelper (so that file is now empty) and in to the StaticPagesController and then instantiated a #page instance variable in each method to pass to the view. In summary, the code now looks like this, which doesn't get the security warning:
app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
before_filter :localized_page, only: [:help, :about, :contact]
def home
if signed_in?
#micropost = current_user.microposts.build
#feed_items = current_user.feed.paginate(page: params[:page])
else
localized_page
end
end
def help
end
def about
end
def contact
end
private
def localized_page
#page = "#{Rails.root}/config/locales/"\
"#{action_name}/#{action_name}.#{params[:locale].to_s}.md"
end
end
app/views/static_pages/about.html.haml
- provide(:title, t('.about_us'))
:markdown
#{render file: #page}

Can you pass ruby objects to haml during a render?

I'm trying to create a haml template that uses some data from my ruby app to fill in some content. Is it possible to pass arguments into haml to make it render correctly? Here's how I'm getting my haml template and rendering it:
template = File.open('path/to/template.haml')
html = Haml::Engine.new(template.read).render
So, is it possible to pass objects from the local Ruby script into the template file so that they the page is rendered correctly? Or, can I have the haml file pull in objects?
If this doesn't work, my only other idea is building up the template as a local string which seems more tedious to me. So, is there a different coding pattern that is more efficient for this job?
Yes,
Check the docs:
http://haml.info/docs/yardoc/Haml/Engine.html#render-instance_method
(String) render(scope = Object.new, locals = {}, &block)
pass them as locals
Here is an example of how to do it using ruby:
template = File.read('path/to/template.haml')
html = Haml::Engine.new(template).render(Object.new, :my_object => my_object)

Partial HAML templating in Ruby without Rails

I really don’t need the overhead of Rails for my very small project, so I’m trying to achieve this just using just plain Ruby and HAML.
I want to include another HAML file inside my HAML template. But I haven’t found a good—or really usable—way of doing this.
For example, I have these two HAML files:
documents.haml
%html
%body
= include(menu.haml) body
%article …
menu.haml
%ul
%li
%a whatever …
Include is obviously not the way to go here. But it does a nice job describing what I’m trying to achieve in this example.
I totally recommend the Tilt gem for these things. It provides a standard interface for rendering many different template langages with the same API, lets you set custom scope and locals, lets you use yield, and is robust and fast. Sinatra is using it for templates.
Example:
require 'haml'
require 'tilt'
template = Tilt.new('path/to/file.haml')
# => #<Tilt::HAMLTemplate #file="path/to/file.haml" ...>
layout = Tilt.new('path/to/layout.haml')
output = layout.render { template.render }
This lets you yield inside the layout to get the rendered template, just like Rails. As for partials, David already described a simple and nice way to go.
But actually, if what you're writing is going to be served over HTTP, i suggest you take a look at Sinatra, which already provides templating, and has the simplest request routing you could imagine.
I've done this before, just for a quick-and-dirty template producer. The easiest way is to just render the HAML inside the parent object:
%p some haml that's interesting
= Haml::Engine.new('%p this is a test').render
%p some more template
You'll more than likely want to build some methods to make this easier--a couple of helper methods. Maybe you write one called render_file that takes a filename as an argument. That method might look something like:
def render_file(filename)
contents = File.read(filename)
Haml::Engine.new(contents).render
end
Then, your template would look more like:
%p some template
= render_file('some_filename.haml')
%p more template
Note, you will probably need to pass self to the original Haml::Engine render so that it knows how to find your render_file method.
I've had great success just using the instructions posted by David Richards in a concatenated way, without variables, like this:
= Haml::Engine.new(File.read('/Volumes/Project/file_to_include.haml')).render
There's obviously a more elegant way. But for someone who just wants to include one or two files, this should work nicely. It's a drawback that all base files using these includes have to be recompiled after some changes to the latter. It might be worthwile to just use php include if the environment allows that.
def render_file(filename)
contents = File.read('views/'+filename)
Haml::Engine.new(contents).render
end
(Adding this semi-redundant answer to show how one might incorporate the techniques from other answers.)
Include something like this in your setup code to monkey-patch the Haml library.
module Haml::Helpers
def render_haml(filename)
contents = File.read(Rails.root.join("app", "assets", "templates", filename))
Haml::Engine.new(contents).render
end
end
Then in your Haml file
.foo
= render_haml('partial.haml')
.bar
Obviously this is a Rails-ish example (because I wanted to use HAML in my asset pipeline instead of as views)... You will want to replace Rails.root.join(...) with a way to find filename in your project's templates or partials directory.

Resources