Powered by AppSignal & Oban Pro

Lumis syntax highlighting

examples/lumis.livemd

Lumis syntax highlighting

Mix.install([
  {:mdex, "~> 0.13"},
  {:lumis, "~> 0.9"},
  {:lumis_wasm_elixir, "~> 0.26"},
  {:lumis_wasm_ruby, "~> 0.26"},
  {:lumis_wasm_rust, "~> 0.26"},
  {:kino, "~> 0.16"}
],
  config: [mdex_native: [syntax_highlighter: :lumis]]
)

What Lumis does

Lumis highlights code with Tree-sitter. It ships themes converted from Neovim colorschemes and gives you a few HTML formatters.

To use Lumis, add {:lumis, "~> 0.9"} to your deps, add a parser for every language you highlight, and configure :mdex_native with syntax_highlighter: :lumis before compiling dependencies. This notebook does all three in Mix.install/2 above: it highlights Elixir, Ruby, and Rust, so it installs those three parsers.

Then use engine: :lumis to enable Lumis for a render:

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

The older Lumis API is still supported for existing code, but it is no longer recommended:

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

For new code, pass Lumis options under :opts and set the engine explicitly.

The language still comes from the Markdown fence. One formatter can handle different languages in the same document.

Parsers are dependencies

Since v0.9 Lumis does not carry the parsers it highlights with. Each language is a WebAssembly module published as its own lumis_wasm_* package: nothing is compiled into the NIF and nothing is downloaded at runtime. Name a language you haven't installed and that fence comes out as plain text.

Documents inject languages too: HTML reaches css and javascript, Elixir reaches comment. Install those as well. A bundle covers a set at once:

{:lumis_wasm_bundle_web, "~> 0.1"}

The language catalog lists every package name.

Parsers compile on first use. In an application, warm them from start/2 so the first request doesn't pay for it:

Lumis.Languages.async_load(~w(elixir ruby rust))

Inline styles

Use :html_inline when the generated HTML needs to stand on its own. This is useful for docs, feeds, emails, or anything where carrying a stylesheet around is a hassle.

options = [
  syntax_highlight: [engine: :lumis, opts: [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. Lumis writes token classes, and your stylesheet supplies the colors.

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

options = [
  syntax_highlight: [engine: :lumis, opts: [formatter: :html_linked]]
]

html =
  ~S|
  # Linked formatter

  ```elixir
  def render(assigns) do
    ~H"""
    <pre><code>{@body}</code></pre>
    """
  end
  ```
  |
  |> MDEx.to_html!(options)

Kino.HTML.new("""
<style>#{theme_css}</style>
#{html}
""")

Formatter options

Lumis formatter options live under opts: [formatter: ...]. See the Lumis formatter type for the full shape:

syntax_highlight: [
  engine: :lumis,
  opts: [
    formatter: {:html_inline, theme: "github_light", pre_class: "code", include_highlights: true}
  ]
]

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

Code fence decorators such as highlight_lines, theme, and pre_class are Lumis options too. See examples/code_block_decorators.livemd.

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