Custom Markdown and Links in Redcarpet

Become a Subscriber

Redcarpet is probably the most popular gem for parsing Markdown in ruby, yet finding great examples on how to do method overloading and custom tags can be difficult. One of the things that we wanted to do for our own website was to have non-internal links open in a new tab (target=“_blank”) and to allow authors to link to resources by id rather than permalink, which works against our SEO. So how did we accomplish this?

A custom markdown renderer

First, you will need to create a custom class that extends from Redcarpet’s html renderer. Put this inside lib/unicorn_markdown.rb:

class UnicornMarkdown < Redcarpet::Render::HTML
  include Redcarpet::Render::SmartyPants
end

You may as well include the SmartyPants Module as it uses a much faster rendering method.

In our case we also had some permalink helpers that we use across the platform. We decided to stick these inside of our UnicornMarkdown class to make them available to our editor. These methods should live within the class you created above:

  def site
    {
      protocol: Rails.env.development? ? 'http' : 'https',
      domain:   Rails.env.development? ? 'unicorn.local:3000' : 'unicorn.tv',
      url:      Rails.env.development? ? 'http://unicorn.local:3000/' : 'https://unicorn.tv/'
    }
  end

  def permalink_to(resource)
    return if (model = resource.try(:model_name)).nil?

    topic = resource.try(:topic)

    if topic && topic.to_sym != :all
      resource.try(:permalink) ? "#{site[:protocol]}://#{topic}.#{site[:domain]}/#{model.route_key}/#{resource.permalink}" : "/#{model.route_key}/resource.try(:id).to_s"
    else
      resource.try(:permalink) ? "#{site[:protocol]}://#{site[:domain]}/#{model.route_key}/#{resource.permalink}" : "/#{model.route_key}/resource.try(:id).to_s"
    end
  end

Finally, we just need to override the link method from Redcarpet. We will drop in some conditionals to check to see if the link looks as we would expect with an http or /. If it does, then we simply check if the link contains unicorn to know whether it should have the target="_blank" attribute. If all of these conditions fail, then we should expect that the link is using our custom syntax: Article:ID.

  def link(link, title, content)
    # If the link starts with http or a forward slash
    if link =~ /^(http)|^\//i
      if link.include?("unicorn")
        "<a href=\"#{link}\" title=\"#{title}\">#{content}</a>"
      else
        "<a href=\"#{link}\" title=\"#{title}\" target=\"_blank\">#{content}</a>"
      end
    # Otherwise it's probably a link to our model: id
    else
      parts = link.gsub(/\s+/, "").split(':')
      return nil unless resource = parts[0].capitalize.constantize.send(:find, parts[1])

      "<a href=\"#{permalink_to(resource)}\" title=\"#{title}\">#{content}</a>"
    end
  end

It’s a bit quick and dirty, but with this code, our authors can now write format internal urls like:

[internal link to some article](Article:5827dab2de5873f507000000)

There are lots of optimizations and refactoring we could do to make this cleaner, but as with all things in dev: go with the quick and dirty, and clean up later. ;)