Let's talk about how to set up middleman-syntax and then about some stuff I've added on top for this blog. First thing's first, of course you need to:

Markdown

I like to write my articles in straight Markdown. It's clean that way. So this tutorial is specifically about enabling fenced code blocks, which you can see in action, for example, in the markup for this specific article on github.

Redcarpet

I recommend using RedCarpet as your Markdown renderer. There's a lot of cool configuration options for customizing your Markdown setup. Below I'm just showing the specific ones needed in your config.rb to enable the features I'm going to discuss:

activate(:syntax) { |syntax|
  syntax.css_class = ''
}

require 'lib/artisanal_markdown'
config[:markdown] = {
  fenced_code_blocks: true,
  renderer: ArtisanalMarkdown,
}
config[:markdown_engine] = :redcarpet

Rouge

Rouge is great for both lexing your code into different span elements and applying CSS themes that will automatically highlight those elements. Since this blog supports both dark and light modes, I chose a different theme that looked good for both scenarios and put them in different files. I created dark_syntax.css.slim and filled it with just this one line:

==Rouge::Themes::MonokaiSublime.render(:scope => '.highlight')

When middleman builds everything, this'll get expanded to all the fancy CSS for colorizing our code in dark mode. I'm using the excellent template language Slim, but if you were using Erb, the file would be named dark_syntax.css.erb and the line would be:

<%= Rouge::Themes::MonokaiSublime.render(:scope => '.highlight') %>

I also created light_syntax.css.slim and inserted:

==Rouge::Themes::Github.render(:scope => '.highlight')

So now we have a CSS file for both light and dark mode syntax highlighting. Next, at the top of article.css, which is included in every article page, I put:

/* ROUGE THEMES */
@import url('/assets/css/light_syntax-41dfb232.css');
@import url('/assets/css/dark_syntax-7b9cc4e9.css') (prefers-color-scheme: dark);

And voilá, we have the CSS we need to colorize things.

Some basic CSS tweaks

middleman-syntax will automatically wrap your code-blocks in <code> tags. Browsers will convert this to a default monospace font, but I specifically like Fira Code, so I added:

code {
  font-family: 'Fira Code', monospace;
}

I also wanted to modify the padding around each block a bit and position it with position: relative. Each block gets wrapped in <div class=highlight><pre> so:

div.highlight {
  margin: 0;
  padding: 0.6em;
  position: relative;
}
div.highlight pre {
  margin: 0;
}

Displaying the language

One thing I always like about CSS-TRICKS is how they display the language of any code block in the upper right corner. I wanted to add the same, but as seamlessly and effortlessly as possible. When creating a code block you're supposed to put the language right after the triple backticks, so the Lexer can parse it accurately. My goal was to reuse that language identifier directly and just put it in the upper right corner. In config.rb I remove any additional classes that might otherwise be applied to the wrapping <pre> tag by saying syntax.css_class = ''. We don't actually need any extra classes there, because the whole thing is also wrapped in a <div class=highlight>, which is all our CSS selectors need. This means our <pre> tags will only have the class of our language, i.e. <pre class=ruby>. Boom. Now we can reuse that class attribute for the content of a pseudo-element in CSS. We also want to position it tightly into the upper right corner. The magic sauce becomes:

div.highlight pre::before {
  color: var(--dim-text-color);
  content: attr(class);
  font-family: var(--cursive-font), cursive;
  font-size: 0.8em;
  position: absolute;
  right: 0.3em;
  top: -0.3em;
}

Adding a click to copy button

The second thing I really wanted to add was a little clickable icon in the bottom right which would copy the code block to the reader's clipboard. I can't use another pseudo-element for this task, because I need to attach a click event listener, and those can't be applied to pseudo-elements. Instead, I tweak the Markdown renderer to add an extra <div> to the bottom of every code block. This brings us to the class ArtisanalMarkdown, which I pull into the config.rb file via require lib/artisanal_markdown, and then I specify for use with the renderer: ArtisanalMarkdown assignment inside the config[:markdown] hash. The parts of the class used for this modification look like this:

require 'middleman-core/renderers/redcarpet'

class ArtisanalMarkdown < Middleman::Renderers::MiddlemanRedcarpetHTML
  def block_code(code, language)
    result = super(code, language)
    result.sub!(%r{</div>\s*$}, '<i class="fas fa-copy clipboard"></i></div>')
    return result
  end
end

What this is doing is: calling the super class to do it's normal thing, then, right before the closing </div>, injecting a slick little icon from Font Awesome. Let's position our injected icon element in the bottom right corner, and set the cursor to be copy as a hint to the user of what it does. Let's also make it change color when the mouse rolls over it and clicks it:

.clipboard {
  bottom: 0.2em;
  color: var(--dim-text-color);
  cursor: copy;
  position: absolute;
  right: 0.2em;
}
.clipboard:hover {
  color: var(--text-color);
}
.clipboard:active:hover {
  color: var(--link-color);
}

Now we just have to add a click event listener to every instance of the clipboard class. It used to be a really ugly hack to inject something into a user's clipboard via Javascript, but we live in a golden age of browsers where navigator.clipboard.writeText() actually works. We just have to extract the textContent from the code block and pass it onward. The entirety of the code to make this work is:

// Copying code blocks to the clipboard.
document.addEventListener('DOMContentLoaded', () => {
  const clipboards = document.getElementsByClassName('clipboard');
  for (let clipboard of clipboards) {
    clipboard.addEventListener('click', (event) => {
      const code = event.target.previousSibling.textContent.trim();
      navigator.clipboard.writeText(code);
    });
  }
});

And there you have it! You needn't look further than all the code blocks on this page to see the end result.

CSS variables

As a final note: several of the CSS examples I have shown are using CSS variables, such as var(--dim-text-color). I leave it as an exercise to the reader to define these in their own CSS files however they like.

What do you think? I'd love to hear from you. Here is a link to the source for this article on github. Please open an issue, send me a pull-request, or just email me at jubi@jubishop.com