»jd« powered logo

February 17th, 2018

Adding (build-time) syntax highlighting to your Jigsaw site

Adding syntax highlighting to your Jigsaw sites code snippets is easy—just drop in a JS library and you’re good to go, right? Right! This is, however, not the most performant way.

The problem

If you, like me, use Jigsaw for your websites and post code snippets regularly, you may want to add (or have already added) syntax highlighting. The easiest way is of course to just drop in highlight.js and let it handle everything for you. This is exactly what I did at first, too.

The problem with this is, that the library is not exactly lightweight (~18 KB with only the default languages included) and highlighting many or large code snippets can noticeably slow down page loading times.

The solution to this problem is only logical: if we already use Jigsaw to pre-build our markdown based content into static HTML files, why don’t we do the syntax highlighting at build time, too?

In this article, let me show you how we can add build-time syntax highlighting to fenced code blocks like this in our markdown:

```php
// baz.php

<?php

$foo = "bar";
```

This will let us turn the rendered HTML from being unstyled:

// baz.php

<?php

$foo = "bar";

to highlighted:

// baz.php

<?php

$foo = "bar";

Preparations

First, we need to pull in the package that will, once wired up, do the actual syntax highlighting for us. There are multiple packages available that will get the job done. For this post (and my site) I choose to use highlight.php by Geert Bergman, which is really just a PHP port of the well known highlight.js by Ivan Sagalaev.

We’ll pull it in via Composer:

composer require scrivo/highlight.php

Next, we need a place to put our code in. It really depends on your preference where to put it. For this example though, we’ll create a new app/ directory at the root of our project and tell composer to autoload the files in this directory.

Add (or extend) the autoload section of your composer.json:

{
    /* ... */

    "require": {
        "tightenco/jigsaw": "^1.0",
        "scrivo/highlight.php": "v9.12.0.1"
    },

    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    },

    /* ... */
}

To finish up our preparations, we need to provide Jigsaw with a bootstrap.php file that we’ll place in the root of our project. If this file is present, Jigsaw will load it before running the build process. This allows us to easily tweak some of its internals and extend the default markdown compilation with our desired syntax highlighting.

Add a basic bootstrap.php file:

// bootstrap.php

<?php

/** @var  $container \Illuminate\Container\Container */
/** @var  $jigsaw \TightenCo\Jigsaw\Jigsaw */

Extending the markdown parser

To add any kind of custom functionality to the built in markdown parser, we need to extend it. Luckily for us, Jigsaw uses Parsedown which makes this task pretty easy. We create our own version of Parsedown on top of the original class in app/Parsedown.php.

In the constructor, we prepare a new instance of our syntax highlighter we required earlier. To change the default behavior of processing detected fenced code blocks, we need to override a function called blockFencedCodeComplete which accepts an associative array containing structured data about the parsed code block, may do any kind of changes to this data and has to return it back finally.

Within this function, we extract the original code snippet and check if a language was provided when defining the fenced code block in the markdown. If a language was provided, we’ll let our syntax highlighter to do all the heavy work of highlighting the code and afterwards inject the result back into the structured data array.

If no language is provided or if anything goes wrong with the highlighting, we’ll just default back to the original way of handling the code block.

Here’s how our completed parser looks:

// app/Parsedown.php

<?php

namespace App;

use Highlight\Highlighter;
use Parsedown as BaseParsedown;

class Parsedown extends BaseParsedown
{
    /**
     * @var  \Highlight\Highlighter
     */
    protected $highlighter;

    /**
     * Parsedown constructor.
     * 
     * Prepare a fresh instance of the syntax highlighter.
     */
    public function __construct()
    {
        $this->highlighter = new Highlighter();
    }

    /**
     * Add syntax highlighting to a fenced code block.
     *
     * @param  array $block
     * @return  array
     */
    protected function blockFencedCodeComplete($block)
    {
        try {
            if ($class = array_get($block, 'element.text.attributes.class', false)) {
                if (starts_with($class, 'language-')) {

                    $code = array_get($block, 'element.text.text', '');
                    $code = $this->highlighter->highlight(str_after($class, 'language-'), $code)->value;
                    array_set($block, 'element.text.text', $code);
                    $block['element']['text']['attributes']['class'] = "hljs {$class}";

                } else {
                    $block = parent::blockFencedCodeComplete($block);
                }
            }

        } catch (\Exception $e) {
           $block = parent::blockFencedCodeComplete($block);
        }

        return $block;
    }
}

Wiring it all up

To let Jigsaw actually use own newly created class, we need to create another class in app/ParsedownParser.php. This will act as the bridge connecting the abstract concept of a markdown parser with our implementation.

// app/ParsedownParser.php

<?php

namespace App;

use Mni\FrontYAML\Markdown\MarkdownParser;

class ParsedownParser implements MarkdownParser
{
    /**
     * ParsedownParser constructor.
     */
    public function __construct()
    {
        $this->parser = new Parsedown();
    }

    /**
     * @param  string $markdown
     * @return  string
     */
    public function parse($markdown)
    {
        return $this->parser->text($markdown);
    }
}

Lastly, we need to override the markdown parser used by Jigsaw with our own implementation. This is done in the bootstrap.php file we created earlier:

<?php

use App\ParsedownParser;
use Mni\FrontYAML\Markdown\MarkdownParser;

/** @var  $container \Illuminate\Container\Container */
/** @var  $jigsaw \TightenCo\Jigsaw\Jigsaw */

$container->bind(MarkdownParser::class, ParsedownParser::class);

Styles!

Now that we have the syntax highlighting in place and after running the Jigsaw build process, we have successfully changed the generated markup for our example fenced code block from above from

<pre><code class="language-php">&lt;?php

$foo = 'bar';</code></pre>

to

<pre><code class="hljs language-php"><span class="hljs-meta">&lt;?php</span>

$foo = <span class="hljs-string">'bar'</span>;</code></pre>

Hooray! 🎉

Well, to be honest, there’s still a final step missing. Now that you have the markup touched up with all the right CSS classes we need to provide the actual styles for these, too. Now, the nice thing about our php based highlighter being a port of highlight.js, is, that we can simply drop in (or better integrate into our sites styles) any of the many available highlight.js themes (preview them on the projects website) and be done.

Before you ask: on this site I use a slightly modified version of the Atom Dark One theme by Daniel Gamage.

Hi there! I’m Jonas Döbertin, a full stack web developer from Hamburg, Germany with a focus on Laravel, Kirby CMS and Vue.js. Sometimes I tweet, share code and post design ideas.

I’m always accepting freelance work so if you want to talk, get in touch at hello@jd-powered.net.