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
Regular Expression Explainer, an invaluable resource for designing and troubleshooting regular expressions