Lumis
Syntax highlighter powered by Tree-sitter and Neovim themes.
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
- Parsers are dependencies - Declared in
mix.exs, compiled on first use
Installation
Add Lumis and a parser for each language you highlight:
def deps do
[
{:lumis, "~> 0.9"},
{:lumis_wasm_elixir, "~> 0.26.0"}
]
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.
For your own, implement Lumis.Formatter and build the output with
Lumis.Formatter.HTML or Lumis.Formatter.ANSI, which hold the same pieces the
built-in formatters use.
Parsers
A parser is an ordinary dependency: add {:lumis_wasm_elixir, "~> 0.26.0"} and
mix deps.get delivers the bytes. Highlighting loads whatever a document
needs, including languages injected inside it, and keeps them for every
later request. Loading is global to the VM, so only the first process pays.
A language no dependency supplies is not fetched. A document's own language
missing is an error — Lumis.ParserError with the package to add — and a
language injected inside it missing costs that block its highlighting, not the
document. So add the ones a document can inject too, not only the ones it
names: Markdown fences reach
whatever language they label, HTML reaches css and javascript, and Elixir
reaches comment. A bundle package installs a set at once, such as
{:lumis_wasm_bundle_web, "~> 0.1"}, and the language catalog at
docs.lumis.sh lists every package name.
# move the compile 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 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 a compile, 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, and custom data 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.
Documentation
Guides for configuration, releases, Phoenix, formatters, themes and recipes are at docs.lumis.sh.
API reference: hexdocs.pm/lumis.