Programming

The Dynamic Side of Markdown

May 3, 2012
--
User avatar
Adrian Perez
@blackxored

Always wanted to bend the Markdown markup language to do things it wasn't designed to do. I love the language's simplicity, and that's precisely what gets in your way if you're thinking about What if it were just a little more dynamic?.

Basically, what we'll do it's to tweak the parser/renderer a little to allow this dynamic processing of our input, and to get in the middle of the rendering process to achieve our results.

Intro

Did you know that you can turn Markdown into a flexible dynamic template engine?

I know for sure that I use Sinatra whenever I want to output some fairly static HTML but with some dynamic content and/or processing. Actually, I constantly use Sinatra for another stuff, such as service endpoints, but that's another story.

My point here is that you could do pretty amazing things using Markdown only. But technically speaking, isn't Markdown the language what you're taking upside down and making it do some crazy things with plain-text, it's your parser, or specifically your parser's renderer.

There are plenty of parsers for Markdown out there, in this post I'll use redcarpet because that's what I'm used to in production-level code, and also because it's one of the easiest libraries to extend.

Our Own Renderer

What's a Renderer?

In redcarpet's terminology a Renderer is unsurprisingly an object which handles the rendering process. That's the second half of the process (the first one is parsing) and you could totally get inside of it. Why would you want that? Because that way you can extend it, of course!

We'll start by implementing a custom renderer on top on the standard HTML renderer.

class MyCustomRenderer < Redcarpet::Render::HTML
end

That's really all it takes to implement a custom renderer in Redcarpet.

This one is sub-classing from the HTML renderer, so we can expect ours to behave just like it. Great, how do we use it? Taking into account that Redcarpet has a very intuitive API, you could for example write something like this:

Redcarpet::Markdown.new(MyCustomRenderer, {})

What's that second parameter I purposedly supplied an empty hash for? Those are the renderer's options and you'll find plenty of them in the Redcarpet's documentation.

I usually pass at least these:

options = {
  hard_wrap: true,          # new lines insert <br> tags
  filter_html: true,
  autolink: true,
  no_intraemphasis: true,   # do not parse emphasis inside of words
  fenced_code_blocks: true, # blocks with 3 ~ or ` are code, wo/need indent
}

Of course our renderer currently does nothing fancy. So let's move on to one of the most implemented features out there: syntax highlighting.

Syntax Highlighting

Now let's get it to work with syntax highlighting. I'm using the CodeRay library for this. The configuration is the simplest that would work, although there are plenty of more options (such as line-numbers) that I'm not covering here. You could of course use any syntax-highlighting library you want.

def block_code(code, language)
  CodeRay.scan(code, language).div
end

One line of code, and we've got syntax highlighting, that's concise! Slowing down, where did the block_code method came from? Again, refer to the documentation. You could easily tap into any part of the rendering process, and block_code is (as you might have guessed) the part that handles rendering the <code> tag.

As a side-note if you're using fenced code, which allows you to start code blocks with ` (backquote), don't have to indent the code, and supply an optional language to the block, you could find out that Vim gets a little annoying since it doesn't escape anything inside that block. In order to fix this, you add the following to your syntax file, where the right place to be is after/syntax/markdown.syntax file inside your VIM_RUNTIME directory. Avoid the temptation of modifying this kind of files in their originals. Of course you'll have to reference that path inside your .vimrc.

syn region markdownCode matchgroup=markdownCodeDelimiter start="\~\~\~ " end="\~\~\~" keepend contains=markdownLineStart

Just a simple note since I was quickly testing on Vim.

Improving Simple TODO lists

If you're like me, you like keeping your TODO items (not the ones for your project, I'd expect) in a simple text file. Most likely, you're placing them in lists. What about when you want some item you added while in the middle to stand out since that's really important? Simple enough, you'd write something like this:

* Some standard TODO item
* !!! Some important TODO item

