Extending Redcarpet for Admonition Blocks

I've been doing a lot of writing in Obsidian, and they recently released a version with these lovely "callout" blocks. I like them a lot, and since I want my Creativity System to feed straight through to my writing, time to hack my blog!
The "callouts" take this form:
> [!tldr] If you hack the base templater for your blog, you will cry.
I like this format because it’s still meaningful as plain text. I'm rendering them like this:
If you hack the base templater for your blog, you will cry.
Pretty straightforward SCSS:
.admonition {
padding: 1em;
margin-top: 2em;
background: transparentize($accent, 0.9);
border-left: 1em solid $accent;
border-radius: .25em;
}
.admonition.tldr {
@media #{$desktop} {
display: grid;
grid-template-columns: auto auto;
grid-column-gap: 1em;
align-items: center;
}
&:before {
content: 'tl;dr:';
font-size: 2em;
font-weight: 800;
}
> p {
margin: 0;
}
}
The really tricky part was ruby. Y'all ever seen the gif where Hal tries to change a lightbulb?
This was one of those changes.
Let's go learn what all I did wrong! 🏃
A tale of woe.
- I found a couple posts about extending markdown in redcarpet. Looks easy enough… right?
- I first got stuck because I wasn't using the right regex globally to fix all instances of the regex. 🤦
- I wanted to render markdown markup like
_italics_
or[links](url)
in the admonition body, so I had to run a markdown renderer inside my markdown renderer. It's heinous, but I don't know how to get around it. - I realized that I could use the same format to display youtube videos, instead of my previous
.erb
based format. Pure markdown for everything! - All this worked beautifully, but while writing this post I realized my changes destroyed syntax highlighting. I believe the
middleman-syntax
was doing some monkeypatching that I sidestepped by creating my ownpreprocess
method… so I just removed the gem and used Rouge directly. - I broke footnotes and header anchors because I wasn't passing options to the new renderer properly.
- emboldened by my success, I decided to strip the
.html.markdown
extension from all my blog posts (I'd like them all to just be.md
files someday!). The rename broke my git changelogs.
Phew! It was a lot of bug chasing. I'm pleased with the result.
Here are the finished changes:
Git Changelog
ruby-git
doesn't support git log --follow
yet, so I had to drop into git and handle it myself.
I tried really hard to pull all the information in one git command using --format
: I tried JSON, YML, even Marshall.dump
in ruby… but calling for each commit was easier and doesn't seem to add to my Github CI times.
<%
@git = `git log --follow --format='%H' #{file}`
.split("\n")
.map!{|sha|
{
date: `git show #{sha} --no-patch --format='format:%ai'`,
title: `git show #{sha} --no-patch --format='format:%s'`,
body: `git show #{sha} --no-patch --format='format:%b'`
}
}
if @git.size > 1
%>
<details class="blog__changes">
<summary>Changelog</summary>
<ul>
<% @git.each do |commit| %>
<li>
<div class="blog__changes__date"><%= commit[:date] %></div>
<div class="blog__changes__title"><%= commit[:title] %></div>
<%= simple_format(commit[:body]) %>
</li>
<% end %>
</ul>
</details>
<% end %>
Custom Markdown Elements
That's the changelog fixed, what about the actual markdown renderer? Let's overly document some hacky ruby code:
# Get our gems
require 'middleman-core/renderers/redcarpet'
require 'rouge/plugins/redcarpet'
# This is the same template from the previous blog post.
YOUTUBE_TEMPLATE = %(
<div class="youtube-container">
<iframe
width="100%%"
height="100%%"
src="https://www.youtube.com/embed/%s"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>
).strip
# This regex describes the `> [!name] content` Admonition block. We have to use
# it twice (I got really stuck trying to make it one call!)
CALLOUT = /^> \[!(?<name>\w+)\] (?<str>.*)$/.freeze
# Renderer: This is going to be our custom renderer. Gotta initialize it with
# some options enabled to make sure we keep our footnotes and TOC anchors.
class Renderer < Redcarpet::Render::HTML
def initialize(extensions = {})
super extensions.merge(
footnotes: true,
with_toc_data: true
)
end
include Rouge::Plugins::Redcarpet
# Here's the heart of the hack
def preprocess(document)
# Instantiate _another_ renderer inside this renderer to render while you
# render. (Eat your heart out Xzibit.)
markdown = Redcarpet::Markdown.new(self, {})
# change every instance of the CALLOUT regex in place.
document.gsub!(CALLOUT) do |match|
# because `match` contains the matching string, not capture groups (and
# `$~` didn't always return the last match, maybe thread-local?!) I'm
# going to use CALLOUT again to grab the named captures.
m = CALLOUT.match(match).named_captures
if m['name'] == 'youtube'
@video = URI.decode_www_form(URI(m['str']).query).to_h['v']
format(YOUTUBE_TEMPLATE, @video)
else
# Render out our little component, with nested markdown render 🤷♂️
"<div class='admonition #{m['name']}'>#{markdown.render m['str']}</div>"
end
end
document
end
end
set :markdown_engine, :redcarpet
set :markdown,
fenced_code_blocks: true,
smartypants: true,
with_toc_data: true,
highlights: true,
footnotes: true,
renderer: Renderer
What a time. I do not recommend this… unless like me you are dead set on having a simple markdown file represent your writing output, and you want to have your Creativity System hooked directly into your blogging system. More to come on that.
Changelog
-
2022-07-12 09:37:50 -0500Update blog
-
2022-06-17 15:13:15 -0500Change some words
-
2022-06-17 11:18:59 -0500Publish
-
2022-06-17 11:17:06 -0500Rename and change date
-
2022-06-17 11:15:35 -0500Finish blog post
-
2022-06-09 14:01:49 -0500updating draft on phone while waiting
-
2022-06-08 11:31:29 -0500Rename articles
-
2022-06-08 11:27:18 -0500Working on new post