Using Tempest Highlight with WordPress
I like to highlight bits of code on my blog. I was using GeSHi - but it has ceased to receive updates and the colours it uses aren't WCAG compliant.
After skimming through a few options, I found Tempest Highlight. It has nearly everything I want in a code highlighter:
- PHP with no 3rd party dependencies.
- Lots of common languages.
- Modern, with regular updates.
- Easy to use functions.
- Range of difference style sheets.
But, on the downside:
- No WordPress plugin.
- Not all languages supported.
- CSS embedded in HTML.
I can live without some esoteric languages, but I don't really want to run composer install
on my blog. I just want a quick WordPress plugin. So, here's how I did it.
Here Be Dragons
This is a quick prototype. It has an audience of one; me. It may break in unexpected ways. Use at your own risk.
The file layout is relatively simple:
WordPress Plugins
├── Highlight_Plugin
│ ├── src/
│ ├── autoload.php
│ ├── index.php
│ └── base.css
The src/
directory contains the src/
directory from Tempest Highlight.
The Art of Loading without Loading
Normally, to install a PHP package, the composer
app creates an autoloader which will magically import everything you need into your project. We can't do that here. Instead, we need to manually load the library.
Create a file in the plugin's directory called autoload.php
- its job is to autoload everything in the src/
directory.
PHP
<?php spl_autoload_register( function ( $class ) { // Project-specific namespace prefix $prefix = "Tempest\\Highlight\\"; // Base directory for the namespace prefix $base_dir = __DIR__ . "/src/"; // Does the class use the namespace prefix? $len = strlen( $prefix ); if ( strncmp( $prefix, $class, $len ) !== 0) { // No, move to the next registered autoloader return; } // Get the relative class name $relative_class = substr( $class, $len ); // Replace namespace separators with directory separators, append with .php $file = $base_dir . str_replace( "\\", "/", $relative_class ) . ".php"; // If the file exists, require it if ( file_exists( $file ) ) { require $file; } });
I don't know if that's the easiest way to do it. But it works!
Testing
The index.php
file can now be tested:
PHP
// Load the Tempest Highlight library require_once __DIR__ . "/autoload.php"; // Set up the namespace use Tempest\Highlight\Highlighter; // Define the theme. $theme = new Tempest\Highlight\Themes\InlineTheme( __DIR__ . "/src/Themes/Css/light-plus.css"); // Create the highlighter. $highlighter = new Tempest\Highlight\Highlighter( $theme ); // Print some formatted HTML echo $highlighter->parse("<em id='foo' class='bar'>test</em>", "html" );
All being well, that should produce this:
<<span style="color: #0000ff;">em</span> id='foo' class='bar'>test</<span style="color: #0000ff;">em</span>>
That has the CSS embedded. Not ideal, but certainly good enough. I picked "light-plus" because it was the only theme which seemed to meet at least WCAG AA when on a white background.
OK, so how do we go from printing out a scrap of HTML to extracting all the code snippets from a WordPress blog?
Draw The Rest of the Owl
In theory the code is relatively straightforward.
Find code snippets
My Markdown plugin transforms this:
```javascript
var a = 2.0;
```
Into this:
HTML
<pre><code class="language-javascript"> var a = 2.0; </code></pre>
No need to use a regex, the new PHP 8.4 HTMLDocument gives us direct programmatic access to the HTML.
PHP
// Load the content into PHP 8.4's HTML DOM. $dom = Dom\HTMLDocument::createFromString( $content, LIBXML_NOERROR | LIBXML_HTML_NOIMPLIED, "UTF-8" ); // Select the code snippets. // `<pre><code class="language-*">` $codeSnippets = $dom->querySelectorAll( "pre>code[class^=language-]" );
Replace the snippets
From the above, I have the language and code, so it can "easily" be replaced.
PHP
// Iterate through each snippet. foreach ( $codeSnippets as $code ) { // Get the HTML from within the <code>. $originalCode = $code->textContent; // Replace the contents of <code> with the highlighted HTML. $code->innerHTML = $highlighter->parse( $originalCode, $language ) }
Replacing the code in that node manipulates the original DOM. Which means, after looping through all the snippets, I can return the altered HTML like so:
PHP
return $dom->saveHTML();
And then…
Obviously, there's a bit more too it than that. It ignores RSS feeds, it adds a base CSS style to the head, some SVGs get embedded, semantic metadata is included, and it all gets a bit tangled and complicated.
ToDo
A few things need to happen to make this even better.
- Encoded comments as well and posts.
- Add new languages.
- Don't in-line the CSS into the HTML, but add it as a separate stylesheet.
But, for now, it is running on my blog and that's good enough for me!
Get the code
You can play about with the WordPress plugin. Bugs reports, pull requests, and suggestions all warmly welcomed.
@Edent Bonus points for the heading "Draw The Rest of the Owl" 🥰
Reply to original comment on dataare.cool
|More comments on Mastodon.