Syntax Highlighting

Written by Ella on , without AI

for some reason, i want good syntax highlighting on this website. however, i am not especially happy with all the pre-existing solutions. therefore, i want to implement my own syntax highlighter! one thats perfect and does everything ever and might even be the best ever not really, i'll implement one that is only useful for this website in particular, and add features over time as needed.

problems with the existing

there are many existing highlighters, but they all have some issues that make them not work quite right for me, some of these issues are:

therefore, i have decided to write my own one

my syntax highlighting

in order to write a syntax highlighter, lets start by working out the inputs and outputs.

the easiest input is the source code, just copy it from the markdown file. for the language to highlight, i wanted to know both the language itself and any properties. i therefore implemented a simple parser to get the value of the properties and the language name.

the properties were chosen to allow more control over the output code, so it can look better for someone reading the rendered code block:

i can specify these properties in the header line of the code block, e.g.:

  1. ```rust diff=true highlight=1,3,4 hide=#

the output was represented per line, with a style for every length of input text:

  1. struct Line {
  2. content: Vec<(Style, &str)>,
  3. kind: Kind,
  4. }
  5. enum Kind {
  6. None,
  7. Highlight,
  8. DiffAdd,
  9. DiffRemove,
  10. }

rust

syntax highlighting rust code is pretty hard. if it is tokenised, it will produce a token tree, not a simple list of tokens. then, if you parse that into a syntax tree, some of the token boundaries will have changed. for example:

  1. tuple.0.1

compared to

  1. tuple(0.1)

has the 0.1 parsed as the tuple accessors, or as a floating point number. we can parse rust using syn, which will take care of all this for us. syn is typically used for procedural macros, however there isn't anything stopping me using it as a generic rust parser, outside of a macro. we can then convert the token stream produced by syn into the output format described above.

converting from syn's output into something usable by a syntax highlighter was a lot more work than i expected. in particular, i had to:

lua

i'm using lua code a decent amount, but don't have it highlighted. i could implement syntax highlighting using the existing parser, but that would be hard, in the same way as it is in rust. if i wait until after ive implemented type checking, then the actual kind of each token can be represented more accurately. i should also be able to include the syntax highlighter in the compiler, to get more consistent output in multiple different environments.

pikchr

to create the diagrams in the site, i currently use pikchr. in each markdown file, the diagrams include the source of the diagram in a code block. the input and output from pikchr is worked out completely separately from the other code blocks

other languages

i've used several other languages so far, but only very briefly. if i decide that those other languages need highlighting, i could use an existing syntax highlighter and work out how to put its output into this highlighter, same as i did for rust.

overall data flow

The overall data flow of the highlighter, for all languages is:

Is Diagram? Yes HTML+SVG Output   No Remove prefix from lines to be hidden Is diff? Yes Split into a and b documents Highlight a and b documents separately Combine documents a, b, and diff markers   No Highlight document Add whole line highlighting Remove lines to be hidden Cleanup output HTML Output

performance

the previous tree-sitter version of this website took approximately 600ms to build, every time. when i removed tree sitter and just output empty strings instead of code blocks, that was reduced to 14ms. now it is 16ms. as far as i can tell, most of the tree-sitter slowness was due to startup time, however as there is so little work to be done, the startup time is actually important. [1]

example

  1. @@ 1,2 @@ my annotation
  2. pub struct A(bool);
  3. impl A {
  4. fn example(&mut self) -> Self { /* ... */ }
  5. }
  6. fn example2(&mut self) -> bool { /* ... */ }

conclusion

it was probably overkill to have done this, but it feels better?