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 eval
ed somewhere in ActionView
, and the
eval
ed 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.