# `Lumis`
[🔗](https://github.com/leandrocp/lumis/blob/hex-lumis/v0.8.0/packages/elixir/lumis/lib/lumis.ex#L1)

<p align="center">
  Syntax highlighter powered by Tree-sitter and Neovim themes.
</p>

<p align="center">
  <a href="https://lumis.sh">https://lumis.sh</a>
</p>

<div align="center">
  <a href="https://hex.pm/packages/lumis">
    <img alt="Hex Version" src="https://img.shields.io/hexpm/v/lumis">
  </a>

  <a href="https://hexdocs.pm/lumis">
    <img alt="Hex Docs" src="http://img.shields.io/badge/hex.pm-docs-green.svg?style=flat">
  </a>

  <a href="https://opensource.org/licenses/MIT">
    <img alt="MIT" src="https://img.shields.io/hexpm/l/lumis">
  </a>
</div>

## Features

- **110+ Tree-sitter languages** - Fast, accurate, and updated syntax parsing
- **250+ built-in Neovim themes** - Updated and curated themes from the Neovim community
- **Built-in formatters** - HTML (inline/linked), Terminal (ANSI), Multi-theme (light/dark), BBCode
- **Custom formatters** - Build your own output
- **Language auto-detection** - File extension, shebang, and emacs-mode support
- **Line highlighting** - Mark and style individual lines, with custom HTML wrappers
- **Streaming-friendly** - Handles incomplete code
- **Load parsers on demand** - Verified and cached, including injected languages

## Installation

```elixir
def deps do
  [
    {:lumis, "~> 0.7"}
  ]
end
```

## Usage

```elixir
iex> Lumis.highlight!("Atom.to_string(:elixir)", formatter: {:html_inline, language: "elixir", theme: "github_light"})
```

The language is optional — Lumis detects it from the source, a filename, or a
shebang. The theme is optional too, but there is no default: without one,
`:html_inline` emits spans with no colors. Themes are named:
`theme: "github_light"`, or a `Lumis.Theme` struct built from your own JSON.

Formatters decide the output: `:html_inline`, `:html_linked`,
`:html_multi_themes`, `:terminal`, `:bbcode_scoped`, or your own.

## Parsers

Highlighting downloads, verifies and loads whatever a document needs, including
languages injected inside it, and caches them for every later request. Loading is
global to the VM, so only the first process pays.

```elixir
# move the download off the first request
Lumis.Languages.load(["elixir", "html", "javascript", "css"])
```

## Application startup

Warm parsers from your application's `start/2` so production does not download
or compile them on the first request:

```elixir
def start(_type, _args) do
  Lumis.Languages.async_load(~w(elixir html javascript css))
  Supervisor.start_link(children(), strategy: :one_for_one, name: MyApp.Supervisor)
end
```

It returns immediately, so the boot never waits on the network, and a failed
warm-up is logged rather than able to stop the application from starting.

See the [deployment guide](https://lumis.hexdocs.pm/deployment.html) for the
full lifecycle example, bundles, the standalone CLI, and custom cache directories.

The NIF is precompiled. Set `LUMIS_BUILD=1` to build it from source instead, or
`LUMIS_USE_LEGACY_ARTIFACTS=1` to take the legacy-CPU variant on a machine
without the newer instruction sets.

It downloads from GitHub Releases, mirrored to Cloudflare R2. Set
`config :lumis, artifact_source: :cloudflare` or `LUMIS_ARTIFACT_SOURCE=cloudflare`
to use the mirror when GitHub is down, see
[where the precompiled NIF comes from](https://lumis.sh/docs/usage/elixir-integration#where-the-precompiled-nif-comes-from).

## Documentation

- [Elixir integration](https://lumis.sh/docs/usage/elixir-integration) — configuration, releases, Phoenix
- [Formatters](https://lumis.sh/docs/usage/formatters) — every formatter and its options
- [Themes](https://lumis.sh/docs/usage/themes) — the theme list, custom themes, CSS files
- [Languages](https://lumis.sh/docs/reference/languages) — what is supported and how detection works
- [Line highlighting](https://lumis.sh/docs/usage/line-highlighting)
- [Recipes](https://lumis.sh/docs/recipes) — LiveView rendering, light/dark, injected languages

API reference: [hexdocs.pm/lumis](https://hexdocs.pm/lumis).

## Acknowledgements

* [Makeup](https://hex.pm/packages/makeup) for setting up the baseline and for the inspiration
* [Inkjet](https://crates.io/crates/inkjet) for the Rust implementation up to v0.2 and for the inspiration

# `formatter`

```elixir
@type formatter() ::
  :html_inline
  | {:html_inline,
     language: language(),
     theme: theme(),
     pre_class: String.t(),
     italic: boolean(),
     include_highlights: boolean(),
     rainbow_brackets: boolean(),
     highlight_lines: html_inline_highlight_lines(),
     header: header()}
  | :html_linked
  | {:html_linked,
     language: language(),
     pre_class: String.t(),
     rainbow_brackets: boolean(),
     highlight_lines: html_linked_highlight_lines(),
     header: header()}
  | :html_multi_themes
  | {:html_multi_themes,
     language: language(),
     themes: keyword(theme()),
     default_theme: String.t(),
     css_variable_prefix: String.t(),
     pre_class: String.t(),
     italic: boolean(),
     include_highlights: boolean(),
     rainbow_brackets: boolean(),
     highlight_lines: html_inline_highlight_lines(),
     header: header()}
  | :terminal
  | {:terminal,
     language: language(),
     theme: theme(),
     background: :theme | String.t() | nil,
     width: pos_integer() | nil,
     rainbow_brackets: boolean()}
  | :bbcode_scoped
  | {:bbcode_scoped, language: language(), rainbow_brackets: boolean()}
```

Highlighter formatter and its options.

Available formatters: `:html_inline`, `:html_linked`, `:html_multi_themes`, `:terminal`, `:bbcode_scoped`

* `:html_inline` - generates `<span>` tags with inline styles for each token, for example: `<span style="color: #6eb4bff;">Atom</span>`.
* `:html_linked` - generates `<span>` tags with `class` representing the token type, for example: `<span class="l-keyword-special">Atom</span>`.
   Must link an external CSS in order to render colors, see more at [HTML Linked](https://hexdocs.pm/lumis/Lumis.html#module-html-linked).
* `:html_multi_themes` - generates HTML with CSS custom properties (variables) for multiple themes, enabling light/dark mode support.
   Inspired by [Shiki Dual Themes](https://shiki.style/guide/dual-themes).
* `:terminal` - generates ANSI escape codes for terminal output.
* `:bbcode_scoped` - generates nested BBCode tags using highlight scope names, for example: `[keyword-elixir]defmodule[/keyword-elixir]`.

You can either pass the formatter as an atom to use default options or a tuple with the formatter name and options, so both are equivalent:

    # passing only the formatter name like below:
    :html_inline
    # is the same as passing an empty list of options:
    {:html_inline, []}

## Available Options:

* `html_inline`:

    - `:language` (`t:language/0` - default: `nil`) - the language used by the formatter. When omitted, Lumis tries to auto-detect it from the source.
    - `:theme` (`t:theme/0` - default: `nil`) - the theme to apply styles on the highlighted source code.
    - `:pre_class` (`t:String.t/0` - default: `nil`) - the CSS class to append into the wrapping `<pre>` tag.
    - `:italic` (`t:boolean/0` - default: `false`) - enable italic style for the highlighted code.
    - `:include_highlights` (`t:boolean/0` - default: `false`) - include the highlight scope name in a `data-highlight` attribute. Useful for debugging.
    - `:rainbow_brackets` (`t:boolean/0` - default: `false`) - render nested brackets with rainbow bracket scopes.
    - `:highlight_lines` (`t:html_inline_highlight_lines/0` - default: `nil`) - highlight specific lines either using the theme `highlighted` style or with custom CSS styling.
    - `:header` (`t:header/0` - default: `nil`) - wrap the highlighted code with custom open and close HTML tags.

* `html_linked`:

    - `:language` (`t:language/0` - default: `nil`) - the language used by the formatter. When omitted, Lumis tries to auto-detect it from the source.
    - `:pre_class` (`t:String.t/0` - default: `nil`) - the CSS class to append into the wrapping `<pre>` tag.
    - `:rainbow_brackets` (`t:boolean/0` - default: `false`) - render nested brackets with rainbow bracket scopes.
    - `:highlight_lines` (`t:html_linked_highlight_lines/0` - default: `nil`) - highlight specific lines either using the `l-highlighted` class from themes or with a custom CSS class.
    - `:header` (`t:header/0` - default: `nil`) - wrap the highlighted code with custom open and close HTML tags.

* `html_multi_themes`:

    - `:language` (`t:language/0` - default: `nil`) - the language used by the formatter. When omitted, Lumis tries to auto-detect it from the source.
    - `:themes` (`keyword(theme())` - required) - keyword list of theme identifiers to theme names/structs. Theme identifiers become CSS class names and CSS variable prefixes. Example: `[light: "github_light", dark: "github_dark"]`.
    - `:default_theme` (`t:String.t/0` - default: `nil`) - controls inline color rendering: specify a theme identifier for inline colors, use `"light-dark()"` for CSS light-dark() function, or `nil` for CSS variables only.
    - `:css_variable_prefix` (`t:String.t/0` - default: `nil`) - CSS variable prefix (defaults to `"--lumis"` if nil). Generates variables like `--lumis-light` (color), `--lumis-light-bg` (background), `--lumis-light-font-style`, etc.
    - `:pre_class` (`t:String.t/0` - default: `nil`) - the CSS class to append into the wrapping `<pre>` tag.
    - `:italic` (`t:boolean/0` - default: `false`) - enable italic style for the highlighted code.
    - `:include_highlights` (`t:boolean/0` - default: `false`) - include the highlight scope name in a `data-highlight` attribute.
    - `:rainbow_brackets` (`t:boolean/0` - default: `false`) - render nested brackets with rainbow bracket scopes.
    - `:highlight_lines` (`t:html_inline_highlight_lines/0` - default: `nil`) - highlight specific lines (same as html_inline).
    - `:header` (`t:header/0` - default: `nil`) - wrap the highlighted code with custom open and close HTML tags.

* `terminal`:

    - `:language` (`t:language/0` - default: `nil`) - the language used by the formatter. When omitted, Lumis tries to auto-detect it from the source.
    - `:theme` (`t:theme/0` - default: `nil`) - the theme to apply styles on the highlighted source code.
    - `:background` (`:theme | t:String.t/0 | nil` - default: `nil`) - fallback background behavior: `nil` inherits the output background, `:theme` uses the theme's normal background color, and a string uses that color.
    - `:width` (`pos_integer() | nil` - default: `nil`) - pad each rendered terminal line to the given width. This is most useful with `:background`.
    - `:rainbow_brackets` (`t:boolean/0` - default: `false`) - render nested brackets with rainbow bracket scopes.

* `bbcode_scoped`:

    - `:language` (`t:language/0` - default: `nil`) - available when passed as `{:bbcode_scoped, ...}`.
    - `:rainbow_brackets` (`t:boolean/0` - default: `false`) - render nested brackets with rainbow bracket scopes.

## Examples

### Inline HTML formatter with default options

    :html_inline

There is no default theme, so this emits `<span>` tags without any `style` attribute.
Pass `:theme` to get colors, or use `:html_linked` to style the output with a CSS file.

### Inline HTML formatter with custom options

    {:html_inline, theme: "onedark", pre_class: "example-01", include_highlights: true}

### HTML Inline: highlight specific lines

    # apply theme's `highlighted` style
    {:html_inline, theme: "onedark", highlight_lines: %{lines: [2..4, 6], style: :theme}}

    # style: :theme is the default
    {:html_inline, theme: "onedark", highlight_lines: %{lines: [1, 2, 3]}}

    # explicitly use theme style
    {:html_inline, theme: "onedark", highlight_lines: %{lines: [1, 2, 3], style: :theme}}

    # overrides default style
    {:html_inline, theme: "onedark", highlight_lines: %{lines: [1, 3..5, 8], style: "background-color: #fff3cd; border-left: 3px solid #ffc107;"}}

    # with only class and no style
    {:html_inline, theme: "onedark", highlight_lines: %{lines: [1, 2, 3], style: nil, class: "transition-colors duration-500 w-full inline-block bg-yellow-500"}}

### HTML Linked: highlight specific lines

    # use default `l-highlighted` class (already present in themes)
    {:html_linked, highlight_lines: %{lines: [2..4, 6]}}

    # use custom class
    {:html_linked, highlight_lines: %{lines: [1, 2, 3], class: "error-line"}}

### Wrap with custom open and close HTML tags

    header = %{
      open_tag: "<div class="code-header"><span>file: app.ex</span>",
      close_tag: "</div>"
    }
    {:html_inline, theme: "onedark", header: header}

### HTML Multi-Themes: Light/Dark mode support

    # Basic dual theme with CSS variables
    {:html_multi_themes, themes: [light: "github_light", dark: "github_dark"]}

    # With light-dark() function for automatic theme switching based on system preference
    {:html_multi_themes,
     themes: [light: "github_light", dark: "github_dark"],
     default_theme: "light-dark()"}

    # With inline colors for default theme and CSS variables for others
    {:html_multi_themes,
     themes: [light: "github_light", dark: "github_dark"],
     default_theme: "light"}

    # Multiple themes with custom prefix
    {:html_multi_themes,
     themes: [light: "github_light", dark: "github_dark", dim: "catppuccin_frappe"],
     css_variable_prefix: "--code"}

    # With Theme structs instead of strings
    light_theme = Lumis.Theme.get("github_light")
    dark_theme = Lumis.Theme.get("github_dark")
    {:html_multi_themes, themes: [light: light_theme, dark: dark_theme]}

### Terminal formatter

    :terminal

    {:terminal, theme: "github_light"}

    {:terminal, theme: "dracula", background: :theme, width: 120}

    {:terminal, theme: "dracula", background: "#282a36", width: 120}

### BBCode Scoped formatter

    :bbcode_scoped

Emits highlight scope names as tags, not standard forum-style BBCode like `[b]`, `[color]`, or `[code]`.

See https://docs.rs/lumis/latest/lumis/enum.FormatterOption.html for more info.

# `header`

```elixir
@type header() :: %{close_tag: String.t(), open_tag: String.t()} | nil
```

Wraps the highlighted code with custom open and close HTML tags.

# `html_inline_highlight_lines`

```elixir
@type html_inline_highlight_lines() ::
  %{
    lines: [pos_integer() | Range.t()],
    style: :theme | String.t() | nil,
    class: String.t() | nil
  }
  | nil
```

Highlight lines options for Inline HTML formatter.

# `html_linked_highlight_lines`

```elixir
@type html_linked_highlight_lines() ::
  %{lines: [pos_integer() | Range.t()], class: String.t()} | nil
```

Highlight lines options for Linked HTML formatter.

# `html_multi_themes_options`

```elixir
@type html_multi_themes_options() ::
  %{
    themes: keyword(theme()),
    default_theme: String.t() | nil,
    css_variable_prefix: String.t() | nil,
    pre_class: String.t() | nil,
    italic: boolean(),
    include_highlights: boolean(),
    highlight_lines: html_inline_highlight_lines() | nil,
    header: header()
  }
  | nil
```

Options for HTML Multi-Themes formatter.

The themes are specified as a keyword list where keys are CSS identifiers (atoms)
and values are theme names (strings) or Theme structs.

# `language`

```elixir
@type language() :: String.t() | nil
```

A language name, filename, or path with extension.

See `Lumis.available_languages/0` to list all available languages or check out a list of [available languages](https://docs.rs/lumis/latest/lumis/#languages-available).

## Examples

    - "elixir"
    - ".ex"
    - "app.ex"
    - "lib/app.ex"

# `language_info`

```elixir
@type language_info() :: %{
  id: String.t(),
  name: String.t(),
  aliases: [String.t()],
  extensions: [String.t()],
  globs: [String.t()],
  emacs_modes: [String.t()],
  shebangs: [String.t()]
}
```

What Lumis knows about one language.

# `options`

```elixir
@type options() :: [
  language: language(),
  formatter: formatter(),
  theme: struct() | binary() | nil,
  inline_style: boolean(),
  pre_class: binary() | nil
]
```

* `:language` (`t:Lumis.language/0`) - *This option is deprecated. Use the :language option inside the formatter tuple instead, eg: {:html_inline, language: "elixir"}*

* `:formatter` (`t:Lumis.formatter/0`) - Formatter to apply on the highlighted source code. See the type doc for more info. The default value is `{:html_inline, []}`.

* `:theme` - *This option is deprecated. Use :formatter instead.*

* `:inline_style` (`t:boolean/0`) - *This option is deprecated. Use :formatter instead.*

* `:pre_class` - *This option is deprecated. Use :formatter instead.*

See each option type for more info.

# `theme`

```elixir
@type theme() :: String.t() | Lumis.Theme.t() | nil
```

Theme used to apply styles on the highlighted source code.

See `Lumis.available_themes/0` to list all available themes or check out a list of [available themes](https://docs.rs/lumis/latest/lumis/#themes-available).

# `theme_info`

```elixir
@type theme_info() :: %{name: String.t(), appearance: String.t()}
```

A built-in theme's name and appearance, without its highlight data.

# `available_languages`

```elixir
@spec available_languages() :: [language_info()]
```

Returns every available language and what the catalog knows about it, sorted
by id.

## Example

    iex> Lumis.available_languages() |> Enum.find(&(&1.id == "elixir"))
    %{
      id: "elixir",
      name: "Elixir",
      aliases: [],
      extensions: ["*.ex", "*.exs"],
      globs: ["*.ex", "*.exs"],
      emacs_modes: ["elixir"],
      shebangs: ["elixir"]
    }

# `available_themes`

```elixir
@spec available_themes() :: [theme_info()]
```

Returns every built-in theme's name and appearance, sorted by name.

Use `Lumis.Theme.get/2` to get the actual theme struct.

## Example

    iex> Lumis.available_themes() |> Enum.find(&(&1.name == "github_light"))
    %{name: "github_light", appearance: "light"}

# `default_options`

```elixir
@spec default_options() :: options()
```

Returns all default options.

# `highlight`

```elixir
@spec highlight(String.t(), options()) :: {:ok, String.t()} | {:error, String.t()}
```

Highlights `source` code and outputs into a formatted string.

Returns `{:error, reason}` when the root language cannot be loaded or the
formatter fails. An injected language that cannot be fetched is not an error:
that block stays plain and the rest of the document still highlights. Use
`highlight!/2` to raise instead.

Invalid *options* still raise, because those are a caller mistake rather than
a runtime condition.

## Options

See `t:options/0`.

## Examples

Defining the language name:

    iex> Lumis.highlight("Atom.to_string(:elixir)", formatter: {:html_inline, language: "elixir"})
    {
      :ok,
      <pre class="lumis" style="color: #abb2bf; background-color: #282c34;"><code class="language-elixir" translate="no" tabindex="0"><div class="l-line" data-line="1"><span style="color: #e5c07b;">Atom</span><span style="color: #56b6c2;">.</span><span style="color: #61afef;">to_string</span><span style="color: #c678dd;">(</span><span style="color: #e06c75;">:elixir</span><span style="color: #c678dd;">)</span>
      </div></code></pre>
    }

Guessing the language based on the provided source code:

    iex> Lumis.highlight("#!/usr/bin/env bash\nID=1")
    {:ok, "<pre class="lumis" ...><code class="language-bash" ...>...</code></pre>"}

With custom options:

    iex> Lumis.highlight("Atom.to_string(:elixir)", formatter: {:html_inline, language: "example.ex", pre_class: "example-elixir"})
    {:ok, "<pre class="lumis example-elixir" ...><code ...>...</code></pre>"}

Terminal formatter:

    iex> Lumis.highlight("Atom.to_string(:elixir)", formatter: {:terminal, language: "elixir"})
    {:ok, "[0m[38;2;229;192;123mAtom[0m[0m[38;2;86;182;194m.[0m[0m[38;2;97;175;239mto_string[0m[0m[38;2;198;120;221m([0m[0m[38;2;224;108;117m:elixir[0m[0m[38;2;198;120;221m)[0m"}

Highlighting specific lines in HTML Inline formatter:

    iex> code = """
    ...> defmodule Example do
    ...>   @lang = :elixir
    ...>   def lang, do: @lang
    ...> end
    ...> """
    iex> highlight_lines = %{lines: [2]}
    iex> Lumis.highlight(code, formatter: {:html_inline, language: "elixir", highlight_lines: highlight_lines})
    # Line 2 will be highlighted with the theme's `highlighted` style:
    <div class="l-line" style="background-color: #414858;" data-line="2">...</div>

Highlighting specific lines in HTML Linked formatter:
    
    iex> code = """
    ...> defmodule Example do
    ...>   @lang = :elixir
    ...>   def lang, do: @lang
    ...> end
    ...> """
    iex> highlight_lines = %{lines: [2]}
    iex> Lumis.highlight(code, formatter: {:html_linked, language: "elixir", highlight_lines: highlight_lines})
    # Line 2 will contain a `l-highlighted` class:
    <div class="l-line l-highlighted" data-line="2">...

Wrapping with custom HTML:

    iex> header = %{
    ...>   open_tag: "<figure><span>file: example.exs</span>",
    ...>   close_tag: "</figure>"
    ...> }
    iex> Lumis.highlight("IO.puts('hello')", formatter: {:html_inline, language: "elixir", header: header})
    # Returns: "<div class='code-block' data-lang='elixir'><pre class='lumis'>...</pre></div>"
    {:ok, "<figure><span>file: example.exs</span><pre...><code ...>...</code></pre></figure>"}

See https://docs.rs/lumis/latest/lumis/fn.highlight.html for more info.

# `highlight`

> This function is deprecated. Use highlight/2 instead.

# `highlight!`

```elixir
@spec highlight!(
  String.t(),
  keyword()
) :: String.t()
```

Same as `highlight/2` but raises `Lumis.HighlightError` in case of failure.

# `highlight!`

> This function is deprecated. Use highlight!/2 instead.

# `loaded_languages`

```elixir
@spec loaded_languages() :: [id :: String.t()]
```

Returns the ids of the languages loaded into this VM, sorted.

The complement of `available_languages/0`: what can be highlighted right now
without a download. Loading is global to the VM, so this is the same list in
every process.

## Example

    iex> Lumis.Languages.load("elixir")
    iex> Lumis.loaded_languages()
    ["elixir"]

# `validate_options!`

```elixir
@spec validate_options!(options()) :: options()
```

Validates the given options against the options schema.

This function validates the provided options using NimbleOptions and the defined schema.
It ensures that all options are valid and properly typed before being passed to the
highlighting functions.

## Examples

    iex> Lumis.validate_options!(formatter: {:html_inline, language: "elixir"})
    [formatter: {:html_inline, [header: nil, highlight_lines: nil, include_highlights: false, italic: false, pre_class: nil, theme: nil, language: "elixir"]}]

    iex> Lumis.validate_options!(formatter: {:html_inline, theme: "dracula"})
    [formatter: {:html_inline, [theme: "dracula", ...]}]

    iex> Lumis.validate_options!(language: :invalid)
    ** (NimbleOptions.ValidationError)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
