# Elixir (/usage/elixir)

## When you want explicit error handling

Use [`highlight/2`](https://hexdocs.pm/lumis/Lumis.html#highlight/2) when invalid user input, formatter options, or language detection should stay in the normal control flow.

```elixir
case Lumis.highlight(source,
       formatter:
         {:html_inline,
          language: "elixir", theme: "catppuccin_latte"}
     ) do
  {:ok, html} ->
    html

  # The parser is not a dependency yet; `:package` is what to add to mix.exs.
  {:error, %Lumis.ParserError{reason: :not_installed, package: package}} ->
    handle_missing_parser(package)

  {:error, error} ->
    handle_error(error)
end
```

There is no default theme. Without `:theme`, `:html_inline` and `:terminal` emit
uncolored output.

`highlight/2` returns `{:ok, output}` or `{:error, exception}`. The exception is
a [`Lumis.ParserError`](https://hexdocs.pm/lumis/Lumis.ParserError.html) when a
language's parser could not be loaded and a
[`Lumis.RenderError`](https://hexdocs.pm/lumis/Lumis.RenderError.html)
otherwise. Both carry a `:reason` atom to match on, so the case a deployment has
to act on is a clause rather than a substring; `Exception.message/1` renders
either one for a log.

## When you want the highlighted output directly

Use [`highlight!/2`](https://hexdocs.pm/lumis/Lumis.html#highlight!/2) when failures should raise immediately, such as in scripts, trusted internal code, or examples.

```elixir
html =
  Lumis.highlight!(source,
    formatter:
      {:html_inline,
       language: "elixir", theme: "catppuccin_latte"}
  )
```

`highlight!/2` returns the highlighted output or raises.

## Language selection

Elixir accepts several forms:

```elixir
Lumis.highlight!(code,
  formatter: {:html_inline, language: "elixir"}
)

Lumis.highlight!(code,
  formatter: {:html_inline, language: ".ex"}
)

Lumis.highlight!(code,
  formatter: {:html_inline, language: "lib/my_app.ex"}
)

Lumis.highlight!(code)
```

Omitting `language` from the formatter enables auto-detection.

## Parser loading

A parser is an ordinary dependency. Declare the languages you highlight in
`mix.exs`, and `Lumis.highlight/2` loads the ones a document turns out to
name, including languages injected inside it, then keeps them for every
later request:

```elixir title="mix.exs"
def deps do
  [
    {:lumis, "~> 0.10"},
    {:lumis_wasm_markdown, "~> 0.26.0"},
    {:lumis_wasm_elixir, "~> 0.26.0"}
  ]
end
```

````elixir
# Loads markdown, then elixir for the fenced
# block, in one pass.
Lumis.highlight!("""
# Title

```elixir
defmodule Demo do
end
```
""",
  formatter:
    {:html_inline,
     language: "markdown",
     theme: "catppuccin_latte"}
)
````

A language no dependency supplies leaves its own block plain rather than failing
the document, and is never fetched. Only the document's own language failing is
an error, a `Lumis.ParserError` whose `:package` names the dependency to add.

Loading is global to the VM. One runtime lives in the NIF, so the first process
to need a language pays for it and every process after it does not.

### Warming parsers during application startup

Parser bytes ship with the `lumis_wasm_*` dependency, so a cold parser costs a
Wasmtime compile and nothing else. Warm the languages your application needs
from its `start/2` so a request never pays it:

```elixir title="lib/my_app/application.ex"
def start(_type, _args) do
  Lumis.Languages.async_load(["markdown", "elixir", "json"])
  Supervisor.start_link(children(),
    strategy: :one_for_one,
    name: MyApp.Supervisor
  )
end
```

`async_load/1` returns before the compile starts, so the boot never waits on
it, and its result is deliberately not matched. The work runs under a
`:temporary` child of Lumis's supervisor: a failure is logged, never retried,
and cannot stop the application from starting. Since highlighting loads on
demand regardless, the worst case is paying on the first request rather than an
outage. Use `Lumis.Languages.load/1` when the caller does want the result.

A `:bundle_*` atom names the same languages as the `@lumis-sh/wasm-bundle-*` package
of that name — `:bundle_web`, `:bundle_web_extra`, `:bundle_system`, `:bundle_backend`
and `:bundle_full` — so a bundle means the same thing in every runtime.
`Lumis.Languages.bundles/0` returns the membership.

Warm the languages you serve rather than `:bundle_full`. Every language in a
process shares one 128 MB Tree-sitter Wasm store, and
[loading the full set can exhaust it](/languages#bundle-full-is-not-the-safe-default).
`:bundle_full` is capped at half that store, so it leaves out the largest
parsers of the less common languages, such as `haskell` and `perl`. Add
`{:lumis_wasm_haskell, "~> 0.26.0"}` and name it if you need it; the
[full list](/languages#bundle-full-is-not-the-safe-default) is there.

Compiled modules land under `config :lumis, data_dir:`, `LUMIS_DATA_DIR`, or
Lumis's `priv/lumis` directory by default, so a restart or a new pod using the
same persistent directory reuses them instead of compiling again.

### Where parsers come from

A parser is an ordinary dependency. `mix deps.get` delivers the bytes, Lumis
reads them from the `priv/parsers` of each installed `lumis_wasm_*` application,
and a language no dependency supplies answers `:not_installed` rather than being
fetched. Nothing in the Elixir runtime reaches the network, at boot or at render.

The data directory is where Wasmtime persists compiled modules. It defaults to
`LUMIS_DATA_DIR`, and configuration takes precedence, which is what a release
usually wants:

```elixir title="config/runtime.exs"
config :lumis, data_dir: "/app/lumis"
```

Hex checks every package it fetches against the checksum in `mix.lock`, so
Lumis loads parsers as `mix deps.get` delivered them and does not hash them
again.

<span id="upgrading-to-09" />
<span id="upgrading-from-06" />

For upgrades from 0.6 or to 0.9, see [Elixir upgrades](/migrate/elixir).

## Where the precompiled NIF comes from

The NIF ships precompiled, and it is downloaded once while the `lumis` dependency
compiles. GitHub Releases serves it by default. The same artifacts are mirrored
to Cloudflare R2, which you can opt into when GitHub is unreachable:

```elixir title="config/config.exs"
config :lumis, artifact_source: :cloudflare
```

`LUMIS_ARTIFACT_SOURCE=cloudflare` does the same thing, and is easier to reach
inside a Dockerfile or a CI job than a config file that has already been baked
into an image. Configuration wins when both are set. Valid values are `:github`
and `:cloudflare`.

This is a switch, not a fallback. The source you choose is the only one tried, so
a failed download does not reach for the other one. Both serve byte-identical
files, and either way the download is checked against the SHA-256 in
`checksum-Elixir.Lumis.Native.exs` from the Hex package before it is used, so the
mirror cannot hand you a different NIF.

Two other escape hatches sit alongside it: `LUMIS_BUILD=1` builds the NIF from
source instead of downloading anything, and `LUMIS_USE_LEGACY_ARTIFACTS=1` takes
the legacy-CPU variant on a machine without the newer instruction sets.

## Discover supported languages and themes

```elixir
Lumis.Languages.available()
Lumis.Theme.available()
Lumis.Languages.loaded()

Lumis.Languages.get("js")
Lumis.Theme.get("catppuccin_latte")
```

`Lumis.Languages.get/2` and `Lumis.Theme.get/2` look one up by name, returning
`nil` (or a default you pass) when there is no such language or theme.

See [`Lumis`](https://hexdocs.pm/lumis/Lumis.html), [`Lumis.Languages`](https://hexdocs.pm/lumis/Lumis.Languages.html) and [`Lumis.Theme`](https://hexdocs.pm/lumis/Lumis.Theme.html) on HexDocs.

## Phoenix and LiveView

Highlighted HTML must be rendered as raw HTML in templates.

```elixir
code =
  Lumis.highlight!(source,
    formatter:
      {:html_inline,
       language: "elixir", theme: "catppuccin_latte"}
  )
```

```heex
{Phoenix.HTML.raw(@code)}
```

For linked CSS output, also mount a theme stylesheet through `Plug.Static` as shown in [CSS Theme Files](/themes/css-files).

## Light/dark mode

See [Light/Dark Mode](/recipes/light-dark) for three approaches to theme switching (CSS `light-dark()`, media queries, and manual toggle).

## Custom themes

Generate a JSON file with the
[Neovim colorscheme recipe](/recipes/generate-theme-from-neovim-colorscheme),
then load it as a theme struct:

```elixir
{:ok, theme} = Lumis.Theme.from_file("/path/to/theme.json")
{:ok, theme} = Lumis.Theme.from_json(theme_json)
```

Then pass the resulting theme struct into the formatter options.

## Validation

```elixir
# validate options before passing to highlight
Lumis.validate_options!(
  formatter:
    {:html_inline,
     language: "elixir", theme: "catppuccin_frappe"}
)

# inspect defaults
Lumis.default_options()
```

Use `validate_options!/1` when your app exposes formatter options to users (e.g., config files, admin panels). It raises on invalid values.