Server Side Syntax Highlighting with Jigsaw

• 3 min read

One of my core principles as a web developer is to only ship as much JavaScript to the visitor as is needed to fulfill the job of the website. Stuff that can be done by a server side language, should be made by the server side language. 1

The job of my personal site is to share my blog posts. Visitors should be able to read these posts when JavaScript is disabled (or when they are on a flaky internet connection).

In the past, syntax highlighting was one of the features were I had to rely on a JavaScript library (like highlight.js). This bugged me, as the output off the JavaScript library was always the same for each visitor. In my opinion, when JavaScript code is parsing text, applies some CSS classes and the output is the same for each visitor, then the code which does that parsing should run server side once and not multiple times client side (why waste CPU power, when literally nothing changes? 2).

When this site was powered by Kirby I've discovered a package called kirby-highlight. When I've ported this site to Jigsaw I wanted to keep the server side syntax highlighting and Jigsaw's Event Listeners make this really easy.

Instead of hooking into Jigsaw internals I've decided to write an afterBuild-Listener. The code loops over my posts-collection, takes the rendered HTML, parses it through a PHP port of highlight.js and writes the updated HTML back to its original file.

namespace App\Listeners;

use Highlight\Highlighter;
use TightenCo\Jigsaw\Jigsaw;

class ApplySyntaxHighlighting
{
    public function handle(Jigsaw $jigsaw)
    {
        $jigsaw->getCollection('posts')->each(function ($post) use ($jigsaw) {
            $fileName = ltrim($post->_meta->path[0] . '/index.html', '/');
            $content = $jigsaw->readOutputFile($fileName);
            $updatedContent = $this->applySyntaxHighlighting($content);
            $jigsaw->writeOutputFile($fileName, $updatedContent);
        });
    }

    /**
     * Apply Syntax Highlighting on a string
     * Adapted from https://github.com/S1SYPHOS/kirby-highlight/blob/master/core/syntax_highlight.php
     * @param  string $value
     * @return string
     */
    private function applySyntaxHighlighting(string $value) : string
    {
        $pattern = '~<pre><code[^>]*>\K.*(?=</code></pre>)~Uis';

        return preg_replace_callback($pattern, function ($match) {
            $highlighter = new Highlighter();
            $highlighter->setAutodetectLanguages([
                'html',
                'php',
                'css',
                'js',
                'shell'
            ]);
            $input = htmlspecialchars_decode($match[0]);

            return $highlighter->highlightAuto($input)->value;
        }, $value);
    }
}

The code is pretty straightforward, but it could be better.

The code works fine, but it's kinda complicated: I don't like how I have to reach into the internals of the $post-object, do some string concatenation just to get the filename.

$fileName = ltrim($post->_meta->path[0] . '/index.html', '/');
$content = $jigsaw->readOutputFile($fileName);
$updatedContent = $this->applySyntaxHighlighting($content);
$jigsaw->writeOutputFile($fileName, $updatedContent);

A cleaner way would look like this:

$content = $post->getContent();
$updatedContent = $this->applySyntaxHighlighting($content);
$post->setContent($updatedContent);

Instead of executing the code on the afterBuild Event, this code would have to be executed on the afterCollections Event. Unfortunately the above code currently doesn't work: The setContent() method does update the in-memory content but the content is not written to the final output file. 3

I wanna look into this over the next days and open an Issue or Pull Request on the Jigsaw repository to get this fixed.

As I said, this wasn't my idea. I just adapted the original code from kirby-highlight made by S1SYPHOS.

Caleb Porzio recently released a new package called GitDown. It uses a public GitHub API endpoint to parse and apply syntax highlighting to your markdown content. It looks like a great package, if you're using Markdown in your Laravel apps. You can read more about it here.


  1. I don't wanna hate on JavaScript, but I still don't trust Browser vendors to execute my JavaScript code on every device and browser without errors.

  2. Call this nitpicking, but I care about those little details.

  3. The getContent() and setContent() method on the $post-object exist, but are currently not officially documented.