Requirements
Our task is to create Rails views in Markdown as we do when we write HTML. The view will need to support all Markdown syntax and Pygments, to prettify any code we have in the view. The code should be wrapped within a tag decorated with a class specifying the language in which the code is written, following standard Markdown syntax.
Let’s first create a new Rails app, so that we can test our feature. This will be a web application about books, so we’ll call it amazing. You can find it at the github repo. for this tutorial
Test
Now that we have the app, let’s start by writing a test. We’ll write an integration test to make sure all parts of our feature work correctly, in a file called /test/integration/markdown_handler_test.rb
.
require 'test_helper'
class MarkdownHandlerTest < ActionDispatch::IntegrationTest
test ".md renders markdown" do
get '/handlers.md'
assert_match '<strong>Markdown</strong> is great.', response.body, "tags"
assert_match '<code class="js">', response.body, "language class"
end
end
We’re expecting to have an index.md
view under handlers
folder, and have the Markdown tags render as HTML. Any code should be wrapped within a code
tag with its language as the CSS class name.
Let’s run the test:
bin/rake test test/integration/markdown_handler_test.rb
As expected, the test fails complaining that the view handlers/index
does not exist. Let’s fix that next.
Controller
We’ll create a controller manually:
touch app/controllers/handlers_controller.rb
with one action:
class HandlersController < ApplicationController
def index
end
end
Then we add the resource to the router
:
resources :handlers, :only => [:index]
View
Since we did not generate the controller with a generator, we need to create the view folder manually:
mkdir app/views/handlers
and add the view file:
touch app/views/handlers/index.md
Let’s add some text with Markdown syntax to pass our test:
**Markdown** is great.
```js
var foo = 111;
```
Next, we finally get to write the Markdown handler.
Handler
We’ll write the handler inside the lib
folder, as we might extract into its own gem in the future:
touch lib/markdown_handler.rb
Since the lib
folder is not auto loaded in Rails, we’ll need to tell Rails to do that. We’ll add the following line to our `config/application.rb’ file:
config.autoload_paths += %W(#{config.root}/lib)
There are a few gems out there that interpret Markdown; we’ll use Redcarpet. We’ll also use Pygmentize for syntax highlighting. Let’s add them to our Gemfile
:
gem 'redcarpet'
gem 'pygmentize'
and run
bundle install
Then we write the handler code:
class MarkdownHandler
class << self
def call(template)
compiled_source = erb.call(template)
"MarkdownHandler.render(begin;#{compiled_source};end)"
end
def render(text)
markdown.render(text).html_safe
end
private
def markdown
@markdown ||= Redcarpet::Markdown.new(HTMLWithPygments, fenced_code_blocks: true, autolink: true, space_after_headers: true)
end
def erb
@erb ||= ActionView::Template.registered_template_handler(:erb)
end
end
end
The template handlers in Rails are plain Ruby classes that respond to a call
method with a template
parameter. Our call
method first compiles the template with the built-in ERB handler. This way we can pass any controller or helper methods and instance variables as we would normally do with ERB templates.
The compiled result is then passed to the render
class method of our handler. The render
method calls the Redcarpet’s render
method. The @markdown
caches an instance of Redcarpet::Markdown
class, to which we have passed a Pygments subclass and some options (you can check the full list of options here.
We’ll write the HTMLWithPygments class next.
Pygments
Inside our MarkdownHandler class let’s add the following code:
class HTMLWithPygments < Redcarpet::Render::HTML
def block_code(code, lang)
lang = lang && lang.split.first || "text"
output = add_code_tags(
Pygmentize.process(code, lang), lang
)
end
def add_code_tags(code, lang)
code = code.sub(/<pre>/,'<div class="lang">' + lang + '</div><pre><code class="' + lang + '">')
code = code.sub(/<\/pre>/,"</code></pre>")
end
end
Our new class overrides the block_code
method of Redcarpet::Render::HTML
to inject Pygmentize for performing syntax highlighting. It also adds some wrapping HTML tags we can use to style the code. There are a few style files on the web that one can implement; you can find the one we use across our websites at the github repo for this tutorial.
Last
Next we need to register our new template handler. We do that within an initializer (/config/initializers/markdown_handler.rb
):
ActionView::Template.register_template_handler :md, MarkdownHandler
If we run our test now, it should pass.
One improvement we could add to our handler is to avoid Markdown rendering, if the text has not changed. So we could rewrite our render
method as follows:
def render(text)
key = cache_key(text)
Rails.cache.fetch key do
markdown.render(text).html_safe
end
end
The cache key can be a hash of our text:
def cache_key(text)
Digest::MD5.hexdigest(text)
end
Now the handler will render Markdown only when the text has changed.