Highlighting Code in Ghost
Syntax highlighting/formatting code makes it much easier to read. Just look at how much better this is:
function test() {
console.log("Hello world!");
}
than this:
function test() {
console.log("Hello world!");
}
Thus, I want the code in my blog posts to be highlighted. I want to be able to use the existing GitHub-style code blocks and not have to write HTML in my posts. I want the code in the editor to be raw code. I also want to see my code highlighted in the preview pane in the editor.
I will show you how to modify Ghost so that GitHub style code blocks are automatically highlighted without affecting the editing processes.
From Markdown to HTML
This is the story of how Ghost turns the Markdown I am typing right now, to the HTML I see in the preview pane to the right and the HTML you are reading now.
Once upon a time, a blogger creates a new post using Markdown. He dreams of sharing his ingenious new program with the world and puts it into a GitHub-style Markdown code block:
```javascript
function test() {
console.log("Hello world!");
}
```
His trusty blogging platform, Ghost, takes the blogger's Markdown and uses the Showdown library to convert the markdown into HTML:
<pre><code class="javascript">function test() {
console.log("Hello World");
}</code></pre>
As the blogger makes changes with the editor, Ghost continuously generates the HTML on the client side and displays it in the preview pane. When the blogger saves the post, Ghost generates the HTML again on the server side and saves it in the database with the post. Soon after publishing, a visitor views the blogger's post and Ghost simply presents the pre-generated HTML.
However, the visitor is disgusted and enraged by the lack of syntax highlighting and unreadability of the code in the post. The visitor throws his laptop violently at the wall and never visits the blogger's site again.
The End.
Prism
The first step is to find a syntax highlighter. While I went with Prism, you can use whatever syntax highlighter suites you. I chose Prism for several reasons:
Powered by a CSS Style Sheet
Contrary to other syntax highlighters which generate HTML elements with in-line styles, Prism generates elements with classes and provides a style sheet. This makes it very easy to customize the colors and format of the various parts of the highlighted code. Further, all languages highlighted with prism use the same style sheet, so your code will look consistent.
Wide Language Support
Prism supports all the languages I use and many more. They sport over 30 languages on their download page.
Plugins
Prism makes it easy for users to add support for new languages and other enhancements. These include automatically turning URLs into links, showing line numbers, displaying white-space characters, and more.
Built-in Support For Node
We can easily use Prism in Node right out of the box.
Downloading Prism
First, we need the Prism JavaScript and CSS files to power the syntax highlighting.
Go to the Prism download page.
Select the languages and plugins you want. You might as well select all languages. The JavaScript will not be downloaded by the end user; So, file size does not matter.
Download the generated JavaScript and CSS files. We will use these latter. I will assume these are named
prism.js
andprism.css
.
Update Theme
We have to update our theme to include the Prism CSS style sheet on each page.
These changes can be seen in this commit.
Install Prism CSS
Copy your Prism CSS style sheet into your theme's CSS asset directory. Mine is:
{ghost}/content/themes/casper_custom/assets/css/prism.css
Update your HTML theme to include your Prism CSS. My theme is a modified version of the default Casper theme so I added
prism.css
under the existingscreen.css
in the page header:
{ghost}/content/themes/casper_custom/default.hbs
(diff):
{{! Styles'n'Scripts }}
<head>
...
<link rel="stylesheet" type="text/css" href="{{asset "css/screen.css"}}" />
<link rel="stylesheet" type="text/css" href="{{asset "css/prism.css"}}" />
...
</head>
Update Ghost
This will require modifying Ghost's source. I will not cover building and deploying Ghost but that information can be found here.
These changes can be seen in this commit.
Install Prism CSS (again)
First, we should include the Prism CSS on the admin pages so our syntax highlighting will show up on the post editor's preview pane. The change to the theme we made previously only affects the end-user-facing pages and not the admin tool.
- Create new subdirectory in the client app directory:
{ghost}/core/client/app/prism
- Copy your Prism CSS style sheet into your new Prism directory:
{ghost}/core/client/app/prism/prism.css
- Add your Prism CSS to the main admin CSS file (diff):
{ghost}/core/client/app/styles/app.css
...
@import "layouts/subscribers.css";
@import "../prism/prism.css";
Install Prism JavaScript
Now we need to install the Prism JavaScript which does all the work. The markdown is converted to HTML twice: client-side (for the preview) and server-side (to save to the database). Therefore we must install the Prism JavaScript server-side as well as client-side.
For client-side, copy your Prism JavaScript into the same Prism directory you created for the CSS: {ghost}/core/client/app/prism/prism.js
.
For server-side, create a new subdirectory in the server directory: {ghost}/core/server/prism
and copy your Prism JavaScript into your new Prism directory: {ghost}/core/server/prism/prism.js
Create Showdown Extension
Ghost uses Showdown to convert the Markup you enter in the editor into the HTML displayed to the user. Lucky for us, Showdown supports extensions. We will need to write an extension that finds code blocks and runs them through Prism for syntax highlighting.
This file can be viewed in its entirety here.
Location
We will need two copies of our extension: one for client-side and one for server-side. Put a copy in your client-side Prism folder:
{ghost}/core/client/app/prism/showdown-extension/prism-syntax-highlighter.js
and your server-side Prism folder: {ghost}/core/server/prism/showdown-extension/prism-syntax-highlighter.js
Access to Prism
First, we need to get access to the Prism library so we can use it to highlight our code. This is the only difference between the client-side and server-side copies:
Client-Side:
import Prism from 'ghost-admin/prism/prism';
Server-Side:
var Prism = require('../prism');
Create Extension Shell
Next, we create the extension.
var prismsyntaxhighlighter = function () {
return [
{
type: 'html',
filter: function (html) {
// ...
}
}
];
};
We use a type of html
which tells Showdown to apply our filter after the markdown has been turned into HTML.
Create Filter
The filter must find all the code blocks and replace the HTML with the highlighted HTML generated from Prism.
Finding Code Blocks
The Markdown code block:
```javascript
function test() {
console.log("Hello world!");
}
```
produces the HTML:
<pre><code class="javascript">function test() {
console.log("Hello World");
}</code></pre>
So, to find code blocks we use the regex:
var regex = new RegExp(
'<pre><code class="' + // start of a code block with a class
'(.*?)' + // extract class name (result[1])
'">' + // end of code tag
'([\\s\\S]*?)' + // extract code (result[2])
'</code></pre>', // end of code block
'g');
var result;
while ((result = regex.exec(html)) !== null) {
// get the extracted class name and code
var className = result[1];
var code = result[2];
// ...
}
Using our example above className
= javascript
and code
=
function test() {
console.log("Hello World");
}
Decoding the Code
You will notice that the code we extracted has some of the HTML reserved characters encoded. We have to decode these first before passing the code to Prism. Showdown only encodes <
, >
, and &
.
Make sure to decode &
last; Otherwise, you will double-decode &lt;
and &gt;
to the incorrect <
and >
instead of the correct <
and >
.
// decode HTML entities encoded by showdown
// the opposite of replacements taken from showdown's _EncodeCode
code = code.replace(/</g, "<");
code = code.replace(/>/g, ">");
code = code.replace(/&/g, "&");
// make sure to decode ampersands last otherwise you will double decode < and >
// original : < makes the '<' symbol
// encoded : &lt; makes the '<' symbol
//
// Wrong:
// replace & : < makes the '<' symbol
// replace < : < makes the < symbol
//
// Correct:
// reaplce < : &lt; makes the '<' symbol
// replace & : < makes the '<' symbol
Check Class Name
Now, we have to check if the class name we extracted is a valid language. A list of Prism's supported languages can be accessed at Prism.languages
. Prism refers to the language object as the "grammar" and the string name of said grammar is the "language".
// get the grammar (language supported by prism) from the class name
var grammar = Prism.languages[className];
if (!grammar) {
// the given class name is not a language supported by prism
// skip to the next code block
continue;
}
// the class name is a valid language
var language = className;
Highlight the Code
At last, we do some syntax highlighting!
// do the highlighting
var highlightedCode = Prism.highlight(code, grammar, language);
Prism.highlight
returns the HTML for the highlighted code. To create the final new HTML code block, we need to place the highlighted code inside <pre><code>
tags. Prism uses the HTML5 compliant class naming convention language-{name of language}
. Prism also relies on the class being set on the <pre>
tag instead of the <code>
tag.
// create the new HTML with the highlighted code and language class
// Prism moves the language class from the <code> element to the <pre> element
// so we will set the class on the <pre> element
var newHTML = '<pre class="language-' + language + '"><code>' + highlightedCode + '</code></pre>';
Replace old HTML
We must now replace the old HTML with the new HTML. The old HTML is the whole match from our regex (result[0]
) and it's starting position in the string is given by result.index
.
// replace the old HTML with the new HTML
var oldHTML = result[0];
var oldHTMLIndex = result.index;
var beforeOldHTML = html.substring(0, oldHTMLIndex);
var afterOldHTML = html.substring(oldHTMLIndex + oldHTML.length);
html = beforeOldHTML + newHTML + afterOldHTML;
Set Regex Index
Finally, because we changed the string we are iterating over, we must set the correct index for the next regex search to start from.
// the next regex search should start after the end of the new HTML
var newHTMLIndex = oldHTMLIndex; // we replaced the old HTML so the new HTML starts at the same place
regex.lastIndex = newHTMLIndex + newHTML.length;
Using Showdown Extension
Now that we have created our extension, we have to tell Ghost to use it. To do this, we need to add it to the list of extensions when creating the Showdown converter on both the client and server-side:
{ghost}/core/client/app/helpers/gh-format-markdown.js
(diff)
import prismsyntaxhighlighter from 'ghost-admin/prism/showdown-extension/prism-syntax-highlighter';
let showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes', 'highlight', prismsyntaxhighlighter]});
{ghost}/core/server/models/post.js
(diff)
converter = new Showdown.converter({extensions: ['ghostgfm', 'footnotes', 'highlight', require('../prism/showdown-extension/prism-syntax-highlighter')]}),
Now, after Showdown generates the HTML, it will use our extension to highlight the code blocks.
Final Steps
Do a re-build with grunt prod
after making all of your changes. This will build all the CSS and JavaScript assets sent to the browser. Also make sure to clear your browsers cache or do a hard refresh so you get the latest assets. Note that you will have to edit and save all your existing posts to generate the HTML with syntax highlighted code.
Enjoy your beautiful code!