Add Line Numbers To Syntect Highlighted Code Blocks In Rust

Introduction

I'm using the syntect^syntect^^ rust crate to add syntax highlighting to my code examples. I've used Prism^prism^^ (which is JavaScript based) in the past, but prefer to have highlights baked in.

One feature Prism has that syntect doesn't provide out of the box is the ability to add line numbers to code samples. Syntect is very robust though and allows generating output that'll provide line numbers with the help of a little CSS.

The CSS

CSS offers a `counter`css` feature that can be used along with `::before`css` pseudo-element to generate the numbers. A minimal implementation looks like this:

.codeLines {
  counter-reset: linenumber;
}

.codeLine {
  counter-increment: linenumber;
}

.codeLine:before {
    display: inline-block;
    color: goldenrod;
    content: counter(linenumber);
    padding-right: 0.7rem;
    text-align: right;
    width: 2rem;
}

Here's an example of what that looks like when used with the HTML further below.


  alfa
  bravo
  
  charlie
    delta
  echo
  
  foxtrot
    golf
  hotel
<pre><code class="codeLines">
  <span class="codeLine">alfa</span>
  <span class="codeLine">bravo</span>
  <span class="codeLine"></span>
  <span class="codeLine">charlie</span>
  <span class="codeLine">  delta</span>
  <span class="codeLine">echo</span>
  <span class="codeLine"></span>
  <span class="codeLine">foxtrot</span>
  <span class="codeLine">  golf</span>
  <span class="codeLine">hotel</span>
</code></pre>

A `<span>`html` is set up to wrap each line. That's what the CSS uses to make the numbers. I haven't found a way to do it without that tag. But, it's not a huge deal since we can add them when doing the rest of the syntax highlighting.

The Rust Code

Syntect offers a bunch of differnet ways to highlight code depending on what your doing. The default ways don't offer the kind of line wrapping we need as far as I can tell. It's easy enough to address though since a line-by-line processing option is available. Using it, we can add the beginning and ending `<span>`html` tags as we iterate through the lines.

Here's the code I'm using to do just that.

Code
#!/usr/bin/env cargo +nightly -Zscript

//! ```cargo
//! [package]
//! edition = "2021"
//! [dependencies]
//! syntect = { version = "5.1.0"}
//! ```

use syntect::easy::HighlightLines;
use syntect::highlighting::{Style, ThemeSet};
use syntect::html::{styled_line_to_highlighted_html, IncludeBackground};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;

fn main() {
    let language = "HTML";
    let code = "<div>\n  Hello, Highligher\n</div>";
    let text = highlight_code_with_line_spans(code, language);
    print!("{}", text);
}

fn highlight_code_with_line_spans(code: &str, language: &str) -> String {
    let mut the_lines = vec![];
    let ps = SyntaxSet::load_defaults_newlines();
    let ts = ThemeSet::load_defaults();
    let syntax = ps.find_syntax_by_name(language).unwrap();
    let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);
    for line in LinesWithEndings::from(code) {
        let ranges: Vec<(Style, &str)> = h.highlight_line(line.trim_end(), &ps).unwrap();
        let highlighted_line = styled_line_to_highlighted_html(&ranges[..], IncludeBackground::No);
        let mut spanned_line = String::from(r#"<span class="codeLine">"#);
        spanned_line.push_str(&highlighted_line.unwrap());
        spanned_line.push_str("</span>");
        the_lines.push(spanned_line);
    }
    the_lines.join("\n")
}

The core of the process is the `highlight_code_with_line_spans`rust` function. Pass it some code and a language and it returns a string of html with everything we need.

Wrapping Up

It would be great if CSS could add line numbers without having to do the `<span>`html` trick. Until that happens, it's easy enough to address as long as you've got enough control of your syntax highlighter.

Notes
  • I'm not sure if the `counter-reset: linenumber;`css` call is necessary. I accidentally left a `.codeLines {}`css` class off and things still worked. I'm leaving it for now until I get a better understanding of what it does

  • This version of the code uses inline `style`html` tags to set the colors for each text token. Changing to a different set of colors is done by changing the theme in the source code and regenerating. I think there's a way to use classes instead. I'll probably look at that at some point, but the inline stuff works fine for now

  • Syntect seems to treat the language names as case sensitive. I'll be putting something in place that translates case-insensitive names to what it's looking for so I don't have to remember to type them a specific way

  • The only style in `.codeLine::before {}`css that's needed for the line numbers to show up is: `content: counter(linenumber);`css`

    I added a few other styles to make the example easier to see (and be more like what I do in prod)

  • The `display: inline-block`css` is necessary for the width to work which lets the numbers be right justified

  • There are other options you can pass to the counter like `decimal-leading-zero`` that adds a leading zero. There's also `upper-roman`` but it's a little hard to process

  • Some examples I've seen use multiple `<code>`html` blocks instead of `<span>`html` tags as the target for the line numbers. I'm not sure if there's any difference, but I like the idea of a single code block better

References

Reference

syntect

"a syntax highlighting library for Rust that uses Sublime Text syntax definitions"

Adding the highlighter to my site made the full build process to from 2sec. to 7sec. There's about 2,000 pages on the site. I've got no idea how many of them trigger the highlighter, but I'm okay with it. Having the syntax highlighing is worht it (and incremental builds still happen virtually instantly)

Reference

CSS counter() - MDN

"returns a string representing the current value of the named counter, if there is one" - See also: counter-reset, counter-set, counter-increment

Reference

CSS @counter-style - MDN

"lets you define counter styles that are not part of the predefined set of styles"

I haven't played with this yet, but it sounds fun

Reference

CSS ::before - MDN

"creates a pseudo-element that is the first child of the selected element" and the key to this whole thing

Reference

Using CSS to add line numbering - sylvain durand

This is the main page I used for the CSS code. It's great. There's no cruft in the examples at all. It's what I strive to do and I wish more posts were like it

Reference

Prism.js

A JavaScript based syntax highlighter. It's what I used before swtiching to syntect. There's a few features it has (like copying code) that I want to build into my site as well. It'll take some work, but I don't mind since I like removing the JS dependency