How to make code snippets in your middleman blog look beautiful using highlight.js
6 May 2015
I started to blog with Middleman recently (this blog you're looking at is the result) and found that getting code blocks to embed and display cleanly was a bit of a pain. I tried a few different approaches which I wasn't happy with, before settling on the one you can see here, which I'm really pleased with. This post will explain how I did it and provide you with code you can use to achieve the same thing.
TBH, most of this is not specific to Middleman and may well be useful for other platforms too, but you'll have to work all that stuff out for yourself ;)
First, I tried to get the Syntax Highlighter javascript plugin that I had seen on Wordpress.org to work. This wasn't too hard to get going, but I didn't feel that the result was particularly pretty. Code is hard enough to make sense of on its own sometimes, so it's better to make it easy on the eye. Also, I like code and this is for my personal blog, so I was keen to find a way to make it beautiful rather than just functional.
Several other plugins later, I still didn't find anything that really looked right.
So, I started to look around for examples of websites with code that was formatted in a way that I liked looking at. I wanted clean, easy to read and pleasing to the eye. In the end, I concluded that the look I wanted was partly down to a good choice of font, font-size and highlight colours, but also due to removing extraneous clutter e.g. different background colors for alternate lines and a gutter with line numbers.
Do we really need line numbers?
Previously, I's always assumed that code blocks needed line numbers because code always has line numbers, but seeing how some sites don't use them at all (and look better for it) made me wonder if I really needed them at all. What do line numbers really achieve in a code snippet anyway? I suddenly realised I had no idea. The normal reason to have them is that you are navigating a large file and need to work out where you are so that you can get to the right place. However, this is not going to happen in a situation where you are deliberately cutting out small snippets of code to be read in isolation. If you need to refer back to a particular line, you are not likely to say "look at line 56 in the code above". You'd just add another snippet to the post to illustrate what you are saying.
By this point, I had found the highlight.js library and was trying it out. I really liked the look of the code it outputted, but would need to find a good colour scheme.
I tried to turn line numbers on in highlight.js to see what they looked like as a comparison, but found that I couldn't. The author had deliberately not written code to provide line numbers. Here's what he says about his decision:
One of the defining design principles for highlight.js from the start was simplicity. [...] The more fancy stuff resides around the code the more it distracts a reader from understanding it.
[...]
The only real use-case that ever was brought up in support of line numbers is referencing code from the descriptive text around it. On my own blog I was always solving this either with comments within the code itself or by breaking the larger snippets into smaller ones and describing each small part separately. I’m not saying that my solution is better. But I don’t see how line numbers are better either. And the only way to show that they are better is to set up some usability research on the subject. I doubt anyone would bother to do it.
Sold! No more line numbers for me. Also, it was clear that the library as a whole is deliberately written with simplicity and clarity in mind. My kind of code!
Installing highlight.js
You will need JQuery installed already.
Go to the download page and select all of the languages you will be using. This will give you a minified file called highlight.pack.js, which you can use as-is.
Put highlight.pack.js into
source/javascripts
Add the highlight.pack.js file to the all.js manifest underneath JQuery:
//= require highlight.pack
Add the following code to all.js under the manifest section:
$(function() { hljs.initHighlightingOnLoad(); });
Switch to Redcarpet as your markdown handler and enable fenced code blocks. This is so that you can specify the language. highlight.js has some language auto detection, but I've found it doesn't always work, especially for short snippets.
# In Gemfile gem 'redcarpet'
# In config.rb set :markdown_engine, :redcarpet set :markdown, :fenced_code_blocks => true
Add a code block to a post using markdown:
```ruby class User < ActiveRecord::Base end ```
Choosing a colour scheme
There are a whole load of themes that come with highlight.js, and I needed one that fit well with the general colour scheme for the blog. Clashing colours are not good, so given that the main colour I'd settled on was a kind of dark aquamarine blue, a light, yellow-tinted background for the code blocks seemed like a good choice. Yellow is blue's complementary colour, given that it is on the opposite side of the colour wheel, so they tend to work well together. This is an important principle when choosing a colour scheme. Randomly chosen colours often clash, but a few simple design principles and an understanding of the colour wheel can make things look good without much effort. This tool is great for finding good combinations quickly based on colour theory if you're in a rush.
As for the colours for the code elements themselves, my inner design nerd tells me that they should ideally be carefully chosen to complement one another as well. Ethan Schoonover's Solarized colour scheme caught my attention some time ago as by far the best attempt to do this, so I was keen to use it. It also conveniently has a pale yellow background colour as part of the light version of the theme. Perfect! Highlight.js has a solarized theme, but I found that the dominant colours for the languages I use most didn't always match the rest of the colour scheme. I fixed this by copying all of the CSS hex values into SASS variables, adding a post to the blog with examples from each of the languages I use, and then messing around with the default theme colours till I was happy.
Here's the final SASS code for both the variables and the altered default styles:
// In variables.scss
// Solarized
$solarized_base03: #002b36;
$solarized_base02: #073642;
$solarized_base01: #586e75;
$solarized_base00: #657b83;
$solarized_base0: #839496;
$solarized_base1: #93a1a1;
$solarized_base2: #eee8d5;
$solarized_base3: #fdf6e3;
$solarized_yellow: #b58900;
$solarized_orange: #cb4b16;
$solarized_red: #dc322f;
$solarized_magenta: #d33682;
$solarized_violet: #6c71c4;
$solarized_blue: #268bd2;
$solarized_cyan: #2aa198;
$solarized_green: #859900;
// In default.scss
@import "variables";
/*
Original style from softwaremaniacs.org (c) Ivan Sagalaev <[email protected]>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #f0f0f0;
-webkit-text-size-adjust: none;
}
.hljs,
.hljs-subst,
.hljs-tag .hljs-title,
.nginx .hljs-title {
color: black;
}
.hljs-string,
.hljs-title,
.hljs-constant,
.hljs-parent,
.hljs-tag .hljs-value,
.hljs-rule .hljs-value,
.hljs-preprocessor,
.hljs-pragma,
.hljs-name,
.haml .hljs-symbol,
.ruby .hljs-symbol,
.ruby .hljs-symbol .hljs-string,
.hljs-template_tag,
.django .hljs-variable,
.smalltalk .hljs-class,
.hljs-addition,
.hljs-flow,
.hljs-stream,
.bash .hljs-variable,
.pf .hljs-variable,
.apache .hljs-tag,
.apache .hljs-cbracket,
.tex .hljs-command,
.tex .hljs-special,
.erlang_repl .hljs-function_or_atom,
.asciidoc .hljs-header,
.markdown .hljs-header,
.coffeescript .hljs-attribute {
color: $solarized_red;
}
.smartquote,
.hljs-comment,
.hljs-annotation,
.diff .hljs-header,
.hljs-chunk,
.asciidoc .hljs-blockquote,
.markdown .hljs-blockquote {
color: $solarized_base01;
}
.hljs-number,
.hljs-date,
.hljs-regexp,
.hljs-literal,
.hljs-hexcolor,
.smalltalk .hljs-symbol,
.smalltalk .hljs-char,
.go .hljs-constant,
.hljs-change,
.lasso .hljs-variable,
.makefile .hljs-variable,
.asciidoc .hljs-bullet,
.markdown .hljs-bullet,
.asciidoc .hljs-link_url,
.markdown .hljs-link_url {
color: $solarized_cyan;
}
.hljs-label,
.hljs-javadoc,
.ruby .hljs-string,
.hljs-decorator,
.hljs-filter .hljs-argument,
.hljs-localvars,
.hljs-array,
.hljs-attr_selector,
.hljs-important,
.hljs-pseudo,
.hljs-pi,
.haml .hljs-bullet,
.hljs-doctype,
.hljs-deletion,
.hljs-envvar,
.hljs-shebang,
.apache .hljs-sqbracket,
.nginx .hljs-built_in,
.tex .hljs-formula,
.erlang_repl .hljs-reserved,
.hljs-prompt,
.asciidoc .hljs-link_label,
.markdown .hljs-link_label,
.vhdl .hljs-attribute,
.clojure .hljs-attribute,
.asciidoc .hljs-attribute,
.lasso .hljs-attribute,
.coffeescript .hljs-property,
.hljs-phony {
color: $solarized_violet;
}
.hljs-keyword,
.hljs-id,
.hljs-title,
.hljs-built_in,
.css .hljs-tag,
.hljs-javadoctag,
.hljs-phpdoc,
.hljs-dartdoc,
.hljs-yardoctag,
.smalltalk .hljs-class,
.hljs-winutils,
.bash .hljs-variable,
.pf .hljs-variable,
.apache .hljs-tag,
.hljs-type,
.hljs-typename,
.tex .hljs-command,
.asciidoc .hljs-strong,
.markdown .hljs-strong,
.hljs-request,
.hljs-status {
font-weight: bold;
}
.asciidoc .hljs-emphasis,
.markdown .hljs-emphasis {
font-style: italic;
}
.nginx .hljs-built_in {
font-weight: normal;
}
.coffeescript .javascript,
.javascript .xml,
.lasso .markup,
.tex .hljs-formula,
.xml .javascript,
.xml .vbscript,
.xml .css,
.xml .hljs-cdata {
opacity: 0.5;
}
Indentation trouble
I also found that I had an issue with the indentation of the code inside the blocks. What I was seeing was this:
class Product < ActiveRecord::Base
searchkick
end
Which is weird. Why would the first line be OK and the rest all pushed to the right? I looked at the source code for the page and found this:
<pre>
<code class="ruby">class Product < ActiveRecord::Base
searchkick
end
</code>
</pre>
Ah. That makes sense. The way a <pre>
block works is that all of the whitespace is preserved, unlike the usual situation
where the browser compresses all the gaps down to single spaces.
Because the HTML had been generated by Middleman from markdown posts, the code was not aligned with the left side of the document in the raw HTML,
but to the left of the <code>
tag. All except the first line that is, which was right up against the <code>
tag.
The syntax highlighter treated this like deliberate indenting and included all the whitespace to the left of the <code>
tag,
shifting all of the code apart from the first line to the right. This is correct behaviour as far as the highlighter goes
because the <pre>
tag should be really used like this:
<pre>
<code class="ruby">
class Product < ActiveRecord::Base
searchkick
end
</code>
</pre>
However, the markdown/templating system in Middleman wasn't playing ball. I tried changing the markdown engine a few times, but none of them did things the right way.
After trying all sorts of hacky work-arounds in the markdown code, I found a
small JQuery plugin that would take the poorly aligned
<pre>
tag content and fix the whitespace in it before the syntax highlighter
did it's job. I made a few tweaks to this, and in the end it worked perfectly, so
if you need the same fix, add the code below to a file which you include in your project:
(function ($) {
"use strict";
$.fn.prettyPre = function (method) {
var defaults = {
ignoreExpression: /\s/ // what should be ignored?
};
var methods = {
init: function (options) {
this.each(function () {
var context = $.extend({}, defaults, options);
var $obj = $(this);
var usingInnerText = true;
var text = $obj.get(0).innerText;
// some browsers support innerText...some don't...some ONLY work with innerText.
if (typeof text === "undefined") {
text = $obj.html();
usingInnerText = false;
}
var lines = text.split("\n");
var line = '';
var leadingSpaces = [];
var length = lines.length;
var zeroFirstLine = false;
/** We assume we are using codeblocks in Markdown.
*
* The first line may be right next to the <pre> tag on the same line,
* so we want to ignore the zero length spacing there and use the
* smallest non-zero one. However, we don't want to do this
* if the code block is correctly placed up against the left side.
*/
for (var h = 0; h < length; h++) {
line = lines[h];
// use the first line as a baseline for how many unwanted leading whitespace characters are present
var currentLineSuperfluousSpaceCount = 0;
var TotalSuperfluousSpaceCount = 0;
var currentChar = line.substring(0, 1);
while (context.ignoreExpression.test(currentChar)) {
if (/\n/.test(currentChar)) {
currentLineSuperfluousSpaceCount = 0;
}
currentLineSuperfluousSpaceCount++;
TotalSuperfluousSpaceCount++;
currentChar = line.substring(TotalSuperfluousSpaceCount, TotalSuperfluousSpaceCount + 1);
}
leadingSpaces.push(currentLineSuperfluousSpaceCount);
}
if (leadingSpaces[0] === 0) {
// If we have this:
// <pre>Line one
// Line two
// Line three
// </pre>
leadingSpaces.shift(); // Remove first count
zeroFirstLine = true;
}
if (leadingSpaces.length === 0) {
// We have a single line code block
leadingSpaces = 0;
} else {
// Smallest of the leading spaces
leadingSpaces = Math.min.apply(Math, leadingSpaces);
}
// reconstruct
var reformattedText = "";
for (var i = 0; i < length; i++) {
// cleanup, and don't append a trailing newline if we are on the last line
if (i === 0 && zeroFirstLine) {
// If the first line was butted up the the <pre> tag, don't chop the beginning off.
reformattedText += lines[i] + ( i === length - 1 ? "" : "\n" );
} else {
reformattedText += lines[i].substring(leadingSpaces) + ( i === length - 1 ? "" : "\n" );
}
}
// modify original
if (usingInnerText) {
$obj.get(0).innerText = reformattedText;
}
else {
// This does not appear to execute code in any browser but the onus is on the developer to not
// put raw input from a user anywhere on a page, even if it doesn't execute!
$obj.html(reformattedText);
}
});
}
};
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
}
else if (typeof method === "object" || !method) {
return methods.init.apply(this, arguments);
}
else {
$.error("Method " + method + " does not exist on jQuery.prettyPre.");
}
};
})(jQuery);
And to actually trigger the highlighting:
$(function() {
$("PRE CODE").prettyPre();
hljs.initHighlightingOnLoad();
});
Word wrap
This seemed great except for one last problem: the lines of code were wrapping inside the code blocks. This is not good, as code is very hard to read (and may not work) if not presented exactly as it is normally written. Line breaks matter! After a load of digging around, it turned out that Bootstrap was to blame. removing the Bootstrap CSS include from the all.css file fixed it.
If you really need to use Bootstrap, then you can use the following CSS to reset the styling for just the code blocks, leaving the rest of the document intact:
pre {
padding: 0;
white-space: pre;
word-break: normal;
word-wrap: normal;
code.hljs {
padding: 2rem;
overflow: auto;
max-height: 80rem;
white-space: pre;
&,
span {
font-size: 1.6rem;
font-weight: 500;
}
span {
white-space: pre;
}
}
}