Server Side Syntax Highlighting with Jigsaw

• 2 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.

Creating the Event Listener

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.

Room for improvements

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.

Acknowledgments

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.

Webmentions

    Replies

  • Photo of Owen Voke Owen Voke mentioned

    I currently use Jigsaw, a static site generator based on the Laravel framework, for my personal website. By default, the blog template ships with Highlight.js which is a great package for providing syntax highlighting for code blocks.

    However, Highlight.js runs in the visitor's browser, and therefore can be quite slow when there are a lot of code blocks. I recently ported over to using Highlight.php (a PHP-port of the Highlight.js package) by following Stefan Zweifel's great article on implementing this. However, I knew that this article was old (from March 2019), and I also knew that some Markdown parser changes had been made to Jigsaw since then.

    I was looking for a way to cut down this code and make it as simple as possible to implement. Back in February 2020, after the Markdown parser was changed to PHP Markdown, a PR was opened to restore the broken language-* prefixes for code blocks (PR #426). This however also added support for the markdownParser object being bound to the container meaning that it is accessible from your project's bootstrap.php.

    One of the configurations that can be set in the boostrap.php is adding a callback function for code_block_content_func which is what gets run to transform the contents of a Markdown code block to HTML. I ended up using this with the following code.

    First, I installed Highlight.php into my Jigsaw project (although this was already installed following Stefan's article).

    $ composer require scrivo/highlight.php
    

    And then, you can just add the following to your bootstrap.php file:

    $container['markdownParser']->code_block_content_func = function ($code, $language) {
        return (new \Highlight\Highlighter())->highlight($language ?? 'plaintext', $code)->value;
    };
    

    Any code blocks without a language specified will fall back to plaintext, and all others will use their specified languages.

    Note, you'll also need to include a stylesheet from the Highlight.php styles directory in your CSS source code. I currently use the a11y-light theme for my site.

    April 30th, 2022