Ever wondered how blogs all over the internet display code blocks and handle other styling? Wonder no more. Many of these use Markdown, which allows you to dictate styles easily.
To get started, we have an existing model called Post with a text field called content.
We'll be using Redcarpet and Rouge to render this field as styled markdown. Add both to your Gemfile and run bundle install
.
We'll start by creating a MarkdownAdapter module under app/lib with an associated test using RSpec. Why create an adapter? Because we might want to be able to swap out our rendering engine in the future, and creating an adapter means we 1) will only have to change the adapter to change the rendering engine and 2) can use this adapter to render any other markdown in the app (not just on Posts).
In spec/lib/markdown_adapter_spec.rb:
require "rails_helper"
include MarkdownAdapter
describe MarkdownAdapter do
describe "#render_content" do
it "should return html_safe rendered markdown" do
content = "# wowzers\n\n_testing_\n\n~~does this work~~\n\n*italics*"
result = render_content(content)
expect(result).to eq(
"<h1>wowzers</h1>\n\n<p><u>testing</u></p>\n\n<p>"/
"<del>does this work</del></p>\n\n<p><em>italics</em></p>\n"
)
end
end
end
Our method to change content to rendered markdown will be called render_content.
From the Rouge and Redcarpet docs, we learn that we can implement this pretty easily. We'll implement most of the extensions.
In lib/markdown_adapter.rb:
require "redcarpet"
require "rouge"
require "rouge/plugins/redcarpet"
module MarkdownAdapter
class HTML < Redcarpet::Render::HTML
include Rouge::Plugins::Redcarpet
end
def render_content(content)
options = {
filter_html: true,
hard_wrap: true,
link_attributes: {rel: "nofollow", target: "_blank"}
}
extensions = {:strikethrough => true,
:superscript => true,
:underline => true,
:highlight => true,
:disable_indented_code_blocks => true,
:space_after_headers => true,
:fenced_code_blocks => true,
:autolink => true,
:lax_spacing => true,
:no_intra_emphasis => true}
renderer = HTML.new(options)
markdown = Redcarpet::Markdown.new(renderer, extensions)
markdown.render(content).html_safe
end
end
Potential gotchas: Rails 5 does not autoload files in the lib directory. You might need to add something like config.autoload_paths << Rails.root.join("lib")
to your application.rb.
We're mostly done at this point. There are a few ways to make the rendered content display (in this case, we only want markdown to render on the show page). We'll make sure the following is in our app/controllers/posts_controller.rb:
class PostsController < ApplicationController
...
include MarkdownAdapter
def show
@post_content = render_content(@post.content)
end
...
end
In the show page, we just use @post_content where we'd have previously used @post.content (and update tests to include markdown).
In app/views/posts/show.html.erb:
<div class="post__content">
<%= @post_content %>
</div>
We'll also grab one of the styles that ships with Rouge and dump it directly in our assets directory with rougify style monokai.sublime > app/assets/stylesheets/rouge_syntax.scss
.
You're done! Now you're rendering markdown.
p.s. there's a good argument that the rendering function should be defined as self.render_content
, and calling it with MarkdownAdapter.render_content
. I'm not really attached to either method, and went for the one that requires less typing but is perhaps less explicit on later readings of the controller.