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.

ruby
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:

sh
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:

sh
touch app/controllers/handlers_controller.rb

with one action:

ruby
class HandlersController < ApplicationController
  def index
  end
end

Then we add the resource to the router:

ruby
resources :handlers, :only => [:index]

View

Since we did not generate the controller with a generator, we need to create the view folder manually:

sh
mkdir app/views/handlers

and add the view file:

sh
touch app/views/handlers/index.md

Let’s add some text with Markdown syntax to pass our test:

text

**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:

sh
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:

ruby
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:

ruby
gem 'redcarpet'
gem 'pygmentize'

and run

sh
bundle install

Then we write the handler code:

ruby
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:

ruby
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):

ruby
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:

ruby
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:

ruby
def cache_key(text)
  Digest::MD5.hexdigest(text)
end

Now the handler will render Markdown only when the text has changed.