The Dynamic Side of Markdown
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:
<li>Some standard TODO item</li>
<li>!!! Some important TODO item</li>
But we can do better. What we (well, at least, I) want is this:
<li>Some standard TODO item</li>
<li style="color: red;">Some important TODO item</li>
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.