How to highlight code blocks using the Trix editor in Rails

Reading time: 8 minutes | Published: 01/19/2020

I've been using the Ruby on Rails framework to build this website, which uses a WYSIWYG editor that the team behind Rails (well, technically Basecamp) built called Trix. I love this feature because it allows me to use rich text in my blog posts right 'out-of-the-box'. This is what allows me to add the links you see above, styling like bold, italic, and underline, as well as images and videos.

Another thing it allows me to do is to format code like this:

function () {
  console.log('sample javascript function');
};

The whole reason I built this website is to write about web development, so it is really useful to be able to show code samples. And while the monospaced font and the gray background create a distinct element on the page that looks like code, I want to be able to highlight and style it so that it looks more like code I write in my own editor (currently Microsoft's Visual Studio Code).

There are a few third-party libraries that you can use to accomplish this, including Prism.js, Highlight.js, and Google's code-prettify. These libraries use javascript and CSS to add styling and formatting to your code samples. In order to do this, you have to add class names to the code elements so that the libraries can find them and apply the changes.

The current implementation of Trix does not support this highlighting. There are a couple of issues that prevent you from being able to do this. First, the Trix code elements use a structure that is not semantically correct. (What the heck does that even mean? This stackoverflow answer explains it pretty clearly - It means that you're calling something what it actually is.) The code block that Trix creates looks like this:

<pre>
  <!--block-->
  function () {
    console.log('sample javascript function');
  };
</pre>

The <pre> element is used to display text 'exactly as written in the HTML file'. This means that white space is preserved, which in turn means that you can correctly display code samples formatted in a way that is more easily readable.

The problem is that the semantically correct way of marking up code blocks is with a <code> tag nested inside the <pre> tag, like this:

<pre>
  <code>
    <!--block-->
    function () {
      console.log('sample javascript function');
    };
  </code>
</pre>

The second problem is that in order to have the code highlighting libraries be able to find the code elements that you want to highlight, you have to add a class to the <code> element that also specifies the code 'language', like this:

<pre>
  <code class="language-class-here">
    <!--block-->
    function () {
      console.log('sample javascript function');
    };
  </code>
</pre>

I started looking into the Trix source code, to try to figure out how to do these two things. There is some custom configuration that you can do, but I was in over my head and even after reaching out to the author of much of the code, I was stumped. Even though I figured out how to use javascript to add the <code> element in and add the class, I couldn't persist the changes because Trix parses and sanitizes the code to strip out anything that it doesn't expect before it saves your HTML document to the database.

So, I came up with an inelegant solution (AKA a hack). I wouldn't worry about making my changes and persisting them to the database. I would just use javascript to make the transformations when I showed my post to the user. The only additional problem is that I needed a way to specify what kind of code I was showing in my sample - like HTML, javascript, or CSS.

I decided that I would just add the class name that the library that I chose (Prism.js) needed to the very end of the code block, and then use that class name for the code element I was injecting:

Snapshot of code block


I've taken a picture here instead of using the actual code block because otherwise it would be transformed to the new format. However, this is how the code block will always look when I'm writing the post in the editor. (or at least until Trix starts supporting this!)

And now for the grand reveal. Here is how the code block looks with all the fancy styling and highlighting. And as a bonus, this is the javascript code you too can use to get the styling to work!

const applyFormattingToPreBlocks = function () {
  const preElements = this.showPostBody.querySelector('.trix-content').querySelectorAll('pre');
  preElements.forEach(function(preElement) {
    const regex = /(?!lang\-\\w\*)lang-\w*\W*/gm;
    const codeElement = document.createElement('code'); 
    if (preElement.childNodes.length > 1) {
      console.error('<pre> element contained nested inline elements (probably styling) and could not be processed. Please remove them and try again.')
    }
    let preElementTextNode = preElement.removeChild(preElement.firstChild);
    let language = preElementTextNode.textContent.match(regex);
    if (language) {
      language = language[0].toString().trim();
      preElementTextNode.textContent = preElementTextNode.textContent.replace(language, '');
      codeElement.classList.add(language, 'line-numbers');
    }
    codeElement.append(preElementTextNode)
    preElement.append(codeElement)
  })
};
lang-js 

The first thing I do on line 2 is grab all the <pre> elements on the page. I'm looking for all the <pre> elements inside a element with a class of 'trix-content', since I know that's the container that Trix uses. I then loop through each <pre> element, make sure it has only one child (the text node that contains the code sample), and remove and store that text node. I create a new code element, and setup a regex to find the language class in the text node. I use that to get the class and remove it from the text node, and if it exists, I add it as a class to the code element. Finally, I append the text node to the new <code> element, and append that <code> element to the original <pre> element. I ran into one bug where if I have used styling in the code block, then it won't format correctly. I was lazy and instead of dealing with that properly, I just throw an error to let you know that you can't do that.

Now that I've made all the changes for Prism.js to be able to hook into, I need to actually include the Prism library into my application. Doing this is pretty easy - I use the code hosted in a CDN, but you can also download the Prism javascript and CSS files and include it in your application. To wire up the library, you have to add the files into your main HTML page. For me that looks like adding the CSS:

<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/themes/prism-okaidia.min.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/plugins/line-numbers/prism-line-numbers.min.css" rel="stylesheet" />
lang-html

and the javascript:

<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/components/prism-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/plugins/autoloader/prism-autoloader.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/plugins/line-numbers/prism-line-numbers.min.js"></script>
lang-html

I'm using two Prism plug-ins, autoloader to preload all possible languages I can style, and line numbers, so I can get the fancy line numbers you see on the left side.

So there it is. That's how I hacked my way around that problem. It took way longer to come up with this solution than I'd like to admit, but now it's done and I've shared my learning with the world. Did you like what you read? Do you have any questions? Do you think I'm a big dummy and have you figured out a more robust or elegant way to do this? Please drop me a comment below, I'd love to hear from you. Thanks for reading!

Stuart Wilson said:

Hello,
First, I really like your work.  I tried to sign up, but the confirmation email never came. I implemented your solution into a stimulus controller, and I did a write up of it here: https://www.stuartlwilson.dev/blogs/trix-code-block-fix-with-stimulus.  I wanted to make sure that it was OK with you if I share it, and also wanted to share my updates to your innovative solution with you.  Thanks.

Over 1 year ago

Eugene van der Merwe said:

Thank you so much! This is an incredible hack for Trix editor.

Over 1 year ago

Mikael Henriksson said:

I don't get it, I've tried in a rails app to make it work but it basically does not work. Any chance you could set up a small app in a GitHub repo showcasing your suggestion?

About 3 years ago

The Big Dummy replied:

Hi Mikael. Take a look at the applyFormattingToPreBlocks function. That's how I'm setting it up, and it's all wrapped in a DOMContentLoaded event listener so that it applies the styling after page load.

Almost 3 years ago

Mikael Henriksson replied:

Cheers 🍻 

Almost 3 years ago

Alexandre Nunes said:

That was a very simple and nice solution. Thanks for sharing!

Almost 4 years ago

The Big Dummy replied:

Glad you found it helpful Alexandre, thanks for stopping by!

Almost 4 years ago