Client-Side Syntax Highlighting

 November, 5 2018 - Guido Flohr
A very common requirement is to have syntax-highlighting applied to code blocks. Best practice with Qgoda is to just mark code blocks semantically correct and let the client do the highlighting when rendering the generated pages.

How Should Code Blocks Be Marked?

Code blocks should be wrapped into a <code> element that is in turn wrapped into a <pre> element. The programming language of the code block should be added as a CSS class in the format language-PROGRAMMING-LANGUAGE:

&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (options.debug) {
    console.log("Options: ", options);
}&lt;/code&gt;&lt;/pre&gt;

It is advisable to put the enclosing <pre> and <code> tags on the same line as the enclosed code. Otherwise ugly line feeds are added to the beginning and the end of the block.

Creating Semantically Correct Code Blocks

Manually Creating Semantically Correct Code Blocks

Remember that (almost) all markup goes through unchanged in Markdown. Nobody stops you from entering your code blocks manually with HTML, as we have just seen:

&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (options.debug) {
    console.log("Options: ", options);
}&lt;/code&gt;&lt;/pre&gt;

Using Fenced Code Blocks

So-called fenced code blocks are supported by many markdown processors. Fenced code blocks are surrounded by triple backticks ```:

&#x60;&#x60;&#x60;
if (options.debug) {
    console.log(&quot;Options: &quot;, options);
}
&#x60;&#x60;&#x60;

This creates exactly the construct we want, the code inside of a <code> element inside of a <pre> element. But none of the tags has any attributes, more specifically the class="language-javascript" is not present. But you can specify the language immediately following the opening "fence":

&#x60;&#x60;&#x60;javascript
if (options.debug) {
    console.log(&quot;Options: &quot;, options);
}
&#x60;&#x60;&#x60;

There is a caveat, though. Qgoda's default Markdown processor Text::Markdown does not support fenced code blocks with a language specifier. It erroneously interprets it as part of the code. You can therefore only use the language specifier, when you use Text::Markdown::Hoedown as your markdown processor.

Syntax-Highlighting With PrismJS

PrismJS is a very popular syntax highlighter written in Javascript that supports an impressive list of languages.

Basic Usage

You can see the minimum required JavaScript and css files in the following code snippet:

&lt;!doctype html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;link href=&quot;/assets/css/prismjs/themes/prism.css&quot; rel=&quot;stylesheet&quot; /&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;script href=&quot;/assets/js/prismjs/prism.js&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;

The default stylesheet gets included in line 4 and the core library in line 7. Make sure that they are found at the locations specified.

Using a PrismJS Theme

PrismJS ships with a number of themes. You can override the default theme by loading an additional stylesheet:

&lt;link href=&quot;/assets/css/prismjs/themes/prism.css&quot; rel=&quot;stylesheet&quot; /&gt;
&lt;link href=&quot;/assets/css/prismjs/themes/prism-coy.css&quot; rel=&quot;stylesheet&quot; /&gt;

That will change the style to the "coy" theme.

Loading Highlighters and PrismJS Plug-Ins

In order to save memory and band-width, PrismJS does not load all of its highlighters and plug-ins automatically but you have to explicitely specify them.

The complete example below would load support for highlighting JavaScript and also load the line-numbers plug-in that uses CSS to prepend every line with a line number:

&lt;!doctype html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;link href=&quot;/assets/css/prismjs/themes/prism.css&quot; rel=&quot;stylesheet&quot; /&gt;
    &lt;link href=&quot;/assets/css/prismjs/themes/prism-coy.css&quot; rel=&quot;stylesheet&quot; /&gt;
    &lt;link href=&quot;/assets/css/prismjs/plugins/line-numbers/prism-line-numbers.css&quot; rel=&quot;stylesheet&quot; /&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;script href=&quot;/assets/js/prismjs/prism.js&quot;&gt;&lt;/script&gt;
    &lt;script href=&quot;/assets/js/prismjs/plugins/line-numbers/prism-line-numbers&quot;&gt;&lt;/script&gt;
    &lt;script href=&quot;/assets/js/prismjs/components/prism-javascript.js&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;

If you want to have syntax highlighting for more languages, just add the corresponding PrismJS component after line 11.

Using the Qgoda Syntax Highlighter Plug-In

Fenced code-blocks are not always enough:

  1. The markdown processor may not support them, or not support a language specification.
  2. You want to set additional attributes on the html elements, especially classes.

Installation

You can install the Qgoda highlighter plug-in like any other Qgoda plug-in. Normally:

$ cd /path/to/your/project
$ npm install gflohr/qgoda-plugin-tt2-highlight

You can use yarn add gflohr/qgoda-plugin-tt2-highlight if you prefer yarn over npm.

Usage

Now you have to use Template Toolkit directives in order to activate the highlighting:

[% USE Highlight %]
[% FILTER $Highlight &quot;language-javascript&quot; &quot;line-numbers&quot;
                     &quot;data-start&quot;=42 %]
if (options.debug) {
    console.log(&quot;Options: &quot;, options);
}
[% END %]

All positional arguments to the filter plug-in (line 2) are added to the CSS class of the surrounding <pre> element. The named arguments are converted to HTML attributes and their corresponding values.

The above example will result in the following HTML code:

&lt;pre class=&quot;language-html line-numbers&quot; data-start=&quot;5&quot;&gt;if (options.debug) {
    console.log(&quot;Options: &quot;, options);
}&lt;/code&gt;&lt;/pre&gt;

Provided that you have correctly loaded the PrismJS line-numbers plug-in, this will highlight the code as JavaScript in the browser, add line numbers in front of every line and start the line numbering with line 5. For instance, the code examples on this very page are produced with directives like this:

Page-wide Defaullts

It is quite likely that all code blocks on a page are in the same programming language and should share their settings. You can therefore pass global arguments in the USE directive, when you load the filter.

[% USE Highlight &quot;language-javascript&quot; &quot;line-numbers&quot; %]
...
[% FILTER $Highlight %]
if (options.debug) {
    console.log(&quot;Options: &quot;, options);
}
...
[% END %]

All FILTER invocations will now share the same settings, respectively, they will receive the sum of the arguments that you specified for USE and those for FILTER.

If you want to disable a certain CSS class for an individual FILTER, just pass the class name with a minus sign (-) prepended:

...
[% FILTER $Highlight &quot;-language-javascript&quot; &quot;language-html&quot; %]
&lt;link href=&quot;styles.css&quot; rel=&quot;stylesheet&quot;>
...
[% END %]

This code block will now as an exception not be highlighted as JavaScript but as HTML.

Alternative: Use a JavaScript Hack

Using the Qgoda syntax highlighter plug-in is very flexible but involves a lot of typing compared to fenced code blocks. On this side we use a little JavaScript hack that allows us to enable the line-numbers plug-in with fenced code blocks like this:

&#x60;&#x60;&#x60;javascript;line-numbers
if (options.debug) {
    console.log(&quot;Options: &quot;, options);
}
&#x60;&#x60;&#x60;

See the JavaScript source code for details!

Caveats

There are two little gotchas that you have to keep in mind.

Code Blocks Without Language Specification

If you use a fenced code block or the qgoda highlighter plug-in without a language specification, PrismJS will not highlight the code block. This will be fixed in a later version of the highlighter plug-in (see https://github.com/gflohr/qgoda-plugin-tt2-highlight/issues/1 and https://github.com/gflohr/qgoda/issues/51). Until then, you either have to mark every affected code block as language "none" (or with the class "language-none") or you have to resort to a little bit of JavaScript:

var codes = document.querySelectorAll('pre>code');
for (var i = 0; i < codes.length; ++i) {
    var parent = codes[i].parentElement;
    if (!parent.hasAttribute('class'))
        parent.setAttribute('class', 'language-none');
}

You may argue that the check in line 4 will miss pre elements that have a class attribute but miss a language-* specification. This cannot happen because all of the blocks are generated, and they will not have any class attribute set.

Well, not 100 % true. If you created such a block with the highlighter plug-in, it could have a class attribute but no language specification. But that's your own fault then, and you know how to fix it.

Using the CSS-Modules And Line-Numbers Plug-In Simultaneously

The other glitch is rather esoteric and caused by a little flaw in the plug-in system of PrismJS. Its css-modules plug-in can be used to automatically use the BEM methodology for automatically namespacing all CSS class names and id attributes.

The line-numbers plug-in on the other hand ignores BEM. It searches for code blocks that have the exact class line-numbers. The workaround is to use both classes, the BEM-style class for styling and the literal one for the JavaScript code.

You can see how this is done in the source code of this page.

The BEM-style class name is stored in the variable css.prism.line_numbers. The hash containing that variable is read in https://github.com/gflohr/qgoda-site/blob/master/_views/functions/css-modules.tt and that function is included at the top of source code of this page.

This website uses cookies and similar technologies to provide certain features, enhance the user experience and deliver content that is relevant to your interests. Depending on their purpose, analysis and marketing cookies may be used in addition to technically necessary cookies. By clicking on "Agree and continue", you declare your consent to the use of the aforementioned cookies. Here you can make detailed settings or revoke your consent (in part if necessary) with effect for the future. For further information, please refer to our Privacy Policy.