Prism is a lightweight, extensible syntax highlighter that supports a broad spectrum of languages and is conveniently pre-packaged with 11ty.
The eleventy-plugin-syntaxhighlight plugin enables syntax highlighting in 11ty, allowing for the display of code blocks on a page. This is useful for showcasing sample code, configurations, code snippets, and more.
However, displaying line numbers is not a feature supported by the syntax highlighter. Some workarounds have been attempted using CSS counters, as shown below from Github issues #10.
To keep my existing code intact, including 11ty's diff-* syntax and line highlights js/4-5/6,7, I wanted to add line numbers to code blocks during the build process. This way, I didn't need to bring in more libraries. Fortunately, Markdown-it makes it possible to modify renderer rules for code blocks to achieve this.
In my syntax code blocks, I primarily highlight code from JavaScript and other web-based languages, along with bash scripts, and configuration files in formats such as TOML and YAML. I may also include Git syntax occasionally.
To support this, I'll need to customize the renderer to support the following:
Highlighting Specific Lines
Utilize 11ty's markdown highlighter to emphasize certain lines of code.
Code Diffs
Support for 11ty's diff-* syntax to display differences between code versions.
Accurate Line Counting
Code blocks should account for white spaces and new lines as individual lines, ensuring an accurate line count.
No Additional Plugins
The implementation should work independently, without the need for extra plugins.
Looking ahead, it would be beneficial to package this functionality into a plugin for easier integration. Additionally, we could enhance the feature set by providing an option to toggle line numbers on or off. We could also introduce a shortcode to specify the starting line number, offering even more flexibility in how code blocks are displayed.
During the parsing step, markdown-it converts the Markdown text into a stream of tokens. Each token represents a piece of Markdown syntax, recognized by a set of parsing rules that identify specific syntax patterns in the text.
In the rendering step, markdown-it converts the token stream into HTML. This is accomplished by a set of rendering rules, where each rule is associated with a specific type of token and generates the corresponding HTML.
Each rendering rule is associated with a specific type of Token and is responsible for generating the HTML that represents that token.
You can customize the rendering process by modifying the renderer rules.
For instance, the rules.fence function is a rendering rule for fenced code block tokens. By replacing this function, you can control how fenced code blocks are rendered, adding features like line numbers or syntax highlighting.
In addition to parsing and rendering, markdown-it allows for additional transformations to be applied in between these steps. You can also directly change the output in a Renderer for many simple cases.
Please note that for versions of @11ty/eleventy-plugin-syntaxhighlight prior to v3.2, the rendering behavior differs slightly. The dependency versions used in this guide are as follows:
If your custom renderer needs to work with raw, unprocessed Markdown (including template syntax), this tutorial's implementation won't work with Nunjucks or any other template engine because these engines process the template syntax before the renderer gets the content.
However, if your renderer works with the tokenized representation of the Markdown content (which is the usual case), it should work fine regardless of the template engine used.
To activate syntax highlighting, we'll leverage 11ty's syntax highlighting features for diffs and highlighting.
You may find the plugin option wrapAllHighlightLines useful, so it's worth experimenting with it.
With this configuration, code blocks processed using the plugin's renderer will include the <pre data-render-src="11ty" ...> attribute.
This configuration will apply to code blocks with language types such as js/4-5, js/4,5/7-8, diff-, and diff-*, which are processed through the plugin.
With 11ty setup, we can begin outlining methods we will use to override the renderer rules from markdown-it.
The markdown-itRenderer Interface Generates HTML from a parsed token stream.
As a reference, we can look to the default implementation to understand what we should generate in the resulting HTML.class RendererConfig {...}
As a future enhancement, you could refactor this as a separate eleventy or markdown-it plugin, support features like starting like numbers using paired shortcodes in 11ty, and add better support for diffs with line numbers but that is beyond the scope of this tutorial.
Note: This tutorial assumes you aren't already overriding the renderer for code blocks. If you are, you'll need to adjust this approach to work alongside your existing code.
To begin, import the RendererConfig class from the renderer-config.js file (or whatever you've chosen to name it). Next, create a new instance of the RendererConfig class, passing in the renderer from the markdown library.
This sets up a new rendering rule for fenced code blocks in your markdown content. The actual implementation of this rule will be added in the next steps, where we'll leverage the methods provided by our RendererConfig class to generate line numbers and format the code block content.
At this point in the rendering process, the Eleventy plugin has already applied syntax highlighting to the content of the code block. We can utilize this pre-existing functionality in our custom renderer rule.
The syntax-highlighted content can be retrieved from the token content and used as follows:
First, we implement a method to extract the language from the Markdown fence token ```. We also split the token content into an array of lines, which will be used to generate line numbers.
Our goal is to match the HTML structure generated by the Prism line-numbers plugin, which uses a NodeList of elements to represent each line number.class RendererConfig {...} - Generate line number elements
The formatLines method accomplishes two things:
It generates line numbers for each line of code, regardless of whether the line is empty or not.
It wraps each line of code in a <div> tag, which can be customized as needed. This encapsulates the syntax-highlighted code.
Later, when we inject our line numbers into the code block HTML using lineNumbers, we can also use htmlLines if we want each highlighted line to be wrapped in a line.
For example, the output could look like this:
This is similar to the default output generated by Prism:
This is optional. I added this for flexibility in how the highlighted lines are displayed, while also ensuring that line numbers are correctly generated and displayed.
Next, we integrate the list of line number elements we've generated into our token content.
In this step, we're inserting our line numbers right before the closing </code> tag, mirroring the approach used by Prism. This placement can be adjusted to suit your specific needs.class RendererConfig {...} - Inject line numbers
Lastly, remember to return the updated HTML in your renderer rule.
With the modifications we've made, a markdown code block like this:
Should now be rendered by the parser into the following HTML:
As mentioned in the Generating Line Numbers section, if you prefer a structure where each line is individually wrapped, you can do this:
If you go this route, you'll have to do the syntax highlighting yourself. This means you'll need to override the default syntax highlighting that comes with the Eleventy plugin. Here's an example of how you can swap out the content in the code blocks.
Please be aware that there are currently some peculiarities when using the diff-* or highlight syntax. While it's not perfect, it accomplishes what I needed for now.
For completeness, I'm also sharing my styles.
Please note that they are still a work in progress, and it's a bit all over the place. Feel free to adapt them to your needs or use them as a starting point.CSS for Line Numbers, Diffs, and Line Highlights
Feel free to experiment with the implementations discussed in this tutorial. As a starting point, here's a sample 11ty project set up with the code from this guide.11ty Sample Project