Rendering Dynamic Markdown in Rails
27 Jan 2018I 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.