Adding A Code Syntax Highlighting CKEditor Plugin To Backdrop CMS

In the last post in this series, I introduced Backdrop text formats and the WYSIWYG editor from the eyes of a Drupal 7 (D7) developer migrating code from D7 to Backdrop. In this post, I’ll continue that discussion and get into the nitty-gritty of adding the code I need to implement code syntax highlighting in my WYSIWYG editor.

As a reminder, I’m using the Shortcode module to render text inserted via a CKEditor modal that eventually looks like the following output.

CKEditor Button

The first step in my editorial workflow will be to click a button on the toolbar and have a modal pop up where I can select a language and enter my code sample.

I’m basically trying to recreate that modal as my first step. In D7, I used “hook_wysiwyg_plugin()” to add the “code” button to the toolbar. The WYSIWYG module connected that code to a text format, and that is how the button made its way into the WYSIWYG editor users saw. Since the WYSIWYG module isn’t needed in Backdrop, I will have to change my code to use “hook_ckeditor_plugins()” instead. The hooks have a lot of similarity to them.

As you can see, I had to guard my D7 plugin to only run when the CKEditor was selected since the WYSIWYG module can have as many editors installed as you would want, and the specific keys used in your plugin might not apply to any other editor than CKEditor.

I didn’t include all of the “hook_ckeditor_plugins()” properties available for you to use, but I tried to include all the properties in the API documentation that I thought I might use.

hook_ckeditor_plugins() Properties

We’ll start at the top of the list with the “path” key. The path key is almost identical to the D7 WYSIWYG hook path key, except you explicitly declare a “file” key as well. The path and file keys are the only required keys the CKEditor module needs to load your plugin.

I think the separation of path and file is due to the fact that you could have many files in your plugin folder and Backdrop would have to have some way of parsing those in order to find the right plugin file to use. Explicitly declaring the plugin filename so the CKEditor module doesn’t have to guess for you seems like a good move to make and an improvement.

The “internal” property declares if the plugin is “is part of the compressed CKEditor library package and already loaded on all instances”. For my use cases, this will always remain FALSE. I’m not going to try and mess around with what Backdrop provides in core for the CKEditor library, and I don’t care to look into the bundled library for plugins not included by default.

With the “CSS” property, you can declare an array of CSS files for CKEditor to load.

“These files are used only when CKEditor is using an iframe wrapper around its content. If a plugin needs to include CSS for inline and iframe versions, it should add its CSS via CKEditor’s JavaScript CKEDITOR.addCss() method.”

I only left that key in since HighlightJS, the library I am using to render the syntax highlighting has to load a CSS file, and I thought maybe I could add it to the page this way. I will likely remove that key as I finish porting my module and load the CSS in some other way, but the CSS property might be useful for you to know about.

I think the “enabled callback” key maps directly to the “load” key in my D7 code. The description in the API file makes me feel a lot better than what I see in “hook_wysiwyg_plugin()” API docs for D7.

// Boolean whether the editor needs to load this plugin. When TRUE,
// the editor will automatically load the plugin based on the 'path'
// variable provided. If FALSE, the plugin either does not need to
// be loaded or is already loaded by something else on the page.
// Most plugins should define TRUE here.

“loaded by something else on the page”…?? What does that mean exactly? In the enabled callback you can use $format and $plugin_name along with Backdrop globals and functions to much better target whether the plugin should be loaded. Another improvement to what I was doing in D7, I think.

Finally, we get to the “buttons” property which contains an array of information like image, label, alternative_image, and dependencies among other information. The most interesting part of that array to me is the “dependencies” key. From what I understand, you can declare that a plugin needs to have another plugin installed in order to work correctly. Dependency checking is a nice feature to not have to implement in your “enabled callback”.

Once I added all that information to my module and enabled it, I can now move a “{code}” button into the toolbar. Cool!

CKEditor JS Issues

Except that now the WYSIWYG editor fails to load due to some sort of JS issue 😦. Luckily, the first JS error wasn’t hard to track down.

Somehow in my D7 code I wasn’t passing in the CKEditor variable but it was still loaded somewhere else. To fix that error, I needed to pass in CKEDITOR so that I could call it inside the function. I ended up copying code from the core backdroplink plugin where those variables are passed in.

My second error of Cannot read property "icons" of null was also an easy thing to track down. It happened because I changed the name of the plugin listed in “hook_ckeditor_plugins()” but failed to make the change in the JS file. Since I am creating a plugin with a modal, I looked at the “backdropimage” plugin files for inspiration. That “plugin.js” file has a lot of useful information with a good amount of comments in-line.

Other than renaming my command and matching it to what was in the hook, I didn’t have to change anything else for the dialog to work. I’ll definitely go back and look at each part of the “plugin.js” file to see if I need to correct/update any of it, but I’m glad that it works for now!

Rather than go through what is in the dialogCommand call, I’ll just link to that file: https://github.com/alexfinnarn/code_syntax_highlighting_bundle/blob/dev/plugins/code_sc/dialogs/code_sc_generator.js

I copied most of that file from one of our other CKEditor plugins, so I can’t comment too much on each of the pieces. I did have to do a bit of kung foo with the newlines to   but it wasn’t too hard to finagle.

Oh happy days! I now have my dialog showing up, and when I click “OK”, it gets added to the body. Sweet! I didn’t have to change much of anything at all to get that to work. Now, let’s see what happens when I load the page.

Filter Processing Order

Not too shabby to start, but we have a couple of issues going on here. The <code> tag is getting picked up by the Backdrop theme’s CSS and styled to look like Medium, basically, which is a good thing showing that the correct HTML is being rendered on the page. It looks yucky so far, but at least I won’t have to mess with my PHPStorm to Gist workflow on this future blog 🤷‍♂.

The issue is that the “highlight.js” assets aren’t getting loaded on the page, but we have a more important issue to fix first. If a line has no characters on it, no &nbsp; characters are inserted. What’s wrong with that you say? Well, let’s look more closely at the filters that are run on render for the filtered text format.

Filter Processor Order

As you can see, converting <br> tags to <p> tags happens after the shortcode output is rendered. I hoped that I could just change the filter order and move the shortcode filters to the bottom of the list; however, the break tag still gets converted to a paragraph. I’ll have to turn off the “Convert line breaks into HTML” filter for now and get back to fixing that later.

Onto highlight.js

Now that my <code> block is all in one place, I can move on to why the “highlight.js” library isn’t getting loaded. Lets look at the code in my D7 module that loaded the library JS and CSS files.

Whoopsie, hook_page_alter() isn’t a thing in Backdrop since the page is built by the Layout system. In our D7 codebase, that hook is used a lot, and most of the time, I think it is abused.

In this case, all I am doing is checking to see the current node’s type matches the content types that can have syntax highlighting enabled and adding the highlight.js assets to the page, if so. In D7, I remember people telling me it was best practice to use the #attached property in a render array, but since I see “hook_preprocess_page()” mentioned in the change notice for removing “hook_page_alter()”, I’m going to be lazy and just try that.

And voila! It’s that easy to convert a Drupal 7 module using CKEditor plugins, the Shortcode module, and external libraries to Backdrop. Granted, you can clean up a lot of what I’ve just done and improve it, but we’ve got many more modules to port already in the pipeline. Stay tuned for more!