Extending Redcarpet - How to include custom tags in a Markdown document

How to use custom logic in your Markdown

Published on May 25, 2018

About 5 minutes read

Markdown is a lightweight markup language that allows you to focus on content rather than markup. The learning curve for Markdown is not steep at all, allowing you to become comfortable in less than a couple of days. You can use this great cheatsheet and tutorial to help you get started.

Middleman allows you to define multiple templating options for each file, based on what extensions that file has. For this website posts I primarily use the .html.markdown extension, allowing me to use Markdown for the majority of the document, as well as raw HTML if I ever need to. There are certain cases that I would like to use ruby partials to display media items such as Youtube embeded videos, that have a lot of boilerplate. I tried extending the file extension but .erb doesn't play well with with the frontmatter on these files, so it was time for plan b! I needed to come up with a way to instruct my Markdown renderer (I'm using Redcarpet) that there are some new elements that it should be aware of.

Enter MyMarkdown

The first thing I did was to create new instance of the Redcarpet renderer. Looking at the documentation for the Middleman Renderers, I created a new module and a new class and inherited everything from Middleman::Renderers::MiddlemanRedcarpetHTML and set out to create my own functions for interpreting these new element tags. I needed support for the following: - Youtube videos - Soundcloud songs - Spotify tracks, albums and playlists

For all of the following I needed to create a naming scheme that would allow the renderer to parse the input and display actual HTML. I settled on the following naming scheme:

['youtube-video video_id'] for Youtube videos

['soundcloud song_id'] for Soundcloud songs and

['spotify album alubm_id'] for Spotify albums

['spotify track track_id'] for Spotify tracks and

['spotify playlist username/playlist/playlist_id'] for Spotify playlists (without the '' marks).

RegEx to the rescue

In order for Redcarpet to be able to process these new tags, we need a way to match the text that was being processed to our new custom tag. As the new elements are block level elements, we need to override the default paragraph function, in order to include some custom logic. In order to do that, we need to create a Regular Expression that the overriden paragraph function will use to match our new tag and execute our custom logic. In case that no custom tag is identified, then the paragraph is returned unchanged and processing continues.

# custom_markdown_extensions.rb

def paragraph(text)
  process_custom_tags("<p>#{text.strip}</p>\n")
end
# custom_markdown_extensions.rb

private 

  def process_custom_tags(text)

    # Youtube videos
    if t = text.match(/(\[youtube-video )(.+)(\])/)
      youtube_video(t[2])

    # Soundcloud tracks
    elsif t = text.match(/(\[)(soundcloud)( )(.+)(\])/)
      soundcloud_resource(t[4])

    # Spotify albums
    elsif t = text.match(/(\[)(spotify)( )(album)( )(.+)(\])/)
      spotify_resource("album", t[6])

    # Spotify playlist, resource should be in the form of: username/playlist/playlist_id  
    elsif t = text.match(/(\[)(spotify)( )(playlist)( )(.+)(\])/)
      spotify_resource("playlist", t[6])

    # Spotify tracks  
    elsif t = text.match(/(\[)(spotify)( )(track)( )(.+)(\])/)
      spotify_resource("track", t[6])

    # if no match is found, just return the text    
    else 
      return text
    end
  end
# custom_markdown_extensions.rb

  def youtube_video(resource_id)
    return <<-EOL
      <p>
      <div class="youtube-video">
      <iframe width="100%" height="100%" src="https://www.youtube.com/embed/#{resource_id}"  \ 
      frameborder="0" allow="autoplay; encrypted-media" allowfullscreen>
      </iframe>
      </div>
      </p>
      EOL
  end

  def soundcloud_resource(resource_id)
    return <<-EOL
      <div class="soundcloud-song">
      <iframe width="100%" height="300" scrolling="no" \
       frameborder="no" allow="autoplay" \
       src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/#{resource_id} \ 
       &color=%23ff5500&auto_play=false&hide_related=false&show_comments=true&show_user=true& \ 
       show_reposts=false&show_teaser=true&visual=true"> \ 
       </iframe>
      </div>
      EOL
  end

  def spotify_resource(resource_type, resource_id)
    if resource_type == "album"
      return <<-EOL
        <div class="spotify-container">
        <iframe src="https://open.spotify.com/embed/album/#{resource_id}"  \ 
        width="100%" height="100%" frameborder="0" allowtransparency="true"></iframe>
        </div>
        EOL
    elsif resource_type == "track"
      return <<-EOL
        <div class="spotify-container">
        <iframe src="https://open.spotify.com/embed/track/#{resource_id}" \
         width="100%" height="100%" frameborder="0" allowtransparency="true"></iframe>
        </div>
        EOL
    elsif resource_type == "playlist"
      return <<-EOL
        <div class="spotify-container">
        <iframe src="https://open.spotify.com/embed/user/#{resource_id}" \
         width="100%" height="100%" frameborder="0" allowtransparency="true" allow="encrypted-media"></iframe>
        </div>
        EOL

    end
  end

The EOL syntax blocks allow you to create a multiline block of code that will be returned if the matcher is activated. A catch when using such a syntax is that the lines of the block should not be indented, otherwise the return statement would not work.

Another interesting point is that I've tweaked the widths and heights of the embeded pages to make them pseudo-responsive, allowing me to control their width and height based on their parent container and not on the embded itself (width and height are based on a percentage, not some hardcoded value as the documentation for each embed suggests).

Putting it all together

In order for our custom renderer and tags to work, we need to let Middleman know that it should load a different Markdown Renderer than the one we've been using until this point. In order to do that we need to make the following entry in the config.rb of our Middleman project:

#Markdown Settings
require 'lib/custom_markdown_extensions' # or wherever you've created your custom renderer file
helpers MarkdownHelper

set :markdown_engine, :redcarpet
set :markdown, :fenced_code_blocks => true,
              :smartypants => true,
              :tables => true,
              :highlight => true,
              :superscript => true,
              :renderer => MarkdownHelper::MyRenderer

We start off by requiring the custom_markdown_extensions file and using the included module as a Helper and then instructing the redcarpet instance to use our custom Renderer instead of its default one. Stop and restart your Middleman server and you should be able to use the tags you've specified in your custom renderer file.

Developer Hint: Whenever Middleman server loads for the first time, it loads all helper files but doesn't reload them whenever you make any changes to them. In order to be able to use a similar logic to that of normal files you need to change the way that helper files are loaded in the first place. You need to use Dir['lib/*'].each(&method(:load)) to load the files instead of require for each individual file.

Going a step further

You can actually extend this feature with any tag you want, as long as you create a Regular Expression for the the matcher to use and a function to process that match. So far I've used it as an indirect method of rendering partials inside my Markdown files, allowing me to hide some of the complexity that is associated with the page embeds. In case that the syntax for an embed changes you can refactor the implementation of the private function responsible for that embed and your front-end workflow won't have to change at all.

Resources