Home
Head's Up: I'm in the middle of upgrading my site. Most things are in place, but there are something missing and/or broken including image alt text. Please bear with me while I'm getting things fixed.

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 [TODO: Code shorthand span ] feature that can be used along with ` : : before [TODO: Code shorthand span ] pseudo - element to generate the numbers. A minimal implementation looks like this :

CSS

.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

A ` < span > [TODO: Code shorthand span ] 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 > [TODO: Code shorthand span ] tags as we iterate through the lines.

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

rust
#!/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 [TODO: Code shorthand span ] 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 > [TODO: Code shorthand span ] trick. Until that happens, it's easy enough to address as long as you've got enough control of your syntax highlighter.

- 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 [TODO: Code shorthand span ] 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 [TODO: Code shorthand span ] but it's a little hard to process

- Some examples I've seen use multiple ` < code > ` html ` blocks instead of ` < span > [TODO: Code shorthand span ] 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

Footnotes And References