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:

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.

A `

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 `

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 `

References

References

  • "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)

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

  • "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

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

  • 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

  • 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