Lumis (Lumis v0.7.0)

Copy Markdown View Source

Syntax highlighter powered by Tree-sitter and Neovim themes.

https://lumis.sh

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

def deps do
  [
    {:lumis, "~> 0.3"}
  ]
end

Usage

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.

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

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

Documentation

API reference: hexdocs.pm/lumis.

Acknowledgements

  • Makeup for setting up the baseline and for the inspiration
  • Inkjet for the Rust implementation up to v0.2 and for the inspiration

Summary

Types

Highlighter formatter and its options.

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

Highlight lines options for Inline HTML formatter.

Highlight lines options for Linked HTML formatter.

Options for HTML Multi-Themes formatter.

A language name, filename, or path with extension.

What Lumis knows about one language.

Theme used to apply styles on the highlighted source code.

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

Functions

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

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

Returns all default options.

Highlights source code and outputs into a formatted string.

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

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

Validates the given options against the options schema.

Types

formatter()

@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.
  • :html_multi_themes - generates HTML with CSS custom properties (variables) for multiple themes, enabling light/dark mode support. Inspired by Shiki 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 (language/0 - default: nil) - the language used by the formatter. When omitted, Lumis tries to auto-detect it from the source.
    • :theme (theme/0 - default: nil) - the theme to apply styles on the highlighted source code.
    • :pre_class (String.t/0 - default: nil) - the CSS class to append into the wrapping <pre> tag.
    • :italic (boolean/0 - default: false) - enable italic style for the highlighted code.
    • :include_highlights (boolean/0 - default: false) - include the highlight scope name in a data-highlight attribute. Useful for debugging.
    • :rainbow_brackets (boolean/0 - default: false) - render nested brackets with rainbow bracket scopes.
    • :highlight_lines (html_inline_highlight_lines/0 - default: nil) - highlight specific lines either using the theme highlighted style or with custom CSS styling.
    • :header (header/0 - default: nil) - wrap the highlighted code with custom open and close HTML tags.
  • html_linked:

    • :language (language/0 - default: nil) - the language used by the formatter. When omitted, Lumis tries to auto-detect it from the source.
    • :pre_class (String.t/0 - default: nil) - the CSS class to append into the wrapping <pre> tag.
    • :rainbow_brackets (boolean/0 - default: false) - render nested brackets with rainbow bracket scopes.
    • :highlight_lines (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 (header/0 - default: nil) - wrap the highlighted code with custom open and close HTML tags.
  • html_multi_themes:

    • :language (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 (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 (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 (String.t/0 - default: nil) - the CSS class to append into the wrapping <pre> tag.
    • :italic (boolean/0 - default: false) - enable italic style for the highlighted code.
    • :include_highlights (boolean/0 - default: false) - include the highlight scope name in a data-highlight attribute.
    • :rainbow_brackets (boolean/0 - default: false) - render nested brackets with rainbow bracket scopes.
    • :highlight_lines (html_inline_highlight_lines/0 - default: nil) - highlight specific lines (same as html_inline).
    • :header (header/0 - default: nil) - wrap the highlighted code with custom open and close HTML tags.
  • terminal:

    • :language (language/0 - default: nil) - the language used by the formatter. When omitted, Lumis tries to auto-detect it from the source.
    • :theme (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 (boolean/0 - default: false) - render nested brackets with rainbow bracket scopes.
  • bbcode_scoped:

    • :language (language/0 - default: nil) - available when passed as {:bbcode_scoped, ...}.
    • :rainbow_brackets (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()

@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()

@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()

@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()

@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()

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

Examples

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

language_info()

@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()

@type options() :: [
  language: language(),
  formatter: formatter(),
  theme: struct() | binary() | nil,
  inline_style: boolean(),
  pre_class: binary() | nil
]
  • :language (Lumis.language/0) - This option is deprecated. Use the :language option inside the formatter tuple instead, eg: {:html_inline, language: "elixir"}

  • :formatter (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 (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()

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

theme_info()

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

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

Functions

available_languages()

@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()

@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()

@spec default_options() :: options()

Returns all default options.

highlight(source, options \\ [])

@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 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, "Atom.to_string(:elixir)"}

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(language, source, options)

This function is deprecated. Use highlight/2 instead.

highlight!(source, options \\ [])

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

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

highlight!(language, source, options)

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

loaded_languages()

@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!(options)

@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)