<!-- livebook:{"persist_outputs":true} -->

# Lumis syntax highlighting

```elixir
Mix.install([
  {:mdex, "~> 0.13"},
  {:lumis, "~> 0.10"},
  {: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](https://hex.pm/packages/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.10"}` 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:

<!-- livebook:{"force_markdown":true} -->

```elixir
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:

<!-- livebook:{"force_markdown":true} -->

```elixir
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_*`](https://hex.pm/packages?search=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:

<!-- livebook:{"force_markdown":true} -->

```elixir
{:lumis_wasm_bundle_web, "~> 0.1"}
```

The [language catalog](https://docs.lumis.sh/reference/languages) 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:

<!-- livebook:{"force_markdown":true} -->

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

## Highlighting budgets

Pass `:budget` alongside `:formatter` under `syntax_highlight[:opts]`:

<!-- livebook:{"force_markdown":true} -->

```elixir
MDEx.to_html!(markdown,
  syntax_highlight: [
    engine: :lumis,
    opts: [
      formatter: :html_linked,
      budget: [time_limit: 1_000, match_limit: 4096]
    ]
  ]
)
```

The budget applies separately to each code block. Omitted or `nil` limits use Lumis's defaults: 5000 milliseconds and 8192 in-progress query matches. Set `time_limit: 0` to disable the time limit.

When time runs out, the whole block renders as escaped plain text with `data-lumis-budget="time"` on its `<pre>`. Reaching the match limit keeps the block highlighted, with some scopes missing, and marks it `data-lumis-budget="matches"`.

The time limit covers parsing and highlighting the code block, excluding language loading. It is checked cooperatively and can be exceeded between checks. It does not bound Markdown parsing, HTML formatting, or the total time to render a document containing several code blocks.

## 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.

````elixir
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.

````elixir
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](https://lumis.hexdocs.pm/Lumis.html#t:formatter/0) for the full shape:

<!-- livebook:{"force_markdown":true} -->

```elixir
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`.
