Helping Myself With HighlightJS



The Problem

As a programmer, writing a blog that will probably be a lot about programming, it helps to be able to show code in my blog. I want to show my code nicely though, I don't want to have to copy and paste images onto the site, because that wouldn't look good.

Thankfully, I don't have to, because I can colour my text in all sorts of lovely manners, thanks to the magic of the web (or CSS).
I can even add all sorts of conditional highlighting and formatting around my page. This means that I can achieve pretty nice-looking syntax highlighting on my page using CSS, instead of having to take images of my code and upload them to the website.

The thing is, it is going to be time-consuming to manually colour and assign properties to all of the code that I upload to this site. This means that I need a way to generate it automatically. There are probably sites or programs that already do this for me, but that is no fun - I want to do it myself. Visual Studio Code does generate HTML formatted code when you copy and paste code from inside of it, but it is just a load of coloured <span> properties, which I don't want as it makes my site quite unmaintainable in case I want to give it a fresh new look. The solution to this is using CSS classes to properly assign the correct highlighting to certain parts of the text.

If I'm doing that, I might as well also make the tool web-based, and host it on this site.

The Solution

Thankfully, there is a solution. HighlightJS is a JavaScript library that easily highlights code in a variety of different languages, outputting it in nice HTML using CSS classes, meaning you can add custom theming with ease.

After a little bit of testing, I was happy with the results of the library. It isn't perfect, but it is certainly better than highlighting all of the code myself.

/* Here is an example, and also some code I use: */
code .symbol::selection {
    background-color: #4ee2fc;
}

Implementation

Now I had determined that HighlightJS would be good enough for my needs (much to my relief, as I didn't want to write my own version), I needed to implement it on my website. Before I did that, I needed to create a basic template, so I created a document with a specially formatted text box. It was here that I discovered the magic of contenteditable, which is an HTML attribute that allows the user to edit the text contents of a <div> tag. This was very helpful for me, as I did not want to use a <textbox>.

I initially tried to highlight the code inside of the <div> as the user typed, but that did not work very well as it kept resetting the user's cursor position, and moving the cursor back to where it was is slightly more complicated than it initially seems. Instead of coming with to a solution to that issue, I decided to move on and instead just add another <div> to display the output.

To format the code, I had to perform a small bit of processing on it, starting with setting the code-output language- class to be the language that the user had configured, then removing the data-highlighted attribute to force HighlightJS to re-highlight the code. I then ran this every time the code input box's text changed.

const output = document.getElementsByClassName("code-output")[0];
output.className = "code-output language-" + this.language;

output.removeAttribute("data-highlighted");
output.textContent = this.createText();

hljs.highlightElement(output, { language: this.language });

// Remove hljs- class (unlucky if you have hljs- in your stuff)
output.innerHTML = output.innerHTML.replaceAll("hljs-", "");

At this point, I ran into a problem - my whitespace was not being preserved in the output of my code, which was almost as inconvenient as having to manually colour my code. This turned out to be quite a hard issue to fix, which is why I didn't. I found a very convenient article by Alberto Gasparin on how to get the plain text from a contenteditable element, which might have been a bit of a cheap way out, but it did save me a lot of time.

This was great, and it produced highlighted code, but the issue was that it only worked on one programming language at that point in time, Ada, the first programming language in my alphabetically ordered select box.

So, I created a function that triggered whenever the dropdown box changed, passing in the name of the language selected inside of the dropdown box.

if (!this.addedScripts.includes(this.language)) {
    this.addedScripts.push(this.language);
    const script = document.createElement("script");

    script.src = HLJS_LINK + this.language + ".min.js"

    let self = this;
    script.addEventListener("onload", function () {
        self.highlight();
    });

    document.head.appendChild(script);
}

When importing the core of HighlightJS, it does not import the all of language packs required, so I need to import them, but instead of importing them when I first load the website, which would slow loading down, I import them afterwards dynamically, whenever the user needs them by adding the script tag to the page, then re-highlighting the page once it has loaded.

When developing this blog, I use a Visual Studio Code extension called Live Server, which refreshes my page as I develop it and add new code. This is very handy, but it was quite annoying to have my dropdown box reset as I was making changes, I wanted it to save between reloads. To solve that, I added some code to read the current language from the search parameters using the URLSearchParams class.

let language = new URLSearchParams(window.location.search).get("language") ?? "ada";

let valid = false;
let dropdown = document.getElementsByClassName("languages")[0];
for (let i = 0; i < dropdown.length; ++i) {
    if (dropdown.options[i].value.toLowerCase() === language.toLowerCase()){
        valid = true;
        break;
    }
}

if (valid === false) {
    // Just set it to the default
    language = "ada";
}

dropdown.value = language.toLowerCase()

This code checks the URLSearchParams to see if they contain a language parameter. It then checks if that language exists inside the dropdown box's options, before setting the dropdown value (and triggering the language loading code that gets activated by that event).

I also wanted to write this language parameter to the search bar, so I didn't have to manually input it and could just use the dropdown. This was very easy, using the same URLSearchParams class.

let url = new URL(window.location.href);
url.searchParams.set("language", this.language);        
history.pushState({}, '', url.href);

After this, I wanted the user to be able to copy the output HTML to their clipboard, instead of having to open the developer tools to copy it. To do that, I created a button and created a function using ClipboardItem to copy the HTML to their clipboard.

I've been using the tool I made to do this to create this whole article, and it has made the experience much better.

You can find it here.