Syntax highlighting

Blogatto supports build-time syntax highlighting for code blocks in your markdown files via Smalto. When enabled, fenced code blocks with a language tag (e.g., gleam `) are highlighted at build time, producing styled HTML with no client-side JavaScript required.

Enabling syntax highlighting

Syntax highlighting is disabled by default. Enable it by passing a SyntaxHighlightingConfig to the markdown configuration:

import blogatto/config/markdown
import blogatto/config/markdown/code

let md =
  markdown.default()
  |> markdown.markdown_path("./blog")
  |> markdown.syntax_highlighting(code.default())

code.default() includes grammars for 28 languages out of the box.

Supported languages

The default configuration supports the following languages (with aliases):

Language Aliases
Bash bash, sh, shell
C c
C++ cpp
CSS css
Dart dart
Dockerfile dockerfile
Elixir elixir
Erlang erlang
Gleam gleam
Go go, golang
Haskell haskell, hs
HTML html
Java java
JavaScript javascript, js
JSON json
Kotlin kotlin, kt
Lua lua
Markdown markdown, md
PHP php
Python python, py
Ruby ruby, rb
Rust rust, rs
Scala scala
SQL sql
Swift swift
TOML toml
TypeScript typescript, ts
XML xml
YAML yaml, yml
Zig zig

Adding custom languages

If a language you need is not in the default set, add it with code.add_language():

import blogatto/config/markdown/code
import smalto/languages/ocaml

let syntax_config =
  code.default()
  |> code.add_language(ocaml.grammar, ["ocaml", "ml"])

The second argument is a list of names that will match the language tag in fenced code blocks.

Styling highlighted code

Smalto renders each token as a <span> with a CSS class corresponding to the token type. By default, the classes are:

Token type CSS class
Keyword smalto-keyword
String smalto-string
Number smalto-number
Comment smalto-comment
Function smalto-function
Operator smalto-operator
Punctuation smalto-punctuation
Type smalto-type
Module smalto-module
Variable smalto-variable
Constant smalto-constant
Builtin smalto-builtin
Tag smalto-tag
Attribute smalto-attribute
Selector smalto-selector
Property smalto-property
Regex smalto-regex

To style your code blocks, add CSS rules for these classes. Here is a minimal dark theme example:

pre.code-block {
  background: #1e1e2e;
  color: #cdd6f4;
  padding: 1rem;
  border-radius: 0.5rem;
  overflow-x: auto;
}

.smalto-keyword { color: #cba6f7; }
.smalto-string { color: #a6e3a1; }
.smalto-number { color: #fab387; }
.smalto-comment { color: #6c7086; font-style: italic; }
.smalto-function { color: #89b4fa; }
.smalto-operator { color: #89dceb; }
.smalto-punctuation { color: #cdd6f4; }
.smalto-type { color: #f9e2af; }
.smalto-module { color: #f9e2af; }
.smalto-variable { color: #cdd6f4; }
.smalto-constant { color: #fab387; }
.smalto-builtin { color: #f38ba8; }
.smalto-tag { color: #f38ba8; }
.smalto-attribute { color: #f9e2af; }
.smalto-selector { color: #a6e3a1; }
.smalto-property { color: #89b4fa; }
.smalto-regex { color: #f5c2e7; }

Custom token rendering

If CSS classes are not enough, you can override how each token type is rendered using the setter functions on SyntaxHighlightingConfig. Each setter takes a function that receives the token text and returns a Lustre element:

import blogatto/config/markdown/code
import lustre/attribute
import lustre/element
import lustre/element/html

let syntax_config =
  code.default()
  |> code.keyword(fn(text) {
    html.span(
      [attribute.style([#("color", "#cba6f7"), #("font-weight", "bold")])],
      [element.text(text)],
    )
  })
  |> code.comment(fn(text) {
    html.span(
      [attribute.style([#("color", "#6c7086"), #("font-style", "italic")])],
      [element.text(text)],
    )
  })

Available token setters: keyword, string, number, comment, function, operator, punctuation, type_, module, variable, constant, builtin, tag, attribute, selector, property, regex.

There is also a custom setter for custom token types emitted by some grammars. It receives both the custom type name and the token text:

code.custom(fn(type_name, text) {
  html.span(
    [attribute.class("smalto-custom-" <> type_name)],
    [element.text(text)],
  )
})

Using with smalto_config

For full control over the underlying Smalto rendering, use code.smalto_config() to pass a custom smalto_lustre.Config directly:

import blogatto/config/markdown/code
import smalto/lustre as smalto_lustre

let smalto_cfg = smalto_lustre.default_config()
  // ... customize via smalto_lustre API ...

let syntax_config =
  code.default()
  |> code.smalto_config(smalto_cfg)

Customizing the code block wrapper

Syntax highlighting controls the contents of code blocks. To customize the wrapping <pre> and <code> elements, use the markdown component setters:

import blogatto/config/markdown
import blogatto/config/markdown/code
import gleam/option

let md =
  markdown.default()
  |> markdown.markdown_path("./blog")
  |> markdown.syntax_highlighting(code.default())
  |> markdown.pre(fn(children) {
    html.pre([attribute.class("code-block")], children)
  })
  |> markdown.code(fn(language, children) {
    let lang_class = case language {
      option.Some(lang) -> "language-" <> lang
      option.None -> ""
    }
    html.code([attribute.class(lang_class)], children)
  })

Complete example

Putting it all together — a markdown config with syntax highlighting, custom wrapper classes, and a custom keyword style:

import blogatto/config/markdown
import blogatto/config/markdown/code
import gleam/option
import lustre/attribute
import lustre/element
import lustre/element/html

let syntax_config =
  code.default()
  |> code.keyword(fn(text) {
    html.span(
      [attribute.class("token-keyword")],
      [element.text(text)],
    )
  })

let md =
  markdown.default()
  |> markdown.markdown_path("./blog")
  |> markdown.route_prefix("blog")
  |> markdown.syntax_highlighting(syntax_config)
  |> markdown.pre(fn(children) {
    html.pre([attribute.class("code-block")], children)
  })
  |> markdown.code(fn(language, children) {
    let lang_class = case language {
      option.Some(lang) -> "language-" <> lang
      option.None -> ""
    }
    html.code([attribute.class(lang_class)], children)
  })