Well, it surely stands up a little:

  • Some standard TODO item
  • !!! Some important TODO item
  • But we can do better. What we (well, at least, I) want is this:

  • Some standard TODO item
  • Some important TODO item
  • Ok, here's the code that implements it (a little hackish but works):

    # ...
    def list_item(text, list_type)
      case text
        when /^!!!\s*(.+)$/
          "<li style='color: red;'>#$1</li>"
        else
          "<li>#{text}</li>"
      end
    end
    # ...

    Simple enough, we do some regular expression matching and return an inline-styled list item, falling back to default behavior if we're not dealing with that particular format.

    Adding some style

    Ok what about some style? Markdown-generated HTML doesn't always have to look ugly, does it? This is the simple CSS I'm working with:

    body {
      margin: 0;
      font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
      font-size: 13px;
      line-height: 18px;
      color: #333;
      background-color: white;
      padding: 15px;
    }
    
    h1, h2, h3, h4, h5, h6 {
      margin: 0;
      font-weight: bold;
      color: #333;
      text-rendering: optimizelegibility;
      line-height: 36px;
    }
    
    h1 {
      font-size: 30px;
    }
    
    h2 {
      font-size: 24px;
    }
    
    h3 {
      font-size: 18px;
      line-height: 27px;
    }
    
    h4 {
      font-size: 14px;
    }
    
    h5 {
      font-size: 12px;
    }
    
    h6 {
      font-size: 11px;
      text-transform: uppercase;
      color: #999;
    }
    
    a {
      color: #08C;
      text-decoration: none;
    }
    
    a:hover, a:active  {
      outline: 0;
    }
    
    a:hover {
      color: #005580;
      text-decoration: underline;
    }
    
    p {
      margin: 0 0 18px;
      line-height: 18px;
    }
    
    ul, ol {
      padding: 0;
      margin: 0 0 9px 25px;
    }
    
    ul {
      list-style: disc;
    }
    
    code, pre {
      padding: 0 3px 2px;
      font-family: Menlo, Monaco, Inconsolata, "Courier New", monospace;
      font-size: 12px;
      color: #333;
      -webkit-border-radius: 3px;
      -moz-border-radius: 3px;
      border-radius: 3px;
    }
    
    pre {
      display: block;
      padding: 8.5px;
      margin: 0 0 9px;
      font-size: 12px;
      line-height: 18px;
      background-color: #f5f5f5;
      border: 1px solid #ccc;
      border: 1px solid rgba(0,0,0, 0.15);
      -webkit-border-radius: 4px;
      -moz-border-radius: 4px;
      border-radius: 4px;
      white-space: pre;
      white-space: pre-wrap;
      word-break: break-all;
      word-wrap: break-word;
    }
    
    li {
      line-height: 18px;
    }
    
    strong {
      font-weight: bold;
    }
    
    em {
      font-style: italic;
    }

    You could now do whatever you want with it, link to it, don't use at all, or like myself, inline it into the HTML file. Here's the code that inlines the minified version of this file:

    def postprocess(full_document)
      # You can also put the css at the end of the file and read it from data
      style = File.read("~/simple.min.css")
      full_document << "<style type='text/css'>#{style}</style>"
    end

    The Code

    Here's the full class inside a quick wrapper script:

    #!/usr/bin/env ruby
    require 'redcarpet'
    require 'coderay'
    
    class MyCustomRenderer < Redcarpet::Render::HTML
      def block_code(code, language)
        CodeRay.scan(code, language).div
      end
    
      # Handle special TODO items.
      # [Pkg] <text>: Generates a link to apt handler
      # [Gem] <text>: Generates a link to gem handler (currently none)
      # !!!   <text>: Colorizes list item to red
      def list_item(text, list_type)
        case text
          when /^\[Pkg\]\s*(.+)$/
            link_to_list_item($1, text, :apt)
          when /^\[Gem\]\s*(.+)$/
            link_to_list_item($1, text, :gem)
          when /^!!!\s*(.+)/
            "<li style='color: red;'>#$1</li>"
          else
            "<li>#{text}</li>" 
        end
      end
      
      def postprocess(full_document)
        style = File.read("~/simple.min.css")
        full_document << "<style type='text/css'>#{style}</style>"
      end
    
      # eval() code removed by the security folks ;)
    
      private
      def link_to_list_item(link, text, proto = :http)
        "<li><a href='#{proto}://#{link}'>#{text}</a></li>"
      end
    end
    
    options = {
      hard_wrap: true,
      filter_html: true,
      autolink: true,
      no_intraemphasis: true,   # do not parse emphasis inside of words
      fenced_code_blocks: true, # blocks with 3 ~ or ` are code, wo/need indent
    }
    
    fail "You need to supply a filename" unless $*[0]
    text = File.read($*[0])
    markdown = Redcarpet::Markdown.new(MyCustomRenderer, options)
    puts markdown.render(text)

    Conclusion

    This only scratched the surface on what can you do with custom renderers and Markdown. A bit of advice is that extensions and custom behavior like this are of course not part of the Markdown at all, so don't break compatibility and readability by taking this too far.

    Nevertheless, you can find this useful. Besides this examples, I've written a ruby code evaluator, a simple stories extension, a custom Changelog renderer, a JS clipboard copier, and a few other mini-utilities.

    ~ EOF ~

    Craftmanship Journey
    λ
    Software Engineering Blog

    Stay in Touch


    © 2020 Adrian Perez