edruder.com i write about anything, or nothing

Rendering Dynamic Markdown in Rails

I wrote about how I enabled Markdown views in my Rails 5 app previously. I wanted to add the ability to create and edit Markdown-formatted articles, posts, etc. in this app, and it turned out to be pretty easy.

The Markdown that I want to render will be created dynamically by the users of my app and stored in the Rails database, as opposed to being in static .md files somewhere in my views directory. I already had the redcarpet gem installed and configured in my app–I just needed to figure out how to use it to render arbitrary Markdown in a view.

My Markdown handler had changed slightly from its initial incarnation–I had tweaked the Redcarpet settings a little and done a tiny bit of refactoring. Here’s what I started with:

# config/initializers/markdown.rb

require 'redcarpet'

module ActionView
  module Template::Handlers
    class Markdown
      class_attribute :default_format
      self.default_format = Mime[:html]

      class << self
        def call(template)
          compiled_source = erb.call(template)
          "#{name}.render(begin;#{compiled_source};end)"
        end

        def render(template)
          markdown.render(template).html_safe
        end

        private

        def extensions
          @md_options ||= {
            autolink: true,
            fenced_code_blocks: true,
            highlight: true,
            quotes: true,
            strikethrough: true,
            tables: true,
            underline: true,
          }
        end

        def markdown
          @markdown ||= Redcarpet::Markdown.new(renderer, extensions)
        end

        def renderer_options
          @renderer_options ||= {
            filter_html: true,
            hard_wrap: true,
          }
        end

        def renderer
          @renderer ||= HTMLWithPants.new(renderer_options)
        end

        def erb
          @erb ||= ActionView::Template.registered_template_handler(:erb)
        end
      end
    end
  end
end

class HTMLWithPants < Redcarpet::Render::HTML
  include Redcarpet::Render::SmartyPants
end

ActionView::Template.register_template_handler(:md, ActionView::Template::Handlers::Markdown)

Notice that all of the Markdown stuff is private–a Rails template handler just needs to respond to :call–it doesn’t need to expose its implementation details (and shouldn’t). Because of what call returns in this particular handler–a String that gets evaled somewhere in ActionView, and the evaled code calls the render method–the render method needs to be public, too.

What I want to end up with is an application-wide markdown(text) method that accepts Markdown-formatted text and returns HTML that can be displayed by any view. The Redcarpet::Markdown object has a render(text) method that’s just what we need–I just need to make that markdown method in my handler public and use it in the helper! The result:

# config/initializers/markdown.rb

# moved into the public section
def markdown
  @markdown ||= Redcarpet::Markdown.new(renderer, extensions)
end

private

# app/helpers/application_helper.rb

def markdown(text)
  ActionView::Template::Handlers::Markdown.markdown.render(text).html_safe
end

Not the prettiest line of Ruby code (and it violates the Law of Demeter), but it works–and it uses the already-instantiated Redcarpet::Markdown object. I can pretty it up, later.

If you want to render the Markdown from your users differently than the Markdown in your own views, you might want to instantiate (and cache) a Redcarpet::Markdown object with different settings. E.g., you might want to be more strict about what is allowed in the Markdown that your users can enter, compared to what you allow in your own views.

That .html_safe is important–without it, the HTML generated from the Markdown will be “escaped” by Rails, displaying a bunch of ugly, raw HTML in your view.

With this helper, you’re able to put a call to markdown in any views that you like! For example, I have a ContentsController whose show method reads a Content record from the database into a @content variable. The Content model has a body field containing Markdown formatted text, so in the views/contents/show.html.erb file, there’s a markdown(@content.body) call that inserts beautiful HTML from the Markdown text that the user entered!

Please leave a note if you have questions or suggestions.