Powered by AppSignal & Oban Pro

Syntax highlighting

examples/syntax_highlight.livemd

Syntax highlighting

Mix.install([
  {:mdex, "~> 0.8"},
  {:kino, "~> 0.16"}
])

How it works

MDEx highlights fenced code blocks with Lumis. Lumis parses code with Tree-sitter, applies a theme converted from Neovim colorschemes, and returns HTML that MDEx inserts into the rendered document.

Pass a Lumis formatter through the :syntax_highlight option:

syntax_highlight: [formatter: {:html_inline, theme: "github_light"}]

If you do not pass anything, MDEx uses {:html_inline, theme: "onedark"}. Set syntax_highlight: nil or syntax_highlight: false to disable built-in syntax highlighting.

Inline styles

The inline formatter writes colors directly into the generated HTML. This is the easiest option for docs, feeds, emails, or any place where carrying an extra stylesheet is annoying.

Each fenced block still gets its language from the Markdown fence. You do not need one formatter per language.

options = [
  syntax_highlight: [formatter: {:html_inline, theme: "catppuccin_latte"}]
]

"""
# Inline formatter

```elixir
def fib(n), do: fib(n, 1, 1)

def fib(0, _a, _b), do: []

def fib(n, a, b) when n > 0 do
  [a | fib(n - 1, b, a + b)]
end
```

```ruby
def fibonacci(n)
  return n if (0..1).include?(n)
  (fibonacci(n - 1) + fibonacci(n - 2))
end
```

```rust
fn fibonacci(n: u32) -> u32 {
  match n {
    0 => 1,
    1 => 1,
    _ => fibonacci(n - 1) + fibonacci(n - 2),
  }
}
```
"""
|> MDEx.to_html!(options)
|> Kino.HTML.new()

Linked CSS

Use :html_linked when your app owns the CSS. The generated HTML uses classes instead of inline token colors, so you can serve one Lumis stylesheet, cache it, and swap it if your site changes themes.

In a Phoenix app, Lumis can expose its packaged CSS files from priv/static/css.

theme_css =
  :lumis
  |> :code.priv_dir()
  |> Path.join("static/css/github_light.css")
  |> File.read!()

options = [
  syntax_highlight: [formatter: :html_linked]
]

html =
  ~S|
  # Linked formatter

  ```elixir
  def render(assigns) do
    ~H"""
    
{@body}
""" end ``` |
|> MDEx.to_html!(options) Kino.HTML.new(""" #{theme_css} #{html} """)

Disabling highlighting

When syntax highlighting is disabled, MDEx still renders fenced code blocks as regular Markdown code blocks. The language from the fence remains as a language-* class, but there are no Lumis wrappers, token spans, or inline styles.

html =
  ~S"""
  ```elixir
  def hello(name), do: "Hello, #{name}!"
  ```
  """
  |> MDEx.to_html!(syntax_highlight: nil)

Kino.Markdown.new("""
```html
#{html}
```
""")

Choosing a formatter

  • :html_inline is the default and needs no external CSS.
  • :html_linked emits CSS classes and expects a Lumis theme stylesheet.
  • :html_multi_themes emits CSS variables for cases like light/dark mode.

For automatic light/dark mode, see examples/light_dark_theme.livemd. For custom theme structs, see examples/custom_theme.livemd.

Theme names such as github_light, github_dark, and catppuccin_latte come from Lumis. You can inspect them with Lumis.available_themes/0.