How to use ActionView/Erubi outside Rails

515 Views Asked by At

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>
2

There are 2 best solutions below

5
Pavel Oganesyan On

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.

0
thesecretmaster On

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.