Code highlighting
Fenced code blocks are highlighted by Typst itself. Register a grammar once with a show rule in your template and it applies to every page:
#show raw.where(lang: "powershell"): set raw(syntaxes: "/highlight/powershell.sublime-syntax")
syntaxes points at a Sublime-syntax grammar (.sublime-syntax). Typst ships grammars for most common languages, and baudelaire adds kdl – the language its own config is written in, and the one it highlights a config show with – so you only need your own for the niche ones. This site adds powershell.sublime-syntax for the Windows install steps and nothing else.
NOTE
Typst builds a syntax set from the files you hand it and nothing else, so a grammar that reaches into another Sublime package (embed: scope:source.regexp, set: scope:source.cs) cannot resolve that reference, and the text it covers is dropped from the output rather than left unhighlighted. Vendoring a grammar from the wild usually means replacing those few contexts with plain handling.
Colors that follow a dark-mode toggle
Typst’s HTML export bakes highlight colors inline, a color style on every span, with no option to emit classes. A color frozen at build time cannot follow a toggle that flips at runtime.
Add an html { highlight } block and baudelaire highlights code blocks itself, emitting a class per token instead:
html {
highlight
}
<span class="sx-keyword">let</span>
The palette then lives in your stylesheet, where the toggle can reach it:
.sx-keyword { color: var(--sx-keyword); }
.sx-string { color: var(--sx-string); }
.sx-comment { color: var(--sx-comment); font-style: italic; }
:root { --sx-keyword: #a12a5e; } /* light */
[data-theme=dark] { --sx-keyword: #e58fa6; } /* dark */
The theme a show raw rule sets is not read at all in this mode, and neither are the bold and italic a .tmTheme puts on a scope: styling is the stylesheet’s, whole. A page that sets one anyway is told once that it is being ignored, since a discarded palette otherwise looks like code that simply came out unstyled.
The vocabulary
A grammar’s scopes are its own (keyword.other.fn.rust, keyword.control.import.python), and a stylesheet naming them would be written once per language. Every grammar funnels into one closed set instead, and so does Typst’s own parser, so #let in a typ block and let in a rust one are both sx-keyword:
| Token | What lands there |
comment |
Comments, and the // that opens one |
string |
Strings, quotes included |
escape |
\n and friends inside one |
number |
Numeric literals |
constant |
true, nil, named constants |
keyword |
Keywords, and storage: let, fn, struct |
operator |
+, ==, -> |
punctuation |
Brackets, separators, terminators |
function |
Definitions and calls |
type |
Type, class and struct names |
namespace |
Module and package names |
tag |
Markup element names |
attribute |
Markup attribute names |
property |
Object keys, struct members |
variable |
Everything else with a name |
parameter |
Names in a signature |
heading |
Headings, in markup languages |
strong |
Bold markup, and a term in a list |
emph |
Italic markup |
link |
Links |
raw |
Inline code, in markup languages |
label |
Labels and references |
invalid |
What the grammar could not parse |
Text no token claims gets no span at all: a code block is mostly plain, and a span around every word would be markup nobody reads.
Naming and narrowing
The block is where a site says what it wants on the page:
html {
highlight {
// What every class starts with. Empty for none.
prefix "tok-"
// Drop the tokens you do not paint; `-name` removes, a bare name adds.
tokens "-punctuation" "-variable"
// Rename one, prefix aside: this writes `tok-kw`.
classes {
keyword "kw"
}
// Keep the grammar's own scope on the span, as `data-scope`.
scopes #true
}
}
A dropped token keeps its text and loses its span, so narrowing tokens is how a small palette gets small markup.
scopes is the way out of the vocabulary when you need to tell one kind of keyword from another:
[data-scope^="keyword.control"] { font-style: italic; }
Without the block nothing changes: the colors stay inline, exactly as Typst emits them, which is also what a site wanting its own .tmTheme rendered as written should keep.
In a theme
All four shipped themes turn highlight on in their theme.kdl and define the palette in style.css. Each maps the twenty-odd tokens onto about eight color variables: the ones a reader has to tell apart get a color of their own, and the rest join whichever is closest.