home ~ projects ~ socials

Syntax Highlighting With Line Numbering And Classes In Rust

This is how I'm doing syntax highlighting in Rust.

This is the main way I'm doing it where instead of adding the styles inline it adds classes.

[] Uses classes instead of inline styles

[] Trim leading empty lines, but not white space before the first character on the first line

[] Trim trailing empty lines

[] Provide HTML that can have line numbers added via CSS

[] Fall back to plain-text if the requested language isn't found

[] link to: 2fbld7l3 for generating stylesheets

```cargo
[dependencies]
regex = "1.10.4"
syntect = "5.2.0"
```

use regex::Regex;
use syntect::html::{ClassedHTMLGenerator, ClassStyle};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;

fn main() {
  let code = r#"


   fn main() {
  println!(`Hello, World
    alfa
    bravo
  `)
}
"#;

  let lang = "rust";
  let output = highlight_code(code, lang);
  println!("{}", output)
}

fn highlight_code(code: &str, lang: &str) -> String {
  let syntax_set = SyntaxSet::load_defaults_newlines();
  let syntax = syntax_set.find_syntax_by_token(&lang).unwrap_or_else(|| syntax_set.find_syntax_plain_text());
  let mut html_generator = ClassedHTMLGenerator::new_with_class_style(syntax, &syntax_set, ClassStyle::Spaced);
  for line in LinesWithEndings::from(&trim_empty_lines(code)) {
      let _ = html_generator.parse_html_for_line_which_includes_newline(line);
  }
  let initial_html = html_generator.finalize();
  let output_html: Vec<_> = initial_html.lines()
    .map(|line| 
        format!(r#"<span class="line-marker"></span>{}"#, line))
    .collect();
    output_html.join("\n")
}

fn trim_empty_lines(source: &str) -> String {
  let re = Regex::new(r"\S").unwrap();
  let trimmed_front = source.split("\n")
    .fold(
      "".to_string(), |acc, l|
      {
        if !acc.is_empty() {
          acc + l + "\n"
        } else  {
          if re.is_match(l) {
            l.to_string() + "\n"
          } else {
            acc
          }
        }
      });
  trimmed_front.trim_end().to_string()
}
Output:
<span class="line-marker"></span><span class="source rust">   <span class="meta function rust"><span class="meta function rust"><span class="storage type function rust">fn</span> </span><span class="entity name function rust">main</span></span><span class="meta function rust"><span class="meta function parameters rust"><span class="punctuation section parameters begin rust">(</span></span><span class="meta function rust"><span class="meta function parameters rust"><span class="punctuation section parameters end rust">)</span></span></span></span><span class="meta function rust"> </span><span class="meta function rust"><span class="meta block rust"><span class="punctuation section block begin rust">{</span>
<span class="line-marker"></span>  <span class="support macro rust">println!</span><span class="meta group rust"><span class="punctuation section group begin rust">(</span></span><span class="meta group rust"></span><span class="meta group rust">`Hello<span class="punctuation separator rust">,</span> World
<span class="line-marker"></span>    alfa
<span class="line-marker"></span>    bravo
<span class="line-marker"></span>  `<span class="punctuation section group end rust">)</span></span>
<span class="line-marker"></span></span><span class="meta block rust"><span class="punctuation section block end rust">}</span></span></span></span>

CSS

.line-numbers {
  counter-reset: lineNumber;
}

.line-marker {
  counter-increment: lineNumber;
}

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

   fn main() {
  println!(`Hello, World
    alfa
    bravo
  `)
}
-- end of line --

References

See this for how to output the styelsheet: