0

I frequently use Sinatra for smallish projects. It's pretty good for what I need but I miss the ability to mark strings as HTML safe, and then ERB knowing when to escape or not escape accordingly.

I'd really like it if I could rip out the patches that Rails makes to Erubi (around here) and then apply those patches to Erubi myself so that tilt can just use the monkey-patched Erubi and everyone lives happily ever after. However, after digging around in the source, it's not clear to me how I could actually accomplish that.

I also tried to find some way to get an interface into ActionView like the render method, but I couldn't even find where that was defined.

How can I use ActionView outside of Rails, ideally by using ActionView's monkey-patches to Erubi, or if that won't work, how else can I use ActionView to go from template string to rendered string outside Rails?

Specifically, I'd like to be able to do the following:

def some_wrapper_func(unescaped_html)
  "<div>#{h unescaped_html}</div>".html_safe
end

# test1.erb
hello world <%= "<script>alert('hi');</script>" %> <%= some_wrapper_func("<span>foobar</span>") %>
#=> hello world &lt;script&gt;alert(&#x27;hi&#x27;);&lt;&#x2F;script&gt; <div>&lt;span&gt;foobar&lt;&#x2F;span&gt;</div>
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
thesecretmaster
  • 1,950
  • 1
  • 27
  • 39

2 Answers2

2

What you need here is ActiveSupport. I'm not sure if it is overkill or not, but you can do this:

#app.rb:
require 'sinatra'
require 'active_support/all'

get '/' do
 erb :index
end

And in a view:

#views/index.erb

Hello, world!
<%= "<script>alert('Hello!')</script>".html_safe %>

Mind that requre 'active_support' will load nothing and requre 'active_support' will load all modules. You can specify what modules do need as described in Active Support Core Extensions.


If the only goal is to enable auto-escaping, there is no need for ActionView at all. It can be done like this (mind the <%== %> tag):

#app.rb
require 'sinatra'
require 'erubis'

set :erb, :escape_html => true

get '/' do
 erb :index
end

 #View
 <%= "<script>alert('Hello, and it will not produce alert!')</script>" %>
 <%== "<script>alert('Hello and it will!')</script>" %>

We will try to get ActionView up and running with Sinatra (or any Ruby program):

require 'sinatra'
require 'action_view'

get '/' do
  av_render :index
end

def av_render view
  paths = ActionView::PathSet.new(["views"])
  lookup_context = ActionView::LookupContext.new(paths)
  renderer = ActionView::Renderer.new(lookup_context)
  view_context = ActionView::Base.new(renderer)
  renderer.render(view_context, template: view)
end

And in the view we use html_safe:

<%=  "<script>alert('Hello, and it will not produce alert!')</script>" %>
<%=  "<script>alert('Hello and it will!')</script>".html_safe %>

Wrapper functions also work with this approach. The only problem here is a custom-render method, but it can be avoided.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Pavel Oganesyan
  • 6,774
  • 4
  • 46
  • 84
  • You're right in that that code will run and work, but it'll also be unescaped if you leave off the `html_safe`, so `<%= "" %>` will still send the alert. I'm looking for non-safe strings to automatically be escaped, as happens in ActionView. – thesecretmaster Jul 19 '19 at 21:28
  • I also know about auto escaping via `<%== %>`, but I'd like to be able to mark string as safe or unsafe, for example if I write a `nav_element_helper` I want it to be able to safely return HTML, but if I just do `<%= "some string" %>`, for that to be automatically escaped, similar to how ActionView does it. – thesecretmaster Jul 19 '19 at 22:13
  • @thesecretmaster That is my final shot at this question, check ActionView render-based solution. – Pavel Oganesyan Jul 19 '19 at 23:29
  • Thanks! This seems like exactly what I needed! – thesecretmaster Jul 19 '19 at 23:35
  • Rather than add "Edit" or "Update" tags, simply add your modifications where you'd have put them if you included them originally. We can see if something was changed if we need to. Remember, SO is like an online encyclopedia and readability and grammar are very important because you're writing a solution to an entry in that book. "Edit" and "Update" merely detract from that readability. – the Tin Man Jul 20 '19 at 05:04
0

If you'd like to avoid ActionView entirely and just use Tilt+Erubi, you can actually create for yourself a SafeString class and have Erubi use it for compilation.

Erubi takes some important options, specifically: - escape: If this is true, then <%= %> will escape by default, otherwise only <%== %> will escape by default - bufval: Internally, erubi uses what is basically an accumulator to build up your template. This is the value that it will initialize that accumulator to. It is important that it has a <<(str) method to concat new pieces on, and a to_s method to get the return value out. - escapefunc: The function that Erubi will use for escaping. It's important to override this, because we'll want to escape anything that isn't a SafeString but let SafeStrings pass through unchanged.

So, first let's define this SafeString class:

# main.rb
require 'tilt'
require 'erubi'

class SafeString
  def initialize(str = '')
    @str = str
  end

  def <<(str)
    if str.is_a? String
      return (@str << str)
    elsif str.is_a? SafeString
      @str = @str << str
      return self
    else
      throw "Can't concat"
    end
  end

  def to_s
    @str
  end

  def self.escape(val)
    if val.is_a? SafeString
      return val.to_s
    else
      return Erubi.h(val.to_s)
    end
  end

  module Helpers
    def raw(content)
      SafeString.new(content)
    end
  end
end

Then, we'll need to include the raw helper we defined and to test it on an ERB file:

include SafeString::Helpers
puts Tilt::ErubiTemplate.new("somefile.erb", bufval: 'SafeString.new', escapefunc: 'SafeString.escape', escape: true).render

# somefile.erb
<%=  "<script>alert('Hello, and it will not produce alert!')</script>" %>
<%=  raw("<script>alert('Hello and it will!')</script>") %>

And this will give us the output we desire!

# stdout
&lt;script&gt;alert(&#39;Hello, and it will not produce alert!&#39;)&lt;/script&gt;
<script>alert('Hello and it will!')</script>

To improve this, instead of this minimal SafeString class, you could use ActiveSupport::SafeBuffer.

thesecretmaster
  • 1,950
  • 1
  • 27
  • 39