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